import type { ImageContent, Message, MessageAttribution, ServiceTierByFamily, TextContent } from "@oh-my-pi/pi-ai"; import { type StructuredSubagentSchemaMode } from "@oh-my-pi/pi-tui/tools/task"; import type { ConfiguredThinkingLevel } from "@oh-my-pi/pi-tui/thinking"; import * as snapcompact from "@oh-my-pi/snapcompact"; import { type ArtifactManagerApi } from "./artifacts.js"; import { type BlobPutOptions, type BlobPutResult } from "./blob-store.js"; import type { CompactionMethod } from "./compaction-methods.js"; import { type BashExecutionMessage, type CustomMessage, type FileMentionMessage, type HookMessage, type PythonExecutionMessage } from "./messages.js"; import type { WorkPoolYieldItem } from "../task/workpool-yield.js"; import type { RetryFallbackRole } from "./retry-fallback-chains.js"; import { type BuildSessionContextOptions, type SessionContext } from "./session-context.js"; import { type FileEntry, type ModelChangeEntry, type ModelUsageEntry, type NewSessionOptions, type SessionEntry, type SessionHeader, type SessionTitleSource, type SessionTreeNode, type UsageStatistics } from "./session-entries.js"; import { type PickerListing, type SessionContinuation, type SessionInfo } from "./session-listing.js"; import { type SessionStorage } from "./session-storage.js"; /** A fresh session id. */ export declare function mintSessionId(): string; /** * `moveTo` refused before anything moved: another live omp process writes the * session, or the session file at the destination. */ export declare class SessionMoveRefusedError extends Error { constructor(message: string); } /** * Copy a session's artifact directory to another session, matching interactive * `/fork`. Never overwrites: a destination file written while the copy runs * (a session that already moved and kept working) wins over the source's copy. * Failures are logged, never thrown. */ export declare function copySessionArtifacts(sourceSessionFile: string, destinationSessionFile: string): Promise; /** * The most recent model role on `branch`, or undefined when no model change * was recorded. `skipEphemeral` passes over temporary switches (retry fallback * hops, prewalk, context promotion), returning the role last assigned. */ export declare function lastModelChangeRole(branch: readonly SessionEntry[], options?: { skipEphemeral?: boolean; }): string | undefined; /** * The newest of `entries` that {@link movesConversation}, when `branch` ends at it: where the * conversation stands. `undefined` when there is none, or the branch sits on an older entry or another branch. */ export declare function conversationTip(entries: readonly SessionEntry[], branch: readonly SessionEntry[]): SessionEntry | undefined; /** * Maintains the derived views over a session's entry list: id lookup, the * parent→children adjacency, the resolved label map, the active leaf, and the * running usage totals. Kept in lockstep with the manager's `#entries` so reads * stay O(1)/O(children) instead of rescanning the whole journal. Also backs * the main thread's read-only view of a worker-homed session's entries. */ export declare class SessionEntryIndex { #private; clear(): void; rebuild(entries: readonly SessionEntry[]): void; insert(entry: SessionEntry): void; has(id: string): boolean; get(id: string): SessionEntry | undefined; /** * The live id→entry map. Read-only for callers (lookups + `generateId` * collision checks); never mutate it directly — go through `insert`/`rebuild`. */ entriesById(): Map; leafId(): string | null; leafEntry(): SessionEntry | undefined; setLeaf(id: string | null): void; /** * Stop extending the issued branch view in place: the next insert starts a * fresh memo. For inserts whose leaf move is undone right after, which must * not grow a view held by callers. */ detachBranchView(): void; childrenOf(parentId: string): SessionEntry[]; labelFor(id: string): string | undefined; labelsInEffect(): IterableIterator<[string, string]>; usageSnapshot(): UsageStatistics; pathTo(id?: string | null | undefined): SessionEntry[]; /** The memoized active-leaf branch, without copying. Callers MUST NOT mutate it. */ branchView(): readonly SessionEntry[]; tree(entries: readonly SessionEntry[]): SessionTreeNode[]; } export type ReadonlySessionManager = Pick; interface SessionManagerStateSnapshot { cwd: string; sessionDir: string; sessionId: string; sessionName: string | undefined; titleSource: SessionTitleSource | undefined; sessionFile: string | undefined; expectedDiskSize: number | null; titleUpdatedAt: string; hasTitleSlot: boolean; onDisk: boolean; needsRewrite: boolean; draftOnlySessionCleanupArmed: boolean; fallbackRuntimeOnly: boolean; /** Stashed sibling family candidate; moveTo drops it, so a rollback must restore it. */ taskSiblingCandidate: string | undefined; header: SessionHeader; entries: SessionEntry[]; } /** * The storage may have published a write that rejected, and an authoritative * repair could not be proven durable. Callers must fail closed until recovery. */ export declare class SessionPersistenceIndeterminateError extends AggregateError { readonly operationError: Error; readonly recoveryErrors: readonly Error[]; constructor(operationError: Error, recoveryErrors: readonly Error[]); } /** * This session moved to a new file and keeps saving there. Not a failure: * nothing is latched. Delivered through {@link SessionManager.onPersistenceNotice}. */ export interface SessionPersistenceNotice { /** * `"open-elsewhere"`: another live omp process wrote `from` first and still * has it open, so this process saves its entries elsewhere instead of mixing * them into that file. `"replaced"`: `from` changed on disk and no longer * reads as this session's journal, so it is left untouched. `"contested"`: * a writer without the ownership lease kept changing `from` through every * read-back-and-retry pass, so this session stops racing it. */ reason: "open-elsewhere" | "replaced" | "contested"; /** The session file this session was saving to. */ from: string; /** The fresh sibling it saves to from now on: same directory, a new session id whose `parentSession` is the old one. */ to: string; } /** * Thrown by {@link SessionManager.forkFrom} when the fork source is missing. * The CLI maps this to a clean session-resolution failure at its own boundary * (this module must not import `main.ts`, where `SessionResolutionError` lives). */ export declare class ForkSourceNotFoundError extends Error { constructor(sourcePath: string); } /** * Stores and navigates an append-only conversation journal. * * A session is a JSONL file: one header line followed by entries. Entries form a * tree by `(id, parentId)`, and the mutable leaf pointer selects which path is * active for future appends and for LLM context construction. * * Durability is software-crash safe but not power-loss safe: completed entries * (user/assistant/toolResult messages, tool_execution_start markers, custom * entries) are handed to the OS synchronously in-body on append and never * `fsync`'d. In-flight streaming text is intentionally not durable until * `message_end` persists the finished message. * * While an in-place atomic rewrite is publishing, a concurrent completed append * supersedes that publish with a synchronous full-body rewrite so the entry is * software-crash durable before the append returns; the abandoned atomic's * `commitGuard` then refuses to clobber the fresher body. * * During {@link moveTo}, appends write a full body to the live relocation path * (source until rename, destination once the rename has landed) so a crash mid- * move still preserves completed entries without recreating a vacated source. * A trailing atomic rewrite still rewrites the header cwd after the path is * repointed. */ export declare class SessionManager { #private; /** * Collab replication tap: invoked for every appended entry with the * in-memory (pre-blob-externalization) entry, so inline images survive. */ onEntryAppended?: (entry: SessionEntry) => void; private constructor(); /** Puts a binary blob into the blob store and returns the blob reference. */ putBlob(data: Buffer, options?: BlobPutOptions): Promise; /** Synchronous variant of {@link putBlob} for rebuild-only render paths. */ putBlobSync(data: Buffer, options?: BlobPutOptions): BlobPutResult; captureState(): SessionManagerStateSnapshot; /** * Create an independent manager for the current logical session and branch. * The clone shares the storage backend but owns its entry index and writer, so * callers can finish session-owned work after this manager switches elsewhere. * Set `persist` false when the original session is intentionally being dropped. */ cloneCurrentSession(options?: { persist?: boolean; }): SessionManager; restoreState(snapshot: SessionManagerStateSnapshot): void; /** * Undo a {@link moveTo} using a {@link captureState} snapshot: rename the * session and artifacts back into the captured bucket, then restore the * captured metadata (cwd, header, additionalDirectories) and the stashed * sibling candidate the forward move dropped. The captured header is * persisted after relocation so a fresh open of the source session sees the * pre-move metadata, including workspace roots the move filtered out. * Rollbacks must not re-enter forward-move hooks, so this bypasses * AgentSession entirely. If the rename-back itself fails, the * manager stays pointed at the actual moved file (restoring the snapshot * would split the transcript across a recreated source and the stranded * target) and the error names where the session file actually lives. */ rollbackMove(snapshot: SessionManagerStateSnapshot): Promise; /** Switch to a different session file (resume / branch). */ setSessionFile(sessionFile: string): Promise; /** * Start a new session and persist its header before returning, except for a * `drop` (/delete) successor. * * The durable empty boundary prevents a later process on another terminal * from selecting the previous conversation as the most recent session. The * drop path does not need it: the fresh terminal breadcrumb already pins * this terminal to the successor, and if deleting the predecessor failed, * that surviving non-empty transcript is the correct resume target. * Materializing the successor there would only leave a zero-turn stub * behind when the user quits, so it keeps the ordinary lazy gate and writes * once it has content. */ newSession(options?: NewSessionOptions): Promise; /** Delete a session file and its artifact directory. ENOENT is treated as success. */ dropSession(sessionPath: string): Promise; /** * Fork the current session into a new file with the same entries. * @returns the old and new session file paths, or undefined when not persisting. */ fork(): Promise<{ oldSessionFile: string; newSessionFile: string; } | undefined>; /** Move the session to a new working directory. */ moveTo(newCwd: string, targetSessionDir?: string): Promise; /** * Force the session onto disk even with no assistant message yet (ACP * session/new must create a discoverable file immediately). */ ensureOnDisk(): Promise; /** Persist this session's transcript as a newly identified OMP session. */ persistCopy(options?: { sessionDir?: string; suppressBreadcrumb?: boolean; }, storage?: SessionStorage): Promise; /** * Stage a synchronous group of entry appends and publish the resulting full * journal with one atomic replace. A failed publish removes only the staged * entries, preserves/reparents entries appended concurrently, restores the * prior durable file view, and clears the failed writer latch for retry. * * The callback MUST be synchronous. */ appendEntriesAtomically(append: () => T): Promise; /** * Replace an uncertain append tail with the authoritative in-memory journal. * Callers must only use this for monotonic recovery where every retained * entry remains intended (for example, an explicit terminal tombstone). */ recoverPersistenceFromCurrentState(): Promise; /** Flush pending writes. Call before switching sessions or on shutdown. */ flush(): Promise; /** * Synchronously makes the current append-only session durable. Avoid rewriting * an already-current file: large restored sessions can contain GiB of compacted * history, and Ctrl+C must not rebuild the whole JSONL string just to flush. */ flushSync(): void; /** Flush, then close the append writer. */ close(): Promise; /** * Raise the terminal write barrier ahead of the final {@link close}. Once * sealed: * - every later append, title change, and rewrite is a dropped no-op — * including work an event handler tries to enqueue while dispose is * awaiting `close()` on the disk tail; * - the disk epoch is bumped, so queued-but-unexecuted tail work is * superseded and an ALREADY-RUNNING fenced/repair rewrite (awaiting the * tail, drain, writer close, or the atomic stage) fails its commit guard * at the rename fence instead of publishing over a revived file. * The final `close()` itself is scheduled after the bump and still runs; * pre-seal hot-path appends are already in the page cache. Idempotent; * terminal. */ seal(): void; /** * Terminal release: drop the in-memory transcript and complete the * {@link seal}. The entry journal and its index mirror the agent's message * array (tool results, file contents, base64 frame images); on a disposed * session — e.g. a parked subagent still referenced by the lifecycle * adoption record — they would otherwise stay pinned for the process * lifetime. * * Closes the append writer; with the seal up, nothing can reopen it. A * revival may reopen the same JSONL through a NEW manager the moment * dispose returns; a late event handler resuming on THIS manager must * never race that writer — and a post-release rewrite would persist the * now-empty entry list, truncating the transcript. Reads after this point * reopen from disk (revival, `history://`). Only call from session * dispose, after the final `close()`; idempotent. */ releaseRetainedEntries(): void; getCwd(): string; /** Recorded cwd from the session header (original project), may differ from runtime {@link getCwd} when fallback retained launch cwd. */ getRecordedCwd(): string | undefined; setCwdWithoutRelocation(newCwd: string): void; adoptRecordedCwd(): void; /** Additional workspace directories beyond cwd (multi-root), absolute and normalized. */ getAdditionalDirectories(): string[]; /** * Add a workspace directory. Normalizes (relative to cwd), dedupes, rejects * the cwd itself, persists to the session header, and triggers an atomic * rewrite so the change survives a crash. Returns the resolved absolute * path or `null` when the directory was already present (no-op). */ addWorkspaceDirectory(directory: string): Promise; /** * Remove a workspace directory by absolute or cwd-relative path. Persists * the trimmed header. Returns the resolved path that was removed, or * `null` when the directory was not an additional root (no-op). */ removeWorkspaceDirectory(directory: string): Promise; /** Seed additional directories from settings or a passed list. Also called on resumed sessions with --add-dir; persists the updated header when the session file is already durable. No-op when the normalized list is unchanged (avoids rewriting large session files on every startup). */ setAdditionalDirectories(directories: string[]): Promise; getUsageStatistics(): UsageStatistics; /** * Open a new per-turn budget window: snapshot the cumulative output baseline, * reset the eval-subagent counter, and set the (optional) ceiling. */ beginTurnBudget(total: number | null, hard: boolean): void; recordEvalSubagentOutput(output: number): void; getTurnBudget(): { total: number | null; spent: number; hard: boolean; }; getSessionDir(): string; getSessionId(): string; getSessionFile(): string | undefined; /** * Whether the current session has actually been materialized to durable * storage (the JSONL exists on disk / in the active storage backend). * * Session persistence is lazy: the file is only written once the history * contains an assistant message (or an explicit {@link ensureOnDisk} * caller forces it). Until then {@link getSessionFile} returns an allocated * path that leads nowhere, so a `--resume ` hint built from it would * always fail. Consumers that advertise a resume command must gate on this * (issue #8860). */ isSessionOnDisk(): boolean; /** * {@link isSessionOnDisk}, counting only what the backing store has * confirmed. Remote storage publishes asynchronously, so a hint printed * just before the process exits must not count a write still in flight: * the exit aborts it and `--resume ` would find nothing. A deferring * store without `existsConfirmedSync` cannot vouch for its writes, so it * reports false. */ isSessionConfirmedOnDisk(): boolean; getArtifactsDir(): string | null; /** Source artifacts a seance fork reads in place; null for ordinary sessions. */ getSeanceSourceArtifactsDir(): string | null; adoptArtifactManager(manager: ArtifactManagerApi): void; getArtifactManager(): ArtifactManagerApi | null; allocateArtifactPath(toolType: string): Promise<{ id?: string; path?: string; }>; saveArtifact(content: string, toolType: string): Promise; getArtifactPath(id: string): Promise; /** * Content of a spill artifact kept in memory by a non-persistent session. * Null for unknown ids and for file-backed sessions, whose artifacts resolve * through {@link getArtifactPath}. */ getArtifactContent(id: string): string | null; saveDraft(text: string): Promise; consumeDraft(): Promise; /** The source that set the session name: "user" (manual/RPC) or "auto" (generated title). */ get titleSource(): SessionTitleSource | undefined; /** Bumped when entries are rewritten in place (`rewriteEntries`, replay sanitizing); appends do not bump it. */ get bodyRevision(): number; /** Tracks user rename requests; background title updates do not invalidate them. */ get titleRevision(): number; /** Malformed JSONL records skipped when this session file was loaded (0 for new sessions). */ get loadedMalformedRecords(): number; /** Invalidate older generated renames before starting a new request. */ reserveTitleRevision(): number; getSessionName(): string | undefined; onSessionNameChanged(cb: () => void): () => void; /** * Subscribe to persistence failures so hosts can surface lost-durability state. * * A failure latched before this call — a store that failed on its first write, * before the host wired its observer — is replayed to the new subscriber. * Without the replay the host sees only a dispose rejection it cannot * attribute to persistence (issue #11493). */ onPersistenceError(cb: (error: Error) => void): () => void; /** * Subscribe to {@link SessionPersistenceNotice}s: this session moved to a new * file and keeps saving there, which the user should hear about but which is * not a failure. Every notice raised before this call is replayed to the new * subscriber, as {@link onPersistenceError} replays a latched failure, so each * subscriber sees each notice once however late it subscribed. */ onPersistenceNotice(cb: (notice: SessionPersistenceNotice) => void): () => void; /** * Set the session display name. * @param source "user" for explicit renames; "auto" for generated titles. * Auto titles are ignored once the user has set a name. */ setSessionName(name: string, source?: SessionTitleSource, trigger?: string): Promise; /** * Rename a session file that is not loaded in this process, e.g. from the * resume picker. Returns the title as stored (the session canonicalizes * it), or null when the rename did not happen. * * The cross-process ownership lease is claimed before the transcript is * loaded and held until the write is done. Claiming first is what makes * the rename safe: another process appending between our load and our * write would leave us parenting the title entry to a stale leaf, so the * reopened session followed the older branch and hid the other process's * turn. Holding the lease from before the load keeps the loaded leaf * stable until the title is written. */ static renameSessionTitle(sessionPath: string, title: string, options?: { cwd?: string; }): Promise; /** * Journal an idle recap for this session in history.db. Recaps never enter * the session file or LLM context; in-memory sessions are not journaled. */ recordRecap(recap: string): void; /** * Append a foreign (host-authored) entry verbatim, preserving its * `id`/`parentId`. Used by collab guests to mirror the host session. */ ingestReplicatedEntry(entry: SessionEntry): void; /** * The live header and entries, for collab replication. Nothing is copied: * the host mutates entries in place on rewrite paths, so callers must not * mutate what this returns and must serialize it synchronously, before any * such rewrite can run. */ snapshotForReplication(): { header: SessionHeader; entries: readonly SessionEntry[]; }; /** * Append a message as a child of the current leaf, then advance the leaf. * CompactionSummaryMessage / BranchSummaryMessage are rejected here — they are * top-level entries via appendCompaction()/branchWithSummary(). */ appendMessage(message: Message | CustomMessage | HookMessage | BashExecutionMessage | PythonExecutionMessage | FileMentionMessage): string; /** * Append to a non-active branch without changing the current leaf. * Used by work that retains ownership of a branch across tree navigation. * * The entry records the active leaf it interrupted (`activeLeafId`) so a * reload replays the same leaf instead of adopting this off-branch tail. * Pass `preserveActiveLeaf: false` when the caller selects the new entry as * the leaf instead (a re-answered `ask`). */ appendMessageToBranch(message: Message | CustomMessage | HookMessage | BashExecutionMessage | PythonExecutionMessage | FileMentionMessage, parentId: string | null, options?: { preserveActiveLeaf?: boolean; }): string; /** Record usage on its initiating branch without moving a successor branch or session. */ appendModelUsage(usage: Pick, owner: { sessionId: string; parentId: string | null; }): string | undefined; /** Append a thinking level change as child of current leaf, then advance leaf. Returns entry id. */ appendThinkingLevelChange(thinkingLevel?: string, configured?: string): string; appendServiceTierChange(serviceTier: ServiceTierByFamily | null): string; appendModeChange(mode: string, data?: Record): string; /** * Append a model change as a child of the current leaf, then advance the leaf. * @param model Model in "provider/modelId" format * @param role Optional role (default: "default") * @param resolvedModelIsFallback Whether this transition selected a retry-fallback model */ appendModelChange(model: string, role?: string, resolvedModelIsFallback?: boolean): string; appendSessionInit(init: { systemPrompt: string[]; task: string; tools: string[]; mountedTools?: string[]; agent?: string; modelRole?: string; resolvedModel?: string; perCallModel?: boolean; perCallModelIdentity?: string; perCallThinkingLevel?: ConfiguredThinkingLevel | null; retryFallback?: RetryFallbackRole; readOnly?: boolean; outputSchema?: unknown; outputSchemaMode?: StructuredSubagentSchemaMode; restrictToolNames?: boolean; spawns?: string; readSummarize?: boolean; advisor?: string; compactionThreshold?: { thresholdPercent: number; thresholdTokens: number; }; isolated?: boolean; workPoolYieldItems?: WorkPoolYieldItem[]; }): string; appendCompaction(summary: string, shortSummary: string | undefined, firstKeptEntryId: string, tokensBefore: number, options?: { details?: T; fromExtension?: boolean; preserveData?: Record; method?: CompactionMethod; providerReplayThroughEntryId?: string; tokensAfter?: number; }): string; /** * Append the durable conversation boundary recorded by `/clear`. The * collapsed live transcript and the model-context rebuild start after the * latest one, while the full history stays on disk (the plain * `transcript:true` export walks it unchanged). */ appendResetBoundary(): string; appendCustomEntry(customType: string, data?: unknown): string; /** * Rewrite the session file after in-place entry updates (e.g. pruning old tool * outputs). Use sparingly. */ rewriteEntries(): Promise; /** * Append a custom message entry (for extensions) that participates in LLM context. * @param customType Hook identifier for filtering on reload * @param content Message content (string or TextContent/ImageContent array) * @param display Whether to show in TUI (true = styled display, false = hidden) * @param details Optional extension-specific metadata (not sent to LLM) * @param attribution Who initiated this message for billing/attribution semantics */ appendCustomMessageEntry(customType: string | undefined, content: string | (TextContent | ImageContent)[] | undefined, display: boolean | undefined, details?: T, attribution?: MessageAttribution | undefined, timestamp?: number): string; /** Append a TTSR injection entry recording which rules were injected. */ appendTtsrInjection(ruleNames: string[]): string; /** All unique TTSR rule names injected on the current branch (root → leaf). */ getInjectedTtsrRules(): string[]; /** Append a credential pin recording which OAuth account served `provider`. */ appendCredentialPin(provider: string, hash: string): string; /** * Latest credential pin per provider on the current branch (root → leaf), * with the effective last-use time of the pinned account. * * Pins are appended only when the serving account *changes*, so a long * session on one account carries a single old pin entry. Any assistant turn * for the same provider after that pin was necessarily served by the pinned * account, so its timestamp advances `lastUsedAt` — a resume seconds after * the last turn seeds a warm sticky instead of a stale one. */ getCredentialPins(): Map; getLeafId(): string | null; getLeafEntry(): SessionEntry | undefined; /** * The most recent model role on the current branch, or undefined when no * model change has been recorded. `skipEphemeral` passes over temporary * switches (retry fallback hops, prewalk, context promotion), returning the * role the session was last assigned. */ getLastModelChangeRole(options?: { skipEphemeral?: boolean; }): string | undefined; /** * The newest model change on the current branch when it selected a model for * this session only (`temporary`, or a prewalk handoff); undefined when the * newest change assigned a role, was a retry fallback hop, or none was recorded. */ getSessionLocalModelChange(): ModelChangeEntry | undefined; getEntry(id: string): SessionEntry | undefined; /** All direct children of an entry. */ getChildren(parentId: string): SessionEntry[]; getLabel(id: string): string | undefined; /** * Set or clear a label on an entry. Pass undefined/empty to clear. */ appendLabelChange(targetId: string, label: string | undefined): string; /** * Walk from an entry to root, returning entries in path order. Includes all * entry types; use buildSessionContext() for the resolved LLM messages. */ getBranch(fromId?: string): SessionEntry[]; /** * The active-leaf branch (same entries as `getBranch()`) without the * defensive copy. The array is the manager's memo: never mutate it. It is * live — an entry appended to the current leaf is pushed onto it — so use * `getBranch()` when you need a snapshot that survives appends. */ getBranchView(): readonly SessionEntry[]; /** * Build the session context (LLM messages), or — with `{ transcript: true }` — * the full-history display transcript, from the current leaf path. */ buildSessionContext(options?: BuildSessionContextOptions): SessionContext; /** Prompt blocks for a snapcompact archive, budgeted and resolved as {@link buildSessionContext} attaches them. */ snapcompactHistoryBlocks(archive: snapcompact.Archive): (TextContent | ImageContent)[]; /** A snapcompact frame's payload, read from the blob store when persistence externalized it. */ resolveFrameData(data: string): snapcompact.LazyFrameData | undefined; /** Strip stale OpenAI Responses replay metadata from loaded assistant entries. */ sanitizeLoadedOpenAIResponsesReplayMetadata(): boolean; getHeader(): SessionHeader | null; static getInheritedProviderPromptCacheKey(header: SessionHeader | undefined): string | undefined; /** Seed automatic affinity without turning a legacy fork identity into an explicit override. */ setTaskPromptCacheKey(key: string): Promise; /** * Stash the parent-derived sibling family candidate without touching the * header. Unlike {@link setTaskPromptCacheKey} this is neither persisted nor * carried into forks, moves, or new-session resets, so a fresh identity * starts without it while the per-request policy gate still decides whether * this session's requests consume it. */ setTaskSiblingCandidate(key: string): void; /** Stashed sibling family candidate; consumed only when the request model opts in. */ getTaskSiblingCandidate(): string | undefined; /** Drop the stash: a committed switch to a different session adopts that session's identity. */ clearTaskSiblingCandidate(): void; /** All session entries (excludes header). Returns a shallow copy. */ getEntries(): SessionEntry[]; /** {@link conversationTip} of the session at its active leaf. */ getConversationTip(): SessionEntry | undefined; /** * The session as a tree. A well-formed session has exactly one root; orphaned * entries (broken parent chain) are returned as roots too. */ getTree(): SessionTreeNode[]; /** * Move the leaf to an earlier entry so the next append forms a new branch. * Existing entries are never modified or deleted. */ branch(branchFromId: string): void; /** Reset the leaf to null so the next append creates a new root entry. */ resetLeaf(): void; /** * Durably move the active branch past a discarded entry. * * The loader reconstructs the active branch from the last physical journal * entry, so changing the in-memory leaf alone is lost on reload. Known * metadata children are chained onto the discarded entry's parent before the * entry is removed. If any child may carry content, the subtree is preserved * off-branch instead. Both paths append a metadata-only branch marker and * rewrite the journal, making the selected path durable. * * A removed assistant turn was still billed, so its usage is re-journaled as * an off-branch `model_usage` entry; session totals and `omp stats` keep * counting it. A subtree preserved off-branch keeps its own usage. */ discardEntryDurably(entryId: string): Promise; /** Like branch(), but also records a branch_summary of the abandoned path. */ branchWithSummary(branchFromId: string | null, summary: string, details?: unknown, fromExtension?: boolean): string; /** * Create a new session file containing only the path from root to `leafId`. * With `copyArtifacts`, the source artifact directory is copied in the background; * the new artifact manager waits for it before allocating ids or resolving * `artifact://`, so kept references stay valid and new ids cannot collide. * Returns the new file path, or undefined when not persisting. */ createBranchedSession(leafId: string, options?: { copyArtifacts?: boolean; }): string | undefined; /** Resolve the canonical default session directory for a cwd. */ static getDefaultSessionDir(cwd: string, agentDir?: string, storage?: SessionStorage): string; /** * Create a new session. * @param cwd Working directory (stored in the session header) * @param sessionDir Optional session directory; defaults to the cwd-derived dir. */ static create(cwd: string, sessionDir?: string, storage?: SessionStorage): SessionManager; /** * Create a fresh empty session file in the default session directory for * `cwd`, writing only the session header. The returned path can be passed to * `setSessionFile` / `AgentSession.switchSession` when a caller explicitly * needs a brand-new persisted session at a cwd-derived path. */ static createEmptySessionFile(cwd: string, storage?: SessionStorage): string; /** * Fork a session into the current project directory: copy history from another * session file while creating a fresh session file in this sessionDir. * * `options.sessionFile` pins the new session's file path (default: an * auto-named `_.jsonl` in `sessionDir`). Artifacts are copied * recursively by default; nested agents that deliberately share their parent's * artifact root may disable this with `copyArtifacts: false`. * * `seance` creates a read-only consult fork instead: artifacts stay in the * source (the header's `seanceSource` points lookups at them), inherited * workspace roots are dropped, and the source's `session_init` contracts * become inert `source_session_init` custom entries so revival never * mistakes them for the fork's own. */ static forkFrom(sourcePath: string, cwd: string, sessionDir?: string, storage?: SessionStorage, options?: { copyArtifacts?: boolean; suppressBreadcrumb?: boolean; sessionFile?: string; resetInheritedCost?: boolean; repairInterruptedTail?: boolean; seance?: boolean; }): Promise; /** * Open a specific session file. * @param sessionDir Optional dir for /new or /branch; defaults to the file's parent. * @param options.initialCwd Cwd to use when the file is empty or missing. * @param options.throwIfMissing Propagate ENOENT instead of creating a new session at a missing path. * @param options.parentSession Parent session file recorded when the file is empty or missing. */ static open(filePath: string, sessionDir?: string, storage?: SessionStorage, options?: { initialCwd?: string; parentSession?: string; suppressBreadcrumb?: boolean; throwIfMissing?: boolean; }): Promise; /** * Whether `session` was recorded in a worktree of `cwd`'s repository that no * longer exists, e.g. a `/wt` worktree removed since. Resume such a session * through {@link openRelocated} into `cwd`: its own directory cannot be entered. * @param sessionDir `cwd`'s session directory; defaults to the cwd-derived one. */ static isFromRemovedWorktree(session: Pick, cwd: string, sessionDir?: string): Promise; /** * Open a session whose recorded directory `recordedCwd` is gone and move it, * artifacts included, into `cwd` so it resumes from there. * @param sessionDir Target session directory; defaults to `cwd`'s. * @throws {SessionMoveRefusedError} when another live omp process writes the session. */ static openRelocated(sessionPath: string, recordedCwd: string, cwd: string, sessionDir?: string): Promise; /** * Lock-free peek for cold subagent revival: returns the recorded working * directory (session header) and the latest `session_init` contract (system * prompt / tools / output schema) WITHOUT taking the single-writer lock that * {@link open} acquires — the caller re-opens for the actual revive. Returns * null when the file can't be read; `init` is null for files written before * `session_init` was recorded (no faithful contract to rebuild from). */ static peekSessionInit(filePath: string, storage?: SessionStorage): Promise<{ cwd: string; init: PersistedSessionInit | null; seanceSource?: string; } | null>; /** * Continue the most recent session, or create a new one if none exists. * * When the chosen session's conversation went on in another file (it moved * off its file; see `findSessionContinuation`), this opens the copy to * resume instead and reports it to `onSessionContinuation`. The session the * terminal's record of its last session names was chosen, as a file named * to `--resume` is, so it is kept when it has turns its continuation lacks * ({@link findSessionContinuation}). Without that record the pick goes by * file time, and a process writes its exit record or an idle cache refresh * long after its last turn, so the pick can be any copy * ({@link findRecentSessionContinuation}). */ static continueRecent(cwd: string, sessionDir?: string, storage?: SessionStorage, options?: { onSessionContinuation?: (continuation: SessionContinuation) => void; }): Promise; /** Create an in-memory session (no file persistence). */ static inMemory(cwd?: string, storage?: SessionStorage): SessionManager; /** * List sessions for a project directory. * @param sessionDir Optional dir; defaults to the cwd-derived dir. */ static list(cwd: string, sessionDir?: string, storage?: SessionStorage): Promise; /** List all sessions across all project directories, pinned sessions first. */ static listAll(storage?: SessionStorage): Promise; /** * Picker-facing project listing in recency order (the picker floats pins), * untitled empties dropped. Titled empties stay — a title is user intent worth resuming. * Arrives in two passes: rows for history.db-indexed titles first, the full * scan in `complete` (see {@link listSessionsForPicker}). * By default includes the same folder in the repository's other git worktrees, * so a session `/wt` moved stays reachable from the checkout it left. * The directory scope limits the picker to its own session bucket. */ static listForPicker(cwd: string, sessionDir?: string, storage?: SessionStorage, worktreeScope?: "directory" | "repo"): Promise; /** Picker-facing cross-project listing, same rules and passes as {@link listForPicker}. */ static listAllForPicker(storage?: SessionStorage): Promise; } /** True when already-loaded entries carry at least one real user/assistant message. */ export declare function hasConversationalHistory(entries: readonly FileEntry[]): boolean; /** * The persisted `session_init` contract a cold revive rebuilds a subagent from: * the {@link SessionInitEntry} payload without its tree bookkeeping fields. */ export interface PersistedSessionInit { systemPrompt: string[]; task: string; tools: string[]; mountedTools?: string[]; agent?: string; modelRole?: string; resolvedModel?: string; perCallModel?: boolean; perCallModelIdentity?: string; perCallThinkingLevel?: ConfiguredThinkingLevel | null; retryFallback?: RetryFallbackRole; readOnly?: boolean; outputSchema?: unknown; outputSchemaMode?: StructuredSubagentSchemaMode; restrictToolNames?: boolean; spawns?: string; readSummarize?: boolean; advisor?: string; compactionThreshold?: { thresholdPercent: number; thresholdTokens: number; }; isolated?: boolean; workPoolYieldItems?: WorkPoolYieldItem[]; } /** * Latest persisted `session_init` contract among already-loaded entries, or * null when the transcript carries none. */ export declare function extractSessionInit(entries: readonly FileEntry[]): PersistedSessionInit | null; /** * If the current session was created by `/move` and contains no real * user/assistant messages, delete it so empty move sessions don't accumulate. */ export declare function cleanupEmptyMoveSession(sessionManager: SessionManager, movedFromEmptySessionFile: string | undefined): Promise; export {};