/** * Runtime session recovery — make "resume the agent's conversation after a * backend restart" deterministic instead of best-effort. * * Background (2026-09-18): PTY sessions die with the backend. Claude Code * and Codex can both resume a conversation by id, but Crewly only knew the * id when the agent volunteered it at registration (it rarely did), and * agents spawned from inside another Claude Code session inherited that * session's markers and stopped saving transcripts altogether — so there * was nothing to resume even when the id was known. * * Three pieces: * - {@link planRuntimeSessionFlags}: Claude Code is launched with a * Crewly-generated `--session-id`, persisted before launch; on restore * the same id goes to `--resume`. Codex has no preset flag, so on * restore its command becomes `codex resume … `. * - {@link discoverCodexSessionId}: Codex writes its rollout file within * seconds of launch; the file name and first line carry the session id * and cwd, which is how Crewly learns the id for a fresh Codex agent. * - {@link stripNestedClaudeSessionEnv}: the env markers a parent Claude * Code session leaves behind, which must never reach an agent. * - Antigravity CLI resumes with `agy --conversation=` (the command agy * itself prints on exit). It creates a conversation only when the first * prompt arrives, so {@link discoverAntigravityConversationId} learns the * id after the registration kickoff from `conversations/.db` and * `cache/last_conversations.json` (workspace → latest id). * * @module services/agent/runtime-session-recovery */ /** * Env vars set by a running Claude Code session for its children. An agent * that inherits them believes it is a nested/child session and disables * transcript saving ("Transcript saving is off — inherited * CLAUDE_CODE_CHILD_SESSION"), which makes `--resume` impossible. Only * session markers are listed; user configuration such as * CLAUDE_CODE_ENABLE_TELEMETRY or CLAUDE_CODE_USE_BEDROCK is left alone. */ export declare const NESTED_CLAUDE_SESSION_ENV_KEYS: readonly string[]; /** * Remove the nested-session markers from an environment. * * @param env - Environment to copy from * @returns A copy without the markers */ export declare function stripNestedClaudeSessionEnv>(env: T): T; /** What the launcher must do about the runtime's conversation id. */ export interface RuntimeSessionPlan { /** CLI flags to inject (Claude Code: `--session-id` or `--resume`). */ flags: string[]; /** Id to resume (Codex: rewrite the command to `codex resume … `). */ resumeSessionId: string | null; /** Freshly generated id that must be persisted before launch (Claude Code). */ presetSessionId: string | null; /** One-line reason for the log. */ note: string; } /** * Decide how to launch a runtime so that its conversation can be found again. * * @param args - Runtime type, whether this is a restored session, the stored * id (if any), the auto-resume setting, and an id generator (tests) * @returns The plan */ export declare function planRuntimeSessionFlags(args: { runtimeType: string; /** Informational: whether the persistence layer flagged this as a restored session. */ isRestored: boolean; storedSessionId: string | null | undefined; autoResume: boolean; /** * Whether the stored conversation still exists on disk. `false` forces a * fresh start (resuming a deleted conversation would fail to boot); * omitted = trust the stored id. */ conversationExists?: boolean; newId?: () => string; }): RuntimeSessionPlan; /** * The agy flag that resumes a conversation: `--conversation=`, exactly * as agy prints it on exit ("Resume with -c (or command below):"). * * @param conversationId - Antigravity conversation id (a UUID) * @returns The flag, with anything but `[A-Za-z0-9-]` removed from the id */ export declare function toAntigravityResumeFlag(conversationId: string): string; /** Options for {@link discoverAntigravityConversationId}. */ export interface AntigravityDiscoveryOptions { /** agy config dir (defaults to ~/.gemini/antigravity-cli) */ configDir?: string; /** The agent's working directory (agy's workspace) */ cwd: string; /** When the launch command was sent */ notBeforeMs: number; /** Ids other sessions already own */ claimed?: ReadonlySet; /** * Text only this agent's first prompt contains (its init prompt file * name). When a conversation's logs mention it, that conversation wins * even if another agent in the same folder started one later. */ marker?: string; } /** * Find the Antigravity conversation a just-launched agent started. * * Candidates are `conversations/.db` files created after the launch that * no other session has claimed. The one whose logs mention the agent's * marker wins; otherwise the id agy recorded as the latest for this * workspace in `cache/last_conversations.json` (what `agy -c` resumes) is * taken when it is a candidate. * * @param opts - Config dir, cwd, launch time, claimed ids, marker * @returns The conversation id, or null when nothing matches yet */ export declare function discoverAntigravityConversationId(opts: AntigravityDiscoveryOptions): string | null; /** * Poll {@link discoverAntigravityConversationId} until a match appears or time runs out. * * @param opts - Discovery options plus `timeoutMs` / `intervalMs` (defaults from ANTIGRAVITY_CONSTANTS) * @returns The conversation id, or null on timeout */ export declare function waitForAntigravityConversationId(opts: AntigravityDiscoveryOptions & { timeoutMs?: number; intervalMs?: number; sleep?: (ms: number) => Promise; }): Promise; /** * Rewrite a Codex launch command so it resumes a conversation. `codex resume` * accepts the same approval/sandbox flags as `codex`, and the id goes last. * * @param command - The configured command, e.g. `codex -a never -s danger-full-access` * @param sessionId - Codex session id * @returns The resume command, or the input unchanged when it does not invoke codex */ export declare function toCodexResumeCommand(command: string, sessionId: string): string; /** Default Codex home (`CODEX_HOME` or `~/.codex`). */ export declare function defaultCodexHome(): string; /** A rollout file's identity, as Crewly needs it. */ export interface CodexRolloutInfo { sessionId: string; cwd: string; filePath: string; mtimeMs: number; /** When the file was created (birth time; last write where the filesystem records no birth time) */ bornAtMs: number; } /** * Find the Codex conversation that a just-launched agent created: the * earliest rollout file under `/sessions/YYYY/MM/DD/` created after * `notBeforeMs`, whose `session_meta.cwd` is the agent's cwd, and whose id * nobody else has claimed. Only today's and yesterday's directories are read * (UTC and local dates), so the scan stays cheap. * * @param opts - Codex home, the agent's cwd, launch time, ids already claimed * @returns The rollout info, or null when nothing matches yet */ export declare function discoverCodexSessionId(opts: { codexHome?: string; cwd: string; notBeforeMs: number; claimed?: ReadonlySet; }): CodexRolloutInfo | null; /** * Poll {@link discoverCodexSessionId} until a match appears or time runs out. * * @param opts - Discovery options plus `timeoutMs` (default 30 s) and `intervalMs` (default 1 s) * @returns The rollout info, or null on timeout */ export declare function waitForCodexSessionId(opts: Parameters[0] & { timeoutMs?: number; intervalMs?: number; sleep?: (ms: number) => Promise; }): Promise; /** * Whether a stored conversation still exists on disk, so a resume will not * fail to boot. Claude Code keeps `~/.claude/projects//.jsonl`; * Codex keeps `/sessions/YYYY/MM/DD/rollout-…-.jsonl`; * Antigravity keeps `~/.gemini/antigravity-cli/conversations/.db`. * * @param args - Runtime, id, the agent's cwd, optional home overrides * @returns True when found; true (benefit of the doubt) for unknown runtimes */ export declare function conversationExists(args: { runtimeType: string; sessionId: string; cwd: string; claudeHome?: string; codexHome?: string; antigravityConfigDir?: string; }): boolean; /** * The default login's Claude home (`~/.claude`). * * @returns Absolute path */ export declare function defaultClaudeHome(): string; /** * Where Claude Code keeps a conversation's transcript. * * Claude Code names the project directory after the *resolved* cwd, so an * agent whose cwd goes through a symlink (`/tmp` → `/private/tmp` on macOS) * has its transcript under the realpath slug. The existing file is returned * when there is one (realpath slug first, then the raw slug); otherwise the * path Claude Code would write to (the realpath slug). * * @param args - Conversation id, the agent's cwd, optional Claude home * (an account's config dir for a session on another Claude Code account) * @returns Absolute path of `/projects//.jsonl` */ export declare function claudeTranscriptPath(args: { sessionId: string; cwd: string; claudeHome?: string; }): string; /** * Find an existing conversation transcript across several Claude homes and * both cwd slugs (realpath first, then raw). * * A session that switched between the owner's Claude Code accounts can have * its stored conversation under either account's config dir, so callers pass * the current account's home first and the default `~/.claude` after it. * * @param args - Conversation id, the agent's cwd, Claude homes in search order * @returns Absolute path of the first existing transcript, or null * * @example * ```typescript * findClaudeTranscript({ sessionId, cwd: '/tmp/proj', claudeHomes: [accountDir, defaultClaudeHome()] }); * ``` */ export declare function findClaudeTranscript(args: { sessionId: string; cwd: string; claudeHomes: readonly string[]; }): string | null; /** * The context the conversation's last real turn carried: fresh input plus * cache reads plus cache writes. Only the end of the file is read. * * @param filePath - Transcript path * @returns Tokens, or null when unreadable or no turn is found */ export declare function lastTurnContextTokens(filePath: string): number | null; /** * What was said and done at the end of a conversation, for the file a fresh * conversation starts from: the plain words delivered to the agent and its * answers, plus one line per tool call and tool result (tool name and the * first {@link ORC_CONVERSATION_CONSTANTS.HANDOVER_TOOL_RESULT_CHARS} * characters of its input / output) so the new conversation knows which files * were read and which commands ran, not only what was said about them * (crewly#1344). Images are a described placeholder, never copied. System * reminders and command wrappers are left out. * * A sub-agent or background command whose tool call never got its result * before the end of the transcript died with the restart: it is listed first, * under {@link HANDOVER_CONSTANTS.INTERRUPTED_HEADING}, so the new * conversation does not take the agent's last "still in progress" words for * work that is running (crewly#1361). That section is capped on its own * (items × characters) and does not count against the body's limit. * * @param filePath - Transcript path * @returns Markdown, newest last; empty when nothing readable was found */ export declare function buildHandoverSummary(filePath: string): string; /** Outcome of {@link copyClaudeConversation}. */ export interface CopiedClaudeConversation { /** The transcript the copy was made from */ source: string; /** The transcript written under the target home */ target: string; /** Files and folders written (the transcript, and its sidecar folder when it has one) */ copied: string[]; } /** * Copy a Claude Code conversation from one Claude home to another so that * `claude --resume ` finds it there (crewly#1344). * * Claude Code keeps a conversation as `/projects//.jsonl` * and, for conversations that ran subagents, a sidecar folder * `/projects///`. Both are copied, under the same slug, * so a session the runtime fallback moves to another of the owner's Claude * Code accounts (a different `CLAUDE_CONFIG_DIR`) keeps its conversation * instead of starting fresh with a handover tail. The copy overwrites an * older copy on the target (the source is the live one). Nothing else of the * account — credentials, settings, history — is touched. * * @param args - Conversation id, the agent's cwd, the homes it may be under (current first) and the home to copy into * @returns What was copied, or null when the transcript was not found or the copy failed * * @example * ```typescript * copyClaudeConversation({ sessionId, cwd, fromHomes: [defaultClaudeHome()], toHome: claudeAccountConfigDir('work') }); * ``` */ export declare function copyClaudeConversation(args: { sessionId: string; cwd: string; fromHomes: readonly string[]; toHome: string; }): CopiedClaudeConversation | null; /** * The threshold at which the orchestrator starts a fresh conversation. * * @param env - Environment (tests) * @returns Tokens */ export declare function orcFreshContextTokens(env?: NodeJS.ProcessEnv): number; /** * The threshold at which a team member (not the orchestrator) starts a fresh * conversation at launch instead of resuming its old one. * * @param env - Environment (tests) * @returns Tokens */ export declare function memberFreshContextTokens(env?: NodeJS.ProcessEnv): number; /** A handover file already written for a session. */ export interface HandoverFileInfo { path: string; mtimeMs: number; } /** * The newest handover file written for a session (oldest-conversation * handovers, runtime switches and restart handovers all live in the same * directory under `-…`). * * @param dir - Handover directory * @param sessionName - Session * @param exceptFile - A file to ignore (the one being written now) * @returns The newest one, or null */ export declare function latestHandoverFile(dir: string, sessionName: string, exceptFile?: string): HandoverFileInfo | null; /** * Text of an earlier handover to carry into a new one, so a second hop * through a runtime whose conversation cannot be read does not lose what the * first handover held. * * @param prev - The earlier handover * @returns Block to embed (its path, and its body without the title line), or '' when unreadable */ export declare function chainedHandoverBlock(prev: HandoverFileInfo | null): string; /** A previous Claude Code conversation found for a session. */ export interface PreviousClaudeConversation { sessionId: string; transcript: string; mtimeMs: number; } /** * The most recent previous Claude Code transcript for a cwd across several * Claude homes (default and account), newer than `maxAgeMs`. * * With `rememberedSessionId` that conversation is looked up directly. Without * it every transcript in the cwd's project directory is a candidate (agents of * one project share a cwd), so a candidate must be unclaimed by another live * session and belong to `sessionName` ({@link transcriptBelongsTo}: its `agentSetting` entry or its first user message names it). The newest such transcript by mtime wins; size never matters. * * @param args - cwd, homes, session name, window, optional remembered id, ids owned by others * @returns The newest match, or null */ export declare function findPreviousClaudeConversation(args: { cwd: string; claudeHomes: readonly string[]; sessionName: string; maxAgeMs?: number; now?: number; rememberedSessionId?: string | null; /** cwd the remembered conversation was last run in (it can differ from `cwd`) */ rememberedCwd?: string | null; claimedByOthers?: ReadonlySet; }): PreviousClaudeConversation | null; /** Remembered last conversation of a stopped session. */ export interface LastConversationEntry { sessionId: string; cwd: string; at: number; } /** * Remember a stopped session's conversation id (a stop drops the stored id * from session persistence; this is what lets the next start find it). * * @param file - JSON file * @param sessionName - Session * @param entry - Conversation id and cwd */ export declare function rememberLastConversation(file: string, sessionName: string, entry: LastConversationEntry): void; /** * Read a stopped session's remembered conversation. * * @param file - JSON file * @param sessionName - Session * @returns The entry, or null */ export declare function readLastConversation(file: string, sessionName: string): LastConversationEntry | null; /** * The conversation a stopped session should resume on its next start. * * A stop (idle eviction by ResourceMode, a manual stop) unregisters the * session and so drops the stored conversation id; the id is kept in the * last-conversations file. A start with no stored id resumes that one, unless * it is older than the handover window or another live session owns it. * * @param args - last-conversations file, session, ids owned by other sessions, clock * @returns The remembered entry, or null */ export declare function rememberedConversationToResume(args: { file: string; sessionName: string; claimedByOthers?: ReadonlySet; now?: number; maxAgeMs?: number; }): LastConversationEntry | null; /** * For a fresh Claude Code conversation that nobody chose (a stop and start, * or a stored conversation that is gone), write a handover from the most * recent previous conversation of the session and return the kickoff note * that points at it. Nothing is written when there is no conversation newer * than a week, when it has nothing readable, or when a handover for the * session is already newer than it (the oversized-conversation, per-task and * runtime-switch paths write their own). * * @param args - Session, cwd, Claude homes, handover dir, remembered-conversation file, ids owned by other sessions * @returns The handover file, the kickoff note and the previous conversation id, or null */ export declare function writeRestartHandover(args: { sessionName: string; cwd: string; claudeHomes: readonly string[]; handoverDir: string; lastConversationsFile?: string; claimedByOthers?: ReadonlySet; now?: Date; }): { file: string; note: string; previousSessionId: string; } | null; //# sourceMappingURL=runtime-session-recovery.d.ts.map