{"version":3,"file":"durable-store.d.ts","sourceRoot":"","sources":["../../../src/core/mission-domain/durable-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAChF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,EAA0C,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE/F,eAAO,MAAM,8BAA8B,GAAa,CAAC;AAMzD,4DAA4D;AAC5D,MAAM,MAAM,gCAAgC,GACzC,WAAW,GACX,QAAQ,GACR,WAAW,GACX,WAAW,GACX,SAAS,GACT,aAAa,CAAC;AAEjB;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,uBAAuB;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,gCAAgC,CAAC;IAC7C,gEAAgE;IAChE,QAAQ,CAAC,EAAE;QACV,MAAM,EAAE,MAAM,CAAC;QACf,aAAa,EAAE,MAAM,CAAC;KACtB,CAAC;CACF;AAED,iEAAiE;AACjE,MAAM,WAAW,wBAAwB;IACxC,sDAAsD;IACtD,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,YAAY,CAAC;IACnB,EAAE,EAAE,YAAY,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACpC,aAAa,EAAE,CAAC,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,OAAO,EAAE,cAAc,CAAC;IACxB,6CAA6C;IAC7C,KAAK,EAAE,YAAY,CAAC;IACpB,iFAAiF;IACjF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,yEAAyE;IACzE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,oDAAoD;IACpD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,4CAA4C;IAC5C,WAAW,EAAE,wBAAwB,EAAE,CAAC;IACxC,8CAA8C;IAC9C,QAAQ,EAAE,uBAAuB,EAAE,CAAC;IACpC;;;;;OAKG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,uDAAuD;IACvD,QAAQ,EAAE,MAAM,CAAC;CACjB;AAMD,MAAM,MAAM,0BAA0B,GACnC;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE,GACtD;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzC,MAAM,MAAM,wBAAwB,GACjC;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE,GAC9C;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhE,MAAM,MAAM,wBAAwB,GACjC;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,GACnB;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GACjF;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAChE;IAAE,MAAM,EAAE,iBAAiB,CAAA;CAAE,CAAC;AAEjC,MAAM,WAAW,yBAAyB;IACzC,iFAAiF;IACjF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,UAAU,CAAC,EAAE,mBAAmB,CAAC;CACjC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,sBAAsB,CAAC,CAAC,IACjC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,oBAAoB,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GACvD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,CAAC;AAE9B,MAAM,MAAM,0BAA0B,CAAC,CAAC,IACrC;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAC1B;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhE;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAEzB,MAAM,CAAC,MAAM,EAAE,oBAAoB,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAC;IAE1E,yEAAyE;IACzE,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAAC;IAE3D,IAAI,CAAC,MAAM,EAAE,oBAAoB,EAAE,OAAO,CAAC,EAAE,yBAAyB,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAAC;IAE3G;;;;OAIG;IACH,MAAM,CAAC,CAAC,EACP,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,sBAAsB,CAAC,CAAC,CAAC,GACpE,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,CAAC;IAE1C,YAAY,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAElC,uDAAuD;IACvD,uBAAuB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAE7C,YAAY,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CACzD;AAMD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CActD;AAED,4DAA4D;AAC5D,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,cAAc,GAAG,OAAO,CAElF;AAMD,MAAM,MAAM,yBAAyB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAWvH;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEtD;AAgCD;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,yBAAyB,CAgMnF;AAMD,MAAM,WAAW,+BAA+B;IAC/C,OAAO,EAAE,cAAc,CAAC;IACxB,mDAAmD;IACnD,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,+BAA+B,GAAG,oBAAoB,CAiBvG","sourcesContent":["/**\n * Durable Mission Store — domain-facing persistence port (2.4.0).\n *\n * Makes a first-class mission durable across Jensen process restarts. The pure\n * types and validation here depend only on the canonical mission domain (and no\n * process/provider/CLI/UI machinery), so a future background/orchestrator or\n * remote executor can use the same store contract without a SessionManager.\n *\n * Authority model (see Reliability Kernel):\n *   - The DurableMissionStore records the mission-domain lifecycle and result.\n *   - MissionRuntime / Completion Gate remain the sole authority for verified\n *     SUCCEEDED. Loading or saving a durable record can never upgrade a\n *     persisted state; SUCCEEDED only round-trips when it was previously\n *     verified, never inferred from exit code / output text / normal shutdown.\n */\n\nimport type { ExecutionLease, ExecutionLeaseProof } from \"./execution-lease.js\";\nimport type { MissionRequest } from \"./mission-request.js\";\nimport { validateMissionRequest } from \"./mission-request.js\";\nimport type { MissionResult } from \"./mission-result.js\";\nimport { isMissionExecutionOutcome } from \"./mission-result.js\";\nimport { isMissionState, isTerminalMissionState, type MissionState } from \"./mission-state.js\";\n\nexport const DURABLE_MISSION_SCHEMA_VERSION = 1 as const;\n\n// =============================================================================\n// Identity / record schema\n// =============================================================================\n\n/** How an execution attempt stopped being authoritative. */\nexport type DurableExecutionAttemptEndReason =\n\t| \"COMPLETED\"\n\t| \"FAILED\"\n\t| \"CANCELLED\"\n\t| \"TIMED_OUT\"\n\t| \"CRASHED\"\n\t| \"INTERRUPTED\";\n\n/**\n * One concrete execution attempt.\n *\n * `attemptId` is the durable, coordinator-allocated identity of the attempt. It\n * is allocated and persisted BEFORE the executor is invoked, so a crash\n * immediately after launch can never erase the fact that an attempt began.\n *\n * `executionId` is the executor-scoped execution id, attached once the executor\n * confirms ownership. It MUST NOT be reused across restart+relaunch unless a\n * future executor can prove it adopted the exact same attempt\n * (ProcessMissionExecutor cannot, so it always allocates a new one).\n */\nexport interface DurableExecutionAttempt {\n\tattemptId: string;\n\texecutionId?: string;\n\tstartedAtMs: number;\n\tfinishedAtMs?: number;\n\tendReason?: DurableExecutionAttemptEndReason;\n\t/** Present when ownership was lost (restart reconciliation). */\n\trecovery?: {\n\t\treason: string;\n\t\trecoveredAtMs: number;\n\t};\n}\n\n/** One authoritative lifecycle transition, appended in order. */\nexport interface DurableMissionTransition {\n\t/** Monotonic transition sequence within a mission. */\n\tseq: number;\n\tfrom: MissionState;\n\tto: MissionState;\n\tatMs: number;\n\treason?: string;\n\texecutionId?: string;\n\t/** Durable attempt identity associated with this transition (LAUNCHING). */\n\tattemptId?: string;\n}\n\n/**\n * The durable record for one logical mission. `missionId` is stable across\n * restart; `currentExecutionId` names the current attempt (cleared on\n * reconciliation); `attempts` preserves prior attempt identity for auditability.\n */\nexport interface DurableMissionRecord {\n\tschemaVersion: 1;\n\tmissionId: string;\n\tparentMissionId?: string;\n\tdepth: number;\n\t/** Immutable once durably created. Conflicting re-create is rejected. */\n\trequest: MissionRequest;\n\t/** Current authoritative lifecycle state. */\n\tstate: MissionState;\n\t/** Durable identity of the currently-owned attempt (allocated before launch). */\n\tcurrentAttemptId?: string;\n\t/** Executor execution id of the current attempt, once launch returns. */\n\tcurrentExecutionId?: string;\n\tcreatedAtMs: number;\n\tupdatedAtMs: number;\n\tstartedAtMs?: number;\n\tfinishedAtMs?: number;\n\t/** Terminal result, present only when `state` is terminal. */\n\tresult?: MissionResult;\n\t/** The execution attempt that produced `result`. */\n\tresultExecutionId?: string;\n\t/** Ordered lifecycle transition history. */\n\ttransitions: DurableMissionTransition[];\n\t/** Prior execution attempts, oldest first. */\n\tattempts: DurableExecutionAttempt[];\n\t/**\n\t * Monotonic ownership epoch. Incremented on every execution-lease\n\t * acquisition and every recovery revocation of an expired lease. It is kept\n\t * on the record (not only inside `lease`) so it survives lease clearing and\n\t * can never move backward.\n\t */\n\tfencingToken: number;\n\t/**\n\t * Current execution-ownership lease. Absent when no valid owner exists.\n\t * Terminal records must never carry a lease (ownership is released).\n\t */\n\tlease?: ExecutionLease;\n\t/** Monotonic generation for stale-update detection. */\n\trevision: number;\n}\n\n// =============================================================================\n// Store port\n// =============================================================================\n\nexport type DurableMissionCreateResult =\n\t| { status: \"created\" }\n\t| { status: \"idempotent\"; record: DurableMissionRecord }\n\t| { status: \"conflict\"; error: string };\n\nexport type DurableMissionLoadResult =\n\t| { status: \"ok\"; record: DurableMissionRecord }\n\t| { status: \"missing\" }\n\t| { status: \"corrupt\"; missionId: string; diagnostic: string };\n\nexport type DurableMissionSaveResult =\n\t| { status: \"saved\" }\n\t| { status: \"stale\"; expectedRevision: number; actualRevision: number | undefined }\n\t| { status: \"stale_owner\"; leaseId: string; fencingToken: number }\n\t| { status: \"lease_not_found\" };\n\nexport interface DurableMissionSaveOptions {\n\t/** Optional optimistic-concurrency guard: reject if on-disk revision differs. */\n\texpectedRevision?: number;\n\t/**\n\t * Execution-authoritative mutation proof. When present the store verifies,\n\t * inside the cross-process critical section, that the current on-disk lease\n\t * still matches this proof (leaseId + fencingToken) and is not expired.\n\t */\n\tleaseProof?: ExecutionLeaseProof;\n}\n\n/**\n * A store-level atomic read-modify-write mutation.\n *\n * The callback receives the currently-persisted record and must be synchronous\n * (pure record → next record / value) so the cross-process critical section is\n * never held across model inference or other slow work.\n */\nexport type DurableMissionMutation<T> =\n\t| { kind: \"write\"; next: DurableMissionRecord; value: T }\n\t| { kind: \"noop\"; value: T };\n\nexport type DurableMissionMutateResult<T> =\n\t| { status: \"ok\"; value: T }\n\t| { status: \"missing\" }\n\t| { status: \"corrupt\"; missionId: string; diagnostic: string };\n\n/**\n * Canonical mission persistence port. Implementations must be crash-conscious\n * (atomic record replacement, schema validation on load, deterministic corrupt\n * handling) and must never fabricate a default successful state.\n */\nexport interface DurableMissionStore {\n\treadonly storeId: string;\n\n\tcreate(record: DurableMissionRecord): Promise<DurableMissionCreateResult>;\n\n\t/** Load a record. Missing and corrupt are distinguished structurally. */\n\tload(missionId: string): Promise<DurableMissionLoadResult>;\n\n\tsave(record: DurableMissionRecord, options?: DurableMissionSaveOptions): Promise<DurableMissionSaveResult>;\n\n\t/**\n\t * Atomically mutate a persisted record across processes. The mutation\n\t * callback is invoked only when a valid record exists; missing and corrupt\n\t * states are surfaced structurally and never passed to the callback.\n\t */\n\tmutate<T>(\n\t\tmissionId: string,\n\t\tmutation: (current: DurableMissionRecord) => DurableMissionMutation<T>,\n\t): Promise<DurableMissionMutateResult<T>>;\n\n\tlistMissions(): Promise<string[]>;\n\n\t/** Healthy, non-terminal (recoverable) mission ids. */\n\tlistNonterminalMissions(): Promise<string[]>;\n\n\tlistChildren(parentMissionId: string): Promise<string[]>;\n}\n\n// =============================================================================\n// Request equality (duplicate-create idempotency vs conflict)\n// =============================================================================\n\n/**\n * Deterministic structural JSON string used for canonical comparison of\n * immutable requests. Object keys are sorted so field insertion order cannot\n * hide a semantic difference.\n */\nexport function stableStringify(value: unknown): string {\n\tif (value === undefined) return \"null\";\n\tif (value === null || typeof value !== \"object\") return JSON.stringify(value);\n\tif (Array.isArray(value)) {\n\t\treturn `[${value.map((item) => stableStringify(item)).join(\",\")}]`;\n\t}\n\tconst record = value as Record<string, unknown>;\n\t// Omit keys whose value is `undefined`: JSON round-trips drop them, so an\n\t// explicitly-undefined optional field is canonically identical to an absent\n\t// one (they are the same immutable request).\n\tconst keys = Object.keys(record)\n\t\t.filter((key) => record[key] !== undefined)\n\t\t.sort();\n\treturn `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(\",\")}}`;\n}\n\n/** Structural equality of two immutable MissionRequests. */\nexport function missionRequestsEqual(a: MissionRequest, b: MissionRequest): boolean {\n\treturn stableStringify(a) === stableStringify(b);\n}\n\n// =============================================================================\n// Validation\n// =============================================================================\n\nexport type DurableMissionParseResult = { ok: true; record: DurableMissionRecord } | { ok: false; diagnostic: string };\n\nconst EXECUTION_END_REASONS: ReadonlySet<string> = new Set<string>([\n\t\"COMPLETED\",\n\t\"FAILED\",\n\t\"CANCELLED\",\n\t\"TIMED_OUT\",\n\t\"CRASHED\",\n\t\"INTERRUPTED\",\n]);\n\n/**\n * A missionId is used as a file path component by the concrete store. Reject\n * anything that could escape the store root or inject a path separator.\n */\nexport function isSafeMissionId(value: string): boolean {\n\treturn /^[A-Za-z0-9._-]+$/u.test(value) && value !== \".\" && value !== \"..\";\n}\n\nfunction invalid(why: string): DurableMissionParseResult {\n\treturn { ok: false, diagnostic: why };\n}\n\nfunction isSafeInteger(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isSafeInteger(value);\n}\n\nfunction validateResult(result: unknown, missionId: string, state: MissionState): string | undefined {\n\tif (typeof result !== \"object\" || result === null) return \"result must be an object\";\n\tconst r = result as Record<string, unknown>;\n\tif (r.missionId !== missionId) return \"result.missionId does not match record.missionId\";\n\tif (r.state !== state) return \"result.state does not match record.state\";\n\tif (r.success !== (state === \"SUCCEEDED\")) return \"result.success is inconsistent with result.state\";\n\tif (!isMissionExecutionOutcome(r.executionOutcome)) return \"result.executionOutcome is invalid\";\n\tif (!Array.isArray(r.evidenceRefs)) return \"result.evidenceRefs must be an array\";\n\tif (!Array.isArray(r.failures)) return \"result.failures must be an array\";\n\tconst verification = r.verification;\n\tif (typeof verification !== \"object\" || verification === null) return \"result.verification must be an object\";\n\tconst vstatus = (verification as Record<string, unknown>).status;\n\tif (vstatus !== \"verified\" && vstatus !== \"unverified\" && vstatus !== \"failed\") {\n\t\treturn \"result.verification.status is invalid\";\n\t}\n\tconst diagnostics = r.executorDiagnostics;\n\tif (typeof diagnostics !== \"object\" || diagnostics === null || Array.isArray(diagnostics)) {\n\t\treturn \"result.executorDiagnostics must be an object\";\n\t}\n\treturn undefined;\n}\n\n/**\n * Validate an untrusted persisted value into a DurableMissionRecord.\n *\n * Corruption is surfaced structurally (`ok: false`) rather than silently\n * becoming a missing record or — worse — a fabricated success.\n */\nexport function parseDurableMissionRecord(value: unknown): DurableMissionParseResult {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\treturn invalid(\"record must be an object\");\n\t}\n\tconst doc = value as Record<string, unknown>;\n\n\tif (doc.schemaVersion !== DURABLE_MISSION_SCHEMA_VERSION) {\n\t\treturn invalid(`unsupported schemaVersion: ${String(doc.schemaVersion)}`);\n\t}\n\tif (typeof doc.missionId !== \"string\" || !isSafeMissionId(doc.missionId)) {\n\t\treturn invalid(\"missionId is missing or unsafe\");\n\t}\n\tconst missionId = doc.missionId;\n\n\tif (\n\t\tdoc.parentMissionId !== undefined &&\n\t\t(typeof doc.parentMissionId !== \"string\" || !isSafeMissionId(doc.parentMissionId))\n\t) {\n\t\treturn invalid(\"parentMissionId is unsafe\");\n\t}\n\tif (!isSafeInteger(doc.depth) || doc.depth < 0) return invalid(\"depth must be a non-negative integer\");\n\n\tif (typeof doc.request !== \"object\" || doc.request === null || Array.isArray(doc.request)) {\n\t\treturn invalid(\"request must be an object\");\n\t}\n\tconst requestValidation = validateMissionRequest(doc.request as MissionRequest);\n\tif (!requestValidation.valid) {\n\t\treturn invalid(`invalid mission request: ${requestValidation.errors.join(\", \")}`);\n\t}\n\tconst request = requestValidation.request;\n\n\tif (!isMissionState(doc.state)) return invalid(`invalid mission state: ${String(doc.state)}`);\n\tconst state = doc.state;\n\n\tif (!isSafeInteger(doc.createdAtMs) || !isSafeInteger(doc.updatedAtMs)) {\n\t\treturn invalid(\"timestamps must be safe integers\");\n\t}\n\tif (doc.startedAtMs !== undefined && !isSafeInteger(doc.startedAtMs))\n\t\treturn invalid(\"startedAtMs must be a safe integer\");\n\tif (doc.finishedAtMs !== undefined && !isSafeInteger(doc.finishedAtMs))\n\t\treturn invalid(\"finishedAtMs must be a safe integer\");\n\tif (!isSafeInteger(doc.revision) || doc.revision < 0) return invalid(\"revision must be a non-negative integer\");\n\n\tif (!Array.isArray(doc.transitions)) return invalid(\"transitions must be an array\");\n\tconst transitions: DurableMissionTransition[] = [];\n\tfor (const entry of doc.transitions) {\n\t\tif (typeof entry !== \"object\" || entry === null || Array.isArray(entry))\n\t\t\treturn invalid(\"transition must be an object\");\n\t\tconst t = entry as Record<string, unknown>;\n\t\tif (!isSafeInteger(t.seq)) return invalid(\"transition.seq must be a safe integer\");\n\t\tif (!isMissionState(t.from) || !isMissionState(t.to)) return invalid(\"transition from/to must be valid states\");\n\t\tif (!isSafeInteger(t.atMs)) return invalid(\"transition.atMs must be a safe integer\");\n\t\ttransitions.push({\n\t\t\tseq: t.seq as number,\n\t\t\tfrom: t.from as MissionState,\n\t\t\tto: t.to as MissionState,\n\t\t\tatMs: t.atMs as number,\n\t\t\treason: typeof t.reason === \"string\" ? t.reason : undefined,\n\t\t\texecutionId: typeof t.executionId === \"string\" ? t.executionId : undefined,\n\t\t\tattemptId: typeof t.attemptId === \"string\" ? t.attemptId : undefined,\n\t\t});\n\t}\n\n\tif (!Array.isArray(doc.attempts)) return invalid(\"attempts must be an array\");\n\tconst attempts: DurableExecutionAttempt[] = [];\n\tfor (const entry of doc.attempts) {\n\t\tif (typeof entry !== \"object\" || entry === null || Array.isArray(entry))\n\t\t\treturn invalid(\"attempt must be an object\");\n\t\tconst a = entry as Record<string, unknown>;\n\t\tif (typeof a.attemptId !== \"string\" || a.attemptId.length === 0)\n\t\t\treturn invalid(\"attempt.attemptId must be a string\");\n\t\tif (a.executionId !== undefined && (typeof a.executionId !== \"string\" || a.executionId.length === 0))\n\t\t\treturn invalid(\"attempt.executionId must be a string when present\");\n\t\tif (!isSafeInteger(a.startedAtMs)) return invalid(\"attempt.startedAtMs must be a safe integer\");\n\t\tif (a.finishedAtMs !== undefined && !isSafeInteger(a.finishedAtMs))\n\t\t\treturn invalid(\"attempt.finishedAtMs must be a safe integer\");\n\t\tif (a.endReason !== undefined && !EXECUTION_END_REASONS.has(String(a.endReason))) {\n\t\t\treturn invalid(`invalid attempt.endReason: ${String(a.endReason)}`);\n\t\t}\n\t\tlet recovery: DurableExecutionAttempt[\"recovery\"];\n\t\tif (a.recovery !== undefined) {\n\t\t\tif (typeof a.recovery !== \"object\" || a.recovery === null || Array.isArray(a.recovery)) {\n\t\t\t\treturn invalid(\"attempt.recovery must be an object\");\n\t\t\t}\n\t\t\tconst rec = a.recovery as Record<string, unknown>;\n\t\t\tif (typeof rec.reason !== \"string\" || !isSafeInteger(rec.recoveredAtMs)) {\n\t\t\t\treturn invalid(\"attempt.recovery must contain reason and recoveredAtMs\");\n\t\t\t}\n\t\t\trecovery = { reason: rec.reason, recoveredAtMs: rec.recoveredAtMs as number };\n\t\t}\n\t\tattempts.push({\n\t\t\tattemptId: a.attemptId,\n\t\t\texecutionId: a.executionId as string | undefined,\n\t\t\tstartedAtMs: a.startedAtMs as number,\n\t\t\tfinishedAtMs: a.finishedAtMs as number | undefined,\n\t\t\tendReason: a.endReason as DurableExecutionAttemptEndReason | undefined,\n\t\t\trecovery,\n\t\t});\n\t}\n\n\t// Current-attempt pointers: optional strings when present; a terminal record\n\t// must not carry a current attempt (ownership is always released on terminal).\n\tif (\n\t\tdoc.currentAttemptId !== undefined &&\n\t\t(typeof doc.currentAttemptId !== \"string\" || doc.currentAttemptId.length === 0)\n\t)\n\t\treturn invalid(\"currentAttemptId must be a string when present\");\n\tif (\n\t\tdoc.currentExecutionId !== undefined &&\n\t\t(typeof doc.currentExecutionId !== \"string\" || doc.currentExecutionId.length === 0)\n\t)\n\t\treturn invalid(\"currentExecutionId must be a string when present\");\n\n\t// Fencing epoch + lease. `fencingToken` defaults to 0 for records written\n\t// before ownership landed (backward compatible on load); when present it must\n\t// be a non-negative safe integer. A lease is optional and must be structurally\n\t// sound; it can never fabricate a newer fence than the record itself.\n\tconst fencingToken = doc.fencingToken === undefined ? 0 : (doc.fencingToken as number);\n\tif (!isSafeInteger(fencingToken) || fencingToken < 0) return invalid(\"fencingToken must be a non-negative integer\");\n\n\tlet lease: ExecutionLease | undefined;\n\tif (doc.lease !== undefined) {\n\t\tif (typeof doc.lease !== \"object\" || doc.lease === null || Array.isArray(doc.lease)) {\n\t\t\treturn invalid(\"lease must be an object\");\n\t\t}\n\t\tconst l = doc.lease as Record<string, unknown>;\n\t\tif (typeof l.ownerId !== \"string\" || l.ownerId.length === 0)\n\t\t\treturn invalid(\"lease.ownerId must be a non-empty string\");\n\t\tif (typeof l.leaseId !== \"string\" || l.leaseId.length === 0)\n\t\t\treturn invalid(\"lease.leaseId must be a non-empty string\");\n\t\tif (!isSafeInteger(l.fencingToken) || (l.fencingToken as number) < 0)\n\t\t\treturn invalid(\"lease.fencingToken must be a non-negative integer\");\n\t\tif (!isSafeInteger(l.acquiredAtMs) || !isSafeInteger(l.renewedAtMs) || !isSafeInteger(l.expiresAtMs))\n\t\t\treturn invalid(\"lease timestamps must be safe integers\");\n\t\tif ((l.renewedAtMs as number) < (l.acquiredAtMs as number))\n\t\t\treturn invalid(\"lease.renewedAtMs must be >= acquiredAtMs\");\n\t\tif ((l.expiresAtMs as number) < (l.renewedAtMs as number))\n\t\t\treturn invalid(\"lease.expiresAtMs must be >= renewedAtMs\");\n\t\tif ((l.fencingToken as number) !== fencingToken)\n\t\t\treturn invalid(\"lease.fencingToken must equal record.fencingToken\");\n\t\tlease = {\n\t\t\townerId: l.ownerId as string,\n\t\t\tleaseId: l.leaseId as string,\n\t\t\tfencingToken: l.fencingToken as number,\n\t\t\tacquiredAtMs: l.acquiredAtMs as number,\n\t\t\trenewedAtMs: l.renewedAtMs as number,\n\t\t\texpiresAtMs: l.expiresAtMs as number,\n\t\t};\n\t}\n\n\t// Terminal/result consistency: a terminal record must carry a matching\n\t// result; a non-terminal record must not carry a result. Terminal records\n\t// also must not carry a lease (ownership is always released on terminal).\n\tif (isTerminalMissionState(state)) {\n\t\tif (doc.currentAttemptId !== undefined || doc.currentExecutionId !== undefined) {\n\t\t\treturn invalid(\"terminal record must not carry a current attempt\");\n\t\t}\n\t\tif (lease !== undefined) {\n\t\t\treturn invalid(\"terminal record must not carry an execution lease\");\n\t\t}\n\t\tconst resultError = validateResult(doc.result, missionId, state);\n\t\tif (resultError) return invalid(resultError);\n\t\tif (doc.resultExecutionId !== undefined && typeof doc.resultExecutionId !== \"string\") {\n\t\t\treturn invalid(\"resultExecutionId must be a string when present\");\n\t\t}\n\t} else if (doc.result !== undefined || doc.resultExecutionId !== undefined) {\n\t\treturn invalid(\"non-terminal record must not carry a terminal result\");\n\t}\n\n\tconst record: DurableMissionRecord = {\n\t\tschemaVersion: DURABLE_MISSION_SCHEMA_VERSION,\n\t\tmissionId,\n\t\tparentMissionId: doc.parentMissionId as string | undefined,\n\t\tdepth: doc.depth as number,\n\t\trequest,\n\t\tstate,\n\t\tcurrentAttemptId: doc.currentAttemptId as string | undefined,\n\t\tcurrentExecutionId: doc.currentExecutionId as string | undefined,\n\t\tcreatedAtMs: doc.createdAtMs as number,\n\t\tupdatedAtMs: doc.updatedAtMs as number,\n\t\tstartedAtMs: doc.startedAtMs as number | undefined,\n\t\tfinishedAtMs: doc.finishedAtMs as number | undefined,\n\t\tresult: doc.result as MissionResult | undefined,\n\t\tresultExecutionId: doc.resultExecutionId as string | undefined,\n\t\ttransitions,\n\t\tattempts,\n\t\tfencingToken,\n\t\tlease,\n\t\trevision: doc.revision as number,\n\t};\n\n\treturn { ok: true, record };\n}\n\n// =============================================================================\n// Record construction helper\n// =============================================================================\n\nexport interface CreateDurableMissionRecordInput {\n\trequest: MissionRequest;\n\t/** Now override for deterministic construction. */\n\tnow?: number;\n}\n\n/**\n * Build an initial durable record at CREATED state with no execution attempt.\n * Validation is the caller's responsibility (use `validateMissionRequest`).\n */\nexport function createDurableMissionRecord(input: CreateDurableMissionRecordInput): DurableMissionRecord {\n\tconst now = input.now ?? Date.now();\n\tconst request = input.request;\n\treturn {\n\t\tschemaVersion: DURABLE_MISSION_SCHEMA_VERSION,\n\t\tmissionId: request.missionId,\n\t\tparentMissionId: request.parentMissionId,\n\t\tdepth: request.depth,\n\t\trequest,\n\t\tstate: \"CREATED\",\n\t\tcreatedAtMs: request.createdAtMs,\n\t\tupdatedAtMs: now,\n\t\ttransitions: [],\n\t\tattempts: [],\n\t\tfencingToken: 0,\n\t\trevision: 1,\n\t};\n}\n"]}