@kontourai/flow-agents 3.9.0 → 3.10.0
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/.github/workflows/ci.yml +8 -0
- package/CHANGELOG.md +17 -0
- package/README.md +1 -1
- package/build/src/cli/assignment-provider.js +4 -18
- package/build/src/cli/continuation-adapter.d.ts +10 -8
- package/build/src/cli/continuation-adapter.js +22 -4
- package/build/src/cli/validate-workflow-artifacts.js +21 -1
- package/build/src/cli/workflow-sidecar.js +15 -20
- package/build/src/cli/workflow.js +58 -11
- package/build/src/continuation-driver.d.ts +27 -3
- package/build/src/continuation-driver.js +97 -14
- package/build/src/index.d.ts +2 -2
- package/build/src/index.js +1 -1
- package/build/src/tools/build-universal-bundles.js +52 -25
- package/build/src/tools/validate-source-tree.js +5 -2
- package/context/scripts/hooks/lib/continuation-turn-authority.js +386 -0
- package/context/scripts/hooks/stop-goal-fit.js +244 -73
- package/context/scripts/hooks/workflow-steering.js +24 -15
- package/context/scripts/telemetry/lib/pricing.sh +18 -6
- package/context/scripts/telemetry/lib/transport.sh +139 -37
- package/context/scripts/telemetry/lib/usage.sh +282 -32
- package/context/scripts/telemetry/lib/usage_model_guard.sh +42 -0
- package/context/scripts/telemetry/telemetry.sh +76 -1
- package/docs/coordination-guide.md +21 -5
- package/docs/fixture-ownership.md +2 -1
- package/docs/public-workflow-cli.md +44 -0
- package/docs/spec/runtime-hook-surface.md +8 -4
- package/evals/acceptance/prove-capture-teeth-declared.sh +13 -0
- package/evals/acceptance/prove-capture-teeth.sh +13 -0
- package/evals/ci/run-baseline.sh +2 -0
- package/evals/fixtures/codex-legacy-agents/5273878130bdafc8a024a650bb5b66c9b003f1f859b5dc6e5b588cbf4ab23228.md +25 -0
- package/evals/fixtures/telemetry/usage-transcript-adversarial.jsonl +2 -0
- package/evals/fixtures/telemetry/usage-transcript-malformed-sandwich.jsonl +3 -0
- package/evals/fixtures/telemetry/usage-transcript-negative-tokens.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-oversized-prefix.jsonl +201 -0
- package/evals/fixtures/telemetry/usage-transcript-oversized-tokens.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-lower.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-upper.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-secret-model-jwt.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-secret-model.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-torn-trailing.jsonl +2 -0
- package/evals/integration/test_actor_identity.sh +63 -0
- package/evals/integration/test_assignment_provider_local_file.sh +34 -0
- package/evals/integration/test_builder_entry_enforcement.sh +5 -1
- package/evals/integration/test_bundle_install.sh +106 -17
- package/evals/integration/test_bundle_lifecycle.sh +17 -3
- package/evals/integration/test_captured_fail_reconciliation.sh +11 -0
- package/evals/integration/test_ci_actor_identity.sh +2 -2
- package/evals/integration/test_command_log_concurrency.sh +11 -0
- package/evals/integration/test_command_log_fork_classification.sh +11 -0
- package/evals/integration/test_command_log_integrity.sh +11 -0
- package/evals/integration/test_current_json_per_actor.sh +416 -21
- package/evals/integration/test_enforcer_expects_driven.sh +11 -0
- package/evals/integration/test_ensure_session_ownership_guard.sh +39 -0
- package/evals/integration/test_evidence_capture_hook.sh +25 -6
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_flowdef_union_floor_regression.sh +11 -0
- package/evals/integration/test_gate_bypass_chain.sh +11 -0
- package/evals/integration/test_gate_lockdown.sh +10 -0
- package/evals/integration/test_goal_fit_ghost_session.sh +11 -0
- package/evals/integration/test_goal_fit_hook.sh +257 -9
- package/evals/integration/test_goal_fit_rederive.sh +11 -0
- package/evals/integration/test_install_merge.sh +141 -3
- package/evals/integration/test_liveness_conflict_injection.sh +39 -3
- package/evals/integration/test_liveness_heartbeat.sh +46 -9
- package/evals/integration/test_phase_map_and_gate_claim.sh +10 -1
- package/evals/integration/test_public_workflow_cli.sh +6 -6
- package/evals/integration/test_session_resume_roundtrip.sh +30 -0
- package/evals/integration/test_stop_hook_release.sh +111 -11
- package/evals/integration/test_telemetry_tool_usage.sh +394 -0
- package/evals/integration/test_verify_hold.sh +64 -1
- package/evals/integration/test_workflow_sidecar_writer.sh +10 -1
- package/evals/integration/test_workflow_steering_hook.sh +20 -4
- package/evals/run.sh +2 -0
- package/evals/static/test_universal_bundles.sh +123 -0
- package/package.json +1 -1
- package/packaging/README.md +21 -0
- package/packaging/codex-legacy-agents-fingerprints.json +33 -0
- package/scripts/README.md +2 -1
- package/scripts/audit-codex-legacy-agents.js +43 -0
- package/scripts/classify-codex-legacy-agents.js +71 -0
- package/scripts/hooks/evidence-capture.js +18 -6
- package/scripts/hooks/lib/actor-identity.js +62 -29
- package/scripts/hooks/lib/continuation-turn-authority.js +386 -0
- package/scripts/hooks/lib/current-pointer.js +114 -12
- package/scripts/hooks/lib/liveness-heartbeat.js +15 -12
- package/scripts/hooks/stop-goal-fit.js +244 -73
- package/scripts/hooks/workflow-steering.js +24 -15
- package/scripts/install-codex-home.sh +8 -7
- package/scripts/telemetry/lib/pricing.sh +18 -6
- package/scripts/telemetry/lib/transport.sh +71 -0
- package/scripts/telemetry/lib/usage.sh +282 -32
- package/scripts/telemetry/lib/usage_model_guard.sh +42 -0
- package/scripts/telemetry/telemetry.sh +76 -1
- package/src/cli/assignment-provider.ts +5 -26
- package/src/cli/builder-flow-runtime.test.mjs +93 -6
- package/src/cli/continuation-adapter.ts +33 -7
- package/src/cli/continuation-driver.test.mjs +741 -2
- package/src/cli/public-api.test.mjs +1 -0
- package/src/cli/validate-workflow-artifacts.ts +19 -1
- package/src/cli/workflow-sidecar.ts +18 -23
- package/src/cli/workflow.ts +71 -11
- package/src/continuation-driver.ts +125 -19
- package/src/index.ts +4 -0
- package/src/tools/build-universal-bundles.ts +61 -25
- package/src/tools/validate-source-tree.ts +5 -2
|
@@ -37,6 +37,7 @@ test("public API retains the documented native-host compatibility surface", asyn
|
|
|
37
37
|
"startBuilderBuildRun", "evaluateBuilderBuildRun", "startBuilderFlowSession",
|
|
38
38
|
"pauseBuilderFlowSession", "resumeBuilderFlowSession", "cancelBuilderFlowSession",
|
|
39
39
|
"archiveBuilderFlowSession", "recoverBuilderFlowSession", "releaseBuilderFlowAssignment",
|
|
40
|
+
"ContinuationAdapterTimeoutError",
|
|
40
41
|
"writeJson", "appendJsonl", "sidecarBase", "writeState", "writeSidecar",
|
|
41
42
|
]) {
|
|
42
43
|
assert.equal(typeof lib[name], "function", `${name} must remain package-root exported`);
|
|
@@ -22,6 +22,10 @@ const sidecarSchemas: Record<string, string> = {
|
|
|
22
22
|
"release.json": "schemas/workflow-release.schema.json",
|
|
23
23
|
"learning.json": "schemas/workflow-learning.schema.json",
|
|
24
24
|
};
|
|
25
|
+
// Runtime coordination records live below a session but are not workflow
|
|
26
|
+
// sidecars. Recursing into them would validate continuation-driver/state.json
|
|
27
|
+
// against the public workflow-state schema and turn an active driver into a
|
|
28
|
+
// false Goal Fit hard block.
|
|
25
29
|
|
|
26
30
|
function readText(file: string): string {
|
|
27
31
|
return fs.readFileSync(file, "utf8");
|
|
@@ -91,11 +95,25 @@ function hasExplicitAcceptance(text: string): boolean {
|
|
|
91
95
|
return /explicitly accepted|accepted by user|user accepted|accepted gap/i.test(text);
|
|
92
96
|
}
|
|
93
97
|
|
|
98
|
+
function isPrivateRuntimeChild(dir: string): boolean {
|
|
99
|
+
if (path.basename(dir) !== "continuation-driver") return false;
|
|
100
|
+
const sessionDir = path.dirname(dir);
|
|
101
|
+
const sessionState = path.join(sessionDir, "state.json");
|
|
102
|
+
try {
|
|
103
|
+
const stat = fs.lstatSync(sessionState);
|
|
104
|
+
if (stat.isSymbolicLink() || !stat.isFile()) return false;
|
|
105
|
+
const state = JSON.parse(readText(sessionState)) as { task_slug?: unknown };
|
|
106
|
+
return state.task_slug === path.basename(sessionDir);
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
94
112
|
function walk(dir: string): string[] {
|
|
95
113
|
const out: string[] = [];
|
|
96
114
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
97
115
|
const p = path.join(dir, entry.name);
|
|
98
|
-
if (entry.isDirectory()) out.push(...walk(p));
|
|
116
|
+
if (entry.isDirectory() && !isPrivateRuntimeChild(p)) out.push(...walk(p));
|
|
99
117
|
else out.push(p);
|
|
100
118
|
}
|
|
101
119
|
return out;
|
|
@@ -275,7 +275,9 @@ function resolveEnsureSessionActor(p: ReturnType<typeof parseArgs>): { actorStru
|
|
|
275
275
|
die("ensure-session --actor value strips to empty under the allowed actor charset ([A-Za-z0-9_.-]) — pass a value containing at least one letter, digit, underscore, period, or hyphen.");
|
|
276
276
|
}
|
|
277
277
|
const explicitActor = explicitActorRaw ? helper.sanitizeSegment(explicitActorRaw) : "";
|
|
278
|
-
const resolved = explicitActor
|
|
278
|
+
const resolved = explicitActor
|
|
279
|
+
? { actor: explicitActor, source: "explicit-override", actorStruct: { runtime: "explicit-override", session_id: explicitActor, host: os.hostname() } }
|
|
280
|
+
: helper.resolveActorIdentity(process.env);
|
|
279
281
|
const unresolved = helper.isUnresolvedActor(resolved.actor);
|
|
280
282
|
const branchActorKey = unresolved ? `unknown-actor-${unknownDisambiguator(resolved.actor)}` : resolved.actor;
|
|
281
283
|
|
|
@@ -286,17 +288,8 @@ function resolveEnsureSessionActor(p: ReturnType<typeof parseArgs>): { actorStru
|
|
|
286
288
|
return { actorStruct: { runtime: "unresolved", session_id: branchActorKey, host: os.hostname() }, actorKey: resolved.actor, branchActorKey, unresolved: true };
|
|
287
289
|
}
|
|
288
290
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
// an ANCESTRY struct — detectRuntime→unknown, runtimeSessionId→'' — so the claim's stored
|
|
292
|
-
// actor_key would not match the CI actor at publish → self not recognized → false-block, the exact
|
|
293
|
-
// bug this issue removes). Uses the SAME helper.detectCiActor as resolveActor, single-sourced.
|
|
294
|
-
const ciActor = resolved.source.startsWith("ci-runtime") ? helper.detectCiActor(process.env) : null;
|
|
295
|
-
const actorStruct: ActorStruct = resolved.source === "explicit-override"
|
|
296
|
-
? { runtime: "explicit-override", session_id: resolved.actor, host: os.hostname() }
|
|
297
|
-
: ciActor && ciActor.session_id
|
|
298
|
-
? { runtime: ciActor.runtime, session_id: ciActor.session_id, host: os.hostname() }
|
|
299
|
-
: { runtime: helper.detectRuntime(process.env), session_id: helper.runtimeSessionId(process.env) || (() => { const seed = helper.ancestorActorSeed(); return seed ? `anc-${seed}` : ""; })(), host: os.hostname() };
|
|
291
|
+
if (!resolved.actorStruct) die("ensure-session could not resolve a canonical actor struct");
|
|
292
|
+
const actorStruct: ActorStruct = resolved.actorStruct;
|
|
300
293
|
const actorKey = helper.serializeActor(actorStruct);
|
|
301
294
|
return { actorStruct, actorKey, branchActorKey, unresolved: false };
|
|
302
295
|
}
|
|
@@ -1613,6 +1606,7 @@ function resolveFirstStep(flowId: string, repoRoot: string): string | null {
|
|
|
1613
1606
|
function loadCurrentPointerHelper(): {
|
|
1614
1607
|
perActorCurrentFile: (flowAgentsDir: string, actorKey: string) => string;
|
|
1615
1608
|
readCurrentPointer: (flowAgentsDir: string, actorKey?: string) => { payload: AnyObj | null; source: "per-actor" | "legacy" | "none"; file: string | null };
|
|
1609
|
+
readOwnCurrentPointer: (flowAgentsDir: string, actorKey?: string) => { payload: AnyObj | null; source: "per-actor" | "legacy" | "none"; file: string | null };
|
|
1616
1610
|
writePerActorCurrent: (flowAgentsDir: string, actorKey: string, payload: AnyObj) => void;
|
|
1617
1611
|
} {
|
|
1618
1612
|
const _req = createRequire(import.meta.url);
|
|
@@ -1620,6 +1614,7 @@ function loadCurrentPointerHelper(): {
|
|
|
1620
1614
|
return _req(helperPath) as {
|
|
1621
1615
|
perActorCurrentFile: (flowAgentsDir: string, actorKey: string) => string;
|
|
1622
1616
|
readCurrentPointer: (flowAgentsDir: string, actorKey?: string) => { payload: AnyObj | null; source: "per-actor" | "legacy" | "none"; file: string | null };
|
|
1617
|
+
readOwnCurrentPointer: (flowAgentsDir: string, actorKey?: string) => { payload: AnyObj | null; source: "per-actor" | "legacy" | "none"; file: string | null };
|
|
1623
1618
|
writePerActorCurrent: (flowAgentsDir: string, actorKey: string, payload: AnyObj) => void;
|
|
1624
1619
|
};
|
|
1625
1620
|
}
|
|
@@ -1758,9 +1753,15 @@ function updateCurrentAgent(root: string, dir: string, agentId: string, status:
|
|
|
1758
1753
|
|
|
1759
1754
|
if (actorKey && !loadActorIdentityHelper().isUnresolvedActor(actorKey)) {
|
|
1760
1755
|
const helper = loadCurrentPointerHelper();
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1756
|
+
// #440 fix-wave 3: read through the legacy-aware path (readOwnCurrentPointer tries the new
|
|
1757
|
+
// collision-resistant filename first, then falls back to the pre-fix-wave-2 legacy filename)
|
|
1758
|
+
// instead of a direct fs read of perActorCurrentFile() alone — a direct read of ONLY the new
|
|
1759
|
+
// filename would silently skip this active_agents/updated_at projection for a pointer a
|
|
1760
|
+
// still-running pre-fix-wave-2 sidecar wrote under the old name. The write below always goes
|
|
1761
|
+
// through writePerActorCurrent (new filename only), so the pointer migrates to the new name
|
|
1762
|
+
// on first touch.
|
|
1763
|
+
const pointer = helper.readOwnCurrentPointer(root, actorKey);
|
|
1764
|
+
const perActor = pointer.payload;
|
|
1764
1765
|
if (perActor && path.resolve(root, perActor.artifact_dir ?? "") === path.resolve(dir)) {
|
|
1765
1766
|
helper.writePerActorCurrent(root, actorKey, applyAgentUpdate(perActor));
|
|
1766
1767
|
}
|
|
@@ -6088,6 +6089,7 @@ export const LIVENESS_TERMINAL = new Set(["delivered", "canceled", "accepted", "
|
|
|
6088
6089
|
*/
|
|
6089
6090
|
function loadActorIdentityHelper(): {
|
|
6090
6091
|
resolveActor: (env: NodeJS.ProcessEnv) => { actor: string; source: string };
|
|
6092
|
+
resolveActorIdentity: (env: NodeJS.ProcessEnv) => { actor: string; source: string; actorStruct: ActorStruct | null };
|
|
6091
6093
|
sanitizeSegment: (value: unknown) => string;
|
|
6092
6094
|
isUnresolvedActor: (actor: string) => boolean;
|
|
6093
6095
|
// #291 Wave 2 Task 2.1: widened (additive only — every existing caller above keeps using only
|
|
@@ -6097,22 +6099,15 @@ function loadActorIdentityHelper(): {
|
|
|
6097
6099
|
// assignment-claim identity (a DIFFERENT identity concept from the flat actorKey used for
|
|
6098
6100
|
// branch-naming/liveness — see resolveEnsureSessionActor's doc comment for why both exist).
|
|
6099
6101
|
serializeActor: (actor: Partial<ActorStruct> | undefined) => string;
|
|
6100
|
-
detectRuntime: (env: NodeJS.ProcessEnv) => string;
|
|
6101
|
-
runtimeSessionId: (env: NodeJS.ProcessEnv) => string;
|
|
6102
|
-
detectCiActor: (env: NodeJS.ProcessEnv) => { runtime: string; session_id: string } | null;
|
|
6103
|
-
ancestorActorSeed: () => string;
|
|
6104
6102
|
} {
|
|
6105
6103
|
const _req = createRequire(import.meta.url);
|
|
6106
6104
|
const helperPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../scripts/hooks/lib/actor-identity.js");
|
|
6107
6105
|
return _req(helperPath) as {
|
|
6108
6106
|
resolveActor: (env: NodeJS.ProcessEnv) => { actor: string; source: string };
|
|
6107
|
+
resolveActorIdentity: (env: NodeJS.ProcessEnv) => { actor: string; source: string; actorStruct: ActorStruct | null };
|
|
6109
6108
|
sanitizeSegment: (value: unknown) => string;
|
|
6110
6109
|
isUnresolvedActor: (actor: string) => boolean;
|
|
6111
6110
|
serializeActor: (actor: Partial<ActorStruct> | undefined) => string;
|
|
6112
|
-
detectRuntime: (env: NodeJS.ProcessEnv) => string;
|
|
6113
|
-
runtimeSessionId: (env: NodeJS.ProcessEnv) => string;
|
|
6114
|
-
detectCiActor: (env: NodeJS.ProcessEnv) => { runtime: string; session_id: string } | null;
|
|
6115
|
-
ancestorActorSeed: () => string;
|
|
6116
6111
|
};
|
|
6117
6112
|
}
|
|
6118
6113
|
function resolveLivenessActor(): string {
|
package/src/cli/workflow.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { flagBool, flagList, flagString, parseArgs } from "../lib/args.js";
|
|
|
15
15
|
import { main as builderRun } from "./builder-run.js";
|
|
16
16
|
import { currentWorkflowSessionDir, isMeaningfulTestCommand, mainFromPublicWorkflow, WORKFLOW_WRITER_CONTRACT_VERSION } from "./workflow-sidecar.js";
|
|
17
17
|
import { resolveCurrentAssignmentActor, withSubjectLock } from "./assignment-provider.js";
|
|
18
|
-
import { executeLoadedContinuationAdapter, loadContinuationAdapterCommand, waitForContinuationBarrier } from "./continuation-adapter.js";
|
|
18
|
+
import { assertLoadedContinuationAdapterIntegrity, executeLoadedContinuationAdapter, loadContinuationAdapterCommand, waitForContinuationBarrier } from "./continuation-adapter.js";
|
|
19
19
|
|
|
20
20
|
type JsonRecord = Record<string, unknown>;
|
|
21
21
|
|
|
@@ -81,16 +81,39 @@ async function drive(sessionDir: string, argv: string[], json: boolean): Promise
|
|
|
81
81
|
const barrierWaitMs = integerFlag(parsed.flags, "barrier-wait-ms", 300_000, 0, 86_400_000);
|
|
82
82
|
const barrierPollMs = integerFlag(parsed.flags, "barrier-poll-ms", 1_000, 1, 60_000);
|
|
83
83
|
const { slug, projectRoot } = readBoundSession(sessionDir);
|
|
84
|
-
|
|
84
|
+
assertOrdinaryMatchingAssignmentActor(sessionDir, slug);
|
|
85
85
|
const adapterCommand = loadContinuationAdapterCommand(adapterCommandFile);
|
|
86
|
-
const result = await withContinuationDriverLock(sessionDir, async () => {
|
|
87
|
-
|
|
86
|
+
const result = await withContinuationDriverLock(sessionDir, async (lock) => {
|
|
87
|
+
assertOrdinaryMatchingAssignmentActor(sessionDir, slug);
|
|
88
88
|
return driveBuilderFlowSession({
|
|
89
89
|
sessionDir,
|
|
90
90
|
maxTurns,
|
|
91
91
|
adapterCommandIdentity: adapterCommand.identity,
|
|
92
|
-
authorizeTurn: async () => {
|
|
93
|
-
|
|
92
|
+
authorizeTurn: async () => { assertOrdinaryMatchingAssignmentActor(sessionDir, slug); },
|
|
93
|
+
issueTurnAuthority: async (request) => {
|
|
94
|
+
const currentAssignment = assertOrdinaryMatchingAssignmentActor(sessionDir, slug);
|
|
95
|
+
assertLoadedContinuationAdapterIntegrity(adapterCommand);
|
|
96
|
+
const authority = loadContinuationTurnAuthority();
|
|
97
|
+
return authority.issueActiveTurnAuthority({
|
|
98
|
+
sessionDir,
|
|
99
|
+
runId: request.run_id,
|
|
100
|
+
definitionId: request.definition_id,
|
|
101
|
+
currentStep: request.current_step,
|
|
102
|
+
iteration: request.iteration,
|
|
103
|
+
maxTurns: request.max_turns,
|
|
104
|
+
adapterCommandIdentity: adapterCommand.identity,
|
|
105
|
+
assignmentActor: currentAssignment.actorKey,
|
|
106
|
+
assignmentActorStruct: currentAssignment.actor,
|
|
107
|
+
lock,
|
|
108
|
+
timeoutMs: turnTimeoutMs,
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
execute: async (request, context) => executeLoadedContinuationAdapter(adapterCommand, request, {
|
|
112
|
+
cwd: projectRoot,
|
|
113
|
+
timeoutMs: turnTimeoutMs,
|
|
114
|
+
continuationTurnSecret: context?.continuationTurnSecret,
|
|
115
|
+
continuationRunId: context?.continuationRunId,
|
|
116
|
+
}),
|
|
94
117
|
waitForBarrier: async (barrier) => waitForContinuationBarrier(barrier, { maxWaitMs: barrierWaitMs, pollMs: barrierPollMs }),
|
|
95
118
|
});
|
|
96
119
|
});
|
|
@@ -99,6 +122,16 @@ async function drive(sessionDir: string, argv: string[], json: boolean): Promise
|
|
|
99
122
|
return 0;
|
|
100
123
|
}
|
|
101
124
|
|
|
125
|
+
function loadContinuationTurnAuthority(): {
|
|
126
|
+
issueActiveTurnAuthority(input: Record<string, unknown>): { runId: string; turnSecret: string; publicKeyDigest: string; cleanup(): boolean };
|
|
127
|
+
validateSignedActiveTurnAssignmentAuthority(input: Record<string, unknown>): { valid: boolean; record?: { assignment_actor: string; assignment_actor_struct: Record<string, unknown> } };
|
|
128
|
+
} {
|
|
129
|
+
return REQUIRE(path.resolve(PACKAGE_ROOT, "scripts", "hooks", "lib", "continuation-turn-authority.js")) as {
|
|
130
|
+
issueActiveTurnAuthority(input: Record<string, unknown>): { runId: string; turnSecret: string; publicKeyDigest: string; cleanup(): boolean };
|
|
131
|
+
validateSignedActiveTurnAssignmentAuthority(input: Record<string, unknown>): { valid: boolean; record?: { assignment_actor: string; assignment_actor_struct: Record<string, unknown> } };
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
102
135
|
async function start(argv: string[]): Promise<number> {
|
|
103
136
|
const parsed = parseArgs(argv);
|
|
104
137
|
assertOnlyFlags(parsed.flags, new Set(["flow", "work-item", "task-slug", "artifact-root", "source-request", "summary", "title", "criterion", "assignment-provider", "effective-state-json"]), "workflow start");
|
|
@@ -518,15 +551,42 @@ function readAssignment(sessionDir: string, slug: string): JsonRecord {
|
|
|
518
551
|
}
|
|
519
552
|
|
|
520
553
|
function assertMatchingAssignmentActor(sessionDir: string, slug: string): ReturnType<typeof resolveCurrentAssignmentActor> {
|
|
521
|
-
const assignment =
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
554
|
+
const { assignment, caller, matches } = assignmentActorContext(sessionDir, slug);
|
|
555
|
+
if (matches) return caller;
|
|
556
|
+
|
|
557
|
+
const authority = loadContinuationTurnAuthority().validateSignedActiveTurnAssignmentAuthority({
|
|
558
|
+
sessionDir,
|
|
559
|
+
runId: process.env.FLOW_AGENTS_CONTINUATION_RUN_ID,
|
|
560
|
+
turnSecret: process.env.FLOW_AGENTS_CONTINUATION_TURN_SECRET,
|
|
561
|
+
});
|
|
562
|
+
if (authority.valid && authority.record
|
|
563
|
+
&& assignment.actor_key === authority.record.assignment_actor
|
|
564
|
+
&& isDeepStrictEqual(normalizeAssignmentActor(assignment.actor), normalizeAssignmentActor(authority.record.assignment_actor_struct))) {
|
|
565
|
+
return { actorKey: authority.record.assignment_actor, actor: normalizeAssignmentActor(authority.record.assignment_actor_struct)! as ReturnType<typeof resolveCurrentAssignmentActor>["actor"] };
|
|
526
566
|
}
|
|
567
|
+
throw new Error("workflow mutation requires the session's active, matching assignment actor");
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function assertOrdinaryMatchingAssignmentActor(sessionDir: string, slug: string): ReturnType<typeof resolveCurrentAssignmentActor> {
|
|
571
|
+
const { caller, matches } = assignmentActorContext(sessionDir, slug);
|
|
572
|
+
if (!matches) throw new Error("workflow mutation requires the session's active, matching assignment actor");
|
|
527
573
|
return caller;
|
|
528
574
|
}
|
|
529
575
|
|
|
576
|
+
function assignmentActorContext(sessionDir: string, slug: string): {
|
|
577
|
+
assignment: JsonRecord;
|
|
578
|
+
caller: ReturnType<typeof resolveCurrentAssignmentActor>;
|
|
579
|
+
matches: boolean;
|
|
580
|
+
} {
|
|
581
|
+
const assignment = readActiveAssignment(sessionDir, slug);
|
|
582
|
+
const caller = resolveCurrentAssignmentActor();
|
|
583
|
+
return { assignment, caller, matches: assignment.actor_key === caller.actorKey && isDeepStrictEqual(normalizeAssignmentActor(assignment.actor), normalizeAssignmentActor(caller.actor)) };
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function normalizeAssignmentActor(value: unknown): JsonRecord | null {
|
|
587
|
+
return value && typeof value === "object" ? { ...(value as JsonRecord), human: (value as JsonRecord).human ?? null } : null;
|
|
588
|
+
}
|
|
589
|
+
|
|
530
590
|
function readActiveAssignment(sessionDir: string, slug: string): JsonRecord {
|
|
531
591
|
const assignment = readAssignment(sessionDir, slug);
|
|
532
592
|
if (assignment.status !== "claimed" || assignment.artifact_dir !== slug || typeof assignment.actor_key !== "string" || !assignment.actor_key || !assignment.actor || typeof assignment.actor !== "object" || Array.isArray(assignment.actor)) {
|
|
@@ -32,6 +32,15 @@ export type ContinuationTurnResult =
|
|
|
32
32
|
| { status: "completed"; summary?: string }
|
|
33
33
|
| { status: "wait"; barrier: ContinuationBarrier; summary?: string };
|
|
34
34
|
|
|
35
|
+
export class ContinuationAdapterTimeoutError extends Error {
|
|
36
|
+
readonly code = "CONTINUATION_ADAPTER_TIMEOUT";
|
|
37
|
+
|
|
38
|
+
constructor(timeoutMs: number) {
|
|
39
|
+
super(`continuation adapter timed out after ${timeoutMs}ms`);
|
|
40
|
+
this.name = "ContinuationAdapterTimeoutError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
35
44
|
export type ContinuationDriverState = {
|
|
36
45
|
schema_version: "1.0";
|
|
37
46
|
run_id: string;
|
|
@@ -40,13 +49,17 @@ export type ContinuationDriverState = {
|
|
|
40
49
|
adapter_command_identity: string | null;
|
|
41
50
|
status: "active" | "waiting" | "done" | "failed" | "budget_exhausted";
|
|
42
51
|
turns_started: number;
|
|
52
|
+
// Added after schema 1.0 shipped. Legacy state files may omit it.
|
|
53
|
+
active_turn_step?: string | null;
|
|
54
|
+
// Anchors the ephemeral signer outside active-turn.json. Legacy state files may omit it.
|
|
55
|
+
active_turn_public_key_digest?: string | null;
|
|
43
56
|
pending_barrier: ContinuationBarrier | null;
|
|
44
57
|
updated_at: string;
|
|
45
58
|
};
|
|
46
59
|
|
|
47
60
|
export type ContinuationDriverEvent = {
|
|
48
61
|
schema_version: "1.0";
|
|
49
|
-
type: "started" | "turn_started" | "turn_completed" | "turn_failed" | "parked" | "resumed" | "done" | "budget_exhausted";
|
|
62
|
+
type: "started" | "turn_started" | "turn_completed" | "gate_not_advanced" | "turn_failed" | "authority_cleanup_failed" | "parked" | "resumed" | "done" | "budget_exhausted";
|
|
50
63
|
run_id: string;
|
|
51
64
|
definition_id: string;
|
|
52
65
|
current_step: string;
|
|
@@ -54,6 +67,7 @@ export type ContinuationDriverEvent = {
|
|
|
54
67
|
at: string;
|
|
55
68
|
barrier?: ContinuationBarrier;
|
|
56
69
|
summary?: string;
|
|
70
|
+
failure_kind?: "adapter_error" | "timeout";
|
|
57
71
|
};
|
|
58
72
|
|
|
59
73
|
export interface ContinuationStateStore {
|
|
@@ -65,9 +79,27 @@ export interface ContinuationStateStore {
|
|
|
65
79
|
export interface ContinuationRuntimePort {
|
|
66
80
|
inspect(): Promise<ContinuationSnapshot>;
|
|
67
81
|
synchronize(): Promise<ContinuationSnapshot>;
|
|
68
|
-
execute(request: ContinuationTurnRequest): Promise<ContinuationTurnResult>;
|
|
82
|
+
execute(request: ContinuationTurnRequest, context?: ContinuationTurnContext): Promise<ContinuationTurnResult>;
|
|
69
83
|
}
|
|
70
84
|
|
|
85
|
+
export type ContinuationTurnContext = {
|
|
86
|
+
continuationTurnSecret?: string;
|
|
87
|
+
continuationRunId?: string;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export type ContinuationDriverLockLease = {
|
|
91
|
+
pid: number;
|
|
92
|
+
token: string;
|
|
93
|
+
created_at: string;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export type ContinuationTurnAuthority = {
|
|
97
|
+
runId: string;
|
|
98
|
+
turnSecret: string;
|
|
99
|
+
publicKeyDigest: string;
|
|
100
|
+
cleanup(): boolean;
|
|
101
|
+
};
|
|
102
|
+
|
|
71
103
|
export type ContinuationDriverOutcome = {
|
|
72
104
|
outcome: "done" | "waiting" | "failed" | "budget_exhausted";
|
|
73
105
|
turns_started: number;
|
|
@@ -82,6 +114,7 @@ export interface RunContinuationDriverInput {
|
|
|
82
114
|
store: ContinuationStateStore;
|
|
83
115
|
waitForBarrier?: (barrier: ContinuationBarrier) => Promise<"ready" | "pending">;
|
|
84
116
|
authorizeTurn?: () => Promise<void>;
|
|
117
|
+
issueTurnAuthority?: (request: ContinuationTurnRequest) => Promise<ContinuationTurnAuthority>;
|
|
85
118
|
now?: () => Date;
|
|
86
119
|
}
|
|
87
120
|
|
|
@@ -92,6 +125,7 @@ export interface DriveBuilderFlowSessionInput {
|
|
|
92
125
|
execute: ContinuationRuntimePort["execute"];
|
|
93
126
|
waitForBarrier?: RunContinuationDriverInput["waitForBarrier"];
|
|
94
127
|
authorizeTurn?: RunContinuationDriverInput["authorizeTurn"];
|
|
128
|
+
issueTurnAuthority?: RunContinuationDriverInput["issueTurnAuthority"];
|
|
95
129
|
now?: () => Date;
|
|
96
130
|
}
|
|
97
131
|
|
|
@@ -100,6 +134,7 @@ export async function runContinuationDriver(input: RunContinuationDriverInput):
|
|
|
100
134
|
const now = input.now ?? (() => new Date());
|
|
101
135
|
const waitForBarrier = input.waitForBarrier ?? (async () => "pending" as const);
|
|
102
136
|
const authorizeTurn = input.authorizeTurn ?? (async () => {});
|
|
137
|
+
const issueTurnAuthority = input.issueTurnAuthority;
|
|
103
138
|
const adapterCommandIdentity = input.adapterCommandIdentity ?? null;
|
|
104
139
|
let snapshot = validateSnapshot(await input.runtime.inspect());
|
|
105
140
|
let state = loadOrCreateState(input.store, snapshot, input.maxTurns, adapterCommandIdentity, now);
|
|
@@ -114,10 +149,10 @@ export async function runContinuationDriver(input: RunContinuationDriverInput):
|
|
|
114
149
|
const barrier = state.pending_barrier;
|
|
115
150
|
const readiness = await waitForBarrier(barrier);
|
|
116
151
|
if (readiness === "pending") {
|
|
117
|
-
state = saveState(input.store, state, { status: "waiting" }, now);
|
|
152
|
+
state = saveState(input.store, state, { status: "waiting", active_turn_step: null, active_turn_public_key_digest: null }, now);
|
|
118
153
|
return { outcome: "waiting", turns_started: state.turns_started, snapshot, barrier };
|
|
119
154
|
}
|
|
120
|
-
state = saveState(input.store, state, { status: "active", pending_barrier: null }, now);
|
|
155
|
+
state = saveState(input.store, state, { status: "active", active_turn_step: null, active_turn_public_key_digest: null, pending_barrier: null }, now);
|
|
121
156
|
appendEvent(input.store, state, snapshot, "resumed", now, { barrier });
|
|
122
157
|
}
|
|
123
158
|
|
|
@@ -127,18 +162,26 @@ export async function runContinuationDriver(input: RunContinuationDriverInput):
|
|
|
127
162
|
if (initialOutcome === "done") return finishDone(input.store, state, snapshot, now);
|
|
128
163
|
if (initialOutcome === "failed") return finishFailed(input.store, state, snapshot, now);
|
|
129
164
|
if (initialOutcome === "waiting") {
|
|
130
|
-
state = saveState(input.store, state, { status: "waiting" }, now);
|
|
165
|
+
state = saveState(input.store, state, { status: "waiting", active_turn_step: null, active_turn_public_key_digest: null }, now);
|
|
131
166
|
return { outcome: "waiting", turns_started: state.turns_started, snapshot };
|
|
132
167
|
}
|
|
133
168
|
|
|
134
169
|
while (state.turns_started < input.maxTurns) {
|
|
135
170
|
await authorizeTurn();
|
|
136
171
|
const iteration = state.turns_started + 1;
|
|
137
|
-
state = saveState(input.store, state, {
|
|
172
|
+
state = saveState(input.store, state, {
|
|
173
|
+
status: "active",
|
|
174
|
+
turns_started: iteration,
|
|
175
|
+
active_turn_step: snapshot.current_step,
|
|
176
|
+
active_turn_public_key_digest: null,
|
|
177
|
+
}, now);
|
|
138
178
|
appendEvent(input.store, state, snapshot, "turn_started", now);
|
|
139
179
|
let result: ContinuationTurnResult;
|
|
180
|
+
let request: ContinuationTurnRequest;
|
|
181
|
+
let authority: ContinuationTurnAuthority | undefined;
|
|
182
|
+
let terminalized = false;
|
|
140
183
|
try {
|
|
141
|
-
|
|
184
|
+
request = Object.freeze({
|
|
142
185
|
schema_version: "1.0",
|
|
143
186
|
run_id: snapshot.run_id,
|
|
144
187
|
definition_id: snapshot.definition_id,
|
|
@@ -146,19 +189,53 @@ export async function runContinuationDriver(input: RunContinuationDriverInput):
|
|
|
146
189
|
iteration,
|
|
147
190
|
max_turns: input.maxTurns,
|
|
148
191
|
next_action: snapshot.next_action ? structuredClone(snapshot.next_action) : null,
|
|
149
|
-
})
|
|
192
|
+
});
|
|
193
|
+
authority = issueTurnAuthority ? await issueTurnAuthority(request) : undefined;
|
|
194
|
+
if (authority) {
|
|
195
|
+
state = saveState(input.store, state, { active_turn_public_key_digest: authority.publicKeyDigest }, now);
|
|
196
|
+
}
|
|
197
|
+
result = validateTurnResult(await input.runtime.execute(request, authority ? {
|
|
198
|
+
continuationTurnSecret: authority.turnSecret,
|
|
199
|
+
continuationRunId: authority.runId,
|
|
200
|
+
} : undefined));
|
|
150
201
|
} catch (error) {
|
|
151
|
-
|
|
202
|
+
const summary = boundedErrorMessage(error);
|
|
203
|
+
appendEvent(input.store, state, snapshot, "turn_failed", now, { summary, failure_kind: error instanceof ContinuationAdapterTimeoutError ? "timeout" : "adapter_error" });
|
|
152
204
|
snapshot = validateSnapshot(await input.runtime.synchronize());
|
|
153
205
|
assertMissionIdentity(state, snapshot);
|
|
154
206
|
const outcome = canonicalOutcome(snapshot);
|
|
155
|
-
if (outcome === "done")
|
|
156
|
-
|
|
207
|
+
if (outcome === "done") {
|
|
208
|
+
terminalized = true;
|
|
209
|
+
return finishDone(input.store, state, snapshot, now);
|
|
210
|
+
}
|
|
211
|
+
if (outcome === "failed") {
|
|
212
|
+
terminalized = true;
|
|
213
|
+
return finishFailed(input.store, state, snapshot, now);
|
|
214
|
+
}
|
|
157
215
|
if (outcome === "waiting") {
|
|
158
216
|
state = saveState(input.store, state, { status: "waiting" }, now);
|
|
159
217
|
return { outcome: "waiting", turns_started: state.turns_started, snapshot };
|
|
160
218
|
}
|
|
161
219
|
continue;
|
|
220
|
+
} finally {
|
|
221
|
+
let cleanupFailure: string | undefined;
|
|
222
|
+
try {
|
|
223
|
+
if (authority && !authority.cleanup()) cleanupFailure = "continuation turn authority cleanup returned false";
|
|
224
|
+
} catch (error) {
|
|
225
|
+
cleanupFailure = boundedErrorMessage(error);
|
|
226
|
+
} finally {
|
|
227
|
+
if (!terminalized) state = saveState(input.store, state, {
|
|
228
|
+
active_turn_step: null,
|
|
229
|
+
active_turn_public_key_digest: null,
|
|
230
|
+
}, now);
|
|
231
|
+
if (cleanupFailure) {
|
|
232
|
+
try {
|
|
233
|
+
appendEvent(input.store, state, snapshot, "authority_cleanup_failed", now, { summary: cleanupFailure });
|
|
234
|
+
} catch {
|
|
235
|
+
// Audit is best effort. A cleanup extension must not replace the adapter outcome.
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
162
239
|
}
|
|
163
240
|
appendEvent(input.store, state, snapshot, "turn_completed", now, { summary: result.summary });
|
|
164
241
|
|
|
@@ -175,6 +252,9 @@ export async function runContinuationDriver(input: RunContinuationDriverInput):
|
|
|
175
252
|
|
|
176
253
|
snapshot = validateSnapshot(await input.runtime.synchronize());
|
|
177
254
|
assertMissionIdentity(state, snapshot);
|
|
255
|
+
if (snapshot.status === "active" && snapshot.current_step === request.current_step) {
|
|
256
|
+
appendEvent(input.store, state, snapshot, "gate_not_advanced", now, { summary: "adapter completed without canonical gate advancement" });
|
|
257
|
+
}
|
|
178
258
|
const outcome = canonicalOutcome(snapshot);
|
|
179
259
|
if (outcome === "done") return finishDone(input.store, state, snapshot, now);
|
|
180
260
|
if (outcome === "failed") return finishFailed(input.store, state, snapshot, now);
|
|
@@ -184,7 +264,11 @@ export async function runContinuationDriver(input: RunContinuationDriverInput):
|
|
|
184
264
|
}
|
|
185
265
|
}
|
|
186
266
|
|
|
187
|
-
state = saveState(input.store, state, {
|
|
267
|
+
state = saveState(input.store, state, {
|
|
268
|
+
status: "budget_exhausted",
|
|
269
|
+
active_turn_step: null,
|
|
270
|
+
active_turn_public_key_digest: null,
|
|
271
|
+
}, now);
|
|
188
272
|
appendEvent(input.store, state, snapshot, "budget_exhausted", now);
|
|
189
273
|
return { outcome: "budget_exhausted", turns_started: state.turns_started, snapshot };
|
|
190
274
|
}
|
|
@@ -203,21 +287,23 @@ export async function driveBuilderFlowSession(input: DriveBuilderFlowSessionInpu
|
|
|
203
287
|
store: createFileContinuationStore(sessionDir),
|
|
204
288
|
...(input.waitForBarrier ? { waitForBarrier: input.waitForBarrier } : {}),
|
|
205
289
|
...(input.authorizeTurn ? { authorizeTurn: input.authorizeTurn } : {}),
|
|
290
|
+
...(input.issueTurnAuthority ? { issueTurnAuthority: input.issueTurnAuthority } : {}),
|
|
206
291
|
...(input.now ? { now: input.now } : {}),
|
|
207
292
|
});
|
|
208
293
|
}
|
|
209
294
|
|
|
210
|
-
export async function withContinuationDriverLock<T>(sessionDirInput: string, body: () => Promise<T>): Promise<T> {
|
|
295
|
+
export async function withContinuationDriverLock<T>(sessionDirInput: string, body: (lease: ContinuationDriverLockLease) => Promise<T>): Promise<T> {
|
|
211
296
|
const sessionDir = path.resolve(sessionDirInput);
|
|
212
297
|
const driverDir = path.join(sessionDir, "continuation-driver");
|
|
213
298
|
const locksDir = path.join(driverDir, "locks");
|
|
214
299
|
ensureSafeDirectory(sessionDir, locksDir);
|
|
215
300
|
const token = randomUUID();
|
|
216
301
|
const lockFile = path.join(locksDir, `${process.pid}-${token}.lock`);
|
|
217
|
-
const
|
|
302
|
+
const lease: ContinuationDriverLockLease = { pid: process.pid, token, created_at: new Date().toISOString() };
|
|
303
|
+
const owner = { schema_version: "1.0" as const, ...lease };
|
|
218
304
|
acquireDriverLock(locksDir, lockFile, owner);
|
|
219
305
|
try {
|
|
220
|
-
return await body();
|
|
306
|
+
return await body(lease);
|
|
221
307
|
} finally {
|
|
222
308
|
releaseDriverLock(lockFile, token);
|
|
223
309
|
}
|
|
@@ -362,6 +448,8 @@ function loadOrCreateState(
|
|
|
362
448
|
adapter_command_identity: adapterCommandIdentity,
|
|
363
449
|
status: "active",
|
|
364
450
|
turns_started: 0,
|
|
451
|
+
active_turn_step: null,
|
|
452
|
+
active_turn_public_key_digest: null,
|
|
365
453
|
pending_barrier: null,
|
|
366
454
|
updated_at: now().toISOString(),
|
|
367
455
|
};
|
|
@@ -375,20 +463,30 @@ function canonicalOutcome(snapshot: ContinuationSnapshot): "done" | "waiting" |
|
|
|
375
463
|
}
|
|
376
464
|
|
|
377
465
|
function finishDone(store: ContinuationStateStore, state: ContinuationDriverState, snapshot: ContinuationSnapshot, now: () => Date): ContinuationDriverOutcome {
|
|
378
|
-
const done = saveState(store, state, {
|
|
466
|
+
const done = saveState(store, state, {
|
|
467
|
+
status: "done",
|
|
468
|
+
active_turn_step: null,
|
|
469
|
+
active_turn_public_key_digest: null,
|
|
470
|
+
pending_barrier: null,
|
|
471
|
+
}, now);
|
|
379
472
|
appendEvent(store, done, snapshot, "done", now);
|
|
380
473
|
return { outcome: "done", turns_started: done.turns_started, snapshot };
|
|
381
474
|
}
|
|
382
475
|
|
|
383
476
|
function finishFailed(store: ContinuationStateStore, state: ContinuationDriverState, snapshot: ContinuationSnapshot, now: () => Date): ContinuationDriverOutcome {
|
|
384
|
-
const failed = saveState(store, state, {
|
|
477
|
+
const failed = saveState(store, state, {
|
|
478
|
+
status: "failed",
|
|
479
|
+
active_turn_step: null,
|
|
480
|
+
active_turn_public_key_digest: null,
|
|
481
|
+
pending_barrier: null,
|
|
482
|
+
}, now);
|
|
385
483
|
return { outcome: "failed", turns_started: failed.turns_started, snapshot };
|
|
386
484
|
}
|
|
387
485
|
|
|
388
486
|
function saveState(
|
|
389
487
|
store: ContinuationStateStore,
|
|
390
488
|
current: ContinuationDriverState,
|
|
391
|
-
patch: Partial<Pick<ContinuationDriverState, "status" | "turns_started" | "pending_barrier">>,
|
|
489
|
+
patch: Partial<Pick<ContinuationDriverState, "status" | "turns_started" | "active_turn_step" | "active_turn_public_key_digest" | "pending_barrier">>,
|
|
392
490
|
now: () => Date,
|
|
393
491
|
): ContinuationDriverState {
|
|
394
492
|
const next = { ...current, ...patch, updated_at: now().toISOString() };
|
|
@@ -402,7 +500,7 @@ function appendEvent(
|
|
|
402
500
|
snapshot: ContinuationSnapshot,
|
|
403
501
|
type: ContinuationDriverEvent["type"],
|
|
404
502
|
now: () => Date,
|
|
405
|
-
extra: Pick<ContinuationDriverEvent, "barrier" | "summary"> = {},
|
|
503
|
+
extra: Pick<ContinuationDriverEvent, "barrier" | "summary" | "failure_kind"> = {},
|
|
406
504
|
): void {
|
|
407
505
|
store.append({
|
|
408
506
|
schema_version: "1.0",
|
|
@@ -414,6 +512,7 @@ function appendEvent(
|
|
|
414
512
|
at: now().toISOString(),
|
|
415
513
|
...(extra.barrier ? { barrier: extra.barrier } : {}),
|
|
416
514
|
...(extra.summary ? { summary: extra.summary } : {}),
|
|
515
|
+
...(extra.failure_kind ? { failure_kind: extra.failure_kind } : {}),
|
|
417
516
|
});
|
|
418
517
|
}
|
|
419
518
|
|
|
@@ -461,6 +560,13 @@ function validateState(state: ContinuationDriverState): void {
|
|
|
461
560
|
}
|
|
462
561
|
if (!Number.isSafeInteger(state.turns_started) || state.turns_started < 0) throw new Error("continuation driver turns_started must be a non-negative integer");
|
|
463
562
|
if (state.turns_started > state.max_turns) throw new Error("continuation driver turns_started cannot exceed max_turns");
|
|
563
|
+
if (state.active_turn_step !== undefined && state.active_turn_step !== null && (typeof state.active_turn_step !== "string" || state.active_turn_step.length === 0)) {
|
|
564
|
+
throw new Error("continuation driver active_turn_step must be a non-empty string or null");
|
|
565
|
+
}
|
|
566
|
+
if (state.active_turn_public_key_digest !== undefined && state.active_turn_public_key_digest !== null
|
|
567
|
+
&& (typeof state.active_turn_public_key_digest !== "string" || !/^[a-f0-9]{64}$/.test(state.active_turn_public_key_digest))) {
|
|
568
|
+
throw new Error("continuation driver active_turn_public_key_digest must be a SHA-256 hex digest or null");
|
|
569
|
+
}
|
|
464
570
|
if (!new Set(["active", "waiting", "done", "failed", "budget_exhausted"]).has(state.status)) throw new Error("continuation driver state status is invalid");
|
|
465
571
|
if (state.status === "budget_exhausted" && state.turns_started !== state.max_turns) {
|
|
466
572
|
throw new Error("continuation driver budget_exhausted state must consume max_turns");
|
package/src/index.ts
CHANGED
|
@@ -46,6 +46,7 @@ export {
|
|
|
46
46
|
export type { BuilderFlowAgentLifecycleInput, BuilderFlowAuthorizedLifecycleInput, BuilderFlowSessionInput, BuilderFlowSessionResult } from "./builder-flow-runtime.js";
|
|
47
47
|
|
|
48
48
|
export {
|
|
49
|
+
ContinuationAdapterTimeoutError,
|
|
49
50
|
createFileContinuationStore,
|
|
50
51
|
driveBuilderFlowSession,
|
|
51
52
|
runContinuationDriver,
|
|
@@ -54,11 +55,14 @@ export {
|
|
|
54
55
|
export type {
|
|
55
56
|
ContinuationBarrier,
|
|
56
57
|
ContinuationDriverEvent,
|
|
58
|
+
ContinuationDriverLockLease,
|
|
57
59
|
ContinuationDriverOutcome,
|
|
58
60
|
ContinuationDriverState,
|
|
59
61
|
ContinuationRuntimePort,
|
|
60
62
|
ContinuationSnapshot,
|
|
61
63
|
ContinuationStateStore,
|
|
64
|
+
ContinuationTurnAuthority,
|
|
65
|
+
ContinuationTurnContext,
|
|
62
66
|
ContinuationTurnRequest,
|
|
63
67
|
ContinuationTurnResult,
|
|
64
68
|
DriveBuilderFlowSessionInput,
|