/** * Frozen v2 event projector retained only for migration/archive verification. * It must remain byte-semantics compatible with historical events.ndjson, but * it is no longer an execution or recovery entrypoint. */ import type { WorkflowEvent } from './schema.js'; import type { ErrorClass, ErrorPayload, OutputRef } from './payloads.js'; export type RunStatus = 'pending' | 'running' | 'waiting' | 'succeeded' | 'failed' | 'cancelled'; export type NodeStatus = 'idle' | 'triggered' | 'running' | 'waiting' | 'retrying' | 'succeeded' | 'failed' | 'skipped' | 'cancelled'; export type ActivityStatus = 'pending' | 'acquired' | 'running' | 'waiting' | 'effectAttempting' | 'succeeded' | 'failed' | 'timedOut' | 'cancelled'; export type AttemptState = { attemptId: string; attemptNumber: number; inputRef: OutputRef; status: ActivityStatus; leaseId?: string; timeoutMs?: number; maxOutputBytes?: number; effectAttempted?: { idempotencyKey: string; inputHash: string; idempotencyTtlMs: number; provider: string; attemptedAtEventId: string; /** Wall-clock timestamp the effectAttempted envelope landed (ms epoch). * Resume uses this to evaluate the TTL boundary against `now()` — * cheaper and more deterministic than re-deriving the time from * `eventId` parsing. */ attemptedAtMs: number; }; /** * Latest reconcileResult matched to this attempt by idempotencyKey. * Resume consults this BEFORE re-running the decision tree: if a * previous resume crashed between writing reconcileResult and the * terminal event, replay surfaces the prior decision so the next * resume can finish what the first started (codex Step 7 round 1 * finding 1). reconcileResult.payload doesn't carry attemptId — we * match by idempotencyKey, which uniquely identifies the attempt. */ latestReconcileResult?: { decision: 'replayed' | 'completedByIdempotentSubmit' | 'manual' | 'freshRetry'; capability: 'readOnlyLookup' | 'idempotentSubmit' | 'none'; evidence: Record; eventId: string; }; /** * Cancel-in-flight marker (Step 9). Populated when a `cancelRequested` * targets this activity (directly via `kind=activity`, or — Step 10 * scheduler concern — fan-out from node/run cancel). Resume reads * this to detect dangling cancels (cancelRequested written, terminal * missing) and complete them with `activityCanceled`. * * Cleared / superseded once `activityCanceled` writes a terminal — * but we keep `cancelOriginEventId` recoverable from the request * event itself, so resume doesn't need an in-flight pointer post- * terminal. */ cancelRequest?: { cancelOriginEventId: string; requestedBy: string; reason: string; delivered: boolean; }; /** * Wait state for human-gate / time / condition activities (Step 8). * Populated by `waitCreated` and updated by `waitResolved` / * `waitDeadlineExceeded`. Resume reads this to recover dangling * wait resolutions (resolved/exceeded but no terminal written). */ wait?: { waitKind: 'human-gate' | 'time' | 'condition'; deadlineAt?: number; /** Inline prompt — only set for small prompts (≤1 KiB producer policy) * AND for historical pre-v0.1.3 events. When the prompt was spilled * to a blob, `prompt` is undefined and consumers must use `promptRef` * (full text on demand) or `promptPreview` (cheap, card-safe). */ prompt?: string; /** Blob spill ref (v0.1.3+). Replay never reads the blob; cards * must render `promptPreview` only and dashboard reads on demand. */ promptRef?: OutputRef; /** Short preview carried inline on waitCreated when promptRef is set; * ≤500 chars by schema, byte-budgeted by the producer. */ promptPreview?: string; /** Default `fail` at the consumer. Recorded only when waitCreated * carries the field; absent means caller never specified. */ onTimeout?: 'fail' | 'success'; /** Open when neither resolution nor deadline event has landed. */ resolution?: { kind: 'resolved'; resolution: 'approved' | 'rejected' | 'external'; by: string; comment?: string; eventId: string; } | { kind: 'deadlineExceeded'; deadlineAt: number; exceededAtMs: number; eventId: string; }; }; output?: OutputRef; externalRefs?: Record; error?: ErrorPayload; runningMs?: number; cancelOriginEventId?: string; }; export type ActivityState = { activityId: string; attempts: AttemptState[]; status: ActivityStatus; currentAttemptId?: string; /** * Node that owns this Activity (recorded from `attemptCreated.nodeId`). * Lets us project node.status when activity-level events arrive * (e.g. activityRunning → node.status = 'running'). */ ownerNodeId?: string; }; export type NodeState = { nodeId: string; status: NodeStatus; activityId?: string; retryCount: number; nextAttemptAt?: number; errorClass?: ErrorClass; conditionEventId?: string; cancelOriginEventId?: string; }; export type RunState = { runId: string; status: RunStatus; workflowId?: string; revisionId?: string; initiator?: string; input?: OutputRef; output?: OutputRef; failedNodeId?: string; rootCauseEventId?: string; cancelOriginEventId?: string; /** * Immutable bot identity snapshot captured at runCreated time * (UI doc §3.4). Read by the runtime when spawning workers so that * subsequent bot-registry rename / re-wire doesn't drift execution * away from what was authored. Absent on legacy runs created before * v0.1.3 introduced the field. */ botSnapshots?: Record; }; export type LoopIterationState = { iteration: number; status: 'running' | 'approved' | 'rejected' | 'failed' | 'cancelled'; bodyActivityIds: string[]; decisionActivityId?: string; waitResolvedEventId?: string; decisionBy?: string; decisionComment?: string; timedOut?: boolean; }; export type LoopState = { loopId: string; status: 'running' | 'succeeded' | 'failed' | 'cancelled'; iteration: number; maxIterations: number; iterations: LoopIterationState[]; output?: OutputRef; errorCode?: string; errorClass?: ErrorClass; }; export type Snapshot = { run: RunState; nodes: Map; activities: Map; loops: Map; /** Convenience: terminal outputs by activityId (succeeded events only). */ outputs: Map; /** Last seq seen. 0 if the log is empty. */ lastSeq: number; /** * activityIds with attemptCreated but whose latest attempt has no terminal * event (succeeded/failed/timedOut/cancelled). Consumed by resume in * Step 7 to drive reconcile decisions. */ danglingActivities: string[]; /** * activityIds whose latest attempt wrote effectAttempted but never reached * a terminal event for that attempt. Subset of danglingActivities. */ danglingEffectAttempted: string[]; /** * activityIds with waitCreated but no waitResolved / waitDeadlineExceeded. */ danglingWaits: string[]; /** * activityIds whose wait was resolved (either by waitResolved or by * waitDeadlineExceeded) but whose attempt never reached a terminal * event. Step 8 resume recovery materializes the terminal from the * recorded resolution. Disjoint from `danglingWaits`. */ danglingWaitResolutions: string[]; /** * activityIds with a `cancelRequested` targeting them (directly or * via fan-out — Step 10 scheduler concern), but no terminal yet. * * NOT disjoint from `danglingEffectAttempted` (Step 9 round 1 * finding 1): cancel + effectAttempted is the central case that * `recoverCancelWithReconcile` handles — reconcile fires FIRST to * capture provider evidence, then writes the cancel-flavored * terminal (`activityCanceled` for completedByIdempotentSubmit / * freshRetry; `activityFailed{manual}` when reconcile is * inconclusive). The orchestrator routes the intersection through * the cancel-with-reconcile path; remaining cancels (no effect) * go through plain `recoverCancel`. * * Disjoint from `danglingWaitResolutions` is still upheld — cancel * + wait combinations are skipped by wait recovery; cancel wins. */ danglingCancels: string[]; /** * Run-level cancel intent (Step 9 codex round 1 finding 3). Set when * a `cancelRequested{kind:run}` lands and not yet `runCanceled`. Replay * surfaces it so schedulers / dashboards can see "this run is being * cancelled" without re-scanning the event list. Cleared when * `runCanceled` lands (run.status === 'cancelled'). */ cancelledRunIntent?: { cancelOriginEventId: string; requestedBy: string; reason: string; }; /** * Per-node cancel intents. Same shape and role as `cancelledRunIntent` * but scoped to a node — set when `cancelRequested{kind:node}` lands * and the node hasn't yet reached `nodeCanceled`. First request wins * on overlap (consistent with the activity-level fan-out semantics). */ cancelledNodeIntents: Map; }; /** * Fold an event log into a state snapshot. Read-only — never executes * activity logic, never calls providers, never writes to the log. Events * doc §5.2. * * Throws on: * - empty event list (caller must supply at least the runCreated event * to derive runId) * - first event is not runCreated (state machine forbids — events doc §2.1) * - event.runId mismatch (cross-contamination) * * Does NOT validate state-machine transitions semantically — the log is * authoritative. If transitions look wrong (e.g. activitySucceeded without * attemptCreated), the resulting snapshot will simply have weird state; * verification is the producer's job. */ export declare function replay(events: WorkflowEvent[]): Snapshot; //# sourceMappingURL=replay.d.ts.map