@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.
- package/.github/workflows/ci.yml +8 -0
- package/CHANGELOG.md +24 -0
- package/README.md +1 -1
- package/build/src/builder-flow-runtime.js +16 -10
- package/build/src/cli/assignment-provider.js +4 -18
- package/build/src/cli/continuation-adapter.d.ts +26 -0
- package/build/src/cli/continuation-adapter.js +243 -0
- 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 +99 -6
- package/build/src/continuation-driver.d.ts +116 -0
- package/build/src/continuation-driver.js +527 -0
- package/build/src/index.d.ts +2 -0
- package/build/src/index.js +1 -0
- 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 +98 -0
- package/docs/spec/runtime-hook-surface.md +8 -4
- package/docs/workflow-usage-guide.md +7 -0
- 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 +4 -4
- 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/builder-flow-runtime.ts +9 -3
- package/src/cli/assignment-provider.ts +5 -26
- package/src/cli/builder-flow-runtime.test.mjs +215 -2
- package/src/cli/continuation-adapter.ts +265 -0
- package/src/cli/continuation-driver.test.mjs +1303 -0
- 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 +113 -6
- package/src/continuation-driver.ts +638 -0
- package/src/index.ts +24 -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,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
|
|
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: "
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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.
|
|
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);
|
|
@@ -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
|
|
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
|
}
|