{"version":3,"file":"mission-control-service.d.ts","sourceRoot":"","sources":["../../../src/core/mission-control/mission-control-service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Mission Control Plane — service (2.9.0).\n *\n * The authoritative application/operator boundary over durable missions.\n *\n * Read models aggregate durable record fields only. Mutations compose the\n * existing DurableMissionCoordinator / DurableMissionDelegator / child-session\n * restore semantics; the control plane never re-implements fencing, heartbeat,\n * recovery, or session binding. Consumers use this service instead of reaching\n * into FileDurableMissionStore, session directories, or lock files.\n */\n\nimport type { AssignmentStore } from \"../assignment/assignment-store.js\";\nimport { type AssignmentRecord, toAssignmentSummary } from \"../assignment/assignment-types.js\";\nimport type { EvidenceArchive } from \"../context-runtime/evidence-archive.js\";\nimport {\n\ttype EvidenceRetrievalOptions,\n\ttype EvidenceRetrievalResult,\n\tretrieveEvidencePage,\n} from \"../context-runtime/evidence-retrieval.js\";\nimport {\n\tbuildChildResumeExecutor,\n\tresolveChildSessionForResume,\n} from \"../durable-child-session/child-session-restore.js\";\nimport { DurableMissionDelegator } from \"../durable-delegation/durable-delegation.js\";\nimport {\n\tDurableMissionCoordinator,\n\ttype DurableMissionCoordinatorOptions,\n} from \"../mission-domain/durable-coordinator.js\";\nimport type { DurableMissionRecord, DurableMissionStore } from \"../mission-domain/durable-store.js\";\nimport type { HeartbeatTelemetry } from \"../mission-domain/execution-heartbeat.js\";\nimport { isExecutionLeaseActive } from \"../mission-domain/execution-lease.js\";\nimport { isTerminalMissionState } from \"../mission-domain/mission-state.js\";\nimport type { ProcessMissionVerifier } from \"../mission-domain/process-mission-executor.js\";\nimport {\n\ttype AttemptHistoryView,\n\ttype AttemptView,\n\ttype BuildResumeLaunch,\n\ttype LeaseStatus,\n\ttype MissionCancellationView,\n\ttype MissionControlActiveExecution,\n\tMissionControlError,\n\ttype MissionControlResumeOutcome,\n\ttype MissionDetail,\n\ttype MissionEvidenceRef,\n\ttype MissionListOptions,\n\ttype MissionListResult,\n\ttype MissionOwnershipView,\n\ttype MissionRequestView,\n\ttype MissionResultView,\n\ttype MissionSummary,\n\ttype MissionTreeNode,\n\ttype Resumability,\n} from \"./mission-control-types.js\";\n\nexport interface MissionControlServiceOptions {\n\tstore: DurableMissionStore;\n\t/** Optional injected delegator (defaults to one built from `store`). */\n\tdelegator?: DurableMissionDelegator;\n\t/** Optional evidence archive for availability checks + bounded retrieval. */\n\tevidenceArchive?: EvidenceArchive;\n\t/** Child AgentSession directory for explicit resume. */\n\tsessionDir?: string;\n\t/** Optional assignment store for surfacing current designation views. */\n\tassignmentStore?: AssignmentStore;\n\t/** Stable executor owner identity (defaults to a fresh host+UUID identity). */\n\townerId?: string;\n\tnow?: () => number;\n\tleaseDurationMs?: number;\n\theartbeatIntervalMs?: number;\n\trenewalSafetyMarginMs?: number;\n\tattemptIdFactory?: () => string;\n\tleaseIdFactory?: () => string;\n}\n\n/** Fraction of the lease window remaining that is reported as EXPIRING. */\nconst EXPIRING_FRACTION = 0.25;\n\n/** Split a deterministic evidence id (`kind:source:hash`) without I/O. */\nfunction parseEvidenceId(evidenceId: string): { kind?: string; source?: string } {\n\tconst first = evidenceId.indexOf(\":\");\n\tif (first <= 0) return {};\n\tconst second = evidenceId.indexOf(\":\", first + 1);\n\tif (second <= 0) return { kind: evidenceId.slice(0, first) };\n\treturn { kind: evidenceId.slice(0, first), source: evidenceId.slice(first + 1, second) };\n}\n\nexport class MissionControlService {\n\tprivate readonly _store: DurableMissionStore;\n\tprivate readonly _delegator: DurableMissionDelegator;\n\tprivate readonly _evidenceArchive?: EvidenceArchive;\n\tprivate readonly _sessionDir?: string;\n\tprivate readonly _assignmentStore?: AssignmentStore;\n\tprivate readonly _now: () => number;\n\tprivate readonly _coordinatorOptions: DurableMissionCoordinatorOptions;\n\tprivate readonly _active = new Map<string, MissionControlActiveExecution>();\n\n\tconstructor(options: MissionControlServiceOptions) {\n\t\tthis._store = options.store;\n\t\tthis._delegator =\n\t\t\toptions.delegator ??\n\t\t\tnew DurableMissionDelegator({\n\t\t\t\tstore: options.store,\n\t\t\t\tnow: options.now,\n\t\t\t\townerId: options.ownerId,\n\t\t\t\tleaseDurationMs: options.leaseDurationMs,\n\t\t\t\theartbeatIntervalMs: options.heartbeatIntervalMs,\n\t\t\t\trenewalSafetyMarginMs: options.renewalSafetyMarginMs,\n\t\t\t\tattemptIdFactory: options.attemptIdFactory,\n\t\t\t\tleaseIdFactory: options.leaseIdFactory,\n\t\t\t});\n\t\tthis._evidenceArchive = options.evidenceArchive;\n\t\tthis._sessionDir = options.sessionDir;\n\t\tthis._assignmentStore = options.assignmentStore;\n\t\tthis._now = options.now ?? (() => Date.now());\n\t\tthis._coordinatorOptions = {\n\t\t\tnow: options.now,\n\t\t\townerId: this._delegator.ownerId,\n\t\t\tleaseDurationMs: options.leaseDurationMs,\n\t\t\theartbeatIntervalMs: options.heartbeatIntervalMs,\n\t\t\trenewalSafetyMarginMs: options.renewalSafetyMarginMs,\n\t\t\tattemptIdFactory: options.attemptIdFactory,\n\t\t\tleaseIdFactory: options.leaseIdFactory,\n\t\t};\n\t}\n\n\t/** The underlying persistence port (read access for loaders/tests). */\n\tget store(): DurableMissionStore {\n\t\treturn this._store;\n\t}\n\n\t/** Stable executor owner identity used for local ownership comparison. */\n\tget ownerId(): string {\n\t\treturn this._delegator.ownerId;\n\t}\n\n\t// =========================================================================\n\t// Read model\n\t// =========================================================================\n\n\tasync listMissions(options: MissionListOptions = {}): Promise<MissionListResult> {\n\t\tconst ids = await this._store.listMissions();\n\t\tconst records = new Map<string, DurableMissionRecord>();\n\t\tconst corrupt: MissionListResult[\"corrupt\"] = [];\n\n\t\tfor (const id of ids) {\n\t\t\tconst loaded = await this._store.load(id);\n\t\t\tif (loaded.status === \"ok\") records.set(id, loaded.record);\n\t\t\telse if (loaded.status === \"corrupt\") corrupt.push({ missionId: id, diagnostic: loaded.diagnostic });\n\t\t\t// missing (deleted between list and load) is skipped.\n\t\t}\n\n\t\t// One pass child counts so listing never becomes O(all children × all missions).\n\t\tconst childCount = new Map<string, number>();\n\t\tfor (const record of records.values()) {\n\t\t\tif (record.parentMissionId) {\n\t\t\t\tchildCount.set(record.parentMissionId, (childCount.get(record.parentMissionId) ?? 0) + 1);\n\t\t\t}\n\t\t}\n\n\t\t// One bulk assignment read so listing never becomes O(assignments × missions).\n\t\tlet currentAssignments: Map<string, AssignmentRecord> | undefined;\n\t\tif (this._assignmentStore) {\n\t\t\tcurrentAssignments = new Map<string, AssignmentRecord>();\n\t\t\tconst { records: assignmentRecords } = await this._assignmentStore.listRecords();\n\t\t\tfor (const assignment of assignmentRecords) {\n\t\t\t\tif (assignment.current) currentAssignments.set(assignment.missionId, assignment);\n\t\t\t}\n\t\t}\n\n\t\tconst now = this._now();\n\t\tlet entries: MissionSummary[] = [];\n\t\tfor (const record of records.values()) {\n\t\t\tentries.push(\n\t\t\t\tthis._toSummary(\n\t\t\t\t\trecord,\n\t\t\t\t\tchildCount.get(record.missionId) ?? 0,\n\t\t\t\t\tnow,\n\t\t\t\t\tcurrentAssignments?.get(record.missionId),\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\tconst filter = options.filter;\n\t\tif (filter) {\n\t\t\tentries = entries.filter((entry) => {\n\t\t\t\tif (filter.state !== undefined && entry.state !== filter.state) return false;\n\t\t\t\tif (filter.parentMissionId !== undefined && entry.parentMissionId !== filter.parentMissionId) return false;\n\t\t\t\tif (filter.terminal !== undefined && entry.terminal !== filter.terminal) return false;\n\t\t\t\tif (filter.interrupted !== undefined && entry.interrupted !== filter.interrupted) return false;\n\t\t\t\tif (filter.owned !== undefined && entry.owned !== filter.owned) return false;\n\t\t\t\tif (filter.resumable !== undefined && entry.resumable !== filter.resumable) return false;\n\t\t\t\tif (filter.depth !== undefined && entry.depth !== filter.depth) return false;\n\t\t\t\treturn true;\n\t\t\t});\n\t\t}\n\n\t\tconst sort = options.sort ?? \"missionId\";\n\t\tconst direction = options.direction ?? \"asc\";\n\t\tentries.sort((a, b) => {\n\t\t\tlet cmp: number;\n\t\t\tif (sort === \"missionId\") cmp = a.missionId < b.missionId ? -1 : a.missionId > b.missionId ? 1 : 0;\n\t\t\telse cmp = a[sort] - b[sort];\n\t\t\treturn direction === \"desc\" ? -cmp : cmp;\n\t\t});\n\n\t\tconst offset = options.offset ?? 0;\n\t\tif (offset > 0) entries = entries.slice(offset);\n\t\tif (options.limit !== undefined) entries = entries.slice(0, options.limit);\n\n\t\treturn { entries, corrupt };\n\t}\n\n\tasync getMission(missionId: string): Promise<MissionDetail> {\n\t\tconst record = await this._requireRecord(missionId);\n\t\tconst now = this._now();\n\t\tconst children = await this._store.listChildren(missionId);\n\t\tconst ownership = this._toOwnership(record, now);\n\t\tconst currentAssignment = await this._currentAssignmentFor(missionId);\n\t\treturn {\n\t\t\tsummary: this._toSummary(record, children.length, now, currentAssignment),\n\t\t\trequest: this._toRequestView(record),\n\t\t\tcurrentAttempt: this._currentAttemptView(record),\n\t\t\townership,\n\t\t\tresult: this._toResultView(record),\n\t\t\tcheckpointSummary: {\n\t\t\t\tobjective: record.request.objective,\n\t\t\t\tconstraints: [...(record.request.constraints ?? [])],\n\t\t\t\tlastTransition: record.transitions[record.transitions.length - 1],\n\t\t\t\tstartedAtMs: record.startedAtMs,\n\t\t\t\tfinishedAtMs: record.finishedAtMs,\n\t\t\t},\n\t\t\tevidenceRefs: await this._evidenceRefs(record),\n\t\t\tchildren,\n\t\t\tresumability: this._toResumability(record, now),\n\t\t\tassignment: currentAssignment ? toAssignmentSummary(currentAssignment) : undefined,\n\t\t};\n\t}\n\n\tasync getAttempts(missionId: string): Promise<AttemptHistoryView> {\n\t\tconst record = await this._requireRecord(missionId);\n\t\treturn {\n\t\t\tmissionId,\n\t\t\tattempts: record.attempts.map((attempt) => ({\n\t\t\t\tattemptId: attempt.attemptId,\n\t\t\t\texecutionId: attempt.executionId,\n\t\t\t\tstartedAtMs: attempt.startedAtMs,\n\t\t\t\tfinishedAtMs: attempt.finishedAtMs,\n\t\t\t\tendReason: attempt.endReason,\n\t\t\t\trecovery: attempt.recovery,\n\t\t\t})),\n\t\t\tcurrentAttemptId: record.currentAttemptId,\n\t\t\tresultExecutionId: record.resultExecutionId,\n\t\t};\n\t}\n\n\tasync getOwnership(missionId: string): Promise<MissionOwnershipView> {\n\t\tconst record = await this._requireRecord(missionId);\n\t\treturn this._toOwnership(record, this._now());\n\t}\n\n\tasync getResult(missionId: string): Promise<MissionResultView> {\n\t\tconst record = await this._requireRecord(missionId);\n\t\treturn this._toResultView(record);\n\t}\n\n\tasync getEvidenceRefs(missionId: string): Promise<MissionEvidenceRef[]> {\n\t\tconst record = await this._requireRecord(missionId);\n\t\treturn this._evidenceRefs(record);\n\t}\n\n\tasync getResumability(missionId: string): Promise<Resumability> {\n\t\tconst record = await this._requireRecord(missionId);\n\t\treturn this._toResumability(record, this._now());\n\t}\n\n\tasync getMissionTree(rootMissionId: string): Promise<MissionTreeNode> {\n\t\treturn this._buildTree(rootMissionId, new Set());\n\t}\n\n\t/** Live local heartbeat telemetry, only when this process owns the execution. */\n\tlocalHeartbeatTelemetry(missionId: string): HeartbeatTelemetry | undefined {\n\t\treturn this._active.get(missionId)?.coordinator.heartbeatTelemetry(missionId);\n\t}\n\n\t/**\n\t * Bounded, integrity-verified evidence content retrieval. Delegates to the\n\t * existing retrieval architecture; returns `not-found` when no archive is\n\t * configured for this control plane.\n\t */\n\tasync resolveEvidence(evidenceId: string, options?: EvidenceRetrievalOptions): Promise<EvidenceRetrievalResult> {\n\t\tif (!this._evidenceArchive) {\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\tstatus: \"not-found\",\n\t\t\t\tevidenceId,\n\t\t\t\treason: \"No evidence archive is configured for Mission Control.\",\n\t\t\t};\n\t\t}\n\t\treturn retrieveEvidencePage(this._evidenceArchive, evidenceId, options);\n\t}\n\n\t// =========================================================================\n\t// Mutations (compose authoritative semantics only)\n\t// =========================================================================\n\n\t/**\n\t * Explicitly resume a durable child mission through the existing fenced\n\t * ownership architecture. The mission must be structurally resumable and\n\t * must have a durable child session identity; other mission kinds expose\n\t * their limitation structurally rather than faking generic execution.\n\t */\n\tasync resumeMission(\n\t\tmissionId: string,\n\t\toptions: {\n\t\t\tbuildResumeLaunch: BuildResumeLaunch;\n\t\t\tsignal?: AbortSignal;\n\t\t\texecutorId?: string;\n\t\t\tverifier?: ProcessMissionVerifier;\n\t\t},\n\t): Promise<MissionControlResumeOutcome> {\n\t\tconst resumability = await this.getResumability(missionId);\n\t\tif (!resumability.resumable) {\n\t\t\tthrow new MissionControlError(\n\t\t\t\tresumability.reasonCode === \"MISSION_TERMINAL\"\n\t\t\t\t\t? \"MISSION_TERMINAL\"\n\t\t\t\t\t: resumability.reasonCode === \"MISSION_ACTIVE\"\n\t\t\t\t\t\t? \"MISSION_ACTIVE\"\n\t\t\t\t\t\t: resumability.reasonCode === \"MISSING_CHILD_SESSION\"\n\t\t\t\t\t\t\t? \"NOT_A_DURABLE_CHILD\"\n\t\t\t\t\t\t\t: \"MISSION_NOT_RESUMABLE\",\n\t\t\t\tresumability.reason ?? `Mission ${missionId} is not resumable`,\n\t\t\t\t{ missionId, reasonCode: resumability.reasonCode },\n\t\t\t);\n\t\t}\n\n\t\tconst resolved = await resolveChildSessionForResume({\n\t\t\tstore: this._store,\n\t\t\tmissionId,\n\t\t\tsessionDir: this._sessionDir,\n\t\t});\n\t\tconst built = buildChildResumeExecutor({\n\t\t\trecord: resolved.record,\n\t\t\tsessionManager: resolved.sessionManager,\n\t\t\tchildSessionId: resolved.childSessionId,\n\t\t\tbuildResumeLaunch: options.buildResumeLaunch,\n\t\t\texecutorId: options.executorId ?? \"mission-control-resume\",\n\t\t\tverifier: options.verifier,\n\t\t});\n\n\t\tconst coordinator = new DurableMissionCoordinator(this._store, built.executor, this._coordinatorOptions);\n\t\tconst resume = coordinator.resume(missionId, { signal: options.signal });\n\t\tthis._active.set(missionId, { coordinator, resume });\n\t\ttry {\n\t\t\tconst terminal = await resume;\n\n\t\t\tconst lastAttempt = terminal.attempts[terminal.attempts.length - 1];\n\t\t\tif (!terminal.result) {\n\t\t\t\tthrow new MissionControlError(\n\t\t\t\t\t\"MISSION_NOT_RESUMABLE\",\n\t\t\t\t\t`Mission ${missionId} did not reach a terminal result`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tmissionId: terminal.missionId,\n\t\t\t\tparentMissionId: terminal.parentMissionId,\n\t\t\t\tchildSessionId: terminal.request.childSessionId,\n\t\t\t\tattemptId: lastAttempt?.attemptId ?? \"\",\n\t\t\t\texecutionId: terminal.resultExecutionId ?? lastAttempt?.executionId,\n\t\t\t\tmissionState: terminal.state,\n\t\t\t\tfencingToken: terminal.fencingToken,\n\t\t\t\tsuccess: terminal.result.success,\n\t\t\t\tresult: terminal.result,\n\t\t\t\trecord: terminal,\n\t\t\t\theartbeatTelemetry: coordinator.heartbeatTelemetry(missionId),\n\t\t\t};\n\t\t} finally {\n\t\t\tthis._active.delete(missionId);\n\t\t}\n\t}\n\n\t/**\n\t * Request cancellation for a mission owned by THIS process. A terminal\n\t * mission no-ops; a remote/other-process owner cannot be signalled live and\n\t * is reported honestly as `remote_owner` (no remote control protocol is\n\t * invented here). When a local execution is active it is aborted and the\n\t * fenced terminal CANCELLED write is awaited so `executorConfirmedStopped`\n\t * is accurate.\n\t */\n\tasync cancelMission(missionId: string, reason?: string): Promise<MissionCancellationView> {\n\t\tconst record = await this._requireRecord(missionId);\n\n\t\tif (isTerminalMissionState(record.state)) {\n\t\t\treturn { missionId, status: \"terminal\", executorConfirmedStopped: false };\n\t\t}\n\n\t\tconst now = this._now();\n\t\tif (!record.lease || !isExecutionLeaseActive(record.lease, now)) {\n\t\t\treturn { missionId, status: \"not_running\", executorConfirmedStopped: false };\n\t\t}\n\n\t\tif (record.lease.ownerId !== this._delegator.ownerId) {\n\t\t\treturn {\n\t\t\t\tmissionId,\n\t\t\t\tstatus: \"remote_owner\",\n\t\t\t\texecutorConfirmedStopped: false,\n\t\t\t\treason: `Execution is owned by another process (owner ${record.lease.ownerId}); live cancellation is not signalled cross-process`,\n\t\t\t};\n\t\t}\n\n\t\tconst active = this._active.get(missionId);\n\t\tif (!active) {\n\t\t\treturn {\n\t\t\t\tmissionId,\n\t\t\t\tstatus: \"not_running\",\n\t\t\t\texecutorConfirmedStopped: false,\n\t\t\t\treason: \"This process holds the lease but has no active local execution to abort\",\n\t\t\t};\n\t\t}\n\n\t\tconst requestedAtMs = now;\n\t\tconst requested = await active.coordinator.requestCancellation(\n\t\t\tmissionId,\n\t\t\treason ?? \"mission cancelled by operator\",\n\t\t);\n\t\tif (requested.status !== \"cancel_requested\") {\n\t\t\treturn { missionId, status: \"not_running\", executorConfirmedStopped: false };\n\t\t}\n\n\t\tlet executorConfirmedStopped = false;\n\t\ttry {\n\t\t\tawait active.resume;\n\t\t\texecutorConfirmedStopped = true;\n\t\t} catch {\n\t\t\t// Authority loss or a commit failure may reject the resume; cancellation\n\t\t\t// request still stands and the durable record is the authority.\n\t\t}\n\n\t\treturn {\n\t\t\tmissionId,\n\t\t\tstatus: executorConfirmedStopped ? \"cancelled\" : \"cancel_requested\",\n\t\t\texecutorConfirmedStopped,\n\t\t\trequestedAtMs,\n\t\t\treason: reason ?? \"mission cancelled by operator\",\n\t\t};\n\t}\n\n\t/** Conservative restart reconciliation. Never auto-runs missions. */\n\tasync recoverMission(\n\t\toptions: { now?: number } = {},\n\t): Promise<Awaited<ReturnType<DurableMissionDelegator[\"recover\"]>>> {\n\t\treturn this._delegator.recover(options);\n\t}\n\n\t// =========================================================================\n\t// Internals\n\t// =========================================================================\n\n\tprivate async _requireRecord(missionId: string): Promise<DurableMissionRecord> {\n\t\tconst loaded = await this._store.load(missionId);\n\t\tif (loaded.status === \"missing\") {\n\t\t\tthrow new MissionControlError(\"MISSION_NOT_FOUND\", `Mission not found: ${missionId}`, { missionId });\n\t\t}\n\t\tif (loaded.status === \"corrupt\") {\n\t\t\tthrow new MissionControlError(\"MISSION_CORRUPT\", `Mission ${missionId} is corrupt: ${loaded.diagnostic}`, {\n\t\t\t\tmissionId,\n\t\t\t\tdiagnostic: loaded.diagnostic,\n\t\t\t});\n\t\t}\n\t\treturn loaded.record;\n\t}\n\n\tprivate _toSummary(\n\t\trecord: DurableMissionRecord,\n\t\tchildCount: number,\n\t\tnow: number,\n\t\tassignment?: AssignmentRecord,\n\t): MissionSummary {\n\t\tconst resumability = this._toResumability(record, now);\n\t\tconst ownership = this._toOwnership(record, now);\n\t\treturn {\n\t\t\tmissionId: record.missionId,\n\t\t\tparentMissionId: record.parentMissionId,\n\t\t\tchildSessionId: record.request.childSessionId,\n\t\t\tdepth: record.depth,\n\t\t\tstate: record.state,\n\t\t\tcreatedAtMs: record.createdAtMs,\n\t\t\tupdatedAtMs: record.updatedAtMs,\n\t\t\tcurrentAttemptId: record.currentAttemptId,\n\t\t\tcurrentExecutionId: record.currentExecutionId,\n\t\t\tattemptCount: record.attempts.length,\n\t\t\tfencingToken: record.fencingToken,\n\t\t\towned: ownership.owned,\n\t\t\tleaseStatus: ownership.leaseStatus,\n\t\t\tterminal: isTerminalMissionState(record.state),\n\t\t\tresultStatus: record.result?.state,\n\t\t\tverificationStatus: record.result?.verification.status,\n\t\t\tinterrupted: record.state === \"INTERRUPTED\",\n\t\t\tresumable: resumability.resumable,\n\t\t\tchildCount,\n\t\t\tassigned: assignment !== undefined,\n\t\t\tcurrentAssignmentId: assignment?.assignmentId,\n\t\t\tassignedExecutorId: assignment?.executorId,\n\t\t};\n\t}\n\n\tprivate async _currentAssignmentFor(missionId: string): Promise<AssignmentRecord | undefined> {\n\t\tif (!this._assignmentStore) return undefined;\n\t\tconst { records } = await this._assignmentStore.listRecords();\n\t\tconst current = records.filter((record) => record.missionId === missionId && record.current);\n\t\tif (current.length > 1) {\n\t\t\tthrow new MissionControlError(\n\t\t\t\t\"MISSION_TREE_CORRUPT\",\n\t\t\t\t`Mission ${missionId} has multiple current assignments`,\n\t\t\t\t{ missionId, assignmentIds: current.map((record) => record.assignmentId) },\n\t\t\t);\n\t\t}\n\t\treturn current[0];\n\t}\n\n\tprivate _toRequestView(record: DurableMissionRecord): MissionRequestView {\n\t\tconst request = record.request;\n\t\treturn {\n\t\t\tmissionId: request.missionId,\n\t\t\tparentMissionId: request.parentMissionId,\n\t\t\tdepth: request.depth,\n\t\t\tobjective: request.objective,\n\t\t\tagent: request.agent,\n\t\t\texecutionMode: request.executionMode,\n\t\t\tacceptanceCriteria: [...request.acceptanceCriteria],\n\t\t\tworkspaceScope: request.workspaceScope ? { ...request.workspaceScope } : undefined,\n\t\t\tbudget: request.budget ? { ...request.budget } : undefined,\n\t\t\tcapabilities: request.capabilities ? [...request.capabilities] : undefined,\n\t\t\tmodelPolicy: request.modelPolicy ? { ...request.modelPolicy } : undefined,\n\t\t\tidempotencyKey: request.idempotencyKey,\n\t\t\tchildSessionId: request.childSessionId,\n\t\t\tconstraints: request.constraints ? [...request.constraints] : undefined,\n\t\t\tcontextKeys: request.context ? Object.keys(request.context).sort() : [],\n\t\t\tcreatedAtMs: request.createdAtMs,\n\t\t};\n\t}\n\n\tprivate _currentAttemptView(record: DurableMissionRecord): AttemptView | undefined {\n\t\tif (!record.currentAttemptId) return undefined;\n\t\tconst attempt = record.attempts.find((a) => a.attemptId === record.currentAttemptId);\n\t\tif (!attempt) return undefined;\n\t\treturn {\n\t\t\tattemptId: attempt.attemptId,\n\t\t\texecutionId: attempt.executionId,\n\t\t\tstartedAtMs: attempt.startedAtMs,\n\t\t\tfinishedAtMs: attempt.finishedAtMs,\n\t\t\tendReason: attempt.endReason,\n\t\t\trecovery: attempt.recovery,\n\t\t};\n\t}\n\n\tprivate _toOwnership(record: DurableMissionRecord, now: number): MissionOwnershipView {\n\t\tconst lease = record.lease;\n\t\tif (!lease) {\n\t\t\treturn { owned: false, fencingToken: record.fencingToken, leaseStatus: \"NONE\" };\n\t\t}\n\t\tconst status = this._leaseStatus(lease, now);\n\t\tconst owned = isExecutionLeaseActive(lease, now);\n\t\tconst view: MissionOwnershipView = {\n\t\t\towned,\n\t\t\townerId: lease.ownerId,\n\t\t\tleaseId: lease.leaseId,\n\t\t\tfencingToken: lease.fencingToken,\n\t\t\tacquiredAtMs: lease.acquiredAtMs,\n\t\t\trenewedAtMs: lease.renewedAtMs,\n\t\t\texpiresAtMs: lease.expiresAtMs,\n\t\t\tremainingMs: Math.max(0, lease.expiresAtMs - now),\n\t\t\tleaseStatus: status,\n\t\t};\n\t\tconst local = this.localHeartbeatTelemetry(record.missionId);\n\t\tif (local) {\n\t\t\tview.localRuntime = { known: true, heartbeatTelemetry: local };\n\t\t}\n\t\treturn view;\n\t}\n\n\tprivate _leaseStatus(lease: NonNullable<DurableMissionRecord[\"lease\"]>, now: number): LeaseStatus {\n\t\tif (lease.expiresAtMs <= now) return \"EXPIRED\";\n\t\tconst window = Math.max(1, lease.expiresAtMs - lease.renewedAtMs);\n\t\tconst remaining = lease.expiresAtMs - now;\n\t\treturn remaining <= window * EXPIRING_FRACTION ? \"EXPIRING\" : \"ACTIVE\";\n\t}\n\n\tprivate _toResumability(record: DurableMissionRecord, now: number): Resumability {\n\t\tif (isTerminalMissionState(record.state)) {\n\t\t\treturn {\n\t\t\t\tresumable: false,\n\t\t\t\treasonCode: \"MISSION_TERMINAL\",\n\t\t\t\treason: `Mission ${record.missionId} is terminal (${record.state})`,\n\t\t\t};\n\t\t}\n\t\tif (record.lease && isExecutionLeaseActive(record.lease, now)) {\n\t\t\treturn {\n\t\t\t\tresumable: false,\n\t\t\t\treasonCode: \"MISSION_ACTIVE\",\n\t\t\t\treason: `Mission ${record.missionId} has an active execution owner`,\n\t\t\t};\n\t\t}\n\t\tif (record.state !== \"CREATED\" && record.state !== \"INTERRUPTED\") {\n\t\t\treturn {\n\t\t\t\tresumable: false,\n\t\t\t\treasonCode: \"MISSION_NOT_RUNNABLE_STATE\",\n\t\t\t\treason: `Mission ${record.missionId} is ${record.state}; reconcile first`,\n\t\t\t};\n\t\t}\n\t\tif (!record.request.childSessionId) {\n\t\t\treturn {\n\t\t\t\tresumable: false,\n\t\t\t\treasonCode: \"MISSING_CHILD_SESSION\",\n\t\t\t\treason: `Mission ${record.missionId} has no durable child session identity`,\n\t\t\t};\n\t\t}\n\t\treturn { resumable: true };\n\t}\n\n\tprivate _toResultView(record: DurableMissionRecord): MissionResultView {\n\t\tif (!record.result) {\n\t\t\treturn { available: false };\n\t\t}\n\t\treturn { available: true, resultExecutionId: record.resultExecutionId, result: record.result };\n\t}\n\n\tprivate async _evidenceRefs(record: DurableMissionRecord): Promise<MissionEvidenceRef[]> {\n\t\tconst ids = record.result?.evidenceRefs ?? [];\n\t\tconst refs: MissionEvidenceRef[] = [];\n\t\tfor (const evidenceId of ids) {\n\t\t\tconst parsed = parseEvidenceId(evidenceId);\n\t\t\tlet available: boolean | undefined;\n\t\t\tif (this._evidenceArchive) {\n\t\t\t\tavailable = await this._evidenceArchive.has(evidenceId);\n\t\t\t}\n\t\t\trefs.push({ evidenceId, kind: parsed.kind, source: parsed.source, available });\n\t\t}\n\t\treturn refs;\n\t}\n\n\tprivate async _buildTree(missionId: string, path: Set<string>): Promise<MissionTreeNode> {\n\t\tif (path.has(missionId)) {\n\t\t\tthrow new MissionControlError(\"MISSION_TREE_CORRUPT\", `Cycle detected at mission ${missionId}`, { missionId });\n\t\t}\n\t\tconst record = await this._requireRecord(missionId);\n\t\tconst now = this._now();\n\t\tconst childIds = await this._store.listChildren(missionId);\n\n\t\t// Verify every listed child actually points back at this parent (durable\n\t\t// relation integrity), then build children deterministically.\n\t\tconst children: MissionTreeNode[] = [];\n\t\tfor (const childId of [...childIds].sort()) {\n\t\t\tconst childLoaded = await this._store.load(childId);\n\t\t\tif (childLoaded.status !== \"ok\") {\n\t\t\t\tthrow new MissionControlError(\n\t\t\t\t\t\"MISSION_TREE_CORRUPT\",\n\t\t\t\t\t`Child mission ${childId} of ${missionId} is missing or corrupt`,\n\t\t\t\t\t{ parentMissionId: missionId, childId, loadStatus: childLoaded.status },\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (childLoaded.record.parentMissionId !== missionId) {\n\t\t\t\tthrow new MissionControlError(\n\t\t\t\t\t\"MISSION_TREE_CORRUPT\",\n\t\t\t\t\t`Child mission ${childId} does not reference parent ${missionId}`,\n\t\t\t\t\t{ parentMissionId: missionId, childId, actualParent: childLoaded.record.parentMissionId },\n\t\t\t\t);\n\t\t\t}\n\t\t\tchildren.push(await this._buildTree(childId, new Set(path).add(missionId)));\n\t\t}\n\n\t\tconst ownership = this._toOwnership(record, now);\n\t\treturn {\n\t\t\tmissionId: record.missionId,\n\t\t\tstate: record.state,\n\t\t\tdepth: record.depth,\n\t\t\tchildSessionId: record.request.childSessionId,\n\t\t\tsummary: {\n\t\t\t\tattemptCount: record.attempts.length,\n\t\t\t\tterminal: isTerminalMissionState(record.state),\n\t\t\t\tresultStatus: record.result?.state,\n\t\t\t\tverificationStatus: record.result?.verification.status,\n\t\t\t},\n\t\t\townership: {\n\t\t\t\towned: ownership.owned,\n\t\t\t\tleaseId: ownership.leaseId,\n\t\t\t\tfencingToken: ownership.fencingToken,\n\t\t\t\tleaseStatus: ownership.leaseStatus,\n\t\t\t},\n\t\t\tchildren,\n\t\t};\n\t}\n}\n"]}