@kontourai/flow-agents 3.11.0 → 3.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/build/src/builder-flow-run-adapter.d.ts +1 -0
- package/build/src/builder-flow-run-adapter.js +2 -0
- package/build/src/builder-flow-runtime.d.ts +5 -0
- package/build/src/builder-flow-runtime.js +90 -68
- package/build/src/builder-gate-action-envelope.d.ts +152 -0
- package/build/src/builder-gate-action-envelope.js +368 -0
- package/build/src/cli/public-contracts.d.ts +139 -0
- package/build/src/cli/public-contracts.js +37 -0
- package/build/src/cli/workflow-sidecar.js +9 -13
- package/build/src/cli/workflow.d.ts +1 -0
- package/build/src/cli/workflow.js +49 -12
- package/build/src/continuation-driver.d.ts +40 -3
- package/build/src/continuation-driver.js +361 -412
- package/build/src/continuation-persistence.d.ts +4 -0
- package/build/src/continuation-persistence.js +296 -0
- package/build/src/continuation-validation.d.ts +9 -0
- package/build/src/continuation-validation.js +169 -0
- package/build/src/flow-kit/action-metadata.d.ts +28 -0
- package/build/src/flow-kit/action-metadata.js +162 -0
- package/build/src/flow-kit/action-repository-validation.d.ts +16 -0
- package/build/src/flow-kit/action-repository-validation.js +237 -0
- package/build/src/flow-kit/validate.d.ts +2 -14
- package/build/src/flow-kit/validate.js +18 -345
- package/build/src/index.d.ts +4 -2
- package/build/src/index.js +2 -1
- package/dist/base/build/package.json +1 -1
- package/dist/base/build/src/builder-flow-run-adapter.d.ts +1 -0
- package/dist/base/build/src/builder-flow-run-adapter.js +2 -0
- package/dist/base/build/src/builder-flow-runtime.d.ts +5 -0
- package/dist/base/build/src/builder-flow-runtime.js +90 -68
- package/dist/base/build/src/builder-gate-action-envelope.d.ts +152 -0
- package/dist/base/build/src/builder-gate-action-envelope.js +368 -0
- package/dist/base/build/src/cli/public-contracts.d.ts +139 -0
- package/dist/base/build/src/cli/public-contracts.js +37 -0
- package/dist/base/build/src/cli/workflow-sidecar.js +9 -13
- package/dist/base/build/src/cli/workflow.d.ts +1 -0
- package/dist/base/build/src/cli/workflow.js +49 -12
- package/dist/base/build/src/continuation-driver.d.ts +40 -3
- package/dist/base/build/src/continuation-driver.js +361 -412
- package/dist/base/build/src/continuation-persistence.d.ts +4 -0
- package/dist/base/build/src/continuation-persistence.js +296 -0
- package/dist/base/build/src/continuation-validation.d.ts +9 -0
- package/dist/base/build/src/continuation-validation.js +169 -0
- package/dist/base/build/src/flow-kit/action-metadata.d.ts +28 -0
- package/dist/base/build/src/flow-kit/action-metadata.js +162 -0
- package/dist/base/build/src/flow-kit/action-repository-validation.d.ts +16 -0
- package/dist/base/build/src/flow-kit/action-repository-validation.js +237 -0
- package/dist/base/build/src/flow-kit/validate.d.ts +2 -14
- package/dist/base/build/src/flow-kit/validate.js +18 -345
- package/dist/base/build/src/index.d.ts +4 -2
- package/dist/base/build/src/index.js +2 -1
- package/dist/base/docs/migrations.md +5 -0
- package/dist/base/docs/public-workflow-cli.md +41 -5
- package/dist/base/docs/spec/builder-flow-runtime.md +117 -0
- package/dist/base/evals/integration/test_builder_step_producers.sh +60 -21
- package/dist/base/evals/integration/test_bundle_install.sh +15 -15
- package/dist/base/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
- package/dist/base/install.sh +1 -1
- package/dist/base/kits/builder/kit.json +88 -14
- package/dist/claude-code/build/package.json +1 -1
- package/dist/claude-code/build/src/builder-flow-run-adapter.d.ts +1 -0
- package/dist/claude-code/build/src/builder-flow-run-adapter.js +2 -0
- package/dist/claude-code/build/src/builder-flow-runtime.d.ts +5 -0
- package/dist/claude-code/build/src/builder-flow-runtime.js +90 -68
- package/dist/claude-code/build/src/builder-gate-action-envelope.d.ts +152 -0
- package/dist/claude-code/build/src/builder-gate-action-envelope.js +368 -0
- package/dist/claude-code/build/src/cli/public-contracts.d.ts +139 -0
- package/dist/claude-code/build/src/cli/public-contracts.js +37 -0
- package/dist/claude-code/build/src/cli/workflow-sidecar.js +9 -13
- package/dist/claude-code/build/src/cli/workflow.d.ts +1 -0
- package/dist/claude-code/build/src/cli/workflow.js +49 -12
- package/dist/claude-code/build/src/continuation-driver.d.ts +40 -3
- package/dist/claude-code/build/src/continuation-driver.js +361 -412
- package/dist/claude-code/build/src/continuation-persistence.d.ts +4 -0
- package/dist/claude-code/build/src/continuation-persistence.js +296 -0
- package/dist/claude-code/build/src/continuation-validation.d.ts +9 -0
- package/dist/claude-code/build/src/continuation-validation.js +169 -0
- package/dist/claude-code/build/src/flow-kit/action-metadata.d.ts +28 -0
- package/dist/claude-code/build/src/flow-kit/action-metadata.js +162 -0
- package/dist/claude-code/build/src/flow-kit/action-repository-validation.d.ts +16 -0
- package/dist/claude-code/build/src/flow-kit/action-repository-validation.js +237 -0
- package/dist/claude-code/build/src/flow-kit/validate.d.ts +2 -14
- package/dist/claude-code/build/src/flow-kit/validate.js +18 -345
- package/dist/claude-code/build/src/index.d.ts +4 -2
- package/dist/claude-code/build/src/index.js +2 -1
- package/dist/claude-code/docs/migrations.md +5 -0
- package/dist/claude-code/docs/public-workflow-cli.md +41 -5
- package/dist/claude-code/docs/spec/builder-flow-runtime.md +117 -0
- package/dist/claude-code/evals/integration/test_builder_step_producers.sh +60 -21
- package/dist/claude-code/evals/integration/test_bundle_install.sh +15 -15
- package/dist/claude-code/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
- package/dist/claude-code/install.sh +1 -1
- package/dist/claude-code/kits/builder/kit.json +88 -14
- package/dist/codex/build/package.json +1 -1
- package/dist/codex/build/src/builder-flow-run-adapter.d.ts +1 -0
- package/dist/codex/build/src/builder-flow-run-adapter.js +2 -0
- package/dist/codex/build/src/builder-flow-runtime.d.ts +5 -0
- package/dist/codex/build/src/builder-flow-runtime.js +90 -68
- package/dist/codex/build/src/builder-gate-action-envelope.d.ts +152 -0
- package/dist/codex/build/src/builder-gate-action-envelope.js +368 -0
- package/dist/codex/build/src/cli/public-contracts.d.ts +139 -0
- package/dist/codex/build/src/cli/public-contracts.js +37 -0
- package/dist/codex/build/src/cli/workflow-sidecar.js +9 -13
- package/dist/codex/build/src/cli/workflow.d.ts +1 -0
- package/dist/codex/build/src/cli/workflow.js +49 -12
- package/dist/codex/build/src/continuation-driver.d.ts +40 -3
- package/dist/codex/build/src/continuation-driver.js +361 -412
- package/dist/codex/build/src/continuation-persistence.d.ts +4 -0
- package/dist/codex/build/src/continuation-persistence.js +296 -0
- package/dist/codex/build/src/continuation-validation.d.ts +9 -0
- package/dist/codex/build/src/continuation-validation.js +169 -0
- package/dist/codex/build/src/flow-kit/action-metadata.d.ts +28 -0
- package/dist/codex/build/src/flow-kit/action-metadata.js +162 -0
- package/dist/codex/build/src/flow-kit/action-repository-validation.d.ts +16 -0
- package/dist/codex/build/src/flow-kit/action-repository-validation.js +237 -0
- package/dist/codex/build/src/flow-kit/validate.d.ts +2 -14
- package/dist/codex/build/src/flow-kit/validate.js +18 -345
- package/dist/codex/build/src/index.d.ts +4 -2
- package/dist/codex/build/src/index.js +2 -1
- package/dist/codex/docs/migrations.md +5 -0
- package/dist/codex/docs/public-workflow-cli.md +41 -5
- package/dist/codex/docs/spec/builder-flow-runtime.md +117 -0
- package/dist/codex/evals/integration/test_builder_step_producers.sh +60 -21
- package/dist/codex/evals/integration/test_bundle_install.sh +15 -15
- package/dist/codex/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
- package/dist/codex/install.sh +1 -1
- package/dist/codex/kits/builder/kit.json +88 -14
- package/dist/kiro/build/package.json +1 -1
- package/dist/kiro/build/src/builder-flow-run-adapter.d.ts +1 -0
- package/dist/kiro/build/src/builder-flow-run-adapter.js +2 -0
- package/dist/kiro/build/src/builder-flow-runtime.d.ts +5 -0
- package/dist/kiro/build/src/builder-flow-runtime.js +90 -68
- package/dist/kiro/build/src/builder-gate-action-envelope.d.ts +152 -0
- package/dist/kiro/build/src/builder-gate-action-envelope.js +368 -0
- package/dist/kiro/build/src/cli/public-contracts.d.ts +139 -0
- package/dist/kiro/build/src/cli/public-contracts.js +37 -0
- package/dist/kiro/build/src/cli/workflow-sidecar.js +9 -13
- package/dist/kiro/build/src/cli/workflow.d.ts +1 -0
- package/dist/kiro/build/src/cli/workflow.js +49 -12
- package/dist/kiro/build/src/continuation-driver.d.ts +40 -3
- package/dist/kiro/build/src/continuation-driver.js +361 -412
- package/dist/kiro/build/src/continuation-persistence.d.ts +4 -0
- package/dist/kiro/build/src/continuation-persistence.js +296 -0
- package/dist/kiro/build/src/continuation-validation.d.ts +9 -0
- package/dist/kiro/build/src/continuation-validation.js +169 -0
- package/dist/kiro/build/src/flow-kit/action-metadata.d.ts +28 -0
- package/dist/kiro/build/src/flow-kit/action-metadata.js +162 -0
- package/dist/kiro/build/src/flow-kit/action-repository-validation.d.ts +16 -0
- package/dist/kiro/build/src/flow-kit/action-repository-validation.js +237 -0
- package/dist/kiro/build/src/flow-kit/validate.d.ts +2 -14
- package/dist/kiro/build/src/flow-kit/validate.js +18 -345
- package/dist/kiro/build/src/index.d.ts +4 -2
- package/dist/kiro/build/src/index.js +2 -1
- package/dist/kiro/docs/migrations.md +5 -0
- package/dist/kiro/docs/public-workflow-cli.md +41 -5
- package/dist/kiro/docs/spec/builder-flow-runtime.md +117 -0
- package/dist/kiro/evals/integration/test_builder_step_producers.sh +60 -21
- package/dist/kiro/evals/integration/test_bundle_install.sh +15 -15
- package/dist/kiro/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
- package/dist/kiro/install.sh +1 -1
- package/dist/kiro/kits/builder/kit.json +88 -14
- package/dist/opencode/build/package.json +1 -1
- package/dist/opencode/build/src/builder-flow-run-adapter.d.ts +1 -0
- package/dist/opencode/build/src/builder-flow-run-adapter.js +2 -0
- package/dist/opencode/build/src/builder-flow-runtime.d.ts +5 -0
- package/dist/opencode/build/src/builder-flow-runtime.js +90 -68
- package/dist/opencode/build/src/builder-gate-action-envelope.d.ts +152 -0
- package/dist/opencode/build/src/builder-gate-action-envelope.js +368 -0
- package/dist/opencode/build/src/cli/public-contracts.d.ts +139 -0
- package/dist/opencode/build/src/cli/public-contracts.js +37 -0
- package/dist/opencode/build/src/cli/workflow-sidecar.js +9 -13
- package/dist/opencode/build/src/cli/workflow.d.ts +1 -0
- package/dist/opencode/build/src/cli/workflow.js +49 -12
- package/dist/opencode/build/src/continuation-driver.d.ts +40 -3
- package/dist/opencode/build/src/continuation-driver.js +361 -412
- package/dist/opencode/build/src/continuation-persistence.d.ts +4 -0
- package/dist/opencode/build/src/continuation-persistence.js +296 -0
- package/dist/opencode/build/src/continuation-validation.d.ts +9 -0
- package/dist/opencode/build/src/continuation-validation.js +169 -0
- package/dist/opencode/build/src/flow-kit/action-metadata.d.ts +28 -0
- package/dist/opencode/build/src/flow-kit/action-metadata.js +162 -0
- package/dist/opencode/build/src/flow-kit/action-repository-validation.d.ts +16 -0
- package/dist/opencode/build/src/flow-kit/action-repository-validation.js +237 -0
- package/dist/opencode/build/src/flow-kit/validate.d.ts +2 -14
- package/dist/opencode/build/src/flow-kit/validate.js +18 -345
- package/dist/opencode/build/src/index.d.ts +4 -2
- package/dist/opencode/build/src/index.js +2 -1
- package/dist/opencode/docs/migrations.md +5 -0
- package/dist/opencode/docs/public-workflow-cli.md +41 -5
- package/dist/opencode/docs/spec/builder-flow-runtime.md +117 -0
- package/dist/opencode/evals/integration/test_builder_step_producers.sh +60 -21
- package/dist/opencode/evals/integration/test_bundle_install.sh +15 -15
- package/dist/opencode/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
- package/dist/opencode/install.sh +1 -1
- package/dist/opencode/kits/builder/kit.json +88 -14
- package/dist/pi/build/package.json +1 -1
- package/dist/pi/build/src/builder-flow-run-adapter.d.ts +1 -0
- package/dist/pi/build/src/builder-flow-run-adapter.js +2 -0
- package/dist/pi/build/src/builder-flow-runtime.d.ts +5 -0
- package/dist/pi/build/src/builder-flow-runtime.js +90 -68
- package/dist/pi/build/src/builder-gate-action-envelope.d.ts +152 -0
- package/dist/pi/build/src/builder-gate-action-envelope.js +368 -0
- package/dist/pi/build/src/cli/public-contracts.d.ts +139 -0
- package/dist/pi/build/src/cli/public-contracts.js +37 -0
- package/dist/pi/build/src/cli/workflow-sidecar.js +9 -13
- package/dist/pi/build/src/cli/workflow.d.ts +1 -0
- package/dist/pi/build/src/cli/workflow.js +49 -12
- package/dist/pi/build/src/continuation-driver.d.ts +40 -3
- package/dist/pi/build/src/continuation-driver.js +361 -412
- package/dist/pi/build/src/continuation-persistence.d.ts +4 -0
- package/dist/pi/build/src/continuation-persistence.js +296 -0
- package/dist/pi/build/src/continuation-validation.d.ts +9 -0
- package/dist/pi/build/src/continuation-validation.js +169 -0
- package/dist/pi/build/src/flow-kit/action-metadata.d.ts +28 -0
- package/dist/pi/build/src/flow-kit/action-metadata.js +162 -0
- package/dist/pi/build/src/flow-kit/action-repository-validation.d.ts +16 -0
- package/dist/pi/build/src/flow-kit/action-repository-validation.js +237 -0
- package/dist/pi/build/src/flow-kit/validate.d.ts +2 -14
- package/dist/pi/build/src/flow-kit/validate.js +18 -345
- package/dist/pi/build/src/index.d.ts +4 -2
- package/dist/pi/build/src/index.js +2 -1
- package/dist/pi/docs/migrations.md +5 -0
- package/dist/pi/docs/public-workflow-cli.md +41 -5
- package/dist/pi/docs/spec/builder-flow-runtime.md +117 -0
- package/dist/pi/evals/integration/test_builder_step_producers.sh +60 -21
- package/dist/pi/evals/integration/test_bundle_install.sh +15 -15
- package/dist/pi/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
- package/dist/pi/install.sh +1 -1
- package/dist/pi/kits/builder/kit.json +88 -14
- package/docs/migrations.md +5 -0
- package/docs/public-workflow-cli.md +41 -5
- package/docs/spec/builder-flow-runtime.md +117 -0
- package/evals/integration/test_builder_step_producers.sh +60 -21
- package/evals/integration/test_bundle_install.sh +15 -15
- package/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
- package/kits/builder/kit.json +88 -14
- package/package.json +1 -1
- package/src/builder-flow-run-adapter.ts +3 -0
- package/src/builder-flow-runtime.ts +73 -46
- package/src/builder-gate-action-envelope.ts +531 -0
- package/src/cli/builder-flow-runtime.test.mjs +289 -58
- package/src/cli/continuation-driver.test.mjs +542 -24
- package/src/cli/kit-metadata-security.test.mjs +161 -8
- package/src/cli/public-contracts.ts +41 -0
- package/src/cli/workflow-sidecar.ts +9 -13
- package/src/cli/workflow.ts +48 -12
- package/src/continuation-driver.ts +466 -378
- package/src/continuation-persistence.ts +228 -0
- package/src/continuation-validation.ts +137 -0
- package/src/flow-kit/action-metadata.ts +155 -0
- package/src/flow-kit/action-repository-validation.ts +192 -0
- package/src/flow-kit/validate.ts +19 -326
- package/src/index.ts +17 -0
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import { constants as fsConstants } from "node:fs";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
1
|
import * as path from "node:path";
|
|
5
2
|
import { inspectBuilderFlowSession, syncBuilderFlowSession } from "./builder-flow-runtime.js";
|
|
6
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
gateActionProgressSnapshot,
|
|
5
|
+
withGateActionPriorProgress,
|
|
6
|
+
type GateActionEnvelope,
|
|
7
|
+
type GateActionPriorProgress,
|
|
8
|
+
type GateActionProgressSnapshot,
|
|
9
|
+
} from "./builder-gate-action-envelope.js";
|
|
10
|
+
import { createFileContinuationStore } from "./continuation-persistence.js";
|
|
11
|
+
import { assertMaxTurns, assertMissionIdentity, validateSnapshot, validateState, validateTurnResult } from "./continuation-validation.js";
|
|
12
|
+
|
|
13
|
+
export { MAX_CONTINUATION_TURNS, createFileContinuationStore, withContinuationDriverLock } from "./continuation-persistence.js";
|
|
14
|
+
export { MAX_CONTINUATION_ADAPTER_EVIDENCE_BYTES, MAX_CONTINUATION_TURN_RESULT_BYTES } from "./continuation-validation.js";
|
|
7
15
|
|
|
8
16
|
export type ContinuationBarrier =
|
|
9
17
|
| { kind: "pid"; pid: number }
|
|
@@ -16,6 +24,10 @@ export type ContinuationSnapshot = {
|
|
|
16
24
|
disposition: "continue" | "waiting" | "done" | "failed";
|
|
17
25
|
current_step: string;
|
|
18
26
|
next_action: Record<string, unknown> | null;
|
|
27
|
+
/** Additive Builder-specific execution context. Generic adapters may ignore it. */
|
|
28
|
+
gate_action_envelope?: GateActionEnvelope;
|
|
29
|
+
/** Canonical progress observation, available even when no action envelope is emitted. */
|
|
30
|
+
progress_snapshot?: GateActionProgressSnapshot;
|
|
19
31
|
};
|
|
20
32
|
|
|
21
33
|
export type ContinuationTurnRequest = {
|
|
@@ -26,12 +38,15 @@ export type ContinuationTurnRequest = {
|
|
|
26
38
|
iteration: number;
|
|
27
39
|
max_turns: number;
|
|
28
40
|
next_action: Record<string, unknown> | null;
|
|
41
|
+
/** Additive schema-1.0 field; older external adapters remain valid. */
|
|
42
|
+
gate_action_envelope?: GateActionEnvelope;
|
|
29
43
|
};
|
|
30
44
|
|
|
31
45
|
export type ContinuationTurnResult =
|
|
32
46
|
| { status: "completed"; summary?: string; evidence?: Record<string, unknown> }
|
|
33
47
|
| { status: "wait"; barrier: ContinuationBarrier; summary?: string };
|
|
34
48
|
|
|
49
|
+
|
|
35
50
|
export class ContinuationAdapterTimeoutError extends Error {
|
|
36
51
|
readonly code = "CONTINUATION_ADAPTER_TIMEOUT";
|
|
37
52
|
|
|
@@ -53,13 +68,21 @@ export type ContinuationDriverState = {
|
|
|
53
68
|
active_turn_step?: string | null;
|
|
54
69
|
// Anchors the ephemeral signer outside active-turn.json. Legacy state files may omit it.
|
|
55
70
|
active_turn_public_key_digest?: string | null;
|
|
71
|
+
/** Durable recovery marker for an adapter turn without a post-turn measurement. */
|
|
72
|
+
active_turn_phase?: "prepared" | "started" | "measured" | null;
|
|
73
|
+
active_turn_progress?: GateActionProgressSnapshot | null;
|
|
74
|
+
/** Crash-recovery journal entry retained until capture and audit are durable. */
|
|
75
|
+
active_turn_capture?: ContinuationAcceptedTurn | null;
|
|
76
|
+
/** Last canonical progress observed after an adapter turn. Additive to state schema 1.0. */
|
|
77
|
+
last_progress?: GateActionProgressSnapshot | null;
|
|
78
|
+
prior_progress?: GateActionPriorProgress | null;
|
|
56
79
|
pending_barrier: ContinuationBarrier | null;
|
|
57
80
|
updated_at: string;
|
|
58
81
|
};
|
|
59
82
|
|
|
60
83
|
export type ContinuationDriverEvent = {
|
|
61
84
|
schema_version: "1.0";
|
|
62
|
-
type: "started" | "turn_started" | "turn_completed" | "gate_not_advanced" | "turn_failed" | "authority_cleanup_failed" | "parked" | "resumed" | "done" | "budget_exhausted";
|
|
85
|
+
type: "started" | "turn_started" | "turn_completed" | "turn_recovered" | "gate_not_advanced" | "turn_failed" | "authority_cleanup_failed" | "parked" | "resumed" | "done" | "budget_exhausted";
|
|
63
86
|
run_id: string;
|
|
64
87
|
definition_id: string;
|
|
65
88
|
current_step: string;
|
|
@@ -68,6 +91,18 @@ export type ContinuationDriverEvent = {
|
|
|
68
91
|
barrier?: ContinuationBarrier;
|
|
69
92
|
summary?: string;
|
|
70
93
|
failure_kind?: "adapter_error" | "timeout";
|
|
94
|
+
progress?: GateActionPriorProgress;
|
|
95
|
+
turn_id?: string;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export type ContinuationAcceptedTurn = {
|
|
99
|
+
schema_version: "1.0";
|
|
100
|
+
turn_id: string;
|
|
101
|
+
iteration: number;
|
|
102
|
+
request: ContinuationTurnRequest;
|
|
103
|
+
result: ContinuationTurnResult;
|
|
104
|
+
progress: GateActionPriorProgress | null;
|
|
105
|
+
captured_at: string;
|
|
71
106
|
};
|
|
72
107
|
|
|
73
108
|
export interface ContinuationStateStore {
|
|
@@ -76,6 +111,15 @@ export interface ContinuationStateStore {
|
|
|
76
111
|
append(event: ContinuationDriverEvent): void;
|
|
77
112
|
}
|
|
78
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Optional durable accepted-turn journal. Kept separate so existing stores that
|
|
116
|
+
* only implement load/save/append remain valid continuation drivers.
|
|
117
|
+
*/
|
|
118
|
+
export interface ContinuationAcceptedTurnJournal {
|
|
119
|
+
captureAcceptedTurn(turn: ContinuationAcceptedTurn): void;
|
|
120
|
+
acceptedTurns(): ContinuationAcceptedTurn[];
|
|
121
|
+
}
|
|
122
|
+
|
|
79
123
|
export interface ContinuationRuntimePort {
|
|
80
124
|
inspect(): Promise<ContinuationSnapshot>;
|
|
81
125
|
synchronize(): Promise<ContinuationSnapshot>;
|
|
@@ -115,6 +159,7 @@ export interface RunContinuationDriverInput {
|
|
|
115
159
|
waitForBarrier?: (barrier: ContinuationBarrier) => Promise<"ready" | "pending">;
|
|
116
160
|
authorizeTurn?: () => Promise<void>;
|
|
117
161
|
issueTurnAuthority?: (request: ContinuationTurnRequest) => Promise<ContinuationTurnAuthority>;
|
|
162
|
+
preflightTurn?: (request: ContinuationTurnRequest) => void | Promise<void>;
|
|
118
163
|
onTurnAccepted?: (request: ContinuationTurnRequest, result: ContinuationTurnResult) => void | Promise<void>;
|
|
119
164
|
now?: () => Date;
|
|
120
165
|
}
|
|
@@ -127,153 +172,298 @@ export interface DriveBuilderFlowSessionInput {
|
|
|
127
172
|
waitForBarrier?: RunContinuationDriverInput["waitForBarrier"];
|
|
128
173
|
authorizeTurn?: RunContinuationDriverInput["authorizeTurn"];
|
|
129
174
|
issueTurnAuthority?: RunContinuationDriverInput["issueTurnAuthority"];
|
|
175
|
+
preflightTurn?: RunContinuationDriverInput["preflightTurn"];
|
|
130
176
|
onTurnAccepted?: RunContinuationDriverInput["onTurnAccepted"];
|
|
131
177
|
now?: () => Date;
|
|
178
|
+
store?: ContinuationStateStore;
|
|
132
179
|
}
|
|
133
180
|
|
|
134
181
|
export async function runContinuationDriver(input: RunContinuationDriverInput): Promise<ContinuationDriverOutcome> {
|
|
135
182
|
assertMaxTurns(input.maxTurns);
|
|
136
183
|
const now = input.now ?? (() => new Date());
|
|
137
|
-
const waitForBarrier = input.waitForBarrier ?? (async () => "pending" as const);
|
|
138
|
-
const authorizeTurn = input.authorizeTurn ?? (async () => {});
|
|
139
|
-
const issueTurnAuthority = input.issueTurnAuthority;
|
|
140
184
|
const adapterCommandIdentity = input.adapterCommandIdentity ?? null;
|
|
141
|
-
|
|
142
|
-
let state = loadOrCreateState(input.store,
|
|
185
|
+
const inspected = validateSnapshot(await input.runtime.inspect());
|
|
186
|
+
let state = loadOrCreateState(input.store, inspected, input.maxTurns, adapterCommandIdentity, now);
|
|
143
187
|
if (state.max_turns !== input.maxTurns) throw new Error(`continuation maxTurns ${input.maxTurns} does not match the persisted mission budget ${state.max_turns}`);
|
|
144
188
|
if (state.adapter_command_identity !== adapterCommandIdentity) throw new Error("continuation adapter command identity does not match the persisted mission adapter");
|
|
189
|
+
let settled = await settleMissionStart(input, state, now);
|
|
190
|
+
if (settled.outcome) return settled.outcome;
|
|
191
|
+
({ state } = settled);
|
|
192
|
+
let snapshot = settled.snapshot;
|
|
193
|
+
while (state.turns_started < input.maxTurns) {
|
|
194
|
+
const turn = await executeContinuationTurn(input, state, snapshot, now);
|
|
195
|
+
if (turn.outcome) return turn.outcome;
|
|
196
|
+
state = turn.state;
|
|
197
|
+
snapshot = turn.snapshot;
|
|
198
|
+
}
|
|
199
|
+
return finishBudgetExhausted(input.store, state, snapshot, now);
|
|
200
|
+
}
|
|
145
201
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (inspectedOutcome === "failed") return finishFailed(input.store, state, snapshot, now);
|
|
202
|
+
type MissionPosition = { state: ContinuationDriverState; snapshot: ContinuationSnapshot; outcome?: ContinuationDriverOutcome };
|
|
203
|
+
type TurnCaptureFailure = Error & { cause: unknown; capture_failure: true };
|
|
149
204
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
205
|
+
async function settleMissionStart(input: RunContinuationDriverInput, state: ContinuationDriverState, now: () => Date): Promise<MissionPosition> {
|
|
206
|
+
let position = await synchronizeAndRecover(input, state, now);
|
|
207
|
+
if (position.state.pending_barrier && new Set(["done", "failed"]).has(canonicalOutcome(position.snapshot))) {
|
|
208
|
+
const barrier = position.state.pending_barrier;
|
|
209
|
+
position.state = saveState(input.store, position.state, { status: "active", pending_barrier: null }, now);
|
|
210
|
+
appendEvent(input.store, position.state, position.snapshot, "resumed", now, { barrier, summary: "canonical Flow resolved the pending barrier" });
|
|
211
|
+
}
|
|
212
|
+
const terminal = resolveCanonicalDisposition(input.store, position.state, position.snapshot, now, false);
|
|
213
|
+
if (terminal) return { ...position, outcome: terminal };
|
|
214
|
+
if (position.state.pending_barrier) {
|
|
215
|
+
const barrier = position.state.pending_barrier;
|
|
216
|
+
const readiness = await (input.waitForBarrier ?? (async () => "pending" as const))(barrier);
|
|
153
217
|
if (readiness === "pending") {
|
|
154
|
-
|
|
155
|
-
return { outcome: "waiting", turns_started:
|
|
218
|
+
const waiting = saveState(input.store, position.state, { status: "waiting" }, now);
|
|
219
|
+
return { state: waiting, snapshot: position.snapshot, outcome: { outcome: "waiting", turns_started: waiting.turns_started, snapshot: position.snapshot, barrier } };
|
|
156
220
|
}
|
|
157
|
-
state = saveState(input.store, state, { status: "active",
|
|
158
|
-
appendEvent(input.store, state, snapshot, "resumed", now, { barrier });
|
|
221
|
+
position.state = saveState(input.store, position.state, { status: "active", pending_barrier: null }, now);
|
|
222
|
+
appendEvent(input.store, position.state, position.snapshot, "resumed", now, { barrier });
|
|
223
|
+
position = await synchronizeAndRecover(input, position.state, now);
|
|
159
224
|
}
|
|
225
|
+
const disposition = resolveCanonicalDisposition(input.store, position.state, position.snapshot, now, true);
|
|
226
|
+
return disposition ? { ...position, outcome: disposition } : position;
|
|
227
|
+
}
|
|
160
228
|
|
|
161
|
-
|
|
229
|
+
async function synchronizeAndRecover(input: RunContinuationDriverInput, state: ContinuationDriverState, now: () => Date): Promise<MissionPosition> {
|
|
230
|
+
const snapshot = validateSnapshot(await input.runtime.synchronize());
|
|
162
231
|
assertMissionIdentity(state, snapshot);
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
232
|
+
const recovered = reconcileInterruptedTurn(input.store, state, snapshot, now);
|
|
233
|
+
return { state: reconcileProgress(input.store, recovered, snapshot, now), snapshot };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function resolveCanonicalDisposition(
|
|
237
|
+
store: ContinuationStateStore,
|
|
238
|
+
state: ContinuationDriverState,
|
|
239
|
+
snapshot: ContinuationSnapshot,
|
|
240
|
+
now: () => Date,
|
|
241
|
+
includeWaiting: boolean,
|
|
242
|
+
): ContinuationDriverOutcome | null {
|
|
243
|
+
const disposition = canonicalOutcome(snapshot);
|
|
244
|
+
if (disposition === "done") return finishDone(store, state, snapshot, now);
|
|
245
|
+
if (disposition === "failed") return finishFailed(store, state, snapshot, now);
|
|
246
|
+
if (includeWaiting && disposition === "waiting") {
|
|
247
|
+
const waiting = saveState(store, state, { status: "waiting" }, now);
|
|
248
|
+
return { outcome: "waiting", turns_started: waiting.turns_started, snapshot };
|
|
169
249
|
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
170
252
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
authority = issueTurnAuthority ? await issueTurnAuthority(request) : undefined;
|
|
196
|
-
if (authority) {
|
|
197
|
-
state = saveState(input.store, state, { active_turn_public_key_digest: authority.publicKeyDigest }, now);
|
|
198
|
-
}
|
|
199
|
-
result = validateTurnResult(await input.runtime.execute(request, authority ? {
|
|
200
|
-
continuationTurnSecret: authority.turnSecret,
|
|
201
|
-
continuationRunId: authority.runId,
|
|
202
|
-
} : undefined));
|
|
203
|
-
} catch (error) {
|
|
204
|
-
const summary = boundedErrorMessage(error);
|
|
205
|
-
appendEvent(input.store, state, snapshot, "turn_failed", now, { summary, failure_kind: error instanceof ContinuationAdapterTimeoutError ? "timeout" : "adapter_error" });
|
|
206
|
-
snapshot = validateSnapshot(await input.runtime.synchronize());
|
|
207
|
-
assertMissionIdentity(state, snapshot);
|
|
208
|
-
const outcome = canonicalOutcome(snapshot);
|
|
209
|
-
if (outcome === "done") {
|
|
210
|
-
terminalized = true;
|
|
211
|
-
return finishDone(input.store, state, snapshot, now);
|
|
212
|
-
}
|
|
213
|
-
if (outcome === "failed") {
|
|
214
|
-
terminalized = true;
|
|
215
|
-
return finishFailed(input.store, state, snapshot, now);
|
|
216
|
-
}
|
|
217
|
-
if (outcome === "waiting") {
|
|
218
|
-
state = saveState(input.store, state, { status: "waiting" }, now);
|
|
219
|
-
return { outcome: "waiting", turns_started: state.turns_started, snapshot };
|
|
220
|
-
}
|
|
221
|
-
continue;
|
|
222
|
-
} finally {
|
|
223
|
-
let cleanupFailure: string | undefined;
|
|
224
|
-
try {
|
|
225
|
-
if (authority && !authority.cleanup()) cleanupFailure = "continuation turn authority cleanup returned false";
|
|
226
|
-
} catch (error) {
|
|
227
|
-
cleanupFailure = boundedErrorMessage(error);
|
|
228
|
-
} finally {
|
|
229
|
-
if (!terminalized) state = saveState(input.store, state, {
|
|
230
|
-
active_turn_step: null,
|
|
231
|
-
active_turn_public_key_digest: null,
|
|
232
|
-
}, now);
|
|
233
|
-
if (cleanupFailure) {
|
|
234
|
-
try {
|
|
235
|
-
appendEvent(input.store, state, snapshot, "authority_cleanup_failed", now, { summary: cleanupFailure });
|
|
236
|
-
} catch {
|
|
237
|
-
// Audit is best effort. A cleanup extension must not replace the adapter outcome.
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
if (input.onTurnAccepted) await input.onTurnAccepted(request, result);
|
|
243
|
-
appendEvent(input.store, state, snapshot, "turn_completed", now, { summary: result.summary });
|
|
244
|
-
|
|
245
|
-
if (result.status === "wait") {
|
|
246
|
-
state = saveState(input.store, state, { status: "waiting", pending_barrier: result.barrier }, now);
|
|
247
|
-
appendEvent(input.store, state, snapshot, "parked", now, { barrier: result.barrier, summary: result.summary });
|
|
248
|
-
const readiness = await waitForBarrier(result.barrier);
|
|
249
|
-
if (readiness === "pending") {
|
|
250
|
-
return { outcome: "waiting", turns_started: state.turns_started, snapshot, barrier: result.barrier };
|
|
251
|
-
}
|
|
252
|
-
state = saveState(input.store, state, { status: "active", pending_barrier: null }, now);
|
|
253
|
-
appendEvent(input.store, state, snapshot, "resumed", now, { barrier: result.barrier });
|
|
254
|
-
}
|
|
253
|
+
async function executeContinuationTurn(
|
|
254
|
+
input: RunContinuationDriverInput,
|
|
255
|
+
current: ContinuationDriverState,
|
|
256
|
+
snapshot: ContinuationSnapshot,
|
|
257
|
+
now: () => Date,
|
|
258
|
+
): Promise<MissionPosition> {
|
|
259
|
+
await (input.authorizeTurn ?? (async () => {}))();
|
|
260
|
+
const request = continuationTurnRequest(input, current, snapshot);
|
|
261
|
+
if (input.preflightTurn) await input.preflightTurn(request);
|
|
262
|
+
let state = beginContinuationTurn(input.store, current, snapshot, request.iteration, now);
|
|
263
|
+
let authority: ContinuationTurnAuthority | undefined;
|
|
264
|
+
try {
|
|
265
|
+
authority = input.issueTurnAuthority ? await input.issueTurnAuthority(request) : undefined;
|
|
266
|
+
if (authority) state = saveState(input.store, state, { active_turn_public_key_digest: authority.publicKeyDigest }, now);
|
|
267
|
+
const result = validateTurnResult(await input.runtime.execute(request, authorityContext(authority)));
|
|
268
|
+
return await recordAcceptedTurn(input, state, snapshot, request, result, now);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
const failed = await recordFailedTurn(input, input.store.load() ?? state, snapshot, error, now);
|
|
271
|
+
if (isTurnCaptureFailure(error)) throw error.cause;
|
|
272
|
+
return failed;
|
|
273
|
+
} finally {
|
|
274
|
+
auditAuthorityCleanup(input.store, state, snapshot, authority, now);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
255
277
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
278
|
+
function continuationTurnRequest(input: RunContinuationDriverInput, state: ContinuationDriverState, snapshot: ContinuationSnapshot): ContinuationTurnRequest {
|
|
279
|
+
const envelope = snapshot.gate_action_envelope
|
|
280
|
+
? withGateActionPriorProgress(snapshot.gate_action_envelope, state.prior_progress ?? initialProgress())
|
|
281
|
+
: undefined;
|
|
282
|
+
return Object.freeze({
|
|
283
|
+
schema_version: "1.0",
|
|
284
|
+
run_id: snapshot.run_id,
|
|
285
|
+
definition_id: snapshot.definition_id,
|
|
286
|
+
current_step: snapshot.current_step,
|
|
287
|
+
iteration: state.turns_started + 1,
|
|
288
|
+
max_turns: input.maxTurns,
|
|
289
|
+
next_action: snapshot.next_action ? structuredClone(snapshot.next_action) : null,
|
|
290
|
+
...(envelope ? { gate_action_envelope: envelope } : {}),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function beginContinuationTurn(store: ContinuationStateStore, state: ContinuationDriverState, snapshot: ContinuationSnapshot, iteration: number, now: () => Date): ContinuationDriverState {
|
|
295
|
+
let started = saveState(store, state, {
|
|
296
|
+
status: "active", turns_started: iteration, active_turn_step: snapshot.current_step,
|
|
297
|
+
active_turn_public_key_digest: null, active_turn_phase: "prepared", active_turn_progress: progressSnapshot(snapshot), active_turn_capture: null,
|
|
298
|
+
}, now);
|
|
299
|
+
appendEvent(store, started, snapshot, "turn_started", now);
|
|
300
|
+
started = saveState(store, started, { active_turn_phase: "started" }, now);
|
|
301
|
+
return started;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function authorityContext(authority: ContinuationTurnAuthority | undefined): ContinuationTurnContext | undefined {
|
|
305
|
+
return authority ? { continuationTurnSecret: authority.turnSecret, continuationRunId: authority.runId } : undefined;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function captureAcceptedTurn(
|
|
309
|
+
callback: NonNullable<RunContinuationDriverInput["onTurnAccepted"]>,
|
|
310
|
+
request: ContinuationTurnRequest,
|
|
311
|
+
result: ContinuationTurnResult,
|
|
312
|
+
): Promise<void> {
|
|
313
|
+
try {
|
|
314
|
+
await callback(request, result);
|
|
315
|
+
} catch (cause) {
|
|
316
|
+
const error = new Error(boundedErrorMessage(cause)) as TurnCaptureFailure;
|
|
317
|
+
error.cause = cause;
|
|
318
|
+
error.capture_failure = true;
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function isTurnCaptureFailure(error: unknown): error is TurnCaptureFailure {
|
|
324
|
+
return error instanceof Error && (error as Partial<TurnCaptureFailure>).capture_failure === true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function recordFailedTurn(
|
|
328
|
+
input: RunContinuationDriverInput,
|
|
329
|
+
state: ContinuationDriverState,
|
|
330
|
+
previous: ContinuationSnapshot,
|
|
331
|
+
error: unknown,
|
|
332
|
+
now: () => Date,
|
|
333
|
+
): Promise<MissionPosition> {
|
|
334
|
+
const measured = state.active_turn_phase === "measured" && state.active_turn_capture
|
|
335
|
+
? { state, snapshot: validateSnapshot(await input.runtime.synchronize()), progress: state.active_turn_capture.progress ?? undefined }
|
|
336
|
+
: await synchronizeTurnMeasurement(input, state, previous, null, null, now);
|
|
337
|
+
appendEvent(input.store, measured.state, measured.snapshot, "turn_failed", now, {
|
|
338
|
+
summary: boundedErrorMessage(error),
|
|
339
|
+
failure_kind: error instanceof ContinuationAdapterTimeoutError ? "timeout" : "adapter_error",
|
|
340
|
+
...(measured.progress ? { progress: measured.progress } : {}),
|
|
341
|
+
...(measured.state.active_turn_capture ? { turn_id: measured.state.active_turn_capture.turn_id } : {}),
|
|
342
|
+
});
|
|
343
|
+
const cleared = clearActiveTurn(input.store, measured.state, now);
|
|
344
|
+
const outcome = resolveCanonicalDisposition(input.store, cleared, measured.snapshot, now, true);
|
|
345
|
+
return outcome ? { state: cleared, snapshot: measured.snapshot, outcome } : { ...measured, state: cleared };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function recordAcceptedTurn(
|
|
349
|
+
input: RunContinuationDriverInput,
|
|
350
|
+
state: ContinuationDriverState,
|
|
351
|
+
previous: ContinuationSnapshot,
|
|
352
|
+
request: ContinuationTurnRequest,
|
|
353
|
+
result: ContinuationTurnResult,
|
|
354
|
+
now: () => Date,
|
|
355
|
+
): Promise<MissionPosition> {
|
|
356
|
+
let measured = await synchronizeTurnMeasurement(input, state, previous, request, result, now);
|
|
357
|
+
const capture = measured.state.active_turn_capture!;
|
|
358
|
+
acceptedTurnJournal(input.store)?.captureAcceptedTurn(capture);
|
|
359
|
+
if (input.onTurnAccepted) await captureAcceptedTurn(input.onTurnAccepted, request, result);
|
|
360
|
+
if (result.status === "wait") return parkAcceptedTurn(input, measured, result, now);
|
|
361
|
+
appendEvent(input.store, measured.state, measured.snapshot, "turn_completed", now, {
|
|
362
|
+
summary: result.summary,
|
|
363
|
+
...(measured.progress ? { progress: measured.progress } : {}),
|
|
364
|
+
turn_id: capture.turn_id,
|
|
365
|
+
});
|
|
366
|
+
measured = { ...measured, state: clearActiveTurn(input.store, measured.state, now) };
|
|
367
|
+
if (measured.snapshot.status === "active" && measured.snapshot.current_step === request.current_step) {
|
|
368
|
+
appendEvent(input.store, measured.state, measured.snapshot, "gate_not_advanced", now, { summary: "adapter completed without canonical gate advancement" });
|
|
369
|
+
}
|
|
370
|
+
const outcome = resolveCanonicalDisposition(input.store, measured.state, measured.snapshot, now, true);
|
|
371
|
+
return outcome ? { state: measured.state, snapshot: measured.snapshot, outcome } : measured;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function parkAcceptedTurn(
|
|
375
|
+
input: RunContinuationDriverInput,
|
|
376
|
+
measured: MissionPosition & { progress?: GateActionPriorProgress },
|
|
377
|
+
result: Extract<ContinuationTurnResult, { status: "wait" }>,
|
|
378
|
+
now: () => Date,
|
|
379
|
+
): Promise<MissionPosition> {
|
|
380
|
+
let state = saveState(input.store, measured.state, { status: "waiting", pending_barrier: result.barrier }, now);
|
|
381
|
+
const turnId = state.active_turn_capture!.turn_id;
|
|
382
|
+
appendEvent(input.store, state, measured.snapshot, "parked", now, {
|
|
383
|
+
barrier: result.barrier, summary: result.summary, ...(measured.progress ? { progress: measured.progress } : {}), turn_id: turnId,
|
|
384
|
+
});
|
|
385
|
+
state = clearActiveTurn(input.store, state, now);
|
|
386
|
+
const readiness = await (input.waitForBarrier ?? (async () => "pending" as const))(result.barrier);
|
|
387
|
+
if (readiness === "pending") {
|
|
388
|
+
return { state, snapshot: measured.snapshot, outcome: { outcome: "waiting", turns_started: state.turns_started, snapshot: measured.snapshot, barrier: result.barrier } };
|
|
268
389
|
}
|
|
390
|
+
state = saveState(input.store, state, { status: "active", pending_barrier: null }, now);
|
|
391
|
+
appendEvent(input.store, state, measured.snapshot, "resumed", now, { barrier: result.barrier });
|
|
392
|
+
const resumed = await synchronizeAndRecover(input, state, now);
|
|
393
|
+
const outcome = resolveCanonicalDisposition(input.store, resumed.state, resumed.snapshot, now, true);
|
|
394
|
+
return outcome ? { ...resumed, outcome } : resumed;
|
|
395
|
+
}
|
|
269
396
|
|
|
270
|
-
|
|
271
|
-
|
|
397
|
+
async function synchronizeTurnMeasurement(
|
|
398
|
+
input: RunContinuationDriverInput,
|
|
399
|
+
state: ContinuationDriverState,
|
|
400
|
+
previous: ContinuationSnapshot,
|
|
401
|
+
request: ContinuationTurnRequest | null,
|
|
402
|
+
result: ContinuationTurnResult | null,
|
|
403
|
+
now: () => Date,
|
|
404
|
+
): Promise<MissionPosition & { progress?: GateActionPriorProgress }> {
|
|
405
|
+
const snapshot = validateSnapshot(await input.runtime.synchronize());
|
|
406
|
+
assertMissionIdentity(state, snapshot);
|
|
407
|
+
const before = state.active_turn_progress ?? state.last_progress ?? progressSnapshot(previous);
|
|
408
|
+
const progress = measureProgress(state, before, progressSnapshot(snapshot));
|
|
409
|
+
const capture = request && result ? acceptedTurnCapture(request, result, progress?.delta ?? null, now) : null;
|
|
410
|
+
const measured = saveState(input.store, state, {
|
|
411
|
+
...(progress ? { last_progress: progress.snapshot, prior_progress: progress.delta } : {}),
|
|
412
|
+
...(capture ? { active_turn_phase: "measured", active_turn_capture: capture } : {}),
|
|
413
|
+
}, now);
|
|
414
|
+
return { state: measured, snapshot, ...(progress ? { progress: progress.delta } : {}) };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function acceptedTurnCapture(request: ContinuationTurnRequest, result: ContinuationTurnResult, progress: GateActionPriorProgress | null, now: () => Date): ContinuationAcceptedTurn {
|
|
418
|
+
return {
|
|
419
|
+
schema_version: "1.0",
|
|
420
|
+
turn_id: `${request.run_id}:${request.iteration}`,
|
|
421
|
+
iteration: request.iteration,
|
|
422
|
+
request: structuredClone(request),
|
|
423
|
+
result: structuredClone(result),
|
|
424
|
+
progress: progress ? structuredClone(progress) : null,
|
|
425
|
+
captured_at: now().toISOString(),
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function clearActiveTurn(store: ContinuationStateStore, state: ContinuationDriverState, now: () => Date): ContinuationDriverState {
|
|
430
|
+
return saveState(store, state, {
|
|
272
431
|
active_turn_step: null,
|
|
273
432
|
active_turn_public_key_digest: null,
|
|
433
|
+
active_turn_phase: null,
|
|
434
|
+
active_turn_progress: null,
|
|
435
|
+
active_turn_capture: null,
|
|
274
436
|
}, now);
|
|
275
|
-
|
|
276
|
-
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function auditAuthorityCleanup(
|
|
440
|
+
store: ContinuationStateStore,
|
|
441
|
+
state: ContinuationDriverState,
|
|
442
|
+
snapshot: ContinuationSnapshot,
|
|
443
|
+
authority: ContinuationTurnAuthority | undefined,
|
|
444
|
+
now: () => Date,
|
|
445
|
+
): void {
|
|
446
|
+
let summary: string | undefined;
|
|
447
|
+
try {
|
|
448
|
+
if (authority && !authority.cleanup()) summary = "continuation turn authority cleanup returned false";
|
|
449
|
+
} catch (error) {
|
|
450
|
+
summary = boundedErrorMessage(error);
|
|
451
|
+
}
|
|
452
|
+
if (!summary) return;
|
|
453
|
+
try {
|
|
454
|
+
appendEvent(store, state, snapshot, "authority_cleanup_failed", now, { summary });
|
|
455
|
+
} catch {
|
|
456
|
+
// Audit is best effort. A cleanup extension must not replace the adapter outcome.
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function finishBudgetExhausted(store: ContinuationStateStore, state: ContinuationDriverState, snapshot: ContinuationSnapshot, now: () => Date): ContinuationDriverOutcome {
|
|
461
|
+
const exhausted = saveState(store, state, {
|
|
462
|
+
status: "budget_exhausted", active_turn_step: null, active_turn_public_key_digest: null,
|
|
463
|
+
active_turn_phase: null, active_turn_progress: null, active_turn_capture: null,
|
|
464
|
+
}, now);
|
|
465
|
+
appendEvent(store, exhausted, snapshot, "budget_exhausted", now);
|
|
466
|
+
return { outcome: "budget_exhausted", turns_started: exhausted.turns_started, snapshot };
|
|
277
467
|
}
|
|
278
468
|
|
|
279
469
|
export async function driveBuilderFlowSession(input: DriveBuilderFlowSessionInput): Promise<ContinuationDriverOutcome> {
|
|
@@ -287,57 +477,16 @@ export async function driveBuilderFlowSession(input: DriveBuilderFlowSessionInpu
|
|
|
287
477
|
maxTurns: input.maxTurns,
|
|
288
478
|
...(input.adapterCommandIdentity ? { adapterCommandIdentity: input.adapterCommandIdentity } : {}),
|
|
289
479
|
runtime,
|
|
290
|
-
store: createFileContinuationStore(sessionDir),
|
|
480
|
+
store: input.store ?? createFileContinuationStore(sessionDir),
|
|
291
481
|
...(input.waitForBarrier ? { waitForBarrier: input.waitForBarrier } : {}),
|
|
292
482
|
...(input.authorizeTurn ? { authorizeTurn: input.authorizeTurn } : {}),
|
|
293
483
|
...(input.issueTurnAuthority ? { issueTurnAuthority: input.issueTurnAuthority } : {}),
|
|
484
|
+
...(input.preflightTurn ? { preflightTurn: input.preflightTurn } : {}),
|
|
294
485
|
...(input.onTurnAccepted ? { onTurnAccepted: input.onTurnAccepted } : {}),
|
|
295
486
|
...(input.now ? { now: input.now } : {}),
|
|
296
487
|
});
|
|
297
488
|
}
|
|
298
489
|
|
|
299
|
-
export async function withContinuationDriverLock<T>(sessionDirInput: string, body: (lease: ContinuationDriverLockLease) => Promise<T>): Promise<T> {
|
|
300
|
-
const sessionDir = path.resolve(sessionDirInput);
|
|
301
|
-
const driverDir = path.join(sessionDir, "continuation-driver");
|
|
302
|
-
const locksDir = path.join(driverDir, "locks");
|
|
303
|
-
ensureSafeDirectory(sessionDir, locksDir);
|
|
304
|
-
const token = randomUUID();
|
|
305
|
-
const lockFile = path.join(locksDir, `${process.pid}-${token}.lock`);
|
|
306
|
-
const lease: ContinuationDriverLockLease = { pid: process.pid, token, created_at: new Date().toISOString() };
|
|
307
|
-
const owner = { schema_version: "1.0" as const, ...lease };
|
|
308
|
-
acquireDriverLock(locksDir, lockFile, owner);
|
|
309
|
-
try {
|
|
310
|
-
return await body(lease);
|
|
311
|
-
} finally {
|
|
312
|
-
releaseDriverLock(lockFile, token);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
export function createFileContinuationStore(sessionDirInput: string): ContinuationStateStore {
|
|
317
|
-
const sessionDir = path.resolve(sessionDirInput);
|
|
318
|
-
const driverDir = path.join(sessionDir, "continuation-driver");
|
|
319
|
-
const stateFile = path.join(driverDir, "state.json");
|
|
320
|
-
const eventsFile = path.join(driverDir, "events.jsonl");
|
|
321
|
-
ensureSafeDirectory(sessionDir, driverDir);
|
|
322
|
-
return {
|
|
323
|
-
load(): ContinuationDriverState | null {
|
|
324
|
-
if (!fs.existsSync(stateFile)) {
|
|
325
|
-
if (fs.existsSync(eventsFile) && fs.statSync(eventsFile).size > 0) throw new Error("continuation driver state is missing while mission events exist");
|
|
326
|
-
return null;
|
|
327
|
-
}
|
|
328
|
-
const state = JSON.parse(readRegularFileNoFollow(stateFile, "continuation driver state")) as ContinuationDriverState;
|
|
329
|
-
reconcileStateWithEvents(state, eventsFile);
|
|
330
|
-
return state;
|
|
331
|
-
},
|
|
332
|
-
save(state: ContinuationDriverState): void {
|
|
333
|
-
atomicWriteJson(sessionDir, stateFile, state);
|
|
334
|
-
},
|
|
335
|
-
append(event: ContinuationDriverEvent): void {
|
|
336
|
-
appendJsonLineNoFollow(sessionDir, eventsFile, event);
|
|
337
|
-
},
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
|
|
341
490
|
function builderSessionSnapshot(result: Awaited<ReturnType<typeof inspectBuilderFlowSession>>): ContinuationSnapshot {
|
|
342
491
|
const nextAction = result.projection.next_action;
|
|
343
492
|
return validateSnapshot({
|
|
@@ -349,88 +498,11 @@ function builderSessionSnapshot(result: Awaited<ReturnType<typeof inspectBuilder
|
|
|
349
498
|
next_action: nextAction && typeof nextAction === "object" && !Array.isArray(nextAction)
|
|
350
499
|
? structuredClone(nextAction as Record<string, unknown>)
|
|
351
500
|
: null,
|
|
501
|
+
progress_snapshot: structuredClone(result.progressSnapshot),
|
|
502
|
+
...(result.gateActionEnvelope ? { gate_action_envelope: structuredClone(result.gateActionEnvelope) } : {}),
|
|
352
503
|
});
|
|
353
504
|
}
|
|
354
505
|
|
|
355
|
-
function appendJsonLineNoFollow(root: string, file: string, value: unknown): void {
|
|
356
|
-
ensureSafeDirectory(root, path.dirname(file));
|
|
357
|
-
if (fs.existsSync(file)) assertRegularFile(file, "continuation driver event log");
|
|
358
|
-
const noFollow = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
|
|
359
|
-
const fd = fs.openSync(file, fsConstants.O_WRONLY | fsConstants.O_APPEND | fsConstants.O_CREAT | noFollow, 0o600);
|
|
360
|
-
try {
|
|
361
|
-
fs.writeFileSync(fd, `${JSON.stringify(value)}\n`, "utf8");
|
|
362
|
-
} finally {
|
|
363
|
-
fs.closeSync(fd);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function assertRegularFile(file: string, label: string): void {
|
|
368
|
-
const stat = fs.lstatSync(file);
|
|
369
|
-
if (stat.isSymbolicLink() || !stat.isFile()) throw new Error(`${label} must be a regular file`);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function readRegularFileNoFollow(file: string, label: string): string {
|
|
373
|
-
assertRegularFile(file, label);
|
|
374
|
-
const noFollow = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
|
|
375
|
-
const fd = fs.openSync(file, fsConstants.O_RDONLY | noFollow);
|
|
376
|
-
try {
|
|
377
|
-
if (!fs.fstatSync(fd).isFile()) throw new Error(`${label} must be a regular file`);
|
|
378
|
-
return fs.readFileSync(fd, "utf8");
|
|
379
|
-
} finally {
|
|
380
|
-
fs.closeSync(fd);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function acquireDriverLock(
|
|
385
|
-
locksDir: string,
|
|
386
|
-
lockFile: string,
|
|
387
|
-
owner: { schema_version: "1.0"; pid: number; token: string; created_at: string },
|
|
388
|
-
): void {
|
|
389
|
-
const noFollow = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
|
|
390
|
-
const fd = fs.openSync(lockFile, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | noFollow, 0o600);
|
|
391
|
-
try {
|
|
392
|
-
fs.writeFileSync(fd, `${JSON.stringify(owner)}\n`, "utf8");
|
|
393
|
-
} finally {
|
|
394
|
-
fs.closeSync(fd);
|
|
395
|
-
}
|
|
396
|
-
try {
|
|
397
|
-
for (const name of fs.readdirSync(locksDir).sort()) {
|
|
398
|
-
const candidate = path.join(locksDir, name);
|
|
399
|
-
if (candidate === lockFile) continue;
|
|
400
|
-
let existing: { schema_version?: unknown; pid?: unknown; token?: unknown };
|
|
401
|
-
try {
|
|
402
|
-
existing = JSON.parse(readRegularFileNoFollow(candidate, "continuation driver lock")) as typeof existing;
|
|
403
|
-
} catch (error) {
|
|
404
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
|
|
405
|
-
throw error;
|
|
406
|
-
}
|
|
407
|
-
if (existing.schema_version !== "1.0" || typeof existing.pid !== "number" || !Number.isSafeInteger(existing.pid) || existing.pid <= 0 || typeof existing.token !== "string") {
|
|
408
|
-
throw new Error("continuation driver lock is invalid");
|
|
409
|
-
}
|
|
410
|
-
if (processAlive(existing.pid)) throw new Error(`continuation driver is already running under pid ${existing.pid}`);
|
|
411
|
-
try {
|
|
412
|
-
fs.unlinkSync(candidate);
|
|
413
|
-
} catch (error) {
|
|
414
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
} catch (error) {
|
|
418
|
-
try {
|
|
419
|
-
fs.unlinkSync(lockFile);
|
|
420
|
-
} catch {}
|
|
421
|
-
throw error;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function releaseDriverLock(lockFile: string, token: string): void {
|
|
426
|
-
try {
|
|
427
|
-
const owner = JSON.parse(readRegularFileNoFollow(lockFile, "continuation driver lock")) as { token?: unknown };
|
|
428
|
-
if (owner.token === token) fs.unlinkSync(lockFile);
|
|
429
|
-
} catch (error) {
|
|
430
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
506
|
function loadOrCreateState(
|
|
435
507
|
store: ContinuationStateStore,
|
|
436
508
|
snapshot: ContinuationSnapshot,
|
|
@@ -454,6 +526,9 @@ function loadOrCreateState(
|
|
|
454
526
|
turns_started: 0,
|
|
455
527
|
active_turn_step: null,
|
|
456
528
|
active_turn_public_key_digest: null,
|
|
529
|
+
active_turn_phase: null,
|
|
530
|
+
active_turn_progress: null,
|
|
531
|
+
active_turn_capture: null,
|
|
457
532
|
pending_barrier: null,
|
|
458
533
|
updated_at: now().toISOString(),
|
|
459
534
|
};
|
|
@@ -466,11 +541,146 @@ function canonicalOutcome(snapshot: ContinuationSnapshot): "done" | "waiting" |
|
|
|
466
541
|
return snapshot.disposition;
|
|
467
542
|
}
|
|
468
543
|
|
|
544
|
+
function reconcileProgress(
|
|
545
|
+
store: ContinuationStateStore,
|
|
546
|
+
state: ContinuationDriverState,
|
|
547
|
+
snapshot: ContinuationSnapshot,
|
|
548
|
+
now: () => Date,
|
|
549
|
+
): ContinuationDriverState {
|
|
550
|
+
const current = progressSnapshot(snapshot);
|
|
551
|
+
if (!current) return state;
|
|
552
|
+
if (!state.last_progress) return saveState(store, state, { last_progress: current, prior_progress: initialProgress() }, now);
|
|
553
|
+
if (sameProgressSnapshot(state.last_progress, current)) return state;
|
|
554
|
+
const progress = measureProgress(state, state.last_progress, current);
|
|
555
|
+
return progress ? saveState(store, state, { last_progress: progress.snapshot, prior_progress: progress.delta }, now) : state;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function reconcileInterruptedTurn(
|
|
559
|
+
store: ContinuationStateStore,
|
|
560
|
+
state: ContinuationDriverState,
|
|
561
|
+
snapshot: ContinuationSnapshot,
|
|
562
|
+
now: () => Date,
|
|
563
|
+
): ContinuationDriverState {
|
|
564
|
+
if (!state.active_turn_phase) return state;
|
|
565
|
+
if (state.active_turn_phase === "prepared") {
|
|
566
|
+
return clearActiveTurn(store, state, now);
|
|
567
|
+
}
|
|
568
|
+
if (state.active_turn_phase === "measured") {
|
|
569
|
+
if (!state.active_turn_capture) throw new Error("continuation measured turn is missing its durable capture");
|
|
570
|
+
const capture = state.active_turn_capture;
|
|
571
|
+
acceptedTurnJournal(store)?.captureAcceptedTurn(capture);
|
|
572
|
+
let recovered = capture.result.status === "wait"
|
|
573
|
+
? saveState(store, state, { status: "waiting", pending_barrier: capture.result.barrier }, now)
|
|
574
|
+
: state;
|
|
575
|
+
appendEvent(store, recovered, snapshot, capture.result.status === "wait" ? "parked" : "turn_completed", now, {
|
|
576
|
+
...(capture.result.status === "wait" ? { barrier: capture.result.barrier } : {}),
|
|
577
|
+
...(capture.result.summary ? { summary: capture.result.summary } : {}),
|
|
578
|
+
...(capture.progress ? { progress: capture.progress } : {}),
|
|
579
|
+
turn_id: capture.turn_id,
|
|
580
|
+
});
|
|
581
|
+
recovered = clearActiveTurn(store, recovered, now);
|
|
582
|
+
appendEvent(store, recovered, snapshot, "turn_recovered", now, {
|
|
583
|
+
summary: "completed durable accepted-turn capture after interruption",
|
|
584
|
+
...(capture.progress ? { progress: capture.progress } : {}),
|
|
585
|
+
turn_id: capture.turn_id,
|
|
586
|
+
});
|
|
587
|
+
return recovered;
|
|
588
|
+
}
|
|
589
|
+
const progress = measureProgress(state, state.active_turn_progress ?? state.last_progress ?? null, progressSnapshot(snapshot));
|
|
590
|
+
const recovered = saveState(store, state, {
|
|
591
|
+
...(progress ? { last_progress: progress.snapshot, prior_progress: progress.delta } : {}),
|
|
592
|
+
active_turn_step: null,
|
|
593
|
+
active_turn_public_key_digest: null,
|
|
594
|
+
active_turn_phase: null,
|
|
595
|
+
active_turn_progress: null,
|
|
596
|
+
active_turn_capture: null,
|
|
597
|
+
}, now);
|
|
598
|
+
appendEvent(store, recovered, snapshot, "turn_recovered", now, {
|
|
599
|
+
summary: "recovered interrupted adapter turn from canonical Flow progress",
|
|
600
|
+
...(progress ? { progress: progress.delta } : {}),
|
|
601
|
+
});
|
|
602
|
+
return recovered;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function sameProgressSnapshot(left: GateActionProgressSnapshot, right: GateActionProgressSnapshot): boolean {
|
|
606
|
+
return left.current_step === right.current_step
|
|
607
|
+
&& sameCanonicalStatus(left.canonical_status, right.canonical_status)
|
|
608
|
+
&& left.canonical_evidence.length === right.canonical_evidence.length
|
|
609
|
+
&& left.canonical_evidence.every((entry, index) => entry === right.canonical_evidence[index])
|
|
610
|
+
&& left.observed_artifacts.length === right.observed_artifacts.length
|
|
611
|
+
&& left.observed_artifacts.every((entry, index) => entry === right.observed_artifacts[index]);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function isTerminalCanonicalStatus(status: string | undefined): boolean {
|
|
615
|
+
return status === "completed" || status === "failed";
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function sameCanonicalStatus(left: string | undefined, right: string | undefined): boolean {
|
|
619
|
+
if (left === right) return true;
|
|
620
|
+
// Pre-status snapshots remain compatible while active. A newly observed
|
|
621
|
+
// terminal status is still a real canonical transition from legacy state.
|
|
622
|
+
if (left === undefined) return !isTerminalCanonicalStatus(right);
|
|
623
|
+
if (right === undefined) return !isTerminalCanonicalStatus(left);
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function progressSnapshot(snapshot: ContinuationSnapshot): GateActionProgressSnapshot | null {
|
|
628
|
+
if (snapshot.progress_snapshot) return structuredClone(snapshot.progress_snapshot);
|
|
629
|
+
return snapshot.gate_action_envelope ? gateActionProgressSnapshot(snapshot.gate_action_envelope) : null;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function acceptedTurnJournal(store: ContinuationStateStore): ContinuationAcceptedTurnJournal | null {
|
|
633
|
+
const candidate = store as Partial<ContinuationAcceptedTurnJournal>;
|
|
634
|
+
return typeof candidate.captureAcceptedTurn === "function" && typeof candidate.acceptedTurns === "function"
|
|
635
|
+
? candidate as ContinuationAcceptedTurnJournal
|
|
636
|
+
: null;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function initialProgress(): GateActionPriorProgress {
|
|
640
|
+
return {
|
|
641
|
+
step_advanced: false,
|
|
642
|
+
evidence_added: [],
|
|
643
|
+
artifact_changes: [],
|
|
644
|
+
no_progress: false,
|
|
645
|
+
consecutive_no_progress: 0,
|
|
646
|
+
stagnation: "none",
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function measureProgress(
|
|
651
|
+
state: ContinuationDriverState,
|
|
652
|
+
before: GateActionProgressSnapshot | null,
|
|
653
|
+
after: GateActionProgressSnapshot | null,
|
|
654
|
+
): { snapshot: GateActionProgressSnapshot; delta: GateActionPriorProgress } | null {
|
|
655
|
+
if (!before || !after) return null;
|
|
656
|
+
const terminalStatusAdvanced = isTerminalCanonicalStatus(after.canonical_status)
|
|
657
|
+
&& before.canonical_status !== after.canonical_status;
|
|
658
|
+
const stepAdvanced = before.current_step !== after.current_step || terminalStatusAdvanced;
|
|
659
|
+
const evidenceAdded = after.canonical_evidence.filter((entry) => !before.canonical_evidence.includes(entry));
|
|
660
|
+
const artifactChanges = after.observed_artifacts.filter((entry) => !before.observed_artifacts.includes(entry));
|
|
661
|
+
const noProgress = !stepAdvanced && evidenceAdded.length === 0 && artifactChanges.length === 0;
|
|
662
|
+
const consecutiveNoProgress = noProgress ? (state.prior_progress?.consecutive_no_progress ?? 0) + 1 : 0;
|
|
663
|
+
return {
|
|
664
|
+
snapshot: after,
|
|
665
|
+
delta: {
|
|
666
|
+
step_advanced: stepAdvanced,
|
|
667
|
+
evidence_added: evidenceAdded,
|
|
668
|
+
artifact_changes: artifactChanges,
|
|
669
|
+
no_progress: noProgress,
|
|
670
|
+
consecutive_no_progress: consecutiveNoProgress,
|
|
671
|
+
stagnation: consecutiveNoProgress >= 2 ? "stagnant" : consecutiveNoProgress === 1 ? "possible" : "none",
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
469
676
|
function finishDone(store: ContinuationStateStore, state: ContinuationDriverState, snapshot: ContinuationSnapshot, now: () => Date): ContinuationDriverOutcome {
|
|
470
677
|
const done = saveState(store, state, {
|
|
471
678
|
status: "done",
|
|
472
679
|
active_turn_step: null,
|
|
473
680
|
active_turn_public_key_digest: null,
|
|
681
|
+
active_turn_phase: null,
|
|
682
|
+
active_turn_progress: null,
|
|
683
|
+
active_turn_capture: null,
|
|
474
684
|
pending_barrier: null,
|
|
475
685
|
}, now);
|
|
476
686
|
appendEvent(store, done, snapshot, "done", now);
|
|
@@ -482,6 +692,9 @@ function finishFailed(store: ContinuationStateStore, state: ContinuationDriverSt
|
|
|
482
692
|
status: "failed",
|
|
483
693
|
active_turn_step: null,
|
|
484
694
|
active_turn_public_key_digest: null,
|
|
695
|
+
active_turn_phase: null,
|
|
696
|
+
active_turn_progress: null,
|
|
697
|
+
active_turn_capture: null,
|
|
485
698
|
pending_barrier: null,
|
|
486
699
|
}, now);
|
|
487
700
|
return { outcome: "failed", turns_started: failed.turns_started, snapshot };
|
|
@@ -490,7 +703,7 @@ function finishFailed(store: ContinuationStateStore, state: ContinuationDriverSt
|
|
|
490
703
|
function saveState(
|
|
491
704
|
store: ContinuationStateStore,
|
|
492
705
|
current: ContinuationDriverState,
|
|
493
|
-
patch: Partial<Pick<ContinuationDriverState, "status" | "turns_started" | "active_turn_step" | "active_turn_public_key_digest" | "pending_barrier">>,
|
|
706
|
+
patch: Partial<Pick<ContinuationDriverState, "status" | "turns_started" | "active_turn_step" | "active_turn_public_key_digest" | "active_turn_phase" | "active_turn_progress" | "active_turn_capture" | "pending_barrier" | "last_progress" | "prior_progress">>,
|
|
494
707
|
now: () => Date,
|
|
495
708
|
): ContinuationDriverState {
|
|
496
709
|
const next = { ...current, ...patch, updated_at: now().toISOString() };
|
|
@@ -504,7 +717,7 @@ function appendEvent(
|
|
|
504
717
|
snapshot: ContinuationSnapshot,
|
|
505
718
|
type: ContinuationDriverEvent["type"],
|
|
506
719
|
now: () => Date,
|
|
507
|
-
extra: Pick<ContinuationDriverEvent, "barrier" | "summary" | "failure_kind"> = {},
|
|
720
|
+
extra: Pick<ContinuationDriverEvent, "barrier" | "summary" | "failure_kind" | "progress" | "turn_id"> = {},
|
|
508
721
|
): void {
|
|
509
722
|
store.append({
|
|
510
723
|
schema_version: "1.0",
|
|
@@ -517,141 +730,16 @@ function appendEvent(
|
|
|
517
730
|
...(extra.barrier ? { barrier: extra.barrier } : {}),
|
|
518
731
|
...(extra.summary ? { summary: extra.summary } : {}),
|
|
519
732
|
...(extra.failure_kind ? { failure_kind: extra.failure_kind } : {}),
|
|
733
|
+
...(extra.progress ? { progress: structuredClone(extra.progress) } : {}),
|
|
734
|
+
...(extra.turn_id ? { turn_id: extra.turn_id } : {}),
|
|
520
735
|
});
|
|
521
736
|
}
|
|
522
737
|
|
|
523
|
-
function validateSnapshot(value: ContinuationSnapshot): ContinuationSnapshot {
|
|
524
|
-
if (!value || typeof value !== "object") throw new Error("continuation runtime returned an invalid canonical snapshot");
|
|
525
|
-
for (const field of ["run_id", "definition_id", "status", "current_step"] as const) {
|
|
526
|
-
if (typeof value[field] !== "string" || value[field].length === 0) throw new Error(`continuation snapshot ${field} must be a non-empty string`);
|
|
527
|
-
}
|
|
528
|
-
if (!new Set(["continue", "waiting", "done", "failed"]).has(value.disposition)) throw new Error("continuation snapshot disposition is invalid");
|
|
529
|
-
if (value.next_action !== null && (typeof value.next_action !== "object" || Array.isArray(value.next_action))) {
|
|
530
|
-
throw new Error("continuation snapshot next_action must be an object or null");
|
|
531
|
-
}
|
|
532
|
-
return structuredClone(value);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
function validateTurnResult(value: ContinuationTurnResult): ContinuationTurnResult {
|
|
536
|
-
if (!value || typeof value !== "object" || (value.status !== "completed" && value.status !== "wait")) {
|
|
537
|
-
throw new Error("continuation adapter must return status completed or wait");
|
|
538
|
-
}
|
|
539
|
-
if (value.status === "wait") validateBarrier(value.barrier);
|
|
540
|
-
if (value.summary !== undefined && typeof value.summary !== "string") throw new Error("continuation adapter summary must be a string");
|
|
541
|
-
if (value.status === "completed" && value.evidence !== undefined) validateAdapterEvidence(value.evidence);
|
|
542
|
-
if (value.status === "wait" && Object.hasOwn(value, "evidence")) throw new Error("continuation wait results must not include evidence");
|
|
543
|
-
const copy = structuredClone(value);
|
|
544
|
-
if (copy.summary && copy.summary.length > 2_000) copy.summary = `${copy.summary.slice(0, 1_997)}...`;
|
|
545
|
-
return copy;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
function validateAdapterEvidence(value: unknown): void {
|
|
549
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
550
|
-
throw new Error("continuation adapter evidence must be a JSON object");
|
|
551
|
-
}
|
|
552
|
-
let encoded: string;
|
|
553
|
-
try {
|
|
554
|
-
encoded = JSON.stringify(value);
|
|
555
|
-
} catch {
|
|
556
|
-
throw new Error("continuation adapter evidence must be JSON-serializable");
|
|
557
|
-
}
|
|
558
|
-
if (encoded === undefined || Buffer.byteLength(encoded, "utf8") > 65_536) {
|
|
559
|
-
throw new Error("continuation adapter evidence must not exceed 65536 bytes");
|
|
560
|
-
}
|
|
561
|
-
const parsed = JSON.parse(encoded) as unknown;
|
|
562
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
563
|
-
throw new Error("continuation adapter evidence must be a JSON object");
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
function validateBarrier(barrier: ContinuationBarrier): void {
|
|
568
|
-
if (!barrier || typeof barrier !== "object") throw new Error("continuation wait result requires a barrier");
|
|
569
|
-
if (barrier.kind === "pid") {
|
|
570
|
-
if (!Number.isSafeInteger(barrier.pid) || barrier.pid <= 0) throw new Error("continuation pid barrier requires a positive integer pid");
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
if (barrier.kind === "deadline") {
|
|
574
|
-
if (typeof barrier.at !== "string" || !Number.isFinite(Date.parse(barrier.at))) throw new Error("continuation deadline barrier requires an ISO date-time");
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
throw new Error("continuation barrier kind must be pid or deadline");
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
function validateState(state: ContinuationDriverState): void {
|
|
581
|
-
if (state.schema_version !== "1.0") throw new Error("continuation driver state schema_version must be 1.0");
|
|
582
|
-
assertMaxTurns(state.max_turns);
|
|
583
|
-
if (state.adapter_command_identity !== null && (typeof state.adapter_command_identity !== "string" || state.adapter_command_identity.length === 0)) {
|
|
584
|
-
throw new Error("continuation driver adapter_command_identity must be a non-empty string or null");
|
|
585
|
-
}
|
|
586
|
-
if (!Number.isSafeInteger(state.turns_started) || state.turns_started < 0) throw new Error("continuation driver turns_started must be a non-negative integer");
|
|
587
|
-
if (state.turns_started > state.max_turns) throw new Error("continuation driver turns_started cannot exceed max_turns");
|
|
588
|
-
if (state.active_turn_step !== undefined && state.active_turn_step !== null && (typeof state.active_turn_step !== "string" || state.active_turn_step.length === 0)) {
|
|
589
|
-
throw new Error("continuation driver active_turn_step must be a non-empty string or null");
|
|
590
|
-
}
|
|
591
|
-
if (state.active_turn_public_key_digest !== undefined && state.active_turn_public_key_digest !== null
|
|
592
|
-
&& (typeof state.active_turn_public_key_digest !== "string" || !/^[a-f0-9]{64}$/.test(state.active_turn_public_key_digest))) {
|
|
593
|
-
throw new Error("continuation driver active_turn_public_key_digest must be a SHA-256 hex digest or null");
|
|
594
|
-
}
|
|
595
|
-
if (!new Set(["active", "waiting", "done", "failed", "budget_exhausted"]).has(state.status)) throw new Error("continuation driver state status is invalid");
|
|
596
|
-
if (state.status === "budget_exhausted" && state.turns_started !== state.max_turns) {
|
|
597
|
-
throw new Error("continuation driver budget_exhausted state must consume max_turns");
|
|
598
|
-
}
|
|
599
|
-
if (state.pending_barrier) validateBarrier(state.pending_barrier);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
function assertMissionIdentity(state: Pick<ContinuationDriverState, "run_id" | "definition_id">, snapshot: ContinuationSnapshot): void {
|
|
603
|
-
if (state.run_id !== snapshot.run_id || state.definition_id !== snapshot.definition_id) {
|
|
604
|
-
throw new Error("continuation driver mission identity does not match the canonical Flow run");
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
function assertMaxTurns(value: number): void {
|
|
609
|
-
if (!Number.isSafeInteger(value) || value < 1 || value > 100) throw new Error("continuation maxTurns must be an integer from 1 through 100");
|
|
610
|
-
}
|
|
611
|
-
|
|
612
738
|
function boundedErrorMessage(error: unknown): string {
|
|
613
739
|
const message = error instanceof Error ? error.message : String(error);
|
|
614
740
|
return message.length <= 2_000 ? message : `${message.slice(0, 1_997)}...`;
|
|
615
741
|
}
|
|
616
742
|
|
|
617
|
-
function processAlive(pid: number): boolean {
|
|
618
|
-
try {
|
|
619
|
-
process.kill(pid, 0);
|
|
620
|
-
return true;
|
|
621
|
-
} catch (error) {
|
|
622
|
-
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
function reconcileStateWithEvents(state: ContinuationDriverState, eventsFile: string): void {
|
|
627
|
-
if (!fs.existsSync(eventsFile)) {
|
|
628
|
-
if (state.turns_started > 0) throw new Error("continuation driver event history is missing for a started mission");
|
|
629
|
-
return;
|
|
630
|
-
}
|
|
631
|
-
const lines = readRegularFileNoFollow(eventsFile, "continuation driver event log").split("\n").filter((line) => line.length > 0);
|
|
632
|
-
let highestStartedTurn = 0;
|
|
633
|
-
let eventBarrier: ContinuationBarrier | null = null;
|
|
634
|
-
for (const line of lines) {
|
|
635
|
-
const event = JSON.parse(line) as Partial<ContinuationDriverEvent>;
|
|
636
|
-
if (event.run_id !== state.run_id || event.definition_id !== state.definition_id) throw new Error("continuation driver event history has a foreign mission identity");
|
|
637
|
-
if (event.type === "turn_started") {
|
|
638
|
-
if (!Number.isSafeInteger(event.turns_started) || (event.turns_started as number) < 1) throw new Error("continuation driver event history has an invalid turn count");
|
|
639
|
-
highestStartedTurn = Math.max(highestStartedTurn, event.turns_started as number);
|
|
640
|
-
}
|
|
641
|
-
if (event.type === "parked") {
|
|
642
|
-
validateBarrier(event.barrier as ContinuationBarrier);
|
|
643
|
-
eventBarrier = structuredClone(event.barrier as ContinuationBarrier);
|
|
644
|
-
}
|
|
645
|
-
if (event.type === "resumed") eventBarrier = null;
|
|
646
|
-
}
|
|
647
|
-
if (highestStartedTurn > state.turns_started) throw new Error("continuation driver state rolled back behind its event history");
|
|
648
|
-
if (state.turns_started > highestStartedTurn + 1) throw new Error("continuation driver state is ahead of its event history");
|
|
649
|
-
if (!state.pending_barrier && eventBarrier) {
|
|
650
|
-
state.pending_barrier = eventBarrier;
|
|
651
|
-
state.status = "waiting";
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
743
|
function builderContinuationDisposition(nextAction: unknown): ContinuationSnapshot["disposition"] {
|
|
656
744
|
if (!nextAction || typeof nextAction !== "object" || Array.isArray(nextAction)) throw new Error("Builder Flow projection is missing its canonical next action");
|
|
657
745
|
const status = (nextAction as { status?: unknown }).status;
|