@kontourai/flow-agents 3.8.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.
Files changed (109) hide show
  1. package/.github/workflows/ci.yml +8 -0
  2. package/CHANGELOG.md +24 -0
  3. package/README.md +1 -1
  4. package/build/src/builder-flow-runtime.js +16 -10
  5. package/build/src/cli/assignment-provider.js +4 -18
  6. package/build/src/cli/continuation-adapter.d.ts +26 -0
  7. package/build/src/cli/continuation-adapter.js +243 -0
  8. package/build/src/cli/validate-workflow-artifacts.js +21 -1
  9. package/build/src/cli/workflow-sidecar.js +15 -20
  10. package/build/src/cli/workflow.js +99 -6
  11. package/build/src/continuation-driver.d.ts +116 -0
  12. package/build/src/continuation-driver.js +527 -0
  13. package/build/src/index.d.ts +2 -0
  14. package/build/src/index.js +1 -0
  15. package/build/src/tools/build-universal-bundles.js +52 -25
  16. package/build/src/tools/validate-source-tree.js +5 -2
  17. package/context/scripts/hooks/lib/continuation-turn-authority.js +386 -0
  18. package/context/scripts/hooks/stop-goal-fit.js +244 -73
  19. package/context/scripts/hooks/workflow-steering.js +24 -15
  20. package/context/scripts/telemetry/lib/pricing.sh +18 -6
  21. package/context/scripts/telemetry/lib/transport.sh +139 -37
  22. package/context/scripts/telemetry/lib/usage.sh +282 -32
  23. package/context/scripts/telemetry/lib/usage_model_guard.sh +42 -0
  24. package/context/scripts/telemetry/telemetry.sh +76 -1
  25. package/docs/coordination-guide.md +21 -5
  26. package/docs/fixture-ownership.md +2 -1
  27. package/docs/public-workflow-cli.md +98 -0
  28. package/docs/spec/runtime-hook-surface.md +8 -4
  29. package/docs/workflow-usage-guide.md +7 -0
  30. package/evals/acceptance/prove-capture-teeth-declared.sh +13 -0
  31. package/evals/acceptance/prove-capture-teeth.sh +13 -0
  32. package/evals/ci/run-baseline.sh +2 -0
  33. package/evals/fixtures/codex-legacy-agents/5273878130bdafc8a024a650bb5b66c9b003f1f859b5dc6e5b588cbf4ab23228.md +25 -0
  34. package/evals/fixtures/telemetry/usage-transcript-adversarial.jsonl +2 -0
  35. package/evals/fixtures/telemetry/usage-transcript-malformed-sandwich.jsonl +3 -0
  36. package/evals/fixtures/telemetry/usage-transcript-negative-tokens.jsonl +1 -0
  37. package/evals/fixtures/telemetry/usage-transcript-oversized-prefix.jsonl +201 -0
  38. package/evals/fixtures/telemetry/usage-transcript-oversized-tokens.jsonl +1 -0
  39. package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-lower.jsonl +1 -0
  40. package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-upper.jsonl +1 -0
  41. package/evals/fixtures/telemetry/usage-transcript-secret-model-jwt.jsonl +1 -0
  42. package/evals/fixtures/telemetry/usage-transcript-secret-model.jsonl +1 -0
  43. package/evals/fixtures/telemetry/usage-transcript-torn-trailing.jsonl +2 -0
  44. package/evals/integration/test_actor_identity.sh +63 -0
  45. package/evals/integration/test_assignment_provider_local_file.sh +34 -0
  46. package/evals/integration/test_builder_entry_enforcement.sh +5 -1
  47. package/evals/integration/test_bundle_install.sh +106 -17
  48. package/evals/integration/test_bundle_lifecycle.sh +17 -3
  49. package/evals/integration/test_captured_fail_reconciliation.sh +11 -0
  50. package/evals/integration/test_ci_actor_identity.sh +2 -2
  51. package/evals/integration/test_command_log_concurrency.sh +11 -0
  52. package/evals/integration/test_command_log_fork_classification.sh +11 -0
  53. package/evals/integration/test_command_log_integrity.sh +11 -0
  54. package/evals/integration/test_current_json_per_actor.sh +416 -21
  55. package/evals/integration/test_enforcer_expects_driven.sh +11 -0
  56. package/evals/integration/test_ensure_session_ownership_guard.sh +39 -0
  57. package/evals/integration/test_evidence_capture_hook.sh +25 -6
  58. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  59. package/evals/integration/test_flowdef_union_floor_regression.sh +11 -0
  60. package/evals/integration/test_gate_bypass_chain.sh +11 -0
  61. package/evals/integration/test_gate_lockdown.sh +10 -0
  62. package/evals/integration/test_goal_fit_ghost_session.sh +11 -0
  63. package/evals/integration/test_goal_fit_hook.sh +257 -9
  64. package/evals/integration/test_goal_fit_rederive.sh +11 -0
  65. package/evals/integration/test_install_merge.sh +141 -3
  66. package/evals/integration/test_liveness_conflict_injection.sh +39 -3
  67. package/evals/integration/test_liveness_heartbeat.sh +46 -9
  68. package/evals/integration/test_phase_map_and_gate_claim.sh +10 -1
  69. package/evals/integration/test_public_workflow_cli.sh +6 -6
  70. package/evals/integration/test_session_resume_roundtrip.sh +30 -0
  71. package/evals/integration/test_stop_hook_release.sh +111 -11
  72. package/evals/integration/test_telemetry_tool_usage.sh +394 -0
  73. package/evals/integration/test_verify_hold.sh +64 -1
  74. package/evals/integration/test_workflow_sidecar_writer.sh +10 -1
  75. package/evals/integration/test_workflow_steering_hook.sh +20 -4
  76. package/evals/run.sh +2 -0
  77. package/evals/static/test_universal_bundles.sh +123 -0
  78. package/package.json +4 -4
  79. package/packaging/README.md +21 -0
  80. package/packaging/codex-legacy-agents-fingerprints.json +33 -0
  81. package/scripts/README.md +2 -1
  82. package/scripts/audit-codex-legacy-agents.js +43 -0
  83. package/scripts/classify-codex-legacy-agents.js +71 -0
  84. package/scripts/hooks/evidence-capture.js +18 -6
  85. package/scripts/hooks/lib/actor-identity.js +62 -29
  86. package/scripts/hooks/lib/continuation-turn-authority.js +386 -0
  87. package/scripts/hooks/lib/current-pointer.js +114 -12
  88. package/scripts/hooks/lib/liveness-heartbeat.js +15 -12
  89. package/scripts/hooks/stop-goal-fit.js +244 -73
  90. package/scripts/hooks/workflow-steering.js +24 -15
  91. package/scripts/install-codex-home.sh +8 -7
  92. package/scripts/telemetry/lib/pricing.sh +18 -6
  93. package/scripts/telemetry/lib/transport.sh +71 -0
  94. package/scripts/telemetry/lib/usage.sh +282 -32
  95. package/scripts/telemetry/lib/usage_model_guard.sh +42 -0
  96. package/scripts/telemetry/telemetry.sh +76 -1
  97. package/src/builder-flow-runtime.ts +9 -3
  98. package/src/cli/assignment-provider.ts +5 -26
  99. package/src/cli/builder-flow-runtime.test.mjs +215 -2
  100. package/src/cli/continuation-adapter.ts +265 -0
  101. package/src/cli/continuation-driver.test.mjs +1303 -0
  102. package/src/cli/public-api.test.mjs +1 -0
  103. package/src/cli/validate-workflow-artifacts.ts +19 -1
  104. package/src/cli/workflow-sidecar.ts +18 -23
  105. package/src/cli/workflow.ts +113 -6
  106. package/src/continuation-driver.ts +638 -0
  107. package/src/index.ts +24 -0
  108. package/src/tools/build-universal-bundles.ts +61 -25
  109. package/src/tools/validate-source-tree.ts +5 -2
@@ -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,29 @@
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
+
20
+ ## [3.9.0](https://github.com/kontourai/flow-agents/compare/v3.8.0...v3.9.0) (2026-07-12)
21
+
22
+
23
+ ### Features
24
+
25
+ * **workflow:** add bounded continuation driver ([#560](https://github.com/kontourai/flow-agents/issues/560)) ([e6365ab](https://github.com/kontourai/flow-agents/commit/e6365aba324c76ee164681c987d20916c315444e))
26
+
3
27
  ## [3.8.0](https://github.com/kontourai/flow-agents/compare/v3.7.0...v3.8.0) (2026-07-12)
4
28
 
5
29
 
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
 
@@ -760,7 +760,9 @@ function projectFlowRun(context, run, sidecar) {
760
760
  const complete = run.state.status === "completed";
761
761
  const paused = run.state.status === "paused";
762
762
  const canceled = run.state.status === "canceled";
763
- const action = complete || paused || canceled ? { skills: [], operations: [] } : stepAction(run.definitionId, run.state.current_step);
763
+ const needsDecision = run.state.status === "needs_decision";
764
+ const failed = run.state.status === "failed";
765
+ const action = complete || paused || canceled || needsDecision || failed ? { skills: [], operations: [] } : stepAction(run.definitionId, run.state.current_step);
764
766
  if (!action) {
765
767
  throw new BuilderBuildRunInputError("kit.flow_step_actions", `does not declare Builder step ${run.state.current_step}`);
766
768
  }
@@ -785,18 +787,22 @@ function projectFlowRun(context, run, sidecar) {
785
787
  ? { status: "done", summary: "Canonical Flow run was canceled by an authorized external request. Artifacts are retained until separately archived." }
786
788
  : paused
787
789
  ? { status: "blocked", summary: "Canonical Flow run is paused. The current assignment actor may resume it with a reason." }
788
- : {
789
- status: "continue",
790
- summary: `Flow step \`${run.state.current_step}\`: ${skillText}${operationText} ${gateText}${routeText} Then synchronize the recorded evidence.`,
791
- skills,
792
- operations,
793
- command: syncCommand,
794
- };
790
+ : needsDecision
791
+ ? { status: "blocked", summary: "Canonical Flow requires an external decision before continuation." }
792
+ : failed
793
+ ? { status: "failed", summary: "Canonical Flow run failed; no continuation turn is allowed." }
794
+ : {
795
+ status: "continue",
796
+ summary: `Flow step \`${run.state.current_step}\`: ${skillText}${operationText} ${gateText}${routeText} Then synchronize the recorded evidence.`,
797
+ skills,
798
+ operations,
799
+ command: syncCommand,
800
+ };
795
801
  const phase = phaseForStep(definition.phase_map, run.state.current_step) ?? sidecar.phase;
796
802
  return {
797
803
  ...sidecar,
798
- status: complete ? "delivered" : canceled ? "canceled" : paused ? "blocked" : (run.state.transitions.length > 0 ? "in_progress" : sidecar.status),
799
- phase: complete || canceled ? "done" : phase,
804
+ status: complete ? "delivered" : canceled ? "canceled" : failed ? "failed" : (paused || needsDecision) ? "blocked" : (run.state.transitions.length > 0 ? "in_progress" : sidecar.status),
805
+ phase: complete || canceled || failed ? "done" : phase,
800
806
  updated_at: run.state.updated_at,
801
807
  flow_run: {
802
808
  run_id: run.runId,
@@ -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.resolveActor(process.env);
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
- // #398: reconstruct the SAME struct resolveActor serialized for a CI actor, mirroring
88
- // resolveEnsureSessionActor (workflow-sidecar.ts) via the shared detectCiActor. Without this the
89
- // else-branch would write `record.actor = {runtime:"unknown", session_id:<the whole triple>}` for a
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);
@@ -0,0 +1,26 @@
1
+ import type { ContinuationBarrier, ContinuationTurnRequest, ContinuationTurnResult } from "../continuation-driver.js";
2
+ export type ContinuationAdapterCommand = {
3
+ argv: string[];
4
+ identity: string;
5
+ integrity: Array<{
6
+ file: string;
7
+ sha256: string;
8
+ }>;
9
+ };
10
+ export declare function loadContinuationAdapterCommand(commandFileInput: string): ContinuationAdapterCommand;
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;
14
+ export declare function waitForContinuationBarrier(barrier: ContinuationBarrier, options: {
15
+ maxWaitMs: number;
16
+ pollMs: number;
17
+ now?: () => number;
18
+ sleep?: (ms: number) => Promise<void>;
19
+ }): Promise<"ready" | "pending">;
20
+ type ContinuationAdapterOptions = {
21
+ cwd: string;
22
+ timeoutMs: number;
23
+ continuationTurnSecret?: string;
24
+ continuationRunId?: string;
25
+ };
26
+ export {};
@@ -0,0 +1,243 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { spawn } from "node:child_process";
4
+ import { createHash } from "node:crypto";
5
+ import { ContinuationAdapterTimeoutError } from "../continuation-driver.js";
6
+ export function loadContinuationAdapterCommand(commandFileInput) {
7
+ const commandFile = path.resolve(commandFileInput);
8
+ const command = validateAdapterCommand(JSON.parse(readRegularFileNoFollow(commandFile, "continuation adapter command file")));
9
+ if (!path.isAbsolute(command.argv[0]))
10
+ throw new Error("continuation adapter executable must be an absolute path");
11
+ const integrity = [...new Set(command.argv.filter((entry) => path.isAbsolute(entry) && regularFileExists(entry)))]
12
+ .map((file) => ({ file, sha256: sha256File(file, "continuation adapter integrity file") }));
13
+ if (!integrity.some((entry) => entry.file === command.argv[0]))
14
+ throw new Error("continuation adapter executable must be a regular file");
15
+ const identity = createHash("sha256").update(JSON.stringify({ ...command, integrity })).digest("hex");
16
+ return { ...command, identity, integrity };
17
+ }
18
+ export async function executeContinuationAdapter(commandFileInput, request, options) {
19
+ const command = loadContinuationAdapterCommand(commandFileInput);
20
+ return executeLoadedContinuationAdapter(command, request, options);
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) {
28
+ for (const entry of command.integrity) {
29
+ if (sha256File(entry.file, "continuation adapter integrity file") !== entry.sha256) {
30
+ throw new Error(`continuation adapter integrity changed after mission binding: ${entry.file}`);
31
+ }
32
+ }
33
+ }
34
+ export async function waitForContinuationBarrier(barrier, options) {
35
+ assertPositiveInteger(options.maxWaitMs, "continuation barrier maxWaitMs", 0, 86_400_000);
36
+ assertPositiveInteger(options.pollMs, "continuation barrier pollMs", 1, 60_000);
37
+ const now = options.now ?? Date.now;
38
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
39
+ const stopAt = now() + options.maxWaitMs;
40
+ if (barrier.kind === "deadline") {
41
+ const deadline = Date.parse(barrier.at);
42
+ if (deadline <= now())
43
+ return "ready";
44
+ const waitMs = Math.min(deadline - now(), Math.max(0, stopAt - now()));
45
+ if (waitMs > 0)
46
+ await sleep(waitMs);
47
+ return Date.parse(barrier.at) <= now() ? "ready" : "pending";
48
+ }
49
+ while (pidAlive(barrier.pid)) {
50
+ const remaining = stopAt - now();
51
+ if (remaining <= 0)
52
+ return "pending";
53
+ await sleep(Math.min(options.pollMs, remaining));
54
+ }
55
+ return "ready";
56
+ }
57
+ function spawnAdapter(command, request, options) {
58
+ return new Promise((resolve, reject) => {
59
+ const child = spawn(command.argv[0], command.argv.slice(1), {
60
+ cwd: options.cwd,
61
+ env: adapterEnvironment(options),
62
+ detached: process.platform !== "win32",
63
+ shell: false,
64
+ stdio: ["pipe", "pipe", "pipe"],
65
+ });
66
+ const stdout = [];
67
+ const stderr = [];
68
+ let stdoutBytes = 0;
69
+ let stderrBytes = 0;
70
+ let settled = false;
71
+ let forcedKill;
72
+ let terminationError;
73
+ const maxBytes = 4 * 1024 * 1024;
74
+ const timeout = setTimeout(() => {
75
+ if (settled)
76
+ return;
77
+ terminationError = new ContinuationAdapterTimeoutError(options.timeoutMs);
78
+ terminateProcessGroup(child.pid, "SIGTERM");
79
+ forcedKill = setTimeout(() => terminateProcessGroup(child.pid, "SIGKILL"), 250);
80
+ }, options.timeoutMs);
81
+ child.stdout.on("data", (chunk) => {
82
+ stdoutBytes += chunk.length;
83
+ if (stdoutBytes > maxBytes) {
84
+ if (!terminationError) {
85
+ terminationError = new Error("continuation adapter stdout exceeded 4 MiB");
86
+ clearTimeout(timeout);
87
+ terminateProcessGroup(child.pid, "SIGTERM");
88
+ forcedKill = setTimeout(() => terminateProcessGroup(child.pid, "SIGKILL"), 250);
89
+ }
90
+ return;
91
+ }
92
+ stdout.push(chunk);
93
+ });
94
+ child.stderr.on("data", (chunk) => {
95
+ stderrBytes += chunk.length;
96
+ if (stderrBytes <= maxBytes)
97
+ stderr.push(chunk);
98
+ });
99
+ child.on("error", (error) => {
100
+ if (settled)
101
+ return;
102
+ settled = true;
103
+ clearTimeout(timeout);
104
+ if (forcedKill)
105
+ clearTimeout(forcedKill);
106
+ reject(error);
107
+ });
108
+ child.on("close", (code, signal) => {
109
+ if (settled)
110
+ return;
111
+ settled = true;
112
+ clearTimeout(timeout);
113
+ if (forcedKill)
114
+ clearTimeout(forcedKill);
115
+ if (terminationError) {
116
+ reject(terminationError);
117
+ return;
118
+ }
119
+ const stderrText = Buffer.concat(stderr).toString("utf8").trim();
120
+ if (code !== 0) {
121
+ scheduleProcessGroupTermination(child.pid);
122
+ reject(new Error(`continuation adapter exited ${code ?? signal ?? "unknown"}${stderrText ? `: ${stderrText}` : ""}`));
123
+ return;
124
+ }
125
+ const output = Buffer.concat(stdout).toString("utf8").trim();
126
+ try {
127
+ const result = JSON.parse(output);
128
+ if (!isValidWaitResult(result))
129
+ scheduleProcessGroupTermination(child.pid);
130
+ resolve(result);
131
+ }
132
+ catch {
133
+ scheduleProcessGroupTermination(child.pid);
134
+ reject(new Error("continuation adapter must emit exactly one JSON result on stdout"));
135
+ }
136
+ });
137
+ child.stdin.on("error", (error) => {
138
+ if (error.code !== "EPIPE" && !settled) {
139
+ settled = true;
140
+ clearTimeout(timeout);
141
+ if (forcedKill)
142
+ clearTimeout(forcedKill);
143
+ scheduleProcessGroupTermination(child.pid);
144
+ reject(error);
145
+ }
146
+ });
147
+ child.stdin.end(`${JSON.stringify(request)}\n`);
148
+ });
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
+ }
164
+ function validateAdapterCommand(value) {
165
+ if (!value || typeof value !== "object" || Array.isArray(value))
166
+ throw new Error("continuation adapter command file must contain an object");
167
+ const argv = value.argv;
168
+ if (!Array.isArray(argv) || argv.length === 0 || argv.length > 128 || argv.some((entry) => typeof entry !== "string" || entry.length === 0 || entry.includes("\0"))) {
169
+ throw new Error("continuation adapter command argv must contain 1 through 128 non-empty strings");
170
+ }
171
+ return { argv: [...argv] };
172
+ }
173
+ function assertRegularFile(file, label) {
174
+ const stat = fs.lstatSync(file);
175
+ if (stat.isSymbolicLink() || !stat.isFile())
176
+ throw new Error(`${label} must be a regular file`);
177
+ }
178
+ function readRegularFileNoFollow(file, label) {
179
+ return readRegularFileBufferNoFollow(file, label).toString("utf8");
180
+ }
181
+ function readRegularFileBufferNoFollow(file, label) {
182
+ assertRegularFile(file, label);
183
+ const noFollow = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
184
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | noFollow);
185
+ try {
186
+ if (!fs.fstatSync(fd).isFile())
187
+ throw new Error(`${label} must be a regular file`);
188
+ return fs.readFileSync(fd);
189
+ }
190
+ finally {
191
+ fs.closeSync(fd);
192
+ }
193
+ }
194
+ function assertPositiveInteger(value, label, min, max) {
195
+ if (!Number.isSafeInteger(value) || value < min || value > max)
196
+ throw new Error(`${label} must be an integer from ${min} through ${max}`);
197
+ }
198
+ function pidAlive(pid) {
199
+ try {
200
+ process.kill(pid, 0);
201
+ return true;
202
+ }
203
+ catch (error) {
204
+ return error.code === "EPERM";
205
+ }
206
+ }
207
+ function terminateProcessGroup(pid, signal) {
208
+ if (!pid)
209
+ return;
210
+ try {
211
+ process.kill(process.platform === "win32" ? pid : -pid, signal);
212
+ }
213
+ catch (error) {
214
+ if (error.code !== "ESRCH")
215
+ throw error;
216
+ }
217
+ }
218
+ function isValidWaitResult(result) {
219
+ if (result?.status !== "wait" || !result.barrier || typeof result.barrier !== "object")
220
+ return false;
221
+ if (result.barrier.kind === "pid")
222
+ return Number.isSafeInteger(result.barrier.pid) && result.barrier.pid > 0;
223
+ return result.barrier.kind === "deadline" && typeof result.barrier.at === "string" && Number.isFinite(Date.parse(result.barrier.at));
224
+ }
225
+ function scheduleProcessGroupTermination(pid) {
226
+ terminateProcessGroup(pid, "SIGTERM");
227
+ const forcedKill = setTimeout(() => terminateProcessGroup(pid, "SIGKILL"), 250);
228
+ forcedKill.unref();
229
+ }
230
+ function regularFileExists(file) {
231
+ try {
232
+ const stat = fs.lstatSync(file);
233
+ return !stat.isSymbolicLink() && stat.isFile();
234
+ }
235
+ catch (error) {
236
+ if (error.code === "ENOENT")
237
+ return false;
238
+ throw error;
239
+ }
240
+ }
241
+ function sha256File(file, label) {
242
+ return createHash("sha256").update(readRegularFileBufferNoFollow(file, label)).digest("hex");
243
+ }
@@ -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 ? { actor: explicitActor, source: "explicit-override" } : helper.resolveActor(process.env);
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
- // #398: the CI-runtime tier must reconstruct the SAME struct resolveActor serialized, or
301
- // `serializeActor(actorStruct)` would diverge from `resolved.actor` (the else-branch would rebuild
302
- // an ANCESTRY struct — detectRuntime→unknown, runtimeSessionId→'' — so the claim's stored
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
- const perActorFile = helper.perActorCurrentFile(root, actorKey);
1771
- let perActor = null;
1772
- try {
1773
- perActor = fs.existsSync(perActorFile) ? JSON.parse(fs.readFileSync(perActorFile, "utf8")) : null;
1774
- }
1775
- catch {
1776
- perActor = null;
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
  }