@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
package/.github/workflows/ci.yml
CHANGED
|
@@ -34,6 +34,10 @@ jobs:
|
|
|
34
34
|
steps:
|
|
35
35
|
- name: Checkout
|
|
36
36
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
37
|
+
with:
|
|
38
|
+
# The Codex legacy-instruction ownership audit independently walks
|
|
39
|
+
# every seed-capable release tag and must not trust the catalog alone.
|
|
40
|
+
fetch-depth: 0
|
|
37
41
|
|
|
38
42
|
- name: Set up Node.js
|
|
39
43
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
@@ -326,6 +330,10 @@ jobs:
|
|
|
326
330
|
continue-on-error: true
|
|
327
331
|
run: bash evals/ci/run-baseline.sh --check telemetry-usage-pipeline-integration
|
|
328
332
|
|
|
333
|
+
- name: Telemetry tool usage integration
|
|
334
|
+
continue-on-error: true
|
|
335
|
+
run: bash evals/ci/run-baseline.sh --check telemetry-tool-usage-integration
|
|
336
|
+
|
|
329
337
|
- name: Economics record integration
|
|
330
338
|
continue-on-error: true
|
|
331
339
|
run: bash evals/ci/run-baseline.sh --check economics-record-integration
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.10.0](https://github.com/kontourai/flow-agents/compare/v3.9.0...v3.10.0) (2026-07-13)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **telemetry:** per-turn model/tokens/cost on tool events ([#568](https://github.com/kontourai/flow-agents/issues/568) slice 1) ([80ac0e7](https://github.com/kontourai/flow-agents/commit/80ac0e73a524fb1f84f0bffac94fe78ce4883b74))
|
|
9
|
+
* **telemetry:** per-turn model/tokens/cost on tool events ([#568](https://github.com/kontourai/flow-agents/issues/568) slice 1) ([2b6276d](https://github.com/kontourai/flow-agents/commit/2b6276d42b30123a61b3d9a9181ebd77affc0e70))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Fixes
|
|
13
|
+
|
|
14
|
+
* **#440:** scope ownership signals to the actor's own per-actor pointer ([#586](https://github.com/kontourai/flow-agents/issues/586)) ([7a8fc82](https://github.com/kontourai/flow-agents/commit/7a8fc82f22201fd916631beff6bb85fe288d461e))
|
|
15
|
+
* **liveness:** identify Codex actors by thread ([#556](https://github.com/kontourai/flow-agents/issues/556)) ([5ac02cb](https://github.com/kontourai/flow-agents/commit/5ac02cbc6f374d0b75bc03c4230f6d255568693a))
|
|
16
|
+
* **packaging:** preserve runtime instruction files ([#575](https://github.com/kontourai/flow-agents/issues/575)) ([f55d288](https://github.com/kontourai/flow-agents/commit/f55d28839bf87f35aaecb5a7aae0ba7fe55f51c7))
|
|
17
|
+
* **packaging:** stop seeding Codex global instructions ([#573](https://github.com/kontourai/flow-agents/issues/573)) ([2d25cb6](https://github.com/kontourai/flow-agents/commit/2d25cb65af2b5403a164a645ae615e2388681640))
|
|
18
|
+
* resolve continuation validator from installed runtime ([#591](https://github.com/kontourai/flow-agents/issues/591)) ([86191c4](https://github.com/kontourai/flow-agents/commit/86191c4e39abcb3123b8823f4604ca7183d0be83))
|
|
19
|
+
|
|
3
20
|
## [3.9.0](https://github.com/kontourai/flow-agents/compare/v3.8.0...v3.9.0) (2026-07-12)
|
|
4
21
|
|
|
5
22
|
|
package/README.md
CHANGED
|
@@ -91,7 +91,7 @@ npx @kontourai/flow-agents init --runtime pi --dest /path/to/workspace --yes
|
|
|
91
91
|
|
|
92
92
|
For Codex global installs, omit `--dest` and use `--global`: Flow Agents installs into `CODEX_HOME` when it is set, otherwise `~/.codex`. Pass `--dest` only when you intentionally want an isolated or test-specific Codex home.
|
|
93
93
|
|
|
94
|
-
Runtime auto-detection is best-effort: it first checks environment markers set by the invoking coding agent (e.g. `CLAUDECODE`, `CODEX_SESSION_ID`, `OPENCODE_SESSION_ID`), then falls back to checking whether exactly one of `~/.claude`, `~/.codex` (or `$CODEX_HOME`), or opencode's global config dir already exists. If neither signal is unambiguous, it defaults to `base`. Pass `--runtime` explicitly to override the detected default at any time.
|
|
94
|
+
Runtime auto-detection is best-effort: it first checks environment markers set by the invoking coding agent (e.g. `CLAUDECODE`, Codex's preferred `CODEX_THREAD_ID`, the backward-compatible `CODEX_SESSION_ID`, or `OPENCODE_SESSION_ID`), then falls back to checking whether exactly one of `~/.claude`, `~/.codex` (or `$CODEX_HOME`), or opencode's global config dir already exists. If neither signal is unambiguous, it defaults to `base`. Pass `--runtime` explicitly to override the detected default at any time. Codex thread identifiers are never written into actor keys verbatim; Flow Agents derives a stable, domain-separated opaque token instead.
|
|
95
95
|
|
|
96
96
|
Working from a checkout (for contributors): `npm install && npm run build`, then `node build/src/cli.js init --dest /path/to/workspace`.
|
|
97
97
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import * as os from "node:os";
|
|
3
2
|
import * as path from "node:path";
|
|
4
3
|
import { randomBytes } from "node:crypto";
|
|
5
4
|
import { createRequire } from "node:module";
|
|
@@ -81,25 +80,12 @@ function loadActorStruct(args) {
|
|
|
81
80
|
}
|
|
82
81
|
export function resolveCurrentAssignmentActor() {
|
|
83
82
|
const helper = loadActorIdentityHelper();
|
|
84
|
-
const resolved = helper.
|
|
83
|
+
const resolved = helper.resolveActorIdentity(process.env);
|
|
85
84
|
if (helper.isUnresolvedActor(resolved.actor))
|
|
86
85
|
throw new Error("could not resolve an actor identity (no --actor-json and no resolvable environment actor); pass --actor-json explicitly");
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
// CI session — actor_key stays correct (so no false-block), but record.actor is malformed and the
|
|
91
|
-
// audit-trail / `assignment-provider status` output for CI sessions would be corrupt.
|
|
92
|
-
const ci = resolved.source.startsWith("ci-runtime") ? helper.detectCiActor(process.env) : null;
|
|
93
|
-
const runtimeSessionId = resolved.source.startsWith("runtime-session-id") ? helper.runtimeSessionId(process.env) : "";
|
|
94
|
-
const ancestrySeed = resolved.source === "process-ancestry" ? helper.ancestorActorSeed() : "";
|
|
95
|
-
const actor = resolved.source === "explicit-override"
|
|
96
|
-
? { runtime: "explicit-override", session_id: resolved.actor, host: os.hostname(), human: null }
|
|
97
|
-
: ci && ci.session_id
|
|
98
|
-
? { runtime: ci.runtime, session_id: ci.session_id, host: os.hostname(), human: null }
|
|
99
|
-
: runtimeSessionId
|
|
100
|
-
? { runtime: helper.detectRuntime(process.env), session_id: runtimeSessionId, host: os.hostname(), human: null }
|
|
101
|
-
: { runtime: helper.detectRuntime(process.env), session_id: ancestrySeed ? `anc-${ancestrySeed}` : resolved.actor, host: os.hostname(), human: null };
|
|
102
|
-
return { actor, actorKey: resolved.actor };
|
|
86
|
+
if (!resolved.actorStruct)
|
|
87
|
+
throw new Error("actor identity resolved without a canonical actor struct");
|
|
88
|
+
return { actor: { ...resolved.actorStruct, human: resolved.actorStruct.human ?? null }, actorKey: resolved.actor };
|
|
103
89
|
}
|
|
104
90
|
export function assignmentFilePath(artifactRoot, subjectId) {
|
|
105
91
|
const sanitized = loadActorIdentityHelper().sanitizeSegment(subjectId);
|
|
@@ -8,17 +8,19 @@ export type ContinuationAdapterCommand = {
|
|
|
8
8
|
}>;
|
|
9
9
|
};
|
|
10
10
|
export declare function loadContinuationAdapterCommand(commandFileInput: string): ContinuationAdapterCommand;
|
|
11
|
-
export declare function executeContinuationAdapter(commandFileInput: string, request: ContinuationTurnRequest, options:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}): Promise<ContinuationTurnResult>;
|
|
15
|
-
export declare function executeLoadedContinuationAdapter(command: ContinuationAdapterCommand, request: ContinuationTurnRequest, options: {
|
|
16
|
-
cwd: string;
|
|
17
|
-
timeoutMs: number;
|
|
18
|
-
}): Promise<ContinuationTurnResult>;
|
|
11
|
+
export declare function executeContinuationAdapter(commandFileInput: string, request: ContinuationTurnRequest, options: ContinuationAdapterOptions): Promise<ContinuationTurnResult>;
|
|
12
|
+
export declare function executeLoadedContinuationAdapter(command: ContinuationAdapterCommand, request: ContinuationTurnRequest, options: ContinuationAdapterOptions): Promise<ContinuationTurnResult>;
|
|
13
|
+
export declare function assertLoadedContinuationAdapterIntegrity(command: ContinuationAdapterCommand): void;
|
|
19
14
|
export declare function waitForContinuationBarrier(barrier: ContinuationBarrier, options: {
|
|
20
15
|
maxWaitMs: number;
|
|
21
16
|
pollMs: number;
|
|
22
17
|
now?: () => number;
|
|
23
18
|
sleep?: (ms: number) => Promise<void>;
|
|
24
19
|
}): Promise<"ready" | "pending">;
|
|
20
|
+
type ContinuationAdapterOptions = {
|
|
21
|
+
cwd: string;
|
|
22
|
+
timeoutMs: number;
|
|
23
|
+
continuationTurnSecret?: string;
|
|
24
|
+
continuationRunId?: string;
|
|
25
|
+
};
|
|
26
|
+
export {};
|
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
|
+
import { ContinuationAdapterTimeoutError } from "../continuation-driver.js";
|
|
5
6
|
export function loadContinuationAdapterCommand(commandFileInput) {
|
|
6
7
|
const commandFile = path.resolve(commandFileInput);
|
|
7
8
|
const command = validateAdapterCommand(JSON.parse(readRegularFileNoFollow(commandFile, "continuation adapter command file")));
|
|
@@ -19,13 +20,16 @@ export async function executeContinuationAdapter(commandFileInput, request, opti
|
|
|
19
20
|
return executeLoadedContinuationAdapter(command, request, options);
|
|
20
21
|
}
|
|
21
22
|
export async function executeLoadedContinuationAdapter(command, request, options) {
|
|
23
|
+
assertLoadedContinuationAdapterIntegrity(command);
|
|
24
|
+
assertPositiveInteger(options.timeoutMs, "continuation adapter timeoutMs", 1, 86_400_000);
|
|
25
|
+
return await spawnAdapter(command, request, options);
|
|
26
|
+
}
|
|
27
|
+
export function assertLoadedContinuationAdapterIntegrity(command) {
|
|
22
28
|
for (const entry of command.integrity) {
|
|
23
29
|
if (sha256File(entry.file, "continuation adapter integrity file") !== entry.sha256) {
|
|
24
30
|
throw new Error(`continuation adapter integrity changed after mission binding: ${entry.file}`);
|
|
25
31
|
}
|
|
26
32
|
}
|
|
27
|
-
assertPositiveInteger(options.timeoutMs, "continuation adapter timeoutMs", 1, 86_400_000);
|
|
28
|
-
return await spawnAdapter(command, request, options);
|
|
29
33
|
}
|
|
30
34
|
export async function waitForContinuationBarrier(barrier, options) {
|
|
31
35
|
assertPositiveInteger(options.maxWaitMs, "continuation barrier maxWaitMs", 0, 86_400_000);
|
|
@@ -54,7 +58,7 @@ function spawnAdapter(command, request, options) {
|
|
|
54
58
|
return new Promise((resolve, reject) => {
|
|
55
59
|
const child = spawn(command.argv[0], command.argv.slice(1), {
|
|
56
60
|
cwd: options.cwd,
|
|
57
|
-
env:
|
|
61
|
+
env: adapterEnvironment(options),
|
|
58
62
|
detached: process.platform !== "win32",
|
|
59
63
|
shell: false,
|
|
60
64
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -70,7 +74,7 @@ function spawnAdapter(command, request, options) {
|
|
|
70
74
|
const timeout = setTimeout(() => {
|
|
71
75
|
if (settled)
|
|
72
76
|
return;
|
|
73
|
-
terminationError = new
|
|
77
|
+
terminationError = new ContinuationAdapterTimeoutError(options.timeoutMs);
|
|
74
78
|
terminateProcessGroup(child.pid, "SIGTERM");
|
|
75
79
|
forcedKill = setTimeout(() => terminateProcessGroup(child.pid, "SIGKILL"), 250);
|
|
76
80
|
}, options.timeoutMs);
|
|
@@ -143,6 +147,20 @@ function spawnAdapter(command, request, options) {
|
|
|
143
147
|
child.stdin.end(`${JSON.stringify(request)}\n`);
|
|
144
148
|
});
|
|
145
149
|
}
|
|
150
|
+
function adapterEnvironment(options) {
|
|
151
|
+
const env = { ...process.env };
|
|
152
|
+
delete env.FLOW_AGENTS_CONTINUATION_TURN_SECRET;
|
|
153
|
+
delete env.FLOW_AGENTS_CONTINUATION_RUN_ID;
|
|
154
|
+
delete env.FLOW_AGENTS_CONTINUATION_TURN_NONCE;
|
|
155
|
+
delete env.FLOW_AGENTS_CONTINUATION_TURN_PUBLIC_KEY_DIGEST;
|
|
156
|
+
delete env.FLOW_AGENTS_CONTINUATION_ACTOR_B64;
|
|
157
|
+
const { continuationTurnSecret: turnSecret, continuationRunId: runId } = options;
|
|
158
|
+
if (!turnSecret || !runId)
|
|
159
|
+
return env;
|
|
160
|
+
env.FLOW_AGENTS_CONTINUATION_TURN_SECRET = turnSecret;
|
|
161
|
+
env.FLOW_AGENTS_CONTINUATION_RUN_ID = runId;
|
|
162
|
+
return env;
|
|
163
|
+
}
|
|
146
164
|
function validateAdapterCommand(value) {
|
|
147
165
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
148
166
|
throw new Error("continuation adapter command file must contain an object");
|
|
@@ -19,6 +19,10 @@ const sidecarSchemas = {
|
|
|
19
19
|
"release.json": "schemas/workflow-release.schema.json",
|
|
20
20
|
"learning.json": "schemas/workflow-learning.schema.json",
|
|
21
21
|
};
|
|
22
|
+
// Runtime coordination records live below a session but are not workflow
|
|
23
|
+
// sidecars. Recursing into them would validate continuation-driver/state.json
|
|
24
|
+
// against the public workflow-state schema and turn an active driver into a
|
|
25
|
+
// false Goal Fit hard block.
|
|
22
26
|
function readText(file) {
|
|
23
27
|
return fs.readFileSync(file, "utf8");
|
|
24
28
|
}
|
|
@@ -81,11 +85,27 @@ function hasEvidence(text) {
|
|
|
81
85
|
function hasExplicitAcceptance(text) {
|
|
82
86
|
return /explicitly accepted|accepted by user|user accepted|accepted gap/i.test(text);
|
|
83
87
|
}
|
|
88
|
+
function isPrivateRuntimeChild(dir) {
|
|
89
|
+
if (path.basename(dir) !== "continuation-driver")
|
|
90
|
+
return false;
|
|
91
|
+
const sessionDir = path.dirname(dir);
|
|
92
|
+
const sessionState = path.join(sessionDir, "state.json");
|
|
93
|
+
try {
|
|
94
|
+
const stat = fs.lstatSync(sessionState);
|
|
95
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
96
|
+
return false;
|
|
97
|
+
const state = JSON.parse(readText(sessionState));
|
|
98
|
+
return state.task_slug === path.basename(sessionDir);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
84
104
|
function walk(dir) {
|
|
85
105
|
const out = [];
|
|
86
106
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
87
107
|
const p = path.join(dir, entry.name);
|
|
88
|
-
if (entry.isDirectory())
|
|
108
|
+
if (entry.isDirectory() && !isPrivateRuntimeChild(p))
|
|
89
109
|
out.push(...walk(p));
|
|
90
110
|
else
|
|
91
111
|
out.push(p);
|
|
@@ -288,7 +288,9 @@ function resolveEnsureSessionActor(p) {
|
|
|
288
288
|
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.");
|
|
289
289
|
}
|
|
290
290
|
const explicitActor = explicitActorRaw ? helper.sanitizeSegment(explicitActorRaw) : "";
|
|
291
|
-
const resolved = explicitActor
|
|
291
|
+
const resolved = explicitActor
|
|
292
|
+
? { actor: explicitActor, source: "explicit-override", actorStruct: { runtime: "explicit-override", session_id: explicitActor, host: os.hostname() } }
|
|
293
|
+
: helper.resolveActorIdentity(process.env);
|
|
292
294
|
const unresolved = helper.isUnresolvedActor(resolved.actor);
|
|
293
295
|
const branchActorKey = unresolved ? `unknown-actor-${unknownDisambiguator(resolved.actor)}` : resolved.actor;
|
|
294
296
|
if (unresolved) {
|
|
@@ -297,17 +299,9 @@ function resolveEnsureSessionActor(p) {
|
|
|
297
299
|
// the ownership guard entirely when unresolved, rather than claiming under a synthetic identity.
|
|
298
300
|
return { actorStruct: { runtime: "unresolved", session_id: branchActorKey, host: os.hostname() }, actorKey: resolved.actor, branchActorKey, unresolved: true };
|
|
299
301
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
// actor_key would not match the CI actor at publish → self not recognized → false-block, the exact
|
|
304
|
-
// bug this issue removes). Uses the SAME helper.detectCiActor as resolveActor, single-sourced.
|
|
305
|
-
const ciActor = resolved.source.startsWith("ci-runtime") ? helper.detectCiActor(process.env) : null;
|
|
306
|
-
const actorStruct = resolved.source === "explicit-override"
|
|
307
|
-
? { runtime: "explicit-override", session_id: resolved.actor, host: os.hostname() }
|
|
308
|
-
: ciActor && ciActor.session_id
|
|
309
|
-
? { runtime: ciActor.runtime, session_id: ciActor.session_id, host: os.hostname() }
|
|
310
|
-
: { runtime: helper.detectRuntime(process.env), session_id: helper.runtimeSessionId(process.env) || (() => { const seed = helper.ancestorActorSeed(); return seed ? `anc-${seed}` : ""; })(), host: os.hostname() };
|
|
302
|
+
if (!resolved.actorStruct)
|
|
303
|
+
die("ensure-session could not resolve a canonical actor struct");
|
|
304
|
+
const actorStruct = resolved.actorStruct;
|
|
311
305
|
const actorKey = helper.serializeActor(actorStruct);
|
|
312
306
|
return { actorStruct, actorKey, branchActorKey, unresolved: false };
|
|
313
307
|
}
|
|
@@ -1767,14 +1761,15 @@ function updateCurrentAgent(root, dir, agentId, status, timestamp, actorKey) {
|
|
|
1767
1761
|
}
|
|
1768
1762
|
if (actorKey && !loadActorIdentityHelper().isUnresolvedActor(actorKey)) {
|
|
1769
1763
|
const helper = loadCurrentPointerHelper();
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1764
|
+
// #440 fix-wave 3: read through the legacy-aware path (readOwnCurrentPointer tries the new
|
|
1765
|
+
// collision-resistant filename first, then falls back to the pre-fix-wave-2 legacy filename)
|
|
1766
|
+
// instead of a direct fs read of perActorCurrentFile() alone — a direct read of ONLY the new
|
|
1767
|
+
// filename would silently skip this active_agents/updated_at projection for a pointer a
|
|
1768
|
+
// still-running pre-fix-wave-2 sidecar wrote under the old name. The write below always goes
|
|
1769
|
+
// through writePerActorCurrent (new filename only), so the pointer migrates to the new name
|
|
1770
|
+
// on first touch.
|
|
1771
|
+
const pointer = helper.readOwnCurrentPointer(root, actorKey);
|
|
1772
|
+
const perActor = pointer.payload;
|
|
1778
1773
|
if (perActor && path.resolve(root, perActor.artifact_dir ?? "") === path.resolve(dir)) {
|
|
1779
1774
|
helper.writePerActorCurrent(root, actorKey, applyAgentUpdate(perActor));
|
|
1780
1775
|
}
|
|
@@ -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
|
export const WORKFLOW_CONTRACT_VERSION = "1.0";
|
|
20
20
|
const PACKAGE_ROOT = flowAgentsPackageRoot();
|
|
21
21
|
const REQUIRE = createRequire(import.meta.url);
|
|
@@ -81,16 +81,39 @@ async function drive(sessionDir, argv, json) {
|
|
|
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
|
});
|
|
@@ -100,6 +123,9 @@ async function drive(sessionDir, argv, json) {
|
|
|
100
123
|
console.log(`Continuation driver ${result.outcome} after ${result.turns_started} turn(s); canonical Flow is ${result.snapshot.status} at ${result.snapshot.current_step}.`);
|
|
101
124
|
return 0;
|
|
102
125
|
}
|
|
126
|
+
function loadContinuationTurnAuthority() {
|
|
127
|
+
return REQUIRE(path.resolve(PACKAGE_ROOT, "scripts", "hooks", "lib", "continuation-turn-authority.js"));
|
|
128
|
+
}
|
|
103
129
|
async function start(argv) {
|
|
104
130
|
const parsed = parseArgs(argv);
|
|
105
131
|
assertOnlyFlags(parsed.flags, new Set(["flow", "work-item", "task-slug", "artifact-root", "source-request", "summary", "title", "criterion", "assignment-provider", "effective-state-json"]), "workflow start");
|
|
@@ -553,14 +579,35 @@ function readAssignment(sessionDir, slug) {
|
|
|
553
579
|
return readJsonFile(file, "workflow assignment");
|
|
554
580
|
}
|
|
555
581
|
function assertMatchingAssignmentActor(sessionDir, slug) {
|
|
556
|
-
const assignment =
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
582
|
+
const { assignment, caller, matches } = assignmentActorContext(sessionDir, slug);
|
|
583
|
+
if (matches)
|
|
584
|
+
return caller;
|
|
585
|
+
const authority = loadContinuationTurnAuthority().validateSignedActiveTurnAssignmentAuthority({
|
|
586
|
+
sessionDir,
|
|
587
|
+
runId: process.env.FLOW_AGENTS_CONTINUATION_RUN_ID,
|
|
588
|
+
turnSecret: process.env.FLOW_AGENTS_CONTINUATION_TURN_SECRET,
|
|
589
|
+
});
|
|
590
|
+
if (authority.valid && authority.record
|
|
591
|
+
&& assignment.actor_key === authority.record.assignment_actor
|
|
592
|
+
&& isDeepStrictEqual(normalizeAssignmentActor(assignment.actor), normalizeAssignmentActor(authority.record.assignment_actor_struct))) {
|
|
593
|
+
return { actorKey: authority.record.assignment_actor, actor: normalizeAssignmentActor(authority.record.assignment_actor_struct) };
|
|
561
594
|
}
|
|
595
|
+
throw new Error("workflow mutation requires the session's active, matching assignment actor");
|
|
596
|
+
}
|
|
597
|
+
function assertOrdinaryMatchingAssignmentActor(sessionDir, slug) {
|
|
598
|
+
const { caller, matches } = assignmentActorContext(sessionDir, slug);
|
|
599
|
+
if (!matches)
|
|
600
|
+
throw new Error("workflow mutation requires the session's active, matching assignment actor");
|
|
562
601
|
return caller;
|
|
563
602
|
}
|
|
603
|
+
function assignmentActorContext(sessionDir, slug) {
|
|
604
|
+
const assignment = readActiveAssignment(sessionDir, slug);
|
|
605
|
+
const caller = resolveCurrentAssignmentActor();
|
|
606
|
+
return { assignment, caller, matches: assignment.actor_key === caller.actorKey && isDeepStrictEqual(normalizeAssignmentActor(assignment.actor), normalizeAssignmentActor(caller.actor)) };
|
|
607
|
+
}
|
|
608
|
+
function normalizeAssignmentActor(value) {
|
|
609
|
+
return value && typeof value === "object" ? { ...value, human: value.human ?? null } : null;
|
|
610
|
+
}
|
|
564
611
|
function readActiveAssignment(sessionDir, slug) {
|
|
565
612
|
const assignment = readAssignment(sessionDir, slug);
|
|
566
613
|
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)) {
|
|
@@ -30,6 +30,10 @@ export type ContinuationTurnResult = {
|
|
|
30
30
|
barrier: ContinuationBarrier;
|
|
31
31
|
summary?: string;
|
|
32
32
|
};
|
|
33
|
+
export declare class ContinuationAdapterTimeoutError extends Error {
|
|
34
|
+
readonly code = "CONTINUATION_ADAPTER_TIMEOUT";
|
|
35
|
+
constructor(timeoutMs: number);
|
|
36
|
+
}
|
|
33
37
|
export type ContinuationDriverState = {
|
|
34
38
|
schema_version: "1.0";
|
|
35
39
|
run_id: string;
|
|
@@ -38,12 +42,14 @@ export type ContinuationDriverState = {
|
|
|
38
42
|
adapter_command_identity: string | null;
|
|
39
43
|
status: "active" | "waiting" | "done" | "failed" | "budget_exhausted";
|
|
40
44
|
turns_started: number;
|
|
45
|
+
active_turn_step?: string | null;
|
|
46
|
+
active_turn_public_key_digest?: string | null;
|
|
41
47
|
pending_barrier: ContinuationBarrier | null;
|
|
42
48
|
updated_at: string;
|
|
43
49
|
};
|
|
44
50
|
export type ContinuationDriverEvent = {
|
|
45
51
|
schema_version: "1.0";
|
|
46
|
-
type: "started" | "turn_started" | "turn_completed" | "turn_failed" | "parked" | "resumed" | "done" | "budget_exhausted";
|
|
52
|
+
type: "started" | "turn_started" | "turn_completed" | "gate_not_advanced" | "turn_failed" | "authority_cleanup_failed" | "parked" | "resumed" | "done" | "budget_exhausted";
|
|
47
53
|
run_id: string;
|
|
48
54
|
definition_id: string;
|
|
49
55
|
current_step: string;
|
|
@@ -51,6 +57,7 @@ export type ContinuationDriverEvent = {
|
|
|
51
57
|
at: string;
|
|
52
58
|
barrier?: ContinuationBarrier;
|
|
53
59
|
summary?: string;
|
|
60
|
+
failure_kind?: "adapter_error" | "timeout";
|
|
54
61
|
};
|
|
55
62
|
export interface ContinuationStateStore {
|
|
56
63
|
load(): ContinuationDriverState | null;
|
|
@@ -60,8 +67,23 @@ export interface ContinuationStateStore {
|
|
|
60
67
|
export interface ContinuationRuntimePort {
|
|
61
68
|
inspect(): Promise<ContinuationSnapshot>;
|
|
62
69
|
synchronize(): Promise<ContinuationSnapshot>;
|
|
63
|
-
execute(request: ContinuationTurnRequest): Promise<ContinuationTurnResult>;
|
|
70
|
+
execute(request: ContinuationTurnRequest, context?: ContinuationTurnContext): Promise<ContinuationTurnResult>;
|
|
64
71
|
}
|
|
72
|
+
export type ContinuationTurnContext = {
|
|
73
|
+
continuationTurnSecret?: string;
|
|
74
|
+
continuationRunId?: string;
|
|
75
|
+
};
|
|
76
|
+
export type ContinuationDriverLockLease = {
|
|
77
|
+
pid: number;
|
|
78
|
+
token: string;
|
|
79
|
+
created_at: string;
|
|
80
|
+
};
|
|
81
|
+
export type ContinuationTurnAuthority = {
|
|
82
|
+
runId: string;
|
|
83
|
+
turnSecret: string;
|
|
84
|
+
publicKeyDigest: string;
|
|
85
|
+
cleanup(): boolean;
|
|
86
|
+
};
|
|
65
87
|
export type ContinuationDriverOutcome = {
|
|
66
88
|
outcome: "done" | "waiting" | "failed" | "budget_exhausted";
|
|
67
89
|
turns_started: number;
|
|
@@ -75,6 +97,7 @@ export interface RunContinuationDriverInput {
|
|
|
75
97
|
store: ContinuationStateStore;
|
|
76
98
|
waitForBarrier?: (barrier: ContinuationBarrier) => Promise<"ready" | "pending">;
|
|
77
99
|
authorizeTurn?: () => Promise<void>;
|
|
100
|
+
issueTurnAuthority?: (request: ContinuationTurnRequest) => Promise<ContinuationTurnAuthority>;
|
|
78
101
|
now?: () => Date;
|
|
79
102
|
}
|
|
80
103
|
export interface DriveBuilderFlowSessionInput {
|
|
@@ -84,9 +107,10 @@ export interface DriveBuilderFlowSessionInput {
|
|
|
84
107
|
execute: ContinuationRuntimePort["execute"];
|
|
85
108
|
waitForBarrier?: RunContinuationDriverInput["waitForBarrier"];
|
|
86
109
|
authorizeTurn?: RunContinuationDriverInput["authorizeTurn"];
|
|
110
|
+
issueTurnAuthority?: RunContinuationDriverInput["issueTurnAuthority"];
|
|
87
111
|
now?: () => Date;
|
|
88
112
|
}
|
|
89
113
|
export declare function runContinuationDriver(input: RunContinuationDriverInput): Promise<ContinuationDriverOutcome>;
|
|
90
114
|
export declare function driveBuilderFlowSession(input: DriveBuilderFlowSessionInput): Promise<ContinuationDriverOutcome>;
|
|
91
|
-
export declare function withContinuationDriverLock<T>(sessionDirInput: string, body: () => Promise<T>): Promise<T>;
|
|
115
|
+
export declare function withContinuationDriverLock<T>(sessionDirInput: string, body: (lease: ContinuationDriverLockLease) => Promise<T>): Promise<T>;
|
|
92
116
|
export declare function createFileContinuationStore(sessionDirInput: string): ContinuationStateStore;
|