/** * Durable Mission Coordinator (2.4.0, ownership 2.7.0). * * Composes the canonical mission lifecycle with a DurableMissionStore at the * mission execution boundary. The parent/domain layer never remembers to write * files; it talks to this coordinator, and the coordinator persists every * authoritative lifecycle transition before and after the executor runs. * * Execution ownership: * - Resume is explicit and always allocates a NEW execution attempt. * - Before any executor work begins, the coordinator atomically acquires a * first-class execution lease with a monotonically increasing fencing * token. Two processes racing to resume the same mission therefore have * exactly one winner; the loser receives a structured MISSION_OWNED error. * - Every execution-authoritative mutation carries lease proof * (`leaseId` + `fencingToken`); the store rejects a fenced owner. * - Terminal transition clears the lease atomically in the same write, so a * completed mission can never be modified by its former owner. * * Design constraints: * - The coordinator depends only on the injected `DurableMissionStore` port * and a `MissionExecutor` seam — never on process/provider/CLI/UI code. * - Opening/recovering the store NEVER auto-runs mission work. * - Recovery revokes only expired (dead) leases; it never steals a live lease. */ import { type DurableMissionRecord, type DurableMissionStore } from "./durable-store.js"; import { type HeartbeatScheduler, type HeartbeatTelemetry } from "./execution-heartbeat.js"; import { type ExecutionLease, type ExecutionLeaseProof } from "./execution-lease.js"; import type { MissionExecutionObserver, MissionExecutor, MissionLaunchOptions } from "./mission-executor.js"; import type { MissionRequest } from "./mission-request.js"; import { type MissionState } from "./mission-state.js"; export interface DurableMissionCoordinatorOptions { now?: () => number; /** * Durable attempt identity factory. Defaults to a UUID-based id. It is * coordinator-scoped and distinct from the executor's `executionId`. */ attemptIdFactory?: () => string; /** * Executor owner identity. Defaults to a fresh host+UUID identity per * coordinator instance (never PID-derived). Pass one stable value per * executor/process lifetime for diagnostics. */ ownerId?: string; /** Execution lease lifetime. Defaults to a conservative 30 minutes. */ leaseDurationMs?: number; /** Lease identity factory (tests). */ leaseIdFactory?: () => string; /** Heartbeat renewal cadence override (defaults to leaseDurationMs / 3). */ heartbeatIntervalMs?: number; /** Heartbeat safety margin override (defaults to leaseDurationMs / 6). */ renewalSafetyMarginMs?: number; /** Injectable timer scheduler for deterministic heartbeat tests. */ heartbeatScheduler?: HeartbeatScheduler; /** Assignment identity used for execution/Governance attribution. */ assignmentId?: string; /** Observer for actual attempt/execution lifecycle events; recovery/polling is excluded. */ executionObserver?: MissionExecutionObserver; } export interface DurableRecoveryReport { scanned: number; /** Missions transitioned to INTERRUPTED during this recovery. */ reconciled: string[]; /** Missions already INTERRUPTED (no additional action taken). */ alreadyRecovered: string[]; /** Terminal or CREATED missions left unchanged. */ unchanged: string[]; /** Corrupt records surfaced structurally (never silently dropped). */ corrupt: { missionId: string; diagnostic: string; }[]; /** Human-readable recovery actions. */ actions: string[]; } /** The authoritative lease acquired for one mission, plus the record after acquisition. */ export interface AcquiredExecutionOwnership { record: DurableMissionRecord; lease: ExecutionLease; } export declare class DurableMissionCoordinator { private readonly _store; private readonly _executor; private readonly _now; private readonly _attemptIdFactory; private readonly _ownerId; private readonly _leaseDurationMs; private readonly _leaseIdFactory; private readonly _heartbeatTiming; private readonly _heartbeatScheduler; private readonly _executionObserver?; private readonly _assignmentId?; private readonly _heartbeats; private readonly _activeExecutions; constructor(store: DurableMissionStore, executor: MissionExecutor, options?: DurableMissionCoordinatorOptions); /** The underlying persistence port (exposed for tests and load paths). */ get store(): DurableMissionStore; get executor(): MissionExecutor; /** Stable executor owner identity used for lease acquisition. */ get ownerId(): string; /** * Latest heartbeat telemetry for a mission this coordinator executed. The * heartbeat object (and thus its telemetry) survives stop so callers can * inspect renewal/loss state after completion. Returns `undefined` before * the coordinator has ever run the mission. */ heartbeatTelemetry(missionId: string): HeartbeatTelemetry | undefined; /** * Request cancellation of a locally-active execution. This aborts the same * combined signal the executor received, so the normal fenced terminal path * persists CANCELLED. It never signals a remote/other-process owner and * never bypasses fencing. */ requestCancellation(missionId: string, reason?: string): Promise<{ status: "cancel_requested" | "not_running"; missionId: string; }>; /** * Persist a mission at CREATED state. Does not launch anything. * * Re-creating the same missionId with an identical immutable request is * idempotent; re-creating with a different request is rejected structurally * (history is never silently overwritten). */ createMission(request: MissionRequest): Promise; /** Load a mission, or `undefined` when missing. Corrupt records throw. */ getMission(missionId: string): Promise; listMissions(): Promise; listNonterminalMissions(): Promise; listChildren(parentMissionId: string): Promise; /** * Atomically acquire execution ownership for a CREATED or INTERRUPTED * mission. Transitions the mission to QUEUED and persists a new execution * lease with a strictly greater fencing token. Exactly one of any set of * racing cross-process callers wins; losers get structured errors. * * Does NOT invoke the executor. */ acquireOwnership(missionId: string): Promise; /** * Renew a live lease. Requires current lease proof and does NOT change the * fencing token (renewal is a heartbeat, not a takeover). */ renewOwnership(missionId: string, proof: ExecutionLeaseProof, options?: { now?: number; }): Promise<{ lease: ExecutionLease; record: DurableMissionRecord; }>; /** * Release a live lease without reaching a terminal state. This is a clean * non-terminal stop: any active non-terminal state moves to INTERRUPTED * (recoverable) and ownership is cleared atomically. Terminal completion * should instead clear the lease as part of the terminal write. */ releaseOwnership(missionId: string, proof: ExecutionLeaseProof, options?: { now?: number; }): Promise; /** * Inspect every persisted non-terminal mission and reconcile lost executor * ownership. Persisted active states (QUEUED/RUNNING/WAITING/BLOCKED/ * RETRYING) are never blindly trusted across a restart: when they have no * live lease they transition to INTERRUPTED with a recovery reason. A live * (non-expired) lease is never stolen. Terminal missions stay terminal; * CREATED missions stay CREATED; already-interrupted missions are left * alone. Nothing is executed. */ recover(options?: { now?: number; }): Promise; /** * Interrupt a stale worker's still-live execution ownership. Unlike * `recover()` (which only revokes EXPIRED leases), this closes the crash * window where a prior worker process died but its execution lease has not * yet expired. The caller must prove the stale owner identity (for example, * via a persisted assignment's `executionOwnerIdentity.ownerId`). * * Exactly like recovery, this NEVER auto-runs work and NEVER fabricates a * terminal result: it moves an active non-terminal mission to INTERRUPTED * (recoverable) and clears the lease for a future explicit resume. */ interruptStaleExecution(missionId: string, options: { staleOwnerId: string; reason?: string; now?: number; }): Promise<{ status: "reconciled" | "unchanged" | "missing"; previousState?: MissionState; }>; /** * Explicitly start a mission (or restart a recovered one) to terminal state. * * The logical mission identity (`missionId`, `parentMissionId`, `depth`, * immutable `request`, and lifecycle history) is preserved, but the executor * allocates a NEW `executionId` for the attempt. The previous attempt remains * in `attempts` for auditability and is never overwritten. */ resume(missionId: string, options?: MissionLaunchOptions): Promise; /** * Build a terminal FAILED record for an executor that rejected `launch`. * No execution id was ever established, so `resultExecutionId` stays unset; * the attempt remains auditable with `endReason: CRASHED`. The lease is * cleared atomically with the terminal transition. */ private _failLaunch; private _emitExecutionEvent; private _buildHeartbeat; private _withTransition; private _commitFenced; private _mapRenewResult; private _revokeInterrupted; } //# sourceMappingURL=durable-coordinator.d.ts.map