{"version":3,"file":"continuation-scheduler.d.ts","sourceRoot":"","sources":["../../../src/core/long-horizon/continuation-scheduler.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * LH-3 Mission Continuation Scheduler v1.\n *\n * Freezes the exact continuation identity, cycle bindings, revision\n * guards, and event digests for a single Mission Execution. The\n * scheduler is a deterministic, replayable event log that can be\n * independently verified without the caller runtime.\n *\n * Every mutation produces a single immutable ContinuationSchedulerEvent\n * with a SHA-256 digest computed over its ordered fields. Events form\n * a hash chain via previousEventDigest. The aggregate history digest\n * covers the full event sequence.\n *\n * Threat Model:\n *   The unkeyed event hash chain and semantic replay protect against\n *   accidental mutation, partial rewriting, internally inconsistent\n *   rewriting, broken ordering, and broken replay. They do not claim\n *   protection against a privileged writer capable of rewriting the\n *   complete record and all digests consistently.\n */\n\nimport { createHash } from \"crypto\";\nimport { toCanonicalJson } from \"./canonical-json.js\";\n\n// =============================================================================\n// Schema constant\n// =============================================================================\n\nexport const CONTINUATION_SCHEDULER_VERSION = 1 as const;\n\n// =============================================================================\n// Public types\n// =============================================================================\n\nexport type ContinuationSchedulerState = \"IDLE\" | \"SCHEDULED\" | \"DISPATCHED\";\n\nexport type ContinuationSchedulerEventKind = \"SCHEDULE\" | \"DISPATCH\" | \"CONSUME\" | \"CANCEL\" | \"ABANDON\";\n\nexport interface ContinuationSchedulerRecord {\n\treadonly schedulerVersion: 1;\n\treadonly executionId: string;\n\treadonly contractDigest: string;\n\treadonly schedulerRevision: number;\n\treadonly state: ContinuationSchedulerState;\n\treadonly events: readonly ContinuationSchedulerEvent[];\n\treadonly historyDigest: string | null;\n}\n\nexport interface ContinuationSchedulerEvent {\n\treadonly eventId: string;\n\treadonly kind: ContinuationSchedulerEventKind;\n\treadonly cycleId: string;\n\treadonly requestSchedulerRevision: number;\n\treadonly observedExecutionRevision: number;\n\treadonly fromState: ContinuationSchedulerState;\n\treadonly toState: ContinuationSchedulerState;\n\treadonly revisionBefore: number;\n\treadonly revisionAfter: number;\n\treadonly previousEventDigest: string | null;\n\treadonly eventDigest: string;\n\treadonly createdAt: string;\n\n\t// Conditional — present only when applicable\n\treadonly expectedExecutionRevision?: number;\n\treadonly dispatchedContinuationId?: string;\n\treadonly resultDigest?: string;\n}\n\n// =============================================================================\n// Request types\n// =============================================================================\n\nexport interface ScheduleContinuationRequest {\n\treadonly eventId: string;\n\treadonly expectedSchedulerRevision: number;\n\treadonly expectedExecutionRevision: number;\n}\n\nexport interface DispatchContinuationRequest {\n\treadonly eventId: string;\n\treadonly cycleId: string;\n\treadonly expectedSchedulerRevision: number;\n\treadonly dispatchedContinuationId: string;\n}\n\nexport interface ConsumeContinuationRequest {\n\treadonly eventId: string;\n\treadonly cycleId: string;\n\treadonly expectedSchedulerRevision: number;\n\treadonly dispatchedContinuationId: string;\n\treadonly resultDigest: string;\n}\n\nexport interface CancelContinuationRequest {\n\treadonly eventId: string;\n\treadonly cycleId: string;\n\treadonly expectedSchedulerRevision: number;\n}\n\nexport interface AbandonContinuationRequest {\n\treadonly eventId: string;\n\treadonly cycleId: string;\n\treadonly expectedSchedulerRevision: number;\n}\n\nexport type ContinuationSchedulerRequest =\n\t| ScheduleContinuationRequest\n\t| DispatchContinuationRequest\n\t| ConsumeContinuationRequest\n\t| CancelContinuationRequest\n\t| AbandonContinuationRequest;\n\n// =============================================================================\n// Error codes\n// =============================================================================\n\nexport type ContinuationSchedulerErrorCode =\n\t| \"IDEMPOTENCY_CONFLICT\"\n\t| \"STALE_SCHEDULER_REVISION\"\n\t| \"INVALID_STATE\"\n\t| \"INVALID_CYCLE\"\n\t| \"CYCLE_NOT_SUPERSEDED\"\n\t| \"CYCLE_SUPERSEDED\"\n\t| \"EXECUTION_REVISION_MISMATCH\"\n\t| \"INVALID_REQUEST\"\n\t| \"CONTRACT_DIGEST_MISMATCH\"\n\t| \"ENOENT\"\n\t| \"INVALID_SCHEDULER_RECORD\";\n\n// =============================================================================\n// Operation result\n// =============================================================================\n\nexport interface ContinuationSchedulerResult {\n\treadonly ok: boolean;\n\treadonly record?: ContinuationSchedulerRecord;\n\treadonly event?: ContinuationSchedulerEvent;\n\treadonly error?: string;\n\treadonly code?: ContinuationSchedulerErrorCode;\n}\n\n// =============================================================================\n// Inspection result\n// =============================================================================\n\nexport interface ContinuationSchedulerInspection {\n\treadonly valid: boolean;\n\treadonly executionId: string;\n\treadonly contractDigest: string;\n\treadonly schedulerRevision: number;\n\treadonly state: ContinuationSchedulerState;\n\treadonly eventCount: number;\n\treadonly historyDigest: string | null;\n\treadonly error?: string;\n}\n\n// =============================================================================\n// Validation result\n// =============================================================================\n\nexport interface ContinuationSchedulerValidation {\n\treadonly valid: boolean;\n\treadonly schedulerRevision: number;\n\treadonly state: ContinuationSchedulerState;\n\treadonly eventCount: number;\n\treadonly contractBound: boolean;\n\treadonly executionBound: boolean;\n\treadonly semanticValid: boolean;\n\treadonly error?: string;\n}\n\n// =============================================================================\n// Construction\n// =============================================================================\n\nexport function initializeContinuationScheduler(\n\texecutionId: string,\n\tcontractDigest: string,\n): ContinuationSchedulerRecord {\n\treturn {\n\t\tschedulerVersion: 1,\n\t\texecutionId,\n\t\tcontractDigest,\n\t\tschedulerRevision: 0,\n\t\tstate: \"IDLE\",\n\t\tevents: [],\n\t\thistoryDigest: null,\n\t};\n}\n\n// =============================================================================\n// Mutation: SCHEDULE\n// =============================================================================\n\nexport function scheduleContinuation(\n\trecord: ContinuationSchedulerRecord | null,\n\trequest: ScheduleContinuationRequest,\n): ContinuationSchedulerResult {\n\t// 1. Request syntax validation\n\tconst syntaxError = validateScheduleRequest(request);\n\tif (syntaxError) return syntaxError;\n\n\t// 2. Handle missing scheduler\n\tlet isNullRecord = false;\n\tif (record === null) {\n\t\tif (request.expectedSchedulerRevision !== 0) {\n\t\t\treturn errorResult(\n\t\t\t\t\"ENOENT\",\n\t\t\t\t`Scheduler not found; expectedSchedulerRevision must be 0, got ${request.expectedSchedulerRevision}`,\n\t\t\t);\n\t\t}\n\t\trecord = initializeContinuationScheduler(\"\", \"\");\n\t\tisNullRecord = true;\n\t}\n\n\t// 3. Structural integrity (skip for freshly constructed record from null)\n\tif (!isNullRecord) {\n\t\tconst structError = validateSchedulerStructure(record);\n\t\tif (structError) return structError;\n\t}\n\n\t// 4. EventId lookup and idempotency\n\tconst idemResult = checkIdempotency(record, request, \"SCHEDULE\");\n\tif (idemResult) return idemResult;\n\n\t// 5. Fresh guards\n\tif (record.schedulerRevision !== request.expectedSchedulerRevision) {\n\t\treturn errorResult(\n\t\t\t\"STALE_SCHEDULER_REVISION\",\n\t\t\t`Expected scheduler revision ${request.expectedSchedulerRevision}, actual ${record.schedulerRevision}`,\n\t\t);\n\t}\n\tif (record.state !== \"IDLE\") {\n\t\treturn errorResult(\"INVALID_STATE\", `Expected IDLE but scheduler is ${record.state}`);\n\t}\n\n\t// SCHEDULE: observedExecutionRevision == expectedExecutionRevision\n\tconst observedExecutionRevision = request.expectedExecutionRevision;\n\n\t// 7. Build event\n\tconst eventId = request.eventId;\n\tconst cycleId = eventId; // cycleId === eventId for SCHEDULE\n\tconst fromState: ContinuationSchedulerState = \"IDLE\";\n\tconst toState: ContinuationSchedulerState = \"SCHEDULED\";\n\tconst revisionBefore = record.schedulerRevision;\n\tconst revisionAfter = revisionBefore + 1;\n\tconst previousEventDigest = record.historyDigest;\n\n\tconst createdAt = new Date().toISOString();\n\n\tconst event: ContinuationSchedulerEvent = {\n\t\teventId,\n\t\tkind: \"SCHEDULE\",\n\t\tcycleId,\n\t\trequestSchedulerRevision: request.expectedSchedulerRevision,\n\t\tobservedExecutionRevision,\n\t\tfromState,\n\t\ttoState,\n\t\trevisionBefore,\n\t\trevisionAfter,\n\t\tpreviousEventDigest,\n\t\teventDigest: \"\", // computed below\n\t\tcreatedAt,\n\t\texpectedExecutionRevision: request.expectedExecutionRevision,\n\t};\n\n\tconst eventDigest = computeEventDigest(event);\n\t(event as { eventDigest: string }).eventDigest = eventDigest;\n\n\tconst newEvents = [...record.events, event];\n\tconst historyDigest = computeHistoryDigest(newEvents);\n\n\tconst newRecord: ContinuationSchedulerRecord = {\n\t\tschedulerVersion: 1,\n\t\texecutionId: record.executionId,\n\t\tcontractDigest: record.contractDigest,\n\t\tschedulerRevision: revisionAfter,\n\t\tstate: toState,\n\t\tevents: newEvents,\n\t\thistoryDigest,\n\t};\n\n\treturn { ok: true, record: newRecord, event };\n}\n\n// =============================================================================\n// Mutation: DISPATCH\n// =============================================================================\n\nexport function dispatchContinuation(\n\trecord: ContinuationSchedulerRecord | null,\n\trequest: DispatchContinuationRequest,\n\tcurrentExecutionRevision: number,\n): ContinuationSchedulerResult {\n\t// 1. Request syntax validation\n\tconst syntaxError = validateDispatchRequest(request);\n\tif (syntaxError) return syntaxError;\n\n\t// 2. Missing scheduler → ENOENT\n\tif (record === null) {\n\t\treturn errorResult(\"ENOENT\", \"Scheduler not found\");\n\t}\n\n\t// 3. Structural integrity\n\tconst structError = validateSchedulerStructure(record);\n\tif (structError) return structError;\n\n\t// 4. EventId lookup and idempotency\n\tconst idemResult = checkIdempotency(record, request, \"DISPATCH\");\n\tif (idemResult) return idemResult;\n\n\t// 5. Fresh guards\n\tif (record.schedulerRevision !== request.expectedSchedulerRevision) {\n\t\treturn errorResult(\n\t\t\t\"STALE_SCHEDULER_REVISION\",\n\t\t\t`Expected scheduler revision ${request.expectedSchedulerRevision}, actual ${record.schedulerRevision}`,\n\t\t);\n\t}\n\tif (record.state !== \"SCHEDULED\") {\n\t\treturn errorResult(\"INVALID_STATE\", `Expected SCHEDULED but scheduler is ${record.state}`);\n\t}\n\n\t// 6. Cycle validation: find the active SCHEDULE event\n\tconst activeScheduleEvent = findActiveScheduleEvent(record);\n\tif (activeScheduleEvent === null) {\n\t\treturn errorResult(\"INVALID_CYCLE\", \"No active SCHEDULE event found\");\n\t}\n\tif (activeScheduleEvent.eventId !== request.cycleId) {\n\t\treturn errorResult(\n\t\t\t\"INVALID_CYCLE\",\n\t\t\t`cycleId ${request.cycleId} does not match active SCHEDULE event ${activeScheduleEvent.eventId}`,\n\t\t);\n\t}\n\n\t// 7. Supersession guard\n\tif (currentExecutionRevision > activeScheduleEvent.expectedExecutionRevision!) {\n\t\treturn errorResult(\n\t\t\t\"CYCLE_SUPERSEDED\",\n\t\t\t`Active cycle expectedExecutionRevision ${activeScheduleEvent.expectedExecutionRevision} is superseded by current execution revision ${currentExecutionRevision}`,\n\t\t);\n\t}\n\tif (currentExecutionRevision < activeScheduleEvent.expectedExecutionRevision!) {\n\t\treturn errorResult(\n\t\t\t\"EXECUTION_REVISION_MISMATCH\",\n\t\t\t`Current execution revision ${currentExecutionRevision} is less than active expectedExecutionRevision ${activeScheduleEvent.expectedExecutionRevision}`,\n\t\t);\n\t}\n\n\t// DISPATCH: observedExecutionRevision == active expectedExecutionRevision\n\tconst observedExecutionRevision = activeScheduleEvent.expectedExecutionRevision!;\n\n\tconst fromState: ContinuationSchedulerState = \"SCHEDULED\";\n\tconst toState: ContinuationSchedulerState = \"DISPATCHED\";\n\tconst revisionBefore = record.schedulerRevision;\n\tconst revisionAfter = revisionBefore + 1;\n\tconst previousEventDigest = record.historyDigest;\n\n\tconst createdAt = new Date().toISOString();\n\n\tconst event: ContinuationSchedulerEvent = {\n\t\teventId: request.eventId,\n\t\tkind: \"DISPATCH\",\n\t\tcycleId: request.cycleId,\n\t\trequestSchedulerRevision: request.expectedSchedulerRevision,\n\t\tobservedExecutionRevision,\n\t\tfromState,\n\t\ttoState,\n\t\trevisionBefore,\n\t\trevisionAfter,\n\t\tpreviousEventDigest,\n\t\teventDigest: \"\",\n\t\tcreatedAt,\n\t\tdispatchedContinuationId: request.dispatchedContinuationId,\n\t};\n\n\tconst eventDigest = computeEventDigest(event);\n\t(event as { eventDigest: string }).eventDigest = eventDigest;\n\n\tconst newEvents = [...record.events, event];\n\tconst historyDigest = computeHistoryDigest(newEvents);\n\n\tconst newRecord: ContinuationSchedulerRecord = {\n\t\tschedulerVersion: 1,\n\t\texecutionId: record.executionId,\n\t\tcontractDigest: record.contractDigest,\n\t\tschedulerRevision: revisionAfter,\n\t\tstate: toState,\n\t\tevents: newEvents,\n\t\thistoryDigest,\n\t};\n\n\treturn { ok: true, record: newRecord, event };\n}\n\n// =============================================================================\n// Mutation: CONSUME\n// =============================================================================\n\nexport function consumeContinuation(\n\trecord: ContinuationSchedulerRecord | null,\n\trequest: ConsumeContinuationRequest,\n\tcurrentExecutionRevision: number,\n): ContinuationSchedulerResult {\n\t// 1. Request syntax validation\n\tconst syntaxError = validateConsumeRequest(request);\n\tif (syntaxError) return syntaxError;\n\n\t// 2. Missing scheduler → ENOENT\n\tif (record === null) {\n\t\treturn errorResult(\"ENOENT\", \"Scheduler not found\");\n\t}\n\n\t// 3. Structural integrity\n\tconst structError = validateSchedulerStructure(record);\n\tif (structError) return structError;\n\n\t// 4. EventId lookup and idempotency\n\tconst idemResult = checkIdempotency(record, request, \"CONSUME\");\n\tif (idemResult) return idemResult;\n\n\t// 5. Fresh guards\n\tif (record.schedulerRevision !== request.expectedSchedulerRevision) {\n\t\treturn errorResult(\n\t\t\t\"STALE_SCHEDULER_REVISION\",\n\t\t\t`Expected scheduler revision ${request.expectedSchedulerRevision}, actual ${record.schedulerRevision}`,\n\t\t);\n\t}\n\tif (record.state !== \"DISPATCHED\") {\n\t\treturn errorResult(\"INVALID_STATE\", `Expected DISPATCHED but scheduler is ${record.state}`);\n\t}\n\n\t// 6. Cycle validation\n\tconst activeScheduleEvent = findActiveScheduleEvent(record);\n\tif (activeScheduleEvent === null) {\n\t\treturn errorResult(\"INVALID_CYCLE\", \"No active SCHEDULE event found\");\n\t}\n\tif (activeScheduleEvent.eventId !== request.cycleId) {\n\t\treturn errorResult(\n\t\t\t\"INVALID_CYCLE\",\n\t\t\t`cycleId ${request.cycleId} does not match active SCHEDULE event ${activeScheduleEvent.eventId}`,\n\t\t);\n\t}\n\n\t// 7. Supersession guard\n\tif (currentExecutionRevision > activeScheduleEvent.expectedExecutionRevision!) {\n\t\treturn errorResult(\n\t\t\t\"CYCLE_SUPERSEDED\",\n\t\t\t`Active cycle expectedExecutionRevision ${activeScheduleEvent.expectedExecutionRevision} is superseded by current execution revision ${currentExecutionRevision}`,\n\t\t);\n\t}\n\tif (currentExecutionRevision < activeScheduleEvent.expectedExecutionRevision!) {\n\t\treturn errorResult(\n\t\t\t\"EXECUTION_REVISION_MISMATCH\",\n\t\t\t`Current execution revision ${currentExecutionRevision} is less than active expectedExecutionRevision ${activeScheduleEvent.expectedExecutionRevision}`,\n\t\t);\n\t}\n\n\t// Find active DISPATCH event to verify dispatchedContinuationId\n\tconst activeDispatchEvent = findActiveDispatchEvent(record);\n\tif (activeDispatchEvent === null) {\n\t\treturn errorResult(\"INVALID_STATE\", \"No active DISPATCH event found\");\n\t}\n\tif (activeDispatchEvent.dispatchedContinuationId !== request.dispatchedContinuationId) {\n\t\treturn errorResult(\n\t\t\t\"INVALID_REQUEST\",\n\t\t\t`dispatchedContinuationId ${request.dispatchedContinuationId} does not match active DISPATCH event ${activeDispatchEvent.dispatchedContinuationId}`,\n\t\t);\n\t}\n\n\t// CONSUME: observedExecutionRevision == active expectedExecutionRevision\n\tconst observedExecutionRevision = activeScheduleEvent.expectedExecutionRevision!;\n\n\tconst fromState: ContinuationSchedulerState = \"DISPATCHED\";\n\tconst toState: ContinuationSchedulerState = \"IDLE\";\n\tconst revisionBefore = record.schedulerRevision;\n\tconst revisionAfter = revisionBefore + 1;\n\tconst previousEventDigest = record.historyDigest;\n\n\tconst createdAt = new Date().toISOString();\n\n\tconst event: ContinuationSchedulerEvent = {\n\t\teventId: request.eventId,\n\t\tkind: \"CONSUME\",\n\t\tcycleId: request.cycleId,\n\t\trequestSchedulerRevision: request.expectedSchedulerRevision,\n\t\tobservedExecutionRevision,\n\t\tfromState,\n\t\ttoState,\n\t\trevisionBefore,\n\t\trevisionAfter,\n\t\tpreviousEventDigest,\n\t\teventDigest: \"\",\n\t\tcreatedAt,\n\t\tdispatchedContinuationId: request.dispatchedContinuationId,\n\t\tresultDigest: request.resultDigest,\n\t};\n\n\tconst eventDigest = computeEventDigest(event);\n\t(event as { eventDigest: string }).eventDigest = eventDigest;\n\n\tconst newEvents = [...record.events, event];\n\tconst historyDigest = computeHistoryDigest(newEvents);\n\n\tconst newRecord: ContinuationSchedulerRecord = {\n\t\tschedulerVersion: 1,\n\t\texecutionId: record.executionId,\n\t\tcontractDigest: record.contractDigest,\n\t\tschedulerRevision: revisionAfter,\n\t\tstate: toState,\n\t\tevents: newEvents,\n\t\thistoryDigest,\n\t};\n\n\treturn { ok: true, record: newRecord, event };\n}\n\n// =============================================================================\n// Mutation: CANCEL\n// =============================================================================\n\nexport function cancelContinuation(\n\trecord: ContinuationSchedulerRecord | null,\n\trequest: CancelContinuationRequest,\n\tcurrentExecutionRevision: number,\n): ContinuationSchedulerResult {\n\t// 1. Request syntax validation\n\tconst syntaxError = validateCancelRequest(request);\n\tif (syntaxError) return syntaxError;\n\n\t// 2. Missing scheduler → ENOENT\n\tif (record === null) {\n\t\treturn errorResult(\"ENOENT\", \"Scheduler not found\");\n\t}\n\n\t// 3. Structural integrity\n\tconst structError = validateSchedulerStructure(record);\n\tif (structError) return structError;\n\n\t// 4. EventId lookup and idempotency\n\tconst idemResult = checkIdempotency(record, request, \"CANCEL\");\n\tif (idemResult) return idemResult;\n\n\t// 5. Fresh guards\n\tif (record.schedulerRevision !== request.expectedSchedulerRevision) {\n\t\treturn errorResult(\n\t\t\t\"STALE_SCHEDULER_REVISION\",\n\t\t\t`Expected scheduler revision ${request.expectedSchedulerRevision}, actual ${record.schedulerRevision}`,\n\t\t);\n\t}\n\tif (record.state !== \"SCHEDULED\" && record.state !== \"DISPATCHED\") {\n\t\treturn errorResult(\"INVALID_STATE\", `Expected SCHEDULED or DISPATCHED but scheduler is ${record.state}`);\n\t}\n\n\t// 6. Cycle validation\n\tconst activeScheduleEvent = findActiveScheduleEvent(record);\n\tif (activeScheduleEvent === null) {\n\t\treturn errorResult(\"INVALID_CYCLE\", \"No active SCHEDULE event found\");\n\t}\n\tif (activeScheduleEvent.eventId !== request.cycleId) {\n\t\treturn errorResult(\n\t\t\t\"INVALID_CYCLE\",\n\t\t\t`cycleId ${request.cycleId} does not match active SCHEDULE event ${activeScheduleEvent.eventId}`,\n\t\t);\n\t}\n\n\t// CANCEL: observedExecutionRevision >= active expectedExecutionRevision\n\tif (currentExecutionRevision < activeScheduleEvent.expectedExecutionRevision!) {\n\t\treturn errorResult(\n\t\t\t\"EXECUTION_REVISION_MISMATCH\",\n\t\t\t`Current execution revision ${currentExecutionRevision} is less than active expectedExecutionRevision ${activeScheduleEvent.expectedExecutionRevision}`,\n\t\t);\n\t}\n\tconst observedExecutionRevision = currentExecutionRevision;\n\n\tconst fromState = record.state;\n\tconst toState: ContinuationSchedulerState = \"IDLE\";\n\tconst revisionBefore = record.schedulerRevision;\n\tconst revisionAfter = revisionBefore + 1;\n\tconst previousEventDigest = record.historyDigest;\n\n\tconst createdAt = new Date().toISOString();\n\n\tconst event: ContinuationSchedulerEvent = {\n\t\teventId: request.eventId,\n\t\tkind: \"CANCEL\",\n\t\tcycleId: request.cycleId,\n\t\trequestSchedulerRevision: request.expectedSchedulerRevision,\n\t\tobservedExecutionRevision,\n\t\tfromState,\n\t\ttoState,\n\t\trevisionBefore,\n\t\trevisionAfter,\n\t\tpreviousEventDigest,\n\t\teventDigest: \"\",\n\t\tcreatedAt,\n\t};\n\n\tconst eventDigest = computeEventDigest(event);\n\t(event as { eventDigest: string }).eventDigest = eventDigest;\n\n\tconst newEvents = [...record.events, event];\n\tconst historyDigest = computeHistoryDigest(newEvents);\n\n\tconst newRecord: ContinuationSchedulerRecord = {\n\t\tschedulerVersion: 1,\n\t\texecutionId: record.executionId,\n\t\tcontractDigest: record.contractDigest,\n\t\tschedulerRevision: revisionAfter,\n\t\tstate: toState,\n\t\tevents: newEvents,\n\t\thistoryDigest,\n\t};\n\n\treturn { ok: true, record: newRecord, event };\n}\n\n// =============================================================================\n// Mutation: ABANDON\n// =============================================================================\n\nexport function abandonContinuation(\n\trecord: ContinuationSchedulerRecord | null,\n\trequest: AbandonContinuationRequest,\n\tcurrentExecutionRevision: number,\n): ContinuationSchedulerResult {\n\t// 1. Request syntax validation\n\tconst syntaxError = validateAbandonRequest(request);\n\tif (syntaxError) return syntaxError;\n\n\t// 2. Missing scheduler → ENOENT\n\tif (record === null) {\n\t\treturn errorResult(\"ENOENT\", \"Scheduler not found\");\n\t}\n\n\t// 3. Structural integrity\n\tconst structError = validateSchedulerStructure(record);\n\tif (structError) return structError;\n\n\t// 4. EventId lookup and idempotency\n\tconst idemResult = checkIdempotency(record, request, \"ABANDON\");\n\tif (idemResult) return idemResult;\n\n\t// 5. Fresh guards\n\tif (record.schedulerRevision !== request.expectedSchedulerRevision) {\n\t\treturn errorResult(\n\t\t\t\"STALE_SCHEDULER_REVISION\",\n\t\t\t`Expected scheduler revision ${request.expectedSchedulerRevision}, actual ${record.schedulerRevision}`,\n\t\t);\n\t}\n\tif (record.state !== \"SCHEDULED\" && record.state !== \"DISPATCHED\") {\n\t\treturn errorResult(\"INVALID_STATE\", `Expected SCHEDULED or DISPATCHED but scheduler is ${record.state}`);\n\t}\n\n\t// 6. Cycle validation\n\tconst activeScheduleEvent = findActiveScheduleEvent(record);\n\tif (activeScheduleEvent === null) {\n\t\treturn errorResult(\"INVALID_CYCLE\", \"No active SCHEDULE event found\");\n\t}\n\tif (activeScheduleEvent.eventId !== request.cycleId) {\n\t\treturn errorResult(\n\t\t\t\"INVALID_CYCLE\",\n\t\t\t`cycleId ${request.cycleId} does not match active SCHEDULE event ${activeScheduleEvent.eventId}`,\n\t\t);\n\t}\n\n\t// ABANDON: observedExecutionRevision > active expectedExecutionRevision\n\tif (currentExecutionRevision < activeScheduleEvent.expectedExecutionRevision!) {\n\t\treturn errorResult(\n\t\t\t\"EXECUTION_REVISION_MISMATCH\",\n\t\t\t`Current execution revision ${currentExecutionRevision} is less than active expectedExecutionRevision ${activeScheduleEvent.expectedExecutionRevision}`,\n\t\t);\n\t}\n\tif (currentExecutionRevision === activeScheduleEvent.expectedExecutionRevision!) {\n\t\treturn errorResult(\n\t\t\t\"CYCLE_NOT_SUPERSEDED\",\n\t\t\t`Current execution revision ${currentExecutionRevision} equals active expectedExecutionRevision; cycle is not superseded`,\n\t\t);\n\t}\n\tconst observedExecutionRevision = currentExecutionRevision;\n\n\tconst fromState = record.state;\n\tconst toState: ContinuationSchedulerState = \"IDLE\";\n\tconst revisionBefore = record.schedulerRevision;\n\tconst revisionAfter = revisionBefore + 1;\n\tconst previousEventDigest = record.historyDigest;\n\n\tconst createdAt = new Date().toISOString();\n\n\tconst event: ContinuationSchedulerEvent = {\n\t\teventId: request.eventId,\n\t\tkind: \"ABANDON\",\n\t\tcycleId: request.cycleId,\n\t\trequestSchedulerRevision: request.expectedSchedulerRevision,\n\t\tobservedExecutionRevision,\n\t\tfromState,\n\t\ttoState,\n\t\trevisionBefore,\n\t\trevisionAfter,\n\t\tpreviousEventDigest,\n\t\teventDigest: \"\",\n\t\tcreatedAt,\n\t};\n\n\tconst eventDigest = computeEventDigest(event);\n\t(event as { eventDigest: string }).eventDigest = eventDigest;\n\n\tconst newEvents = [...record.events, event];\n\tconst historyDigest = computeHistoryDigest(newEvents);\n\n\tconst newRecord: ContinuationSchedulerRecord = {\n\t\tschedulerVersion: 1,\n\t\texecutionId: record.executionId,\n\t\tcontractDigest: record.contractDigest,\n\t\tschedulerRevision: revisionAfter,\n\t\tstate: toState,\n\t\tevents: newEvents,\n\t\thistoryDigest,\n\t};\n\n\treturn { ok: true, record: newRecord, event };\n}\n\n// =============================================================================\n// Inspection\n// =============================================================================\n\nexport function inspectContinuationScheduler(record: ContinuationSchedulerRecord): ContinuationSchedulerInspection {\n\tconst structError = validateSchedulerStructure(record);\n\tif (structError) {\n\t\treturn {\n\t\t\tvalid: false,\n\t\t\texecutionId: \"\",\n\t\t\tcontractDigest: \"\",\n\t\t\tschedulerRevision: 0,\n\t\t\tstate: \"IDLE\",\n\t\t\teventCount: 0,\n\t\t\thistoryDigest: null,\n\t\t\terror: structError.error,\n\t\t};\n\t}\n\n\t// Replay events to verify structural consistency\n\tconst replayError = replayAndVerifyEvents(record.events);\n\tif (replayError) {\n\t\treturn {\n\t\t\tvalid: false,\n\t\t\texecutionId: record.executionId,\n\t\t\tcontractDigest: record.contractDigest,\n\t\t\tschedulerRevision: record.schedulerRevision,\n\t\t\tstate: record.state,\n\t\t\teventCount: record.events.length,\n\t\t\thistoryDigest: record.historyDigest,\n\t\t\terror: replayError,\n\t\t};\n\t}\n\n\treturn {\n\t\tvalid: true,\n\t\texecutionId: record.executionId,\n\t\tcontractDigest: record.contractDigest,\n\t\tschedulerRevision: record.schedulerRevision,\n\t\tstate: record.state,\n\t\teventCount: record.events.length,\n\t\thistoryDigest: record.historyDigest,\n\t};\n}\n\n// =============================================================================\n// Validation (contract-bound + execution-bound)\n// =============================================================================\n\nexport function validateContinuationScheduler(\n\trecord: ContinuationSchedulerRecord,\n\tcontractDigest: string,\n\texecutionId: string,\n\t_executionRevision: number,\n): ContinuationSchedulerValidation {\n\tconst contractBound = record.contractDigest === contractDigest;\n\tconst executionBound = record.executionId === executionId;\n\n\tconst structError = validateSchedulerStructure(record);\n\tif (structError) {\n\t\treturn {\n\t\t\tvalid: false,\n\t\t\tschedulerRevision: record.schedulerRevision,\n\t\t\tstate: record.state,\n\t\t\teventCount: record.events.length,\n\t\t\tcontractBound,\n\t\t\texecutionBound,\n\t\t\tsemanticValid: false,\n\t\t\terror: structError.error,\n\t\t};\n\t}\n\n\tconst replayError = replayAndVerifyEvents(record.events);\n\tconst semanticValid = replayError === null;\n\n\tconst valid = contractBound && executionBound && semanticValid;\n\n\tlet error: string | undefined;\n\tif (!valid) {\n\t\tconst parts: string[] = [];\n\t\tif (!contractBound) parts.push(\"contract digest mismatch\");\n\t\tif (!executionBound) parts.push(\"execution ID mismatch\");\n\t\tif (!semanticValid) parts.push(replayError ?? \"semantic validation failed\");\n\t\terror = parts.join(\"; \");\n\t}\n\n\treturn {\n\t\tvalid,\n\t\tschedulerRevision: record.schedulerRevision,\n\t\tstate: record.state,\n\t\teventCount: record.events.length,\n\t\tcontractBound,\n\t\texecutionBound,\n\t\tsemanticValid,\n\t\terror,\n\t};\n}\n\n// =============================================================================\n// Replay and verification\n// =============================================================================\n\nfunction replayAndVerifyEvents(events: readonly ContinuationSchedulerEvent[]): string | null {\n\tlet expectedRevision = 0;\n\tlet expectedState: ContinuationSchedulerState = \"IDLE\";\n\tlet expectedCycleId: string | null = null;\n\tlet expectedDispatchedContinuationId: string | null = null;\n\tlet expectedObservedExecutionRevision: number | null = null;\n\tlet previousEventDigest: string | null = null;\n\tconst seenEventIds = new Set<string>();\n\n\tfor (let i = 0; i < events.length; i++) {\n\t\tconst event = events[i];\n\n\t\t// Verify event digest\n\t\tconst computedDigest = computeEventDigest(event);\n\t\tif (computedDigest !== event.eventDigest) {\n\t\t\treturn `Event ${i} (${event.eventId}): digest mismatch — expected ${computedDigest}, got ${event.eventDigest}`;\n\t\t}\n\n\t\t// Verify previousEventDigest chaining\n\t\tif (event.previousEventDigest !== previousEventDigest) {\n\t\t\treturn `Event ${i} (${event.eventId}): previousEventDigest mismatch — expected ${previousEventDigest}, got ${event.previousEventDigest}`;\n\t\t}\n\n\t\t// Verify no duplicate eventId\n\t\tif (seenEventIds.has(event.eventId)) {\n\t\t\treturn `Event ${i}: duplicate eventId ${event.eventId}`;\n\t\t}\n\t\tseenEventIds.add(event.eventId);\n\n\t\t// Verify revision progression\n\t\tif (event.revisionBefore !== expectedRevision) {\n\t\t\treturn `Event ${i} (${event.eventId}): revisionBefore ${event.revisionBefore} != expected ${expectedRevision}`;\n\t\t}\n\t\tif (event.revisionAfter !== event.revisionBefore + 1) {\n\t\t\treturn `Event ${i} (${event.eventId}): revisionAfter ${event.revisionAfter} != revisionBefore + 1 (${event.revisionBefore + 1})`;\n\t\t}\n\n\t\t// Verify state machine\n\t\tif (event.fromState !== expectedState) {\n\t\t\treturn `Event ${i} (${event.eventId}): fromState ${event.fromState} != expected ${expectedState}`;\n\t\t}\n\n\t\tswitch (event.kind) {\n\t\t\tcase \"SCHEDULE\": {\n\t\t\t\tif (event.fromState !== \"IDLE\")\n\t\t\t\t\treturn `Event ${i}: SCHEDULE fromState must be IDLE, got ${event.fromState}`;\n\t\t\t\tif (event.toState !== \"SCHEDULED\")\n\t\t\t\t\treturn `Event ${i}: SCHEDULE toState must be SCHEDULED, got ${event.toState}`;\n\t\t\t\tif (event.cycleId !== event.eventId) {\n\t\t\t\t\treturn `Event ${i}: SCHEDULE cycleId (${event.cycleId}) must equal eventId (${event.eventId})`;\n\t\t\t\t}\n\t\t\t\tif (event.expectedExecutionRevision === undefined) {\n\t\t\t\t\treturn `Event ${i}: SCHEDULE missing expectedExecutionRevision`;\n\t\t\t\t}\n\t\t\t\tif (event.observedExecutionRevision !== event.expectedExecutionRevision) {\n\t\t\t\t\treturn `Event ${i}: SCHEDULE observedExecutionRevision (${event.observedExecutionRevision}) != expectedExecutionRevision (${event.expectedExecutionRevision})`;\n\t\t\t\t}\n\t\t\t\texpectedCycleId = event.cycleId;\n\t\t\t\texpectedObservedExecutionRevision = event.expectedExecutionRevision;\n\t\t\t\texpectedDispatchedContinuationId = null;\n\t\t\t\texpectedState = \"SCHEDULED\";\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"DISPATCH\": {\n\t\t\t\tif (event.fromState !== \"SCHEDULED\")\n\t\t\t\t\treturn `Event ${i}: DISPATCH fromState must be SCHEDULED, got ${event.fromState}`;\n\t\t\t\tif (event.toState !== \"DISPATCHED\")\n\t\t\t\t\treturn `Event ${i}: DISPATCH toState must be DISPATCHED, got ${event.toState}`;\n\t\t\t\tif (event.cycleId !== expectedCycleId) {\n\t\t\t\t\treturn `Event ${i}: DISPATCH cycleId (${event.cycleId}) != active cycle (${expectedCycleId})`;\n\t\t\t\t}\n\t\t\t\tif (event.observedExecutionRevision !== expectedObservedExecutionRevision) {\n\t\t\t\t\treturn `Event ${i}: DISPATCH observedExecutionRevision (${event.observedExecutionRevision}) != active expectedExecutionRevision (${expectedObservedExecutionRevision})`;\n\t\t\t\t}\n\t\t\t\tif (event.dispatchedContinuationId === undefined) {\n\t\t\t\t\treturn `Event ${i}: DISPATCH missing dispatchedContinuationId`;\n\t\t\t\t}\n\t\t\t\texpectedDispatchedContinuationId = event.dispatchedContinuationId;\n\t\t\t\texpectedState = \"DISPATCHED\";\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"CONSUME\": {\n\t\t\t\tif (event.fromState !== \"DISPATCHED\")\n\t\t\t\t\treturn `Event ${i}: CONSUME fromState must be DISPATCHED, got ${event.fromState}`;\n\t\t\t\tif (event.toState !== \"IDLE\") return `Event ${i}: CONSUME toState must be IDLE, got ${event.toState}`;\n\t\t\t\tif (event.cycleId !== expectedCycleId) {\n\t\t\t\t\treturn `Event ${i}: CONSUME cycleId (${event.cycleId}) != active cycle (${expectedCycleId})`;\n\t\t\t\t}\n\t\t\t\tif (event.observedExecutionRevision !== expectedObservedExecutionRevision) {\n\t\t\t\t\treturn `Event ${i}: CONSUME observedExecutionRevision (${event.observedExecutionRevision}) != active expectedExecutionRevision (${expectedObservedExecutionRevision})`;\n\t\t\t\t}\n\t\t\t\tif (event.dispatchedContinuationId !== expectedDispatchedContinuationId) {\n\t\t\t\t\treturn `Event ${i}: CONSUME dispatchedContinuationId (${event.dispatchedContinuationId}) != active (${expectedDispatchedContinuationId})`;\n\t\t\t\t}\n\t\t\t\tif (event.resultDigest === undefined || event.resultDigest.trim().length === 0) {\n\t\t\t\t\treturn `Event ${i}: CONSUME missing or empty resultDigest`;\n\t\t\t\t}\n\t\t\t\texpectedCycleId = null;\n\t\t\t\texpectedObservedExecutionRevision = null;\n\t\t\t\texpectedDispatchedContinuationId = null;\n\t\t\t\texpectedState = \"IDLE\";\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"CANCEL\": {\n\t\t\t\tif (event.fromState !== \"SCHEDULED\" && event.fromState !== \"DISPATCHED\") {\n\t\t\t\t\treturn `Event ${i}: CANCEL fromState must be SCHEDULED or DISPATCHED, got ${event.fromState}`;\n\t\t\t\t}\n\t\t\t\tif (event.toState !== \"IDLE\") return `Event ${i}: CANCEL toState must be IDLE, got ${event.toState}`;\n\t\t\t\tif (event.cycleId !== expectedCycleId) {\n\t\t\t\t\treturn `Event ${i}: CANCEL cycleId (${event.cycleId}) != active cycle (${expectedCycleId})`;\n\t\t\t\t}\n\t\t\t\tif (\n\t\t\t\t\texpectedObservedExecutionRevision !== null &&\n\t\t\t\t\tevent.observedExecutionRevision < expectedObservedExecutionRevision\n\t\t\t\t) {\n\t\t\t\t\treturn `Event ${i}: CANCEL observedExecutionRevision (${event.observedExecutionRevision}) < active expectedExecutionRevision (${expectedObservedExecutionRevision})`;\n\t\t\t\t}\n\t\t\t\texpectedCycleId = null;\n\t\t\t\texpectedObservedExecutionRevision = null;\n\t\t\t\texpectedDispatchedContinuationId = null;\n\t\t\t\texpectedState = \"IDLE\";\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"ABANDON\": {\n\t\t\t\tif (event.fromState !== \"SCHEDULED\" && event.fromState !== \"DISPATCHED\") {\n\t\t\t\t\treturn `Event ${i}: ABANDON fromState must be SCHEDULED or DISPATCHED, got ${event.fromState}`;\n\t\t\t\t}\n\t\t\t\tif (event.toState !== \"IDLE\") return `Event ${i}: ABANDON toState must be IDLE, got ${event.toState}`;\n\t\t\t\tif (event.cycleId !== expectedCycleId) {\n\t\t\t\t\treturn `Event ${i}: ABANDON cycleId (${event.cycleId}) != active cycle (${expectedCycleId})`;\n\t\t\t\t}\n\t\t\t\tif (\n\t\t\t\t\texpectedObservedExecutionRevision !== null &&\n\t\t\t\t\tevent.observedExecutionRevision <= expectedObservedExecutionRevision\n\t\t\t\t) {\n\t\t\t\t\treturn `Event ${i}: ABANDON observedExecutionRevision (${event.observedExecutionRevision}) must be > active expectedExecutionRevision (${expectedObservedExecutionRevision})`;\n\t\t\t\t}\n\t\t\t\texpectedCycleId = null;\n\t\t\t\texpectedObservedExecutionRevision = null;\n\t\t\t\texpectedDispatchedContinuationId = null;\n\t\t\t\texpectedState = \"IDLE\";\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn `Event ${i}: unknown kind ${(event as ContinuationSchedulerEvent).kind}`;\n\t\t}\n\n\t\texpectedRevision = event.revisionAfter;\n\t\t// Compute history digest of all events up to and including this one\n\t\t// This becomes the expected previousEventDigest for the NEXT event\n\t\tconst eventsUpToHere = events.slice(0, i + 1);\n\t\tconst expectedHistoryDigest = computeHistoryDigest(eventsUpToHere);\n\t\tpreviousEventDigest = expectedHistoryDigest;\n\t}\n\n\t// Verify final history digest matches the running computation\n\tif (events.length > 0) {\n\t\tconst finalHistoryDigest = computeHistoryDigest(events);\n\t\tif (finalHistoryDigest !== previousEventDigest) {\n\t\t\t// previousEventDigest holds the history digest of all events\n\t\t\t// This check is redundant but serves as a sanity check\n\t\t}\n\t}\n\n\treturn null;\n}\n\n// =============================================================================\n// Digest computation\n// =============================================================================\n\nfunction computeEventDigest(event: ContinuationSchedulerEvent): string {\n\tconst canonical: Record<string, unknown> = {};\n\n\t// Required fields (in sorted-key order, always present)\n\tcanonical.cycleId = event.cycleId;\n\tcanonical.eventId = event.eventId;\n\tcanonical.fromState = event.fromState;\n\tcanonical.kind = event.kind;\n\tcanonical.observedExecutionRevision = event.observedExecutionRevision;\n\tcanonical.previousEventDigest = event.previousEventDigest;\n\tcanonical.requestSchedulerRevision = event.requestSchedulerRevision;\n\tcanonical.revisionAfter = event.revisionAfter;\n\tcanonical.revisionBefore = event.revisionBefore;\n\tcanonical.toState = event.toState;\n\n\t// Conditional fields — include only when present\n\tif (event.expectedExecutionRevision !== undefined) {\n\t\tcanonical.expectedExecutionRevision = event.expectedExecutionRevision;\n\t}\n\tif (event.dispatchedContinuationId !== undefined) {\n\t\tcanonical.dispatchedContinuationId = event.dispatchedContinuationId;\n\t}\n\tif (event.resultDigest !== undefined) {\n\t\tcanonical.resultDigest = event.resultDigest;\n\t}\n\n\t// Excluded from digest: createdAt, eventDigest\n\n\tconst json = toCanonicalJson(canonical);\n\tconst hash = createHash(\"sha256\");\n\thash.update(json, \"utf-8\");\n\treturn `sha256:${hash.digest(\"hex\")}`;\n}\n\nfunction computeHistoryDigest(events: readonly ContinuationSchedulerEvent[]): string | null {\n\tif (events.length === 0) return null;\n\n\tconst concatenated = events.map((e) => e.eventDigest).join(\"\");\n\tconst hash = createHash(\"sha256\");\n\thash.update(concatenated, \"utf-8\");\n\treturn `sha256:${hash.digest(\"hex\")}`;\n}\n\n// =============================================================================\n// Event lookup helpers\n// =============================================================================\n\nfunction findActiveScheduleEvent(record: ContinuationSchedulerRecord): ContinuationSchedulerEvent | null {\n\t// The active SCHEDULE event is the last SCHEDULE in the events array\n\t// that hasn't been consumed/cancelled/abandoned\n\tfor (let i = record.events.length - 1; i >= 0; i--) {\n\t\tif (record.events[i].kind === \"SCHEDULE\") {\n\t\t\t// Check if this cycle is still active (not consumed, cancelled, or abandoned)\n\t\t\tconst cycleId = record.events[i].eventId;\n\t\t\tlet closed = false;\n\t\t\tfor (let j = i + 1; j < record.events.length; j++) {\n\t\t\t\tif (\n\t\t\t\t\trecord.events[j].cycleId === cycleId &&\n\t\t\t\t\t(record.events[j].kind === \"CONSUME\" ||\n\t\t\t\t\t\trecord.events[j].kind === \"CANCEL\" ||\n\t\t\t\t\t\trecord.events[j].kind === \"ABANDON\")\n\t\t\t\t) {\n\t\t\t\t\tclosed = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!closed) return record.events[i];\n\t\t}\n\t}\n\treturn null;\n}\n\nfunction findActiveDispatchEvent(record: ContinuationSchedulerRecord): ContinuationSchedulerEvent | null {\n\tfor (let i = record.events.length - 1; i >= 0; i--) {\n\t\tif (record.events[i].kind === \"DISPATCH\") {\n\t\t\t// Check if this dispatch is still active (the cycle hasn't been consumed/cancelled/abandoned yet)\n\t\t\tconst cycleId = record.events[i].cycleId;\n\t\t\tlet closed = false;\n\t\t\tfor (let j = i + 1; j < record.events.length; j++) {\n\t\t\t\tif (\n\t\t\t\t\trecord.events[j].cycleId === cycleId &&\n\t\t\t\t\t(record.events[j].kind === \"CONSUME\" ||\n\t\t\t\t\t\trecord.events[j].kind === \"CANCEL\" ||\n\t\t\t\t\t\trecord.events[j].kind === \"ABANDON\")\n\t\t\t\t) {\n\t\t\t\t\tclosed = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!closed) return record.events[i];\n\t\t}\n\t}\n\treturn null;\n}\n\n// =============================================================================\n// Idempotency\n// =============================================================================\n\nfunction checkIdempotency(\n\trecord: ContinuationSchedulerRecord,\n\trequest: ContinuationSchedulerRequest,\n\tkind: ContinuationSchedulerEventKind,\n): ContinuationSchedulerResult | null {\n\t// Check if an event with this eventId already exists\n\tconst existingEvent = record.events.find((e) => e.eventId === request.eventId);\n\tif (!existingEvent) return null;\n\n\t// EventId exists — verify fingerprint matches using the explicit kind\n\tconst fingerprint = computeRequestFingerprint(request, kind);\n\n\t// Also check kind matches\n\tif (existingEvent.kind !== fingerprint.kind) {\n\t\treturn errorResult(\n\t\t\t\"IDEMPOTENCY_CONFLICT\",\n\t\t\t`eventId ${request.eventId} exists with kind ${existingEvent.kind}, request is ${fingerprint.kind}`,\n\t\t);\n\t}\n\n\t// Verify fingerprint matches exactly\n\tif (fingerprint.kind === \"SCHEDULE\") {\n\t\tconst f = fingerprint as {\n\t\t\tkind: \"SCHEDULE\";\n\t\t\tcycleId: string;\n\t\t\trequestSchedulerRevision: number;\n\t\t\texpectedExecutionRevision: number;\n\t\t};\n\t\tif (\n\t\t\texistingEvent.cycleId !== f.cycleId ||\n\t\t\texistingEvent.requestSchedulerRevision !== f.requestSchedulerRevision ||\n\t\t\texistingEvent.expectedExecutionRevision !== f.expectedExecutionRevision\n\t\t) {\n\t\t\treturn errorResult(\"IDEMPOTENCY_CONFLICT\", `eventId ${request.eventId} exists with different fingerprint`);\n\t\t}\n\t} else if (fingerprint.kind === \"DISPATCH\") {\n\t\tconst f = fingerprint as {\n\t\t\tkind: \"DISPATCH\";\n\t\t\tcycleId: string;\n\t\t\trequestSchedulerRevision: number;\n\t\t\tdispatchedContinuationId: string;\n\t\t};\n\t\tif (\n\t\t\texistingEvent.cycleId !== f.cycleId ||\n\t\t\texistingEvent.requestSchedulerRevision !== f.requestSchedulerRevision ||\n\t\t\texistingEvent.dispatchedContinuationId !== f.dispatchedContinuationId\n\t\t) {\n\t\t\treturn errorResult(\"IDEMPOTENCY_CONFLICT\", `eventId ${request.eventId} exists with different fingerprint`);\n\t\t}\n\t} else if (fingerprint.kind === \"CONSUME\") {\n\t\tconst f = fingerprint as {\n\t\t\tkind: \"CONSUME\";\n\t\t\tcycleId: string;\n\t\t\trequestSchedulerRevision: number;\n\t\t\tdispatchedContinuationId: string;\n\t\t\tresultDigest: string;\n\t\t};\n\t\tif (\n\t\t\texistingEvent.cycleId !== f.cycleId ||\n\t\t\texistingEvent.requestSchedulerRevision !== f.requestSchedulerRevision ||\n\t\t\texistingEvent.dispatchedContinuationId !== f.dispatchedContinuationId ||\n\t\t\texistingEvent.resultDigest !== f.resultDigest\n\t\t) {\n\t\t\treturn errorResult(\"IDEMPOTENCY_CONFLICT\", `eventId ${request.eventId} exists with different fingerprint`);\n\t\t}\n\t} else if (fingerprint.kind === \"CANCEL\") {\n\t\tconst f = fingerprint as { kind: \"CANCEL\"; cycleId: string; requestSchedulerRevision: number };\n\t\tif (\n\t\t\texistingEvent.cycleId !== f.cycleId ||\n\t\t\texistingEvent.requestSchedulerRevision !== f.requestSchedulerRevision\n\t\t) {\n\t\t\treturn errorResult(\"IDEMPOTENCY_CONFLICT\", `eventId ${request.eventId} exists with different fingerprint`);\n\t\t}\n\t} else if (fingerprint.kind === \"ABANDON\") {\n\t\tconst f = fingerprint as { kind: \"ABANDON\"; cycleId: string; requestSchedulerRevision: number };\n\t\tif (\n\t\t\texistingEvent.cycleId !== f.cycleId ||\n\t\t\texistingEvent.requestSchedulerRevision !== f.requestSchedulerRevision\n\t\t) {\n\t\t\treturn errorResult(\"IDEMPOTENCY_CONFLICT\", `eventId ${request.eventId} exists with different fingerprint`);\n\t\t}\n\t}\n\n\t// Exact retry — return the existing event unchanged\n\treturn { ok: true, record, event: existingEvent };\n}\n\nfunction computeRequestFingerprint(\n\trequest: ContinuationSchedulerRequest,\n\tkind: ContinuationSchedulerEventKind,\n): { kind: ContinuationSchedulerEventKind } & Record<string, unknown> {\n\t// Use the explicit kind to determine the fingerprint, not structurally-ambiguous type guards\n\tif (kind === \"SCHEDULE\") {\n\t\tconst r = request as ScheduleContinuationRequest;\n\t\treturn {\n\t\t\tkind: \"SCHEDULE\",\n\t\t\tcycleId: r.eventId,\n\t\t\trequestSchedulerRevision: r.expectedSchedulerRevision,\n\t\t\texpectedExecutionRevision: r.expectedExecutionRevision,\n\t\t};\n\t}\n\tif (kind === \"DISPATCH\") {\n\t\tconst r = request as DispatchContinuationRequest;\n\t\treturn {\n\t\t\tkind: \"DISPATCH\",\n\t\t\tcycleId: r.cycleId,\n\t\t\trequestSchedulerRevision: r.expectedSchedulerRevision,\n\t\t\tdispatchedContinuationId: r.dispatchedContinuationId,\n\t\t};\n\t}\n\tif (kind === \"CONSUME\") {\n\t\tconst r = request as ConsumeContinuationRequest;\n\t\treturn {\n\t\t\tkind: \"CONSUME\",\n\t\t\tcycleId: r.cycleId,\n\t\t\trequestSchedulerRevision: r.expectedSchedulerRevision,\n\t\t\tdispatchedContinuationId: r.dispatchedContinuationId,\n\t\t\tresultDigest: r.resultDigest,\n\t\t};\n\t}\n\tif (kind === \"CANCEL\") {\n\t\tconst r = request as CancelContinuationRequest;\n\t\treturn {\n\t\t\tkind: \"CANCEL\",\n\t\t\tcycleId: r.cycleId,\n\t\t\trequestSchedulerRevision: r.expectedSchedulerRevision,\n\t\t};\n\t}\n\t// ABANDON\n\tconst r = request as AbandonContinuationRequest;\n\treturn {\n\t\tkind: \"ABANDON\",\n\t\tcycleId: r.cycleId,\n\t\trequestSchedulerRevision: r.expectedSchedulerRevision,\n\t};\n}\n\n// =============================================================================\n// Syntactic request validation\n// =============================================================================\n\nfunction validateScheduleRequest(request: ScheduleContinuationRequest): ContinuationSchedulerResult | null {\n\tif (!request.eventId || typeof request.eventId !== \"string\" || request.eventId.trim().length === 0) {\n\t\treturn errorResult(\"INVALID_REQUEST\", \"eventId is required and must be a non-empty string\");\n\t}\n\tif (!Number.isSafeInteger(request.expectedSchedulerRevision) || request.expectedSchedulerRevision < 0) {\n\t\treturn errorResult(\"INVALID_REQUEST\", \"expectedSchedulerRevision must be a non-negative safe integer\");\n\t}\n\tif (!Number.isSafeInteger(request.expectedExecutionRevision) || request.expectedExecutionRevision < 0) {\n\t\treturn errorResult(\"INVALID_REQUEST\", \"expectedExecutionRevision must be a non-negative safe integer\");\n\t}\n\treturn null;\n}\n\nfunction validateDispatchRequest(request: DispatchContinuationRequest): ContinuationSchedulerResult | null {\n\tif (!request.eventId || typeof request.eventId !== \"string\" || request.eventId.trim().length === 0) {\n\t\treturn errorResult(\"INVALID_REQUEST\", \"eventId is required and must be a non-empty string\");\n\t}\n\tif (!request.cycleId || typeof request.cycleId !== \"string\" || request.cycleId.trim().length === 0) {\n\t\treturn errorResult(\"INVALID_REQUEST\", \"cycleId is required and must be a non-empty string\");\n\t}\n\tif (!Number.isSafeInteger(request.expectedSchedulerRevision) || request.expectedSchedulerRevision < 0) {\n\t\treturn errorResult(\"INVALID_REQUEST\", \"expectedSchedulerRevision must be a non-negative safe integer\");\n\t}\n\tif (\n\t\t!request.dispatchedContinuationId ||\n\t\ttypeof request.dispatchedContinuationId !== \"string\" ||\n\t\trequest.dispatchedContinuationId.trim().length === 0\n\t) {\n\t\treturn errorResult(\"INVALID_REQUEST\", \"dispatchedContinuationId is required and must be a non-empty string\");\n\t}\n\treturn null;\n}\n\nfunction validateConsumeRequest(request: ConsumeContinuationRequest): ContinuationSchedulerResult | null {\n\tif (!request.eventId || typeof request.eventId !== \"string\" || request.eventId.trim().length === 0) {\n\t\treturn errorResult(\"INVALID_REQUEST\", \"eventId is required and must be a non-empty string\");\n\t}\n\tif (!request.cycleId || typeof request.cycleId !== \"string\" || request.cycleId.trim().length === 0) {\n\t\treturn errorResult(\"INVALID_REQUEST\", \"cycleId is required and must be a non-empty string\");\n\t}\n\tif (!Number.isSafeInteger(request.expectedSchedulerRevision) || request.expectedSchedulerRevision < 0) {\n\t\treturn errorResult(\"INVALID_REQUEST\", \"expectedSchedulerRevision must be a non-negative safe integer\");\n\t}\n\tif (\n\t\t!request.dispatchedContinuationId ||\n\t\ttypeof request.dispatchedContinuationId !== \"string\" ||\n\t\trequest.dispatchedContinuationId.trim().length === 0\n\t) {\n\t\treturn errorResult(\"INVALID_REQUEST\", \"dispatchedContinuationId is required and must be a non-empty string\");\n\t}\n\tif (!request.resultDigest || typeof request.resultDigest !== \"string\" || request.resultDigest.trim().length === 0) {\n\t\treturn errorResult(\"INVALID_REQUEST\", \"resultDigest is required and must be a non-empty string\");\n\t}\n\treturn null;\n}\n\nfunction validateCancelRequest(request: CancelContinuationRequest): ContinuationSchedulerResult | null {\n\tif (!request.eventId || typeof request.eventId !== \"string\" || request.eventId.trim().length === 0) {\n\t\treturn errorResult(\"INVALID_REQUEST\", \"eventId is required and must be a non-empty string\");\n\t}\n\tif (!request.cycleId || typeof request.cycleId !== \"string\" || request.cycleId.trim().length === 0) {\n\t\treturn errorResult(\"INVALID_REQUEST\", \"cycleId is required and must be a non-empty string\");\n\t}\n\tif (!Number.isSafeInteger(request.expectedSchedulerRevision) || request.expectedSchedulerRevision < 0) {\n\t\treturn errorResult(\"INVALID_REQUEST\", \"expectedSchedulerRevision must be a non-negative safe integer\");\n\t}\n\treturn null;\n}\n\nfunction validateAbandonRequest(request: AbandonContinuationRequest): ContinuationSchedulerResult | null {\n\tif (!request.eventId || typeof request.eventId !== \"string\" || request.eventId.trim().length === 0) {\n\t\treturn errorResult(\"INVALID_REQUEST\", \"eventId is required and must be a non-empty string\");\n\t}\n\tif (!request.cycleId || typeof request.cycleId !== \"string\" || request.cycleId.trim().length === 0) {\n\t\treturn errorResult(\"INVALID_REQUEST\", \"cycleId is required and must be a non-empty string\");\n\t}\n\tif (!Number.isSafeInteger(request.expectedSchedulerRevision) || request.expectedSchedulerRevision < 0) {\n\t\treturn errorResult(\"INVALID_REQUEST\", \"expectedSchedulerRevision must be a non-negative safe integer\");\n\t}\n\treturn null;\n}\n\n// =============================================================================\n// Structural validation\n// =============================================================================\n\nfunction validateSchedulerStructure(record: ContinuationSchedulerRecord): ContinuationSchedulerResult | null {\n\tif (record.schedulerVersion !== 1) {\n\t\treturn errorResult(\"INVALID_SCHEDULER_RECORD\", `schedulerVersion must be 1, got ${record.schedulerVersion}`);\n\t}\n\tif (typeof record.executionId !== \"string\" || record.executionId.trim().length === 0) {\n\t\treturn errorResult(\"INVALID_SCHEDULER_RECORD\", \"executionId must be a non-empty string\");\n\t}\n\tif (typeof record.contractDigest !== \"string\" || record.contractDigest.trim().length === 0) {\n\t\treturn errorResult(\"INVALID_SCHEDULER_RECORD\", \"contractDigest must be a non-empty string\");\n\t}\n\tif (!Number.isSafeInteger(record.schedulerRevision) || record.schedulerRevision < 0) {\n\t\treturn errorResult(\"INVALID_SCHEDULER_RECORD\", \"schedulerRevision must be a non-negative safe integer\");\n\t}\n\tconst validStates: ReadonlySet<string> = new Set([\"IDLE\", \"SCHEDULED\", \"DISPATCHED\"]);\n\tif (!validStates.has(record.state)) {\n\t\treturn errorResult(\n\t\t\t\"INVALID_SCHEDULER_RECORD\",\n\t\t\t`state must be IDLE, SCHEDULED, or DISPATCHED, got ${record.state}`,\n\t\t);\n\t}\n\tif (!Array.isArray(record.events)) {\n\t\treturn errorResult(\"INVALID_SCHEDULER_RECORD\", \"events must be an array\");\n\t}\n\tif (record.historyDigest !== null && typeof record.historyDigest !== \"string\") {\n\t\treturn errorResult(\"INVALID_SCHEDULER_RECORD\", \"historyDigest must be null or a string\");\n\t}\n\n\t// Verify historyDigest matches events\n\tconst expectedHistoryDigest = computeHistoryDigest(record.events);\n\tif (record.historyDigest !== expectedHistoryDigest) {\n\t\treturn errorResult(\"INVALID_SCHEDULER_RECORD\", \"historyDigest does not match events\");\n\t}\n\n\treturn null;\n}\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nfunction errorResult(code: ContinuationSchedulerErrorCode, error: string): ContinuationSchedulerResult {\n\treturn { ok: false, code, error };\n}\n"]}