{"version":3,"file":"child-session-restore.d.ts","sourceRoot":"","sources":["../../../src/core/durable-child-session/child-session-restore.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Durable Child AgentSession Restore (2.6.0).\n *\n * Closes the boundary between a durable delegated child *mission* and a durable\n * child *AgentSession*. A delegated child no longer executes through the\n * ephemeral `--no-session` path: it acquires a stable `childSessionId` (stored\n * on the immutable MissionRequest) BEFORE external execution, persists its\n * conversation/todo/memory/evidence state through the standard SessionManager,\n * binds that session to exactly one mission, and can be explicitly resumed\n * after process interruption as the SAME mission + SAME session with a NEW\n * execution attempt.\n *\n * Invariants:\n *   - child process lifetime != child AgentSession lifetime != child mission lifetime.\n *   - resume != rerun from scratch.\n *   - The DurableMissionStore remains the authority for mission lifecycle and\n *     terminal result; the session can never fabricate SUCCEEDED.\n */\n\nimport { join } from \"node:path\";\nimport { getAgentDir } from \"../../config.js\";\nimport {\n\tcheckpointToRehydrationPreamble,\n\tcreateMissionContextCheckpoint,\n\ttype MissionContextCheckpoint,\n} from \"../context-runtime/index.js\";\nimport type { DurableMissionDelegator } from \"../durable-delegation/durable-delegation.js\";\nimport type { DurableMissionRecord, DurableMissionStore } from \"../mission-domain/durable-store.js\";\nimport type { MissionExecutor } from \"../mission-domain/mission-executor.js\";\nimport { isResumableMissionState, isTerminalMissionState } from \"../mission-domain/mission-state.js\";\nimport {\n\tProcessMissionExecutor,\n\ttype ProcessMissionLaunch,\n\ttype ProcessMissionVerifier,\n} from \"../mission-domain/process-mission-executor.js\";\nimport {\n\ttype ChildMissionBinding,\n\tgetLatestCompactionEntry,\n\tSessionManager,\n\tvalidateSessionFile,\n} from \"../session-manager.js\";\n\nexport type { ChildMissionBinding } from \"../session-manager.js\";\n\n// =============================================================================\n// Paths\n// =============================================================================\n\n/** Dedicated directory for durable child AgentSessions (discoverable by id). */\nexport function defaultChildSessionDir(agentDir: string = getAgentDir()): string {\n\treturn join(agentDir, \"child-sessions\");\n}\n\n// =============================================================================\n// Structured errors\n// =============================================================================\n\nexport type ChildSessionRestoreErrorCode =\n\t| \"MISSION_NOT_FOUND\"\n\t| \"MISSION_CORRUPT\"\n\t| \"MISSION_NOT_RESUMABLE\"\n\t| \"MISSION_TERMINAL\"\n\t| \"NOT_A_DURABLE_CHILD\"\n\t| \"CHILD_SESSION_NOT_FOUND\"\n\t| \"CHILD_SESSION_CORRUPT\"\n\t| \"CHILD_SESSION_BINDING_MISMATCH\";\n\nexport class ChildSessionRestoreError extends Error {\n\treadonly code: ChildSessionRestoreErrorCode;\n\treadonly details: Record<string, unknown>;\n\n\tconstructor(code: ChildSessionRestoreErrorCode, message: string, details: Record<string, unknown> = {}) {\n\t\tsuper(message);\n\t\tthis.name = \"ChildSessionRestoreError\";\n\t\tthis.code = code;\n\t\tthis.details = details;\n\t}\n}\n\n// =============================================================================\n// Binding\n// =============================================================================\n\n/**\n * Establish or validate the durable child session<->mission binding.\n *\n * On first execution no binding exists: it is persisted (identity correlation\n * only, never mission state). On restore the binding must already match;\n * otherwise the load fails closed and the session is never silently rebound.\n */\nexport function bindChildSession(\n\tsessionManager: SessionManager,\n\tmissionId: string,\n\tparentMissionId?: string,\n): ChildMissionBinding {\n\tconst sessionId = sessionManager.getSessionId();\n\tconst existing = sessionManager.getLatestChildBinding();\n\tif (existing) {\n\t\tif (existing.sessionId !== sessionId || existing.missionId !== missionId) {\n\t\t\tthrow new ChildSessionRestoreError(\n\t\t\t\t\"CHILD_SESSION_BINDING_MISMATCH\",\n\t\t\t\t`Session ${sessionId} is bound to mission ${existing.missionId}, not ${missionId}`,\n\t\t\t\t{ sessionId, expectedMissionId: missionId, actualMissionId: existing.missionId },\n\t\t\t);\n\t\t}\n\t\treturn existing;\n\t}\n\tconst binding: ChildMissionBinding = { sessionId, missionId, parentMissionId };\n\tsessionManager.appendChildBinding(binding);\n\treturn binding;\n}\n\n// =============================================================================\n// Resolve (locate + validate) for explicit resume\n// =============================================================================\n\nexport interface ResolvedChildSession {\n\trecord: DurableMissionRecord;\n\tsessionManager: SessionManager;\n\tchildSessionId: string;\n\tbinding: ChildMissionBinding;\n}\n\n/**\n * Load a durable child mission and resolve + validate its bound AgentSession.\n *\n * Fails conservatively (never fabricates or rebinds) for: missing/corrupt\n * mission, terminal mission, mission without a durable child session id, missing\n * session file, corrupt session file, or a binding mismatch.\n */\nexport async function resolveChildSessionForResume(options: {\n\tstore: DurableMissionStore;\n\tmissionId: string;\n\tsessionDir?: string;\n}): Promise<ResolvedChildSession> {\n\tconst { store, missionId, sessionDir } = options;\n\tconst loaded = await store.load(missionId);\n\tif (loaded.status === \"missing\") {\n\t\tthrow new ChildSessionRestoreError(\"MISSION_NOT_FOUND\", `Mission not found: ${missionId}`, { missionId });\n\t}\n\tif (loaded.status === \"corrupt\") {\n\t\tthrow new ChildSessionRestoreError(\"MISSION_CORRUPT\", `Mission ${missionId} is corrupt: ${loaded.diagnostic}`, {\n\t\t\tmissionId,\n\t\t\tdiagnostic: loaded.diagnostic,\n\t\t});\n\t}\n\tconst record = loaded.record;\n\n\tif (isTerminalMissionState(record.state)) {\n\t\tthrow new ChildSessionRestoreError(\n\t\t\t\"MISSION_TERMINAL\",\n\t\t\t`Cannot resume terminal mission ${missionId} (${record.state})`,\n\t\t\t{ missionId, state: record.state },\n\t\t);\n\t}\n\tif (!isResumableMissionState(record.state) || (record.state !== \"INTERRUPTED\" && record.state !== \"CREATED\")) {\n\t\tthrow new ChildSessionRestoreError(\n\t\t\t\"MISSION_NOT_RESUMABLE\",\n\t\t\t`Cannot resume mission ${missionId} from state ${record.state}; reconcile (recover) first`,\n\t\t\t{ missionId, state: record.state },\n\t\t);\n\t}\n\n\tconst childSessionId = record.request.childSessionId;\n\tif (!childSessionId) {\n\t\tthrow new ChildSessionRestoreError(\n\t\t\t\"NOT_A_DURABLE_CHILD\",\n\t\t\t`Mission ${missionId} has no durable child session identity`,\n\t\t\t{ missionId },\n\t\t);\n\t}\n\n\tconst dir = sessionDir ?? defaultChildSessionDir();\n\tconst info = await SessionManager.findByExactIdInDir(childSessionId, dir);\n\tif (!info) {\n\t\tthrow new ChildSessionRestoreError(\n\t\t\t\"CHILD_SESSION_NOT_FOUND\",\n\t\t\t`Child session not found for mission ${missionId}: ${childSessionId}`,\n\t\t\t{ missionId, childSessionId },\n\t\t);\n\t}\n\tconst validation = validateSessionFile(info.path);\n\tif (!validation.ok) {\n\t\tthrow new ChildSessionRestoreError(\n\t\t\t\"CHILD_SESSION_CORRUPT\",\n\t\t\t`Child session ${childSessionId} is corrupt: ${validation.reason}`,\n\t\t\t{ missionId, childSessionId, reason: validation.reason },\n\t\t);\n\t}\n\n\tconst sessionManager = SessionManager.open(info.path, dir);\n\tif (sessionManager.getSessionId() !== childSessionId) {\n\t\tthrow new ChildSessionRestoreError(\n\t\t\t\"CHILD_SESSION_BINDING_MISMATCH\",\n\t\t\t`Child session identity mismatch: expected ${childSessionId}, loaded ${sessionManager.getSessionId()}`,\n\t\t\t{ missionId, expectedSessionId: childSessionId, actualSessionId: sessionManager.getSessionId() },\n\t\t);\n\t}\n\n\tconst binding = sessionManager.getLatestChildBinding();\n\tif (!binding || binding.missionId !== missionId || binding.sessionId !== childSessionId) {\n\t\tthrow new ChildSessionRestoreError(\n\t\t\t\"CHILD_SESSION_BINDING_MISMATCH\",\n\t\t\t`Child session ${childSessionId} is not bound to mission ${missionId}`,\n\t\t\t{ missionId, childSessionId, binding },\n\t\t);\n\t}\n\n\treturn { record, sessionManager, childSessionId, binding };\n}\n\n// =============================================================================\n// Operational checkpoint reconstruction\n// =============================================================================\n\n/**\n * Rebuild a bounded operational checkpoint from the durable session state plus\n * the immutable mission request. This is the same projection the live\n * ContextGovernor uses; here it is rebuilt from a cold SessionManager so a\n * resumed child continues the same mission rather than replaying it.\n */\nexport function buildChildResumeCheckpoint(\n\trecord: DurableMissionRecord,\n\tsessionManager: SessionManager,\n): MissionContextCheckpoint {\n\tconst todos = sessionManager.getLatestSessionTodos();\n\tconst memory = sessionManager.getLatestSessionMemory();\n\tconst tasks = sessionManager.getLatestSessionTasks();\n\tconst evidenceRefs = sessionManager.getLatestSessionEvidenceRefs();\n\n\tconst completedSteps = todos.filter((t) => t.status === \"completed\").map((t) => t.content);\n\tconst pendingSteps = todos\n\t\t.filter((t) => t.status !== \"completed\")\n\t\t.map((t) => (t.status === \"in_progress\" ? t.activeForm : t.content));\n\tconst nextActions = tasks\n\t\t.filter((t) => t.status !== \"completed\")\n\t\t.map((t) => t.activeForm ?? t.subject)\n\t\t.slice(0, 12);\n\n\tconst findings = memory.slice(0, 24).map((m) => ({ subject: m.key, detail: m.value }));\n\tconst decisions = memory\n\t\t.slice(0, 24)\n\t\t.filter((m) => m.key.startsWith(\"decision\") || m.key.startsWith(\"decisions\"))\n\t\t.map((m) => ({ decision: m.key, rationale: m.value }));\n\n\tconst activeFiles: string[] = [];\n\tconst compaction = getLatestCompactionEntry(sessionManager.getBranch());\n\tconst details = compaction?.details as { readFiles?: string[]; modifiedFiles?: string[] } | undefined;\n\tconst fileSet = new Set<string>();\n\tfor (const f of details?.readFiles ?? []) fileSet.add(f);\n\tfor (const f of details?.modifiedFiles ?? []) fileSet.add(f);\n\tactiveFiles.push(...[...fileSet].slice(0, 64));\n\n\treturn createMissionContextCheckpoint(record.missionId, {\n\t\tobjective: record.request.objective,\n\t\tconstraints: [...(record.request.constraints ?? [])],\n\t\tdecisions,\n\t\tplan: \"\",\n\t\tcompletedSteps,\n\t\tpendingSteps,\n\t\tactiveFiles,\n\t\tfindings,\n\t\tevidenceRefs: evidenceRefs.slice(-24),\n\t\ttestState: {},\n\t\tblockers: [],\n\t\tnextActions,\n\t});\n}\n\n/** The explicit-resume prompt a child receives to CONTINUE rather than replay. */\nexport function buildChildResumePrompt(record: DurableMissionRecord, sessionManager: SessionManager): string {\n\tconst checkpoint = buildChildResumeCheckpoint(record, sessionManager);\n\tconst preamble = checkpointToRehydrationPreamble(checkpoint);\n\treturn [\n\t\t\"<resume-instruction>\",\n\t\t`You are resuming durable child mission ${record.missionId}.`,\n\t\t`Bound child AgentSession: ${record.request.childSessionId ?? \"(unknown)\"}.`,\n\t\t\"This is a NEW execution attempt; the previous attempt was interrupted.\",\n\t\t\"Continue the REMAINING work. Do not replay already-completed steps. First verify current filesystem and test state against the checkpoint below, then proceed conservatively and surface any ambiguity.\",\n\t\t\"</resume-instruction>\",\n\t\t\"\",\n\t\tpreamble,\n\t].join(\"\\n\");\n}\n\n// =============================================================================\n// Explicit resume orchestration\n// =============================================================================\n\nexport interface ResumeChildMissionOptions {\n\tstore: DurableMissionStore;\n\tmissionId: string;\n\tsessionDir?: string;\n\t/** Builds the concrete child CLI launch for the resume execution attempt. */\n\tbuildResumeLaunch: (input: {\n\t\trequest: DurableMissionRecord[\"request\"];\n\t\tresumePrompt: string;\n\t\tchildSessionId: string;\n\t}) => ProcessMissionLaunch;\n\t/** Executor identity (tests). */\n\texecutorId?: string;\n\t/** Optional verifier that can promote a clean exit-0 child to SUCCEEDED. */\n\tverifier?: ProcessMissionVerifier;\n\t/** Attempt identity factory (tests). */\n\tattemptIdFactory?: () => string;\n\tnow?: () => number;\n}\n\nexport interface BuiltChildResume {\n\tchildSessionId: string;\n\tresumePrompt: string;\n\texecutor: MissionExecutor;\n}\n\n/**\n * Build the continue-not-replay child executor from a resolved session. This is\n * the single executor-construction path shared by the direct resume helper and\n * the Mission Control plane, so the two can never drift in how a resume launch\n * is produced.\n */\nexport function buildChildResumeExecutor(options: {\n\trecord: DurableMissionRecord;\n\tsessionManager: SessionManager;\n\tchildSessionId: string;\n\tbuildResumeLaunch: ResumeChildMissionOptions[\"buildResumeLaunch\"];\n\texecutorId?: string;\n\tverifier?: ProcessMissionVerifier;\n}): BuiltChildResume {\n\tconst { record, sessionManager, childSessionId } = options;\n\tconst resumePrompt = buildChildResumePrompt(record, sessionManager);\n\tconst executor = new ProcessMissionExecutor({\n\t\texecutorId: options.executorId ?? \"subagent-process-resume\",\n\t\tverifier: options.verifier,\n\t\tbuildLaunch: (request) => options.buildResumeLaunch({ request, resumePrompt, childSessionId }),\n\t});\n\treturn { childSessionId, resumePrompt, executor };\n}\n\n/**\n * Explicitly resume an interrupted durable child to terminal state.\n *\n * Resolves the SAME mission + SAME session, validates the binding, builds a\n * continue-not-replay prompt from the durable checkpoint, and drives the\n * coordinator to allocate a NEW attempt/execution before launching the child.\n */\nexport async function resumeChildMission(\n\tdelegator: DurableMissionDelegator,\n\toptions: ResumeChildMissionOptions,\n): Promise<ReturnType<DurableMissionDelegator[\"resumeMission\"]>> {\n\tconst resolved = await resolveChildSessionForResume({\n\t\tstore: options.store,\n\t\tmissionId: options.missionId,\n\t\tsessionDir: options.sessionDir,\n\t});\n\tconst built = buildChildResumeExecutor({\n\t\trecord: resolved.record,\n\t\tsessionManager: resolved.sessionManager,\n\t\tchildSessionId: resolved.childSessionId,\n\t\tbuildResumeLaunch: options.buildResumeLaunch,\n\t\texecutorId: options.executorId,\n\t\tverifier: options.verifier,\n\t});\n\n\treturn delegator.resumeMission(options.missionId, built.executor);\n}\n"]}