From 64e7f9c2864ec08e053879454f734150c4a4d16f Mon Sep 17 00:00:00 2001 From: Miya Date: Sun, 18 Oct 2026 17:24:23 +0200 Subject: [PATCH 1/3] fix(sdk): deliver parallel authored runs through hosted standalone --- evidence/hosted-parallel-2-0-53/README.md | 105 ++++++++++++++++++ .../hosted-repro.flow.ts | 9 ++ .../hosted-parallel-2-0-53/inspect-journal.py | 45 ++++++++ .../hosted-parallel-2-0-53/logs/build.txt | 4 + .../logs/bun-function-source.txt | 15 +++ .../logs/focused-tests.txt | 15 +++ .../logs/function-source-diff.txt | 24 ++++ .../logs/manual-journal-after-resume.txt | 7 ++ .../logs/manual-journal.txt | 7 ++ .../logs/manual-resume.txt | 1 + .../logs/manual-run.txt | 1 + .../logs/node-function-source.txt | 8 ++ .../logs/standalone-build.txt | 6 + .../logs/standalone-parallel.txt | 11 ++ .../logs/standalone-serial.txt | 11 ++ .../logs/typecheck-tests.txt | 4 + .../hosted-parallel-2-0-53/logs/typecheck.txt | 4 + packages/sdk/src/authored-node-entry.ts | 14 +++ packages/sdk/src/authored-node-runner.ts | 23 +++- .../sdk/src/authored-parallel-eligibility.ts | 45 +++++--- packages/sdk/src/authored-root.ts | 21 +++- .../sdk/tests/authored-node-result.test.ts | 39 ++++++- .../sdk/tests/authored-node-runtime.test.ts | 36 +++++- .../authored-parallel-eligibility.test.ts | 25 ++++- packages/sdk/tests/authored-root.test.ts | 19 ++++ 25 files changed, 472 insertions(+), 27 deletions(-) create mode 100644 evidence/hosted-parallel-2-0-53/README.md create mode 100644 evidence/hosted-parallel-2-0-53/hosted-repro.flow.ts create mode 100644 evidence/hosted-parallel-2-0-53/inspect-journal.py create mode 100644 evidence/hosted-parallel-2-0-53/logs/build.txt create mode 100644 evidence/hosted-parallel-2-0-53/logs/bun-function-source.txt create mode 100644 evidence/hosted-parallel-2-0-53/logs/focused-tests.txt create mode 100644 evidence/hosted-parallel-2-0-53/logs/function-source-diff.txt create mode 100644 evidence/hosted-parallel-2-0-53/logs/manual-journal-after-resume.txt create mode 100644 evidence/hosted-parallel-2-0-53/logs/manual-journal.txt create mode 100644 evidence/hosted-parallel-2-0-53/logs/manual-resume.txt create mode 100644 evidence/hosted-parallel-2-0-53/logs/manual-run.txt create mode 100644 evidence/hosted-parallel-2-0-53/logs/node-function-source.txt create mode 100644 evidence/hosted-parallel-2-0-53/logs/standalone-build.txt create mode 100644 evidence/hosted-parallel-2-0-53/logs/standalone-parallel.txt create mode 100644 evidence/hosted-parallel-2-0-53/logs/standalone-serial.txt create mode 100644 evidence/hosted-parallel-2-0-53/logs/typecheck-tests.txt create mode 100644 evidence/hosted-parallel-2-0-53/logs/typecheck.txt diff --git a/evidence/hosted-parallel-2-0-53/README.md b/evidence/hosted-parallel-2-0-53/README.md new file mode 100644 index 000000000..4c4b80fef --- /dev/null +++ b/evidence/hosted-parallel-2-0-53/README.md @@ -0,0 +1,105 @@ +# Hosted parallel `f.run` repair (2026-10-18) + +## Cause + +The 2.0.52 standalone CLI is compiled with Bun. Bun reformats the authored +body's `Function.prototype.toString()` result: it changes whitespace and quote +style and can combine adjacent `const` declarations. Parallel admission +compared that string byte-for-byte with the original `.flow.ts` body, so a +source-faithful hosted flow was pinned to `executionMode: serial`. The npm +Node CLI kept the original function text and admitted the same flow as parallel. + +After source admission was repaired, the standalone result verifier exposed a +second blocker. It required the numeric suffixes of all child IDs to be a +single `1..N` sequence. Parallel IDs use an ordinal **per causal predecessor**, +so three valid dependent runs can all end in `-1`. The verifier rejected their +durable success as `authored runtime result has no matching durable completion`. + +## Repair and safety checks + +- Bun admits parallel only from a closed, parsed, hash-pinned source module. + Its in-memory body never executes: the embedded Node runner loads the pinned + source. The Node CLI retains exact function/source identity comparison. +- Before any parallel child starts, the Node runner independently verifies the + pinned source hash, closed module grammar, and Node-loaded body grammar. + The executor still checks Promise and Array intrinsics before execution. +- For a parallel result, the standalone verifier compares every claimed child + against the root's durable `authored-steps` index, requires no unclaimed or + unfinished child, and then reads every child journal and completion marker. + Serial roots retain their global ordinal check. +- Set `FLOWS_PARALLEL_DEBUG=1` on the CLI to log why a new root selected serial + mode (`source_graph`, `extensions`, `shared_budget`, `runtime_intrinsics`, or + `source_certificate`). The reason is deliberately not stored in root + metadata: changing a 2.0.52 root's admission spec hash could disrupt an + in-flight resume after upgrade. + +## Source reproduction + +The exact hosted-reported shape is [hosted-repro.flow.ts](hosted-repro.flow.ts). +These commands ran from `/tmp/hv52`, which contained that source and the +published 2.0.52 Surface package: + +```sh +bun -e "import handle from './par.flow.ts'; import {getFlowDefinition} from '@relayflows/surface/runtime'; console.log(Function.prototype.toString.call(getFlowDefinition(handle).body));" +node --experimental-strip-types --input-type=module -e "import handle from './par.flow.ts'; import {getFlowDefinition} from '@relayflows/surface/runtime'; console.log(Function.prototype.toString.call(getFlowDefinition(handle).body));" +``` + +Captured complete outputs: [Bun](logs/bun-function-source.txt), +[Node](logs/node-function-source.txt), and their [literal diff](logs/function-source-diff.txt). +The old byte-for-byte body comparison fails on that diff; the parser unit test +also asserts that the Node path still refuses a mismatched in-memory body. + +## Final-build checks + +Commands ran from `packages/sdk` at the final source tree. Each link is the +complete captured stdout/stderr for the adjacent literal command (all exited 0): + +| Command | Captured output | +| --- | --- | +| `npm run typecheck` | [typecheck.txt](logs/typecheck.txt) | +| `npm run build` | [build.txt](logs/build.txt) | +| `npm run typecheck:tests` | [typecheck-tests.txt](logs/typecheck-tests.txt) | +| `./node_modules/.bin/vitest run tests/authored-root.test.ts tests/authored-parallel-body.test.ts tests/authored-parallel-eligibility.test.ts tests/authored-node-result.test.ts --pool=forks --maxWorkers=2 --minWorkers=1` | [focused-tests.txt](logs/focused-tests.txt) | +| `PATH=/tmp/ms-flows-parallel-node22/node_modules/node-bin-darwin-arm64/bin:$PATH RELAYFLOWD_BIN=/tmp/hv52/node_modules/@relayflows/runtime-darwin-arm64/bin/relayflowd /tmp/ms-flows-parallel-node22/node_modules/node-bin-darwin-arm64/bin/node ./node_modules/vitest/vitest.mjs run tests/authored-node-runtime.test.ts -t 'runs a certified Promise.all group' --pool=forks --maxWorkers=1 --minWorkers=1` | [standalone-parallel.txt](logs/standalone-parallel.txt) | +| `PATH=/tmp/ms-flows-parallel-node22/node_modules/node-bin-darwin-arm64/bin:$PATH RELAYFLOWD_BIN=/tmp/hv52/node_modules/@relayflows/runtime-darwin-arm64/bin/relayflowd /tmp/ms-flows-parallel-node22/node_modules/node-bin-darwin-arm64/bin/node ./node_modules/vitest/vitest.mjs run tests/authored-node-runtime.test.ts -t 'awaits agent plus three run steps' --pool=forks --maxWorkers=1 --minWorkers=1` | [standalone-serial.txt](logs/standalone-serial.txt) | + +The parallel live test builds the standalone CLI, checks parallel root metadata, +proves all three route starts precede the first route end, and checks the +dependents and join. The serial case includes a completed-root resume with no +repeated effects. The verifier unit test accepts repeated causal `-1` suffixes +and refuses mismatched or extra root-index records. + +## Manual standalone journal + +The final standalone binary was built with this command from the repo root; +its complete output is [standalone-build.txt](logs/standalone-build.txt): + +```sh +node scripts/build-standalone-cli.mjs bun-darwin-arm64 /tmp/ms-flows-parallel-hosted/flows +``` + +The CLI commands below ran from a fixture with the published 2.0.52 Surface +package, the copied [source](hosted-repro.flow.ts), published 2.0.52 daemon, +and Node 22.16.0 embedded runner. The inspector commands ran from the repo root: + +```sh +FLOWS_AUTHORED_NODE=/tmp/ms-flows-parallel-node22/node_modules/node-bin-darwin-arm64/bin/node RELAYFLOWD_BIN=/tmp/hv52/node_modules/@relayflows/runtime-darwin-arm64/bin/relayflowd /tmp/ms-flows-parallel-hosted/flows run par.flow.ts --data-dir /tmp/ms-flows-parallel-hosted/data6 --json --no-observer-link --input '{}' +python3 evidence/hosted-parallel-2-0-53/inspect-journal.py /tmp/ms-flows-parallel-hosted/data6 01M57SMB30E9AD784XVP5T2X9S +FLOWS_AUTHORED_NODE=/tmp/ms-flows-parallel-node22/node_modules/node-bin-darwin-arm64/bin/node RELAYFLOWD_BIN=/tmp/hv52/node_modules/@relayflows/runtime-darwin-arm64/bin/relayflowd /tmp/ms-flows-parallel-hosted/flows resume 01M57SMB30E9AD784XVP5T2X9S --data-dir /tmp/ms-flows-parallel-hosted/data6 --json --no-observer-link +python3 evidence/hosted-parallel-2-0-53/inspect-journal.py /tmp/ms-flows-parallel-hosted/data6 01M57SMB30E9AD784XVP5T2X9S +``` + +Captured outputs: [run](logs/manual-run.txt), +[journal](logs/manual-journal.txt), [resume](logs/manual-resume.txt), and +[journal after resume](logs/manual-journal-after-resume.txt). The two journal +outputs are identical: + +```text +root executionMode: parallel +run-1: started=1792336867426 ended=1792336869451 +run-2: started=1792336867424 ended=1792336869450 +run-3: started=1792336867422 ended=1792336869450 +route start spread ms: 4 +all routes overlapped: True +run journals: 9 attempt starts: 9 +``` diff --git a/evidence/hosted-parallel-2-0-53/hosted-repro.flow.ts b/evidence/hosted-parallel-2-0-53/hosted-repro.flow.ts new file mode 100644 index 000000000..760d63e30 --- /dev/null +++ b/evidence/hosted-parallel-2-0-53/hosted-repro.flow.ts @@ -0,0 +1,9 @@ +import { flow } from '@relayflows/surface'; +export default flow('hv52-parallel', async f => { + const a = async () => { await f.run('echo A-start $(date +%s%3N); sleep 2; echo A-end $(date +%s%3N)'); await f.run('echo a-dep $(date +%s%3N)'); }; + const b = async () => { await f.run('echo B-start $(date +%s%3N); sleep 2; echo B-end $(date +%s%3N)'); await f.run('echo b-dep $(date +%s%3N)'); }; + const c = async () => { await f.run('echo C-start $(date +%s%3N); sleep 2; echo C-end $(date +%s%3N)'); await f.run('echo c-dep $(date +%s%3N)'); }; + await Promise.all([a(), b(), c()]); + await f.run('echo join $(date +%s%3N)'); + f.done('success'); +}); diff --git a/evidence/hosted-parallel-2-0-53/inspect-journal.py b/evidence/hosted-parallel-2-0-53/inspect-journal.py new file mode 100644 index 000000000..b44b9fc31 --- /dev/null +++ b/evidence/hosted-parallel-2-0-53/inspect-journal.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Read one local standalone reproduction's SQLite journals.""" + +import json +import sqlite3 +import sys +from pathlib import Path + + +data = Path(sys.argv[1]) +root_id = sys.argv[2] +runs = sorted((data / "runs").glob("*.sqlite3")) +root = sqlite3.connect(data / "runs" / f"{root_id}.sqlite3") +root_spawn = next(json.loads(payload) for kind, payload in root.execute( + "select entry_type, payload from entries where entry_type='run.spawned'" +) if kind == "run.spawned") +metadata = json.loads(root_spawn["spec"]["steps"][0]["instruction"]) +print("root executionMode:", metadata["executionMode"]) + +attempts = 0 +routes = [] +for path in runs: + entries = sqlite3.connect(path).execute( + "select entry_type, at_ms, payload from entries order by seq" + ).fetchall() + attempts += sum(kind == "step.attempt.started" for kind, _, _ in entries) + spawned = next((json.loads(payload) for kind, _, payload in entries + if kind == "run.spawned"), None) + if spawned is None: + continue + step = spawned["spec"]["steps"][0] + command = step.get("command", "") + if not any(f"{name}-start" in command for name in "ABC"): + continue + started = next(at for kind, at, _ in entries if kind == "step.attempt.started") + ended = next(at for kind, at, _ in entries if kind == "step.completed") + routes.append((step["id"], started, ended)) + +for step_id, started, ended in sorted(routes): + print(f"{step_id}: started={started} ended={ended}") +print("route start spread ms:", max(start for _, start, _ in routes) + - min(start for _, start, _ in routes)) +print("all routes overlapped:", max(start for _, start, _ in routes) + < min(end for _, _, end in routes)) +print("run journals:", len(runs), "attempt starts:", attempts) diff --git a/evidence/hosted-parallel-2-0-53/logs/build.txt b/evidence/hosted-parallel-2-0-53/logs/build.txt new file mode 100644 index 000000000..60c2dbc33 --- /dev/null +++ b/evidence/hosted-parallel-2-0-53/logs/build.txt @@ -0,0 +1,4 @@ + +> @relayflows/sdk@2.0.52 build +> tsc && node scripts/make-cli-executable.mjs + diff --git a/evidence/hosted-parallel-2-0-53/logs/bun-function-source.txt b/evidence/hosted-parallel-2-0-53/logs/bun-function-source.txt new file mode 100644 index 000000000..ca2bef740 --- /dev/null +++ b/evidence/hosted-parallel-2-0-53/logs/bun-function-source.txt @@ -0,0 +1,15 @@ +async (f) => { + const a = async () => { + await f.run("echo A-start $(date +%s%3N); sleep 2; echo A-end $(date +%s%3N)"); + await f.run("echo a-dep $(date +%s%3N)"); + }, b = async () => { + await f.run("echo B-start $(date +%s%3N); sleep 2; echo B-end $(date +%s%3N)"); + await f.run("echo b-dep $(date +%s%3N)"); + }, c = async () => { + await f.run("echo C-start $(date +%s%3N); sleep 2; echo C-end $(date +%s%3N)"); + await f.run("echo c-dep $(date +%s%3N)"); + }; + await Promise.all([a(), b(), c()]); + await f.run("echo join $(date +%s%3N)"); + f.done("success"); +} diff --git a/evidence/hosted-parallel-2-0-53/logs/focused-tests.txt b/evidence/hosted-parallel-2-0-53/logs/focused-tests.txt new file mode 100644 index 000000000..c43163b1a --- /dev/null +++ b/evidence/hosted-parallel-2-0-53/logs/focused-tests.txt @@ -0,0 +1,15 @@ + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + + ✓ tests/authored-node-result.test.ts (46 tests) 12ms + ✓ tests/authored-parallel-eligibility.test.ts (11 tests) 20ms +(node:19316) [FLOWS_ROOT_LEASE_LOST] Warning: authored root run_id=root-run attempt=1: lease_conflict: attempt has no active worker lease. Waiting for the kernel to retry it. +(Use `node --trace-warnings ...` to show where the warning was created) + ✓ tests/authored-root.test.ts (42 tests) 753ms + ✓ tests/authored-parallel-body.test.ts (5 tests) 9ms + + Test Files 4 passed (4) + Tests 104 passed (104) + Start at 17:19:59 + Duration 4.26s (transform 1.04s, setup 40ms, collect 4.30s, tests 794ms, environment 0ms, prepare 260ms) + diff --git a/evidence/hosted-parallel-2-0-53/logs/function-source-diff.txt b/evidence/hosted-parallel-2-0-53/logs/function-source-diff.txt new file mode 100644 index 000000000..bf5719d98 --- /dev/null +++ b/evidence/hosted-parallel-2-0-53/logs/function-source-diff.txt @@ -0,0 +1,24 @@ +--- evidence/hosted-parallel-2-0-53/logs/node-function-source.txt 2026-10-18 17:23:27 ++++ evidence/hosted-parallel-2-0-53/logs/bun-function-source.txt 2026-10-18 17:23:24 +@@ -1,8 +1,15 @@ +-async f => { +- const a = async () => { await f.run('echo A-start $(date +%s%3N); sleep 2; echo A-end $(date +%s%3N)'); await f.run('echo a-dep $(date +%s%3N)'); }; +- const b = async () => { await f.run('echo B-start $(date +%s%3N); sleep 2; echo B-end $(date +%s%3N)'); await f.run('echo b-dep $(date +%s%3N)'); }; +- const c = async () => { await f.run('echo C-start $(date +%s%3N); sleep 2; echo C-end $(date +%s%3N)'); await f.run('echo c-dep $(date +%s%3N)'); }; ++async (f) => { ++ const a = async () => { ++ await f.run("echo A-start $(date +%s%3N); sleep 2; echo A-end $(date +%s%3N)"); ++ await f.run("echo a-dep $(date +%s%3N)"); ++ }, b = async () => { ++ await f.run("echo B-start $(date +%s%3N); sleep 2; echo B-end $(date +%s%3N)"); ++ await f.run("echo b-dep $(date +%s%3N)"); ++ }, c = async () => { ++ await f.run("echo C-start $(date +%s%3N); sleep 2; echo C-end $(date +%s%3N)"); ++ await f.run("echo c-dep $(date +%s%3N)"); ++ }; + await Promise.all([a(), b(), c()]); +- await f.run('echo join $(date +%s%3N)'); +- f.done('success'); ++ await f.run("echo join $(date +%s%3N)"); ++ f.done("success"); + } diff --git a/evidence/hosted-parallel-2-0-53/logs/manual-journal-after-resume.txt b/evidence/hosted-parallel-2-0-53/logs/manual-journal-after-resume.txt new file mode 100644 index 000000000..23d1a22f6 --- /dev/null +++ b/evidence/hosted-parallel-2-0-53/logs/manual-journal-after-resume.txt @@ -0,0 +1,7 @@ +root executionMode: parallel +run-1: started=1792336867426 ended=1792336869451 +run-2: started=1792336867424 ended=1792336869450 +run-3: started=1792336867422 ended=1792336869450 +route start spread ms: 4 +all routes overlapped: True +run journals: 9 attempt starts: 9 diff --git a/evidence/hosted-parallel-2-0-53/logs/manual-journal.txt b/evidence/hosted-parallel-2-0-53/logs/manual-journal.txt new file mode 100644 index 000000000..23d1a22f6 --- /dev/null +++ b/evidence/hosted-parallel-2-0-53/logs/manual-journal.txt @@ -0,0 +1,7 @@ +root executionMode: parallel +run-1: started=1792336867426 ended=1792336869451 +run-2: started=1792336867424 ended=1792336869450 +run-3: started=1792336867422 ended=1792336869450 +route start spread ms: 4 +all routes overlapped: True +run journals: 9 attempt starts: 9 diff --git a/evidence/hosted-parallel-2-0-53/logs/manual-resume.txt b/evidence/hosted-parallel-2-0-53/logs/manual-resume.txt new file mode 100644 index 000000000..e0637b9bc --- /dev/null +++ b/evidence/hosted-parallel-2-0-53/logs/manual-resume.txt @@ -0,0 +1 @@ +{"ok":true,"command":"resume","resolutions":[],"diagnostics":[],"rootRunId":"01M57SMB30E9AD784XVP5T2X9S","runId":"01M57SMB30E9AD784XVP5T2X9S","socketPath":"/Users/khaliqgant/.agent-tmp/relayflowd-9fb8f3c4039e.sock","completedSteps":8,"status":"completed","completionReason":"success","subscriptions":[]} diff --git a/evidence/hosted-parallel-2-0-53/logs/manual-run.txt b/evidence/hosted-parallel-2-0-53/logs/manual-run.txt new file mode 100644 index 000000000..fc10d4533 --- /dev/null +++ b/evidence/hosted-parallel-2-0-53/logs/manual-run.txt @@ -0,0 +1 @@ +{"ok":true,"command":"run","resolutions":[],"diagnostics":[],"path":"par.flow.ts","rootRunId":"01M57SMB30E9AD784XVP5T2X9S","runId":"01M57SMB30E9AD784XVP5T2X9S","socketPath":"/Users/khaliqgant/.agent-tmp/relayflowd-9fb8f3c4039e.sock","completedSteps":8,"status":"completed","completionReason":"success","subscriptions":[]} diff --git a/evidence/hosted-parallel-2-0-53/logs/node-function-source.txt b/evidence/hosted-parallel-2-0-53/logs/node-function-source.txt new file mode 100644 index 000000000..517b8ab5e --- /dev/null +++ b/evidence/hosted-parallel-2-0-53/logs/node-function-source.txt @@ -0,0 +1,8 @@ +async f => { + const a = async () => { await f.run('echo A-start $(date +%s%3N); sleep 2; echo A-end $(date +%s%3N)'); await f.run('echo a-dep $(date +%s%3N)'); }; + const b = async () => { await f.run('echo B-start $(date +%s%3N); sleep 2; echo B-end $(date +%s%3N)'); await f.run('echo b-dep $(date +%s%3N)'); }; + const c = async () => { await f.run('echo C-start $(date +%s%3N); sleep 2; echo C-end $(date +%s%3N)'); await f.run('echo c-dep $(date +%s%3N)'); }; + await Promise.all([a(), b(), c()]); + await f.run('echo join $(date +%s%3N)'); + f.done('success'); +} diff --git a/evidence/hosted-parallel-2-0-53/logs/standalone-build.txt b/evidence/hosted-parallel-2-0-53/logs/standalone-build.txt new file mode 100644 index 000000000..64489f3d4 --- /dev/null +++ b/evidence/hosted-parallel-2-0-53/logs/standalone-build.txt @@ -0,0 +1,6 @@ +Bundled 796 modules in 778ms + + authored-node.mjs 12.19 MB (entry point) + + [376ms] bundle 768 modules +[3.933s] compile /tmp/ms-flows-parallel-hosted/flows diff --git a/evidence/hosted-parallel-2-0-53/logs/standalone-parallel.txt b/evidence/hosted-parallel-2-0-53/logs/standalone-parallel.txt new file mode 100644 index 000000000..00d6894c2 --- /dev/null +++ b/evidence/hosted-parallel-2-0-53/logs/standalone-parallel.txt @@ -0,0 +1,11 @@ + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + + ✓ tests/authored-node-runtime.test.ts (18 tests | 17 skipped) 10524ms + ✓ Bun 1.4.x standalone → native Node authored lifecycle > runs a certified Promise.all group in parallel through the embedded Node runner 5735ms + + Test Files 1 passed (1) + Tests 1 passed | 17 skipped (18) + Start at 17:20:08 + Duration 12.32s (transform 759ms, setup 13ms, collect 1.65s, tests 10.52s, environment 0ms, prepare 37ms) + diff --git a/evidence/hosted-parallel-2-0-53/logs/standalone-serial.txt b/evidence/hosted-parallel-2-0-53/logs/standalone-serial.txt new file mode 100644 index 000000000..4c119027b --- /dev/null +++ b/evidence/hosted-parallel-2-0-53/logs/standalone-serial.txt @@ -0,0 +1,11 @@ + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + + ✓ tests/authored-node-runtime.test.ts (18 tests | 17 skipped) 11006ms + ✓ Bun 1.4.x standalone → native Node authored lifecycle > awaits agent plus three run steps and resumes without repeating effects 5383ms + + Test Files 1 passed (1) + Tests 1 passed | 17 skipped (18) + Start at 17:20:24 + Duration 13.53s (transform 905ms, setup 12ms, collect 2.34s, tests 11.01s, environment 0ms, prepare 46ms) + diff --git a/evidence/hosted-parallel-2-0-53/logs/typecheck-tests.txt b/evidence/hosted-parallel-2-0-53/logs/typecheck-tests.txt new file mode 100644 index 000000000..5d52b5d5b --- /dev/null +++ b/evidence/hosted-parallel-2-0-53/logs/typecheck-tests.txt @@ -0,0 +1,4 @@ + +> @relayflows/sdk@2.0.52 typecheck:tests +> tsc -p tsconfig.tests.json + diff --git a/evidence/hosted-parallel-2-0-53/logs/typecheck.txt b/evidence/hosted-parallel-2-0-53/logs/typecheck.txt new file mode 100644 index 000000000..4865f8c4f --- /dev/null +++ b/evidence/hosted-parallel-2-0-53/logs/typecheck.txt @@ -0,0 +1,4 @@ + +> @relayflows/sdk@2.0.52 typecheck +> tsc --noEmit && tsc -p tsconfig.type-tests.json + diff --git a/packages/sdk/src/authored-node-entry.ts b/packages/sdk/src/authored-node-entry.ts index b4bd59ca5..29ac55a99 100644 --- a/packages/sdk/src/authored-node-entry.ts +++ b/packages/sdk/src/authored-node-entry.ts @@ -73,6 +73,20 @@ try { // NODE_OPTIONS remain the child's inherited values. const agentEnvironment = validatedLocalAgentEnvironment(request.agentEnvironment); const loaded = await loadPinnedAuthoredSource(request.metadata, true); + if (request.metadata.identityVersion === 2 && request.metadata.executionMode === 'parallel') { + // Bun admitted the root from the source AST because its Function.toString + // is reformatted. Reconfirm that exact source and the Node-loaded body + // before executing any child. A changed file or a changed body fails closed. + const bytes = readFileSync(request.metadata.flowPath); + if (createHash('sha256').update(bytes).digest('hex') !== request.metadata.sourceSha256) { + throw new Error('authored root source authority mismatch'); + } + const { parallelPinnedSourceEligible, parallelBodySyntaxEligible } = await import('./authored-parallel-eligibility.js'); + if (!parallelPinnedSourceEligible(bytes.toString('utf8')) + || !parallelBodySyntaxEligible(loaded.getDefinition(loaded.handle).body)) { + throw new AuthoredFlowExecutionError('step_failed', 'parallel f.run body lost its source certificate'); + } + } if (request.localAgentStream !== request.metadata.localAgentStream) throw new Error('authored root local agent surface mismatch'); if (request.workerCapacity !== undefined && !isAgentCapacity(request.workerCapacity)) throw new Error('invalid authored worker capacity'); client = new JournalClient(request.socketPath, { readBudgetMs: FLOW_READ_BUDGET_MS, budgetHandshake: true }); diff --git a/packages/sdk/src/authored-node-runner.ts b/packages/sdk/src/authored-node-runner.ts index a530b4a1b..94d2c5c7e 100644 --- a/packages/sdk/src/authored-node-runner.ts +++ b/packages/sdk/src/authored-node-runner.ts @@ -20,6 +20,7 @@ import type { import { HUMAN_WAIT_ID } from './authored-human.js'; import { assertAuthoredPromiseHooks } from './authored-runtime-capability.js'; import { settlingCompletion } from './step-settling.js'; +import { readAuthoredStepIndex } from './authored-step-index.js'; import { isTransportRetries } from './validate.js'; let embeddedSource: string | undefined; @@ -347,10 +348,16 @@ export async function verifyAuthoredNodeResult( const authored = result.journalSteps.filter(step => typeof step?.id === 'string' && !isGate(step.id)); const gates = result.journalSteps.filter(step => typeof step?.id === 'string' && isGate(step.id)); if (!Number.isSafeInteger(count) || authored.length !== count || isGate(terminal.id)) invalid(`count ${authored.length}!=${count}`); + const parallel = metadata.identityVersion === 2 && metadata.executionMode === 'parallel'; const ordinal = (id: string): number => Number(/-([1-9][0-9]*)$/.exec(id)?.[1]); - // Parallel awaits may finish in either order; validate a copy in declaration order. - const ordered = [...authored].sort((a,b)=>ordinal(a.id)-ordinal(b.id)); + // Serial ids have one global ordinal. Parallel ids have one ordinal per + // causal predecessor, so several valid siblings end in `-1`. Validate the + // complete parallel claim against the root's durable child index instead. + const ordered = parallel ? [...authored] : [...authored].sort((a,b)=>ordinal(a.id)-ordinal(b.id)); const authoredIds = new Set(ordered.map(step => step.id)); + if (parallel && (gates.length !== 0 || authoredIds.size !== count + || ordered.some(step => step === terminal ? !/^complete-[1-9][0-9]*$/u.test(step.id) + : !/^run-(?:[1-9][0-9]*|after-[a-f0-9]{64}-[1-9][0-9]*)$/u.test(step.id)))) invalid('parallel ids'); for (const gate of gates) { if (!authoredIds.has(gate.id.slice(0, -'.gate'.length))) invalid(`orphan ${gate.id}`); } @@ -361,9 +368,19 @@ export async function verifyAuthoredNodeResult( await journal.connect(); try { await journal.hello('flows-authored-result-verifier'); + if (parallel) { + const indexed = await readAuthoredStepIndex(journal, rootRunId); + const claimed = new Map(ordered.map(step => [step.id, step])); + if (indexed.length !== count || indexed.some(step => { + const matching = claimed.get(step.step); + return matching === undefined || step.state !== 'completed' + || step.runId !== matching.runId || step.completionReason !== matching.completionReason; + })) invalid('parallel index'); + } for (const [index, claimed] of [...ordered, ...gates].entries()) { if (!claimed || typeof claimed.id !== 'string' || typeof claimed.runId !== 'string' - || (index < ordered.length && ordinal(claimed.id) !== index+1) || !['success', 'timeout'].includes(claimed.completionReason) + || (!parallel && index < ordered.length && ordinal(claimed.id) !== index+1) + || !['success', 'timeout'].includes(claimed.completionReason) || runs.has(claimed.runId)) invalid(`claim ${claimed?.id}`); runs.add(claimed.runId); const state = await journal.runGet(claimed.runId); diff --git a/packages/sdk/src/authored-parallel-eligibility.ts b/packages/sdk/src/authored-parallel-eligibility.ts index 43f0f1e1d..e9b0fa843 100644 --- a/packages/sdk/src/authored-parallel-eligibility.ts +++ b/packages/sdk/src/authored-parallel-eligibility.ts @@ -14,12 +14,27 @@ export function parallelRunEligible(body: Function, moduleSource: string): boole // Function.toString cannot see a module-level lexical binding, so inspect // the complete source before using the body's narrower grammar. const bodySource = authoredFunctionSource(body); - return moduleUsesIntrinsicPromise(moduleSource, bodySource) && parallelBodySyntaxEligible(body); + return certifiedModuleBody(moduleSource, bodySource) !== undefined && parallelBodySyntaxEligible(body); +} + +/** + * The compiled Bun CLI owns the root lease, but the pinned source is executed + * by its embedded Node child. Bun reformats function toString output, so its + * in-memory body cannot be compared byte-for-byte with the source. Certify + * the closed source module instead; the Node child loads those hash-pinned + * bytes and independently checks the body's grammar before any child starts. + */ +export function parallelPinnedSourceEligible(moduleSource: string): boolean { + const bodySource = certifiedModuleBody(moduleSource); + return bodySource !== undefined && parallelBodySourceSyntaxEligible(bodySource); } /** Independently re-check the closed body grammar at execution time. */ export function parallelBodySyntaxEligible(body: Function): boolean { - const bodySource = authoredFunctionSource(body); + return parallelBodySourceSyntaxEligible(authoredFunctionSource(body)); +} + +function parallelBodySourceSyntaxEligible(bodySource: string): boolean { const file = ts.createSourceFile('authored-body.js', `const body = ${bodySource};`, ts.ScriptTarget.ES2022, true, ts.ScriptKind.JS); if (file.statements.length !== 1) return false; @@ -159,10 +174,10 @@ export function parallelBodySyntaxEligible(body: Function): boolean { } /** Reject any source that can change what a free `Promise` identifier means. */ -function moduleUsesIntrinsicPromise(source: string, bodySource: string): boolean { +function certifiedModuleBody(source: string, bodySource?: string): string | undefined { const file = ts.createSourceFile('authored-module.ts', source, ts.ScriptTarget.ES2022, true, ts.ScriptKind.TS); if (((file as ts.SourceFile & { parseDiagnostics?: readonly ts.Diagnostic[] }).parseDiagnostics?.length ?? 0) > 0) { - return false; + return undefined; } // Top-level code can alter the constructor or its methods without binding // the name. Leave these modules on the established serial scheduler too. @@ -172,17 +187,18 @@ function moduleUsesIntrinsicPromise(source: string, bodySource: string): boolean }; let definesDefaultBody = false; let importsSurfaceFlow = false; + let certifiedBody: string | undefined; for (const statement of file.statements) { if (ts.isImportDeclaration(statement)) { // Imports can execute code before the body starts. Only the direct, // immutable Surface binding can certify the exported body's identity. if (statement.moduleSpecifier.getText(file) !== "'@relayflows/surface'" - && statement.moduleSpecifier.getText(file) !== '"@relayflows/surface"') return false; + && statement.moduleSpecifier.getText(file) !== '"@relayflows/surface"') return undefined; const bindings = statement.importClause?.namedBindings; if (statement.importClause?.name !== undefined || bindings === undefined || !ts.isNamedImports(bindings) || bindings.elements.length !== 1 || bindings.elements[0]?.name.text !== 'flow' - || bindings.elements[0]?.propertyName !== undefined || importsSurfaceFlow) return false; + || bindings.elements[0]?.propertyName !== undefined || importsSurfaceFlow) return undefined; importsSurfaceFlow = true; continue; } @@ -191,19 +207,20 @@ function moduleUsesIntrinsicPromise(source: string, bodySource: string): boolean // not let an expression around flow() mutate Promise while we skip it. const call = statement.expression; if (statement.isExportEquals || !ts.isCallExpression(call) - || !ts.isIdentifier(call.expression) || call.expression.text !== 'flow') return false; + || !ts.isIdentifier(call.expression) || call.expression.text !== 'flow') return undefined; const args = call.arguments; - if (args.length !== 2 || !ts.isStringLiteral(args[0]!)) return false; - if (!ts.isArrowFunction(args[args.length - 1]!)) return false; - if (args[args.length - 1]!.getText(file) !== bodySource) return false; - if (args.slice(0, -1).some(touchesPromiseOutsideDefinition)) return false; - if (definesDefaultBody) return false; + if (args.length !== 2 || !ts.isStringLiteral(args[0]!)) return undefined; + if (!ts.isArrowFunction(args[args.length - 1]!)) return undefined; + certifiedBody = args[args.length - 1]!.getText(file); + if (bodySource !== undefined && certifiedBody !== bodySource) return undefined; + if (args.slice(0, -1).some(touchesPromiseOutsideDefinition)) return undefined; + if (definesDefaultBody) return undefined; definesDefaultBody = true; continue; } // The certified body cannot use module captures. Other top-level code is // unnecessary for it and may schedule a later mutation through an alias. - return false; + return undefined; } const forbidden = new Set(['Promise', 'globalThis']); const bindsForbidden = (name: ts.BindingName): boolean => ts.isIdentifier(name) @@ -241,5 +258,5 @@ function moduleUsesIntrinsicPromise(source: string, bodySource: string): boolean visit(file); // The body may come from another module, where a free Promise name can be // shadowed without appearing in this source. In that case use serial IDs. - return importsSurfaceFlow && definesDefaultBody && !unsafe; + return importsSurfaceFlow && definesDefaultBody && !unsafe ? certifiedBody : undefined; } diff --git a/packages/sdk/src/authored-root.ts b/packages/sdk/src/authored-root.ts index 3fe1fe5ff..b716b2b6d 100644 --- a/packages/sdk/src/authored-root.ts +++ b/packages/sdk/src/authored-root.ts @@ -95,14 +95,27 @@ export async function executeDurableAuthoredFlow( }))); const definition = loaded.getDefinition(loaded.handle); let parallelEligible = false; - if (loaded.graph.length === 1 && (loaded.extensions?.length ?? 0) === 0 - && definition.header.budget === undefined && parallelPromiseIntrinsicsIntact()) { + let parallelIneligibleReason: string | undefined; + if (loaded.graph.length !== 1) parallelIneligibleReason = 'source_graph'; + else if ((loaded.extensions?.length ?? 0) !== 0) parallelIneligibleReason = 'extensions'; + else if (definition.header.budget !== undefined) parallelIneligibleReason = 'shared_budget'; + else if (!parallelPromiseIntrinsicsIntact()) parallelIneligibleReason = 'runtime_intrinsics'; + else { // The parser is needed only for an authored run, never for an ordinary // CLI check or watch reload. Recheck intrinsics after the async import; // author code could have replaced them while it was loading. - const { parallelRunEligible } = await import('./authored-parallel-eligibility.js'); + const { parallelRunEligible, parallelPinnedSourceEligible } = await import('./authored-parallel-eligibility.js'); parallelEligible = parallelPromiseIntrinsicsIntact() - && parallelRunEligible(definition.body, source.toString('utf8')); + && (process.versions['bun'] === undefined + ? parallelRunEligible(definition.body, source.toString('utf8')) + : parallelPinnedSourceEligible(source.toString('utf8'))); + if (!parallelEligible) parallelIneligibleReason = parallelPromiseIntrinsicsIntact() + ? 'source_certificate' : 'runtime_intrinsics'; + } + // Opt-in operator diagnostic only: changing root metadata would change its + // admission spec hash and could disrupt a 2.0.52 in-flight serial root. + if (parallelIneligibleReason !== undefined && process.env['FLOWS_PARALLEL_DEBUG'] === '1') { + process.stderr.write(`[flows] parallel f.run admission: serial (${parallelIneligibleReason})\n`); } const executionMode = parallelEligible ? 'parallel' : 'serial'; const metadata: AuthoredRootMetadata = Object.freeze({ diff --git a/packages/sdk/tests/authored-node-result.test.ts b/packages/sdk/tests/authored-node-result.test.ts index c74c806da..7d5f0030f 100644 --- a/packages/sdk/tests/authored-node-result.test.ts +++ b/packages/sdk/tests/authored-node-result.test.ts @@ -5,10 +5,10 @@ import { EXCERPT_BYTES, formatStepExcerpt } from '../src/cli/step-excerpt.js'; import type { AuthoredFlowExecutionResult } from '../src/authored-flow-executor.js'; import type { AuthoredRootMetadata } from '../src/authored-root.js'; -const mocks=vi.hoisted(()=>({connect:vi.fn(),hello:vi.fn(),close:vi.fn(),runGet:vi.fn(),journalRead:vi.fn()})); +const mocks=vi.hoisted(()=>({connect:vi.fn(),hello:vi.fn(),close:vi.fn(),runGet:vi.fn(),journalRead:vi.fn(),streamRead:vi.fn()})); vi.mock('../src/journal-client.js',()=>({JournalClient:class { connect=mocks.connect;hello=mocks.hello;close=mocks.close; - runGet=mocks.runGet;journalRead=mocks.journalRead; + runGet=mocks.runGet;journalRead=mocks.journalRead;streamRead=mocks.streamRead; }})); const metadata={flowName:'example'} as AuthoredRootMetadata; function result():AuthoredFlowExecutionResult { @@ -25,8 +25,13 @@ function entries(command=':', id='complete-2'):Array> { ]; } let records:Map>>; +let streamRecords:Array>; beforeEach(()=>{ vi.clearAllMocks(); + streamRecords=[]; + mocks.streamRead.mockImplementation(async (_runId:string,_stream:string,offset:number)=>({ + messages:offset===0?streamRecords:[],next_offset:streamRecords.length, + })); records=new Map([['child-1',entries(':','run-1')],['child-2',entries()]]); mocks.runGet.mockImplementation(async (run_id:string)=>({run_id,status:'completed',steps:{ [run_id==='child-1'?'run-1':'complete-2']:{state:'done'}, @@ -37,6 +42,36 @@ beforeEach(()=>{ })); }); describe('authored IPC result durable verification',()=>{ + it('accepts causal parallel ids only when every claimed child matches the durable root index', async () => { + const dependent = `run-after-${'a'.repeat(64)}-1`; + const claimed: AuthoredFlowExecutionResult = { rootRunId:'root', name:'example', completionReason:'success', + journalSteps:[ + {id:'run-1',runId:'child-1',completionReason:'success'}, + {id:'run-2',runId:'child-2',completionReason:'success'}, + {id:dependent,runId:'child-3',completionReason:'success'}, + {id:'complete-4',runId:'child-4',completionReason:'success'}, + ] }; + const parallelMetadata = { ...metadata, identityVersion:2, executionMode:'parallel' } as AuthoredRootMetadata; + records.set('child-2',entries(':','run-2')); + records.set('child-3',entries(':',dependent)); + records.set('child-4',entries(':','complete-4')); + const runIds = new Map(claimed.journalSteps.map(step => [step.runId,step.id])); + mocks.runGet.mockImplementation(async (run_id:string)=>({run_id,status:'completed', + steps:{[runIds.get(run_id)!]:{state:'done'}}})); + streamRecords = claimed.journalSteps.map(step => ({index:'relayflows.authored-step.v1', + step:step.id,runId:step.runId,state:'completed',completionReason:'success'})); + await expect(verifyAuthoredNodeResult(claimed,parallelMetadata,'root','socket')).resolves.toBeUndefined(); + streamRecords[1] = {...streamRecords[1],runId:'unclaimed-child'}; + await expect(verifyAuthoredNodeResult(claimed,parallelMetadata,'root','socket')) + .rejects.toThrow('no matching durable completion'); + streamRecords[1] = {index:'relayflows.authored-step.v1',step:'run-2',runId:'child-2', + state:'completed',completionReason:'success'}; + streamRecords.push({index:'relayflows.authored-step.v1',step:'run-9',runId:'extra-child', + state:'completed',completionReason:'success'}); + await expect(verifyAuthoredNodeResult(claimed,parallelMetadata,'root','socket')) + .rejects.toThrow('no matching durable completion'); + }); + it('accepts only a declared, durably settled agent timeout', async () => { const claimed = result(); claimed.journalSteps[0]!.completionReason = 'timeout'; diff --git a/packages/sdk/tests/authored-node-runtime.test.ts b/packages/sdk/tests/authored-node-runtime.test.ts index 051fa91bf..9baf3e0a4 100644 --- a/packages/sdk/tests/authored-node-runtime.test.ts +++ b/packages/sdk/tests/authored-node-runtime.test.ts @@ -1,11 +1,12 @@ import { spawn, spawnSync } from 'node:child_process'; -import { chmodSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'; +import { chmodSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { JournalClient } from '../src/journal-client.js'; import { socketFor } from '../src/cli/run.js'; import { assertAuthoredNodeVersion } from '../src/authored-runtime-capability.js'; +import { readAuthoredRootMetadata } from '../src/authored-root.js'; const root = resolve('../..'), sdk = resolve('.'); const fixtures: string[] = []; @@ -15,7 +16,7 @@ const bun = process.env['FLOWS_BUILD_BUN'] ?? 'bun'; const wrapperHelper = resolve('../../testdata/preflight/wrapper-session.mjs'); beforeAll(() => { - expect(spawnSync(bun, ['--version'], { encoding: 'utf8' }).stdout.trim()).toBe('1.4.0'); + expect(spawnSync(bun, ['--version'], { encoding: 'utf8' }).stdout.trim()).toMatch(/^1\.4\.\d+$/); expect(existsSync(daemon), 'build the current kernel or set RELAYFLOWD_BIN').toBe(true); stage = mkdtempSync(join(tmpdir(), 'authored-standalone-build-')); cli = join(stage, 'flows'); @@ -91,7 +92,36 @@ async function entries(directory: string, runId: string) { finally { client.close(); } } -describe('Bun 1.4.0 standalone → native Node authored lifecycle', () => { +describe('Bun 1.4.x standalone → native Node authored lifecycle', () => { + it('runs a certified Promise.all group in parallel through the embedded Node runner', async () => { + const f = fixture(''); + writeFileSync(join(f.directory, 'case.flow.ts'), `import { flow } from '@relayflows/surface'; +export default flow('runtime-case', async f => { + const a = async () => { await f.run('touch a-start; sleep 2; touch a-end'); await f.run('touch a-dep'); }; + const b = async () => { await f.run('touch b-start; sleep 2; touch b-end'); await f.run('touch b-dep'); }; + const c = async () => { await f.run('touch c-start; sleep 2; touch c-end'); await f.run('touch c-dep'); }; + await Promise.all([a(), b(), c()]); + await f.run('touch joined'); + f.done('success'); +}); +`); + const first = f.run(); + expect(first.status, first.stderr + first.stdout).toBe(0); + const report = JSON.parse(first.stdout) as { runId: string }; + const reader = new JournalClient(socketFor(join(f.directory, 'data'))); + await reader.connect(); await reader.hello('standalone-parallel-test'); + try { + expect(await readAuthoredRootMetadata(reader, report.runId)) + .toMatchObject({ executionMode: 'parallel', identityVersion: 2 }); + } finally { reader.close(); } + const timestamp = (name: string) => statSync(join(f.directory, name)).mtimeMs; + const starts = ['a', 'b', 'c'].map(name => timestamp(`${name}-start`)); + const ends = ['a', 'b', 'c'].map(name => timestamp(`${name}-end`)); + expect(Math.max(...starts)).toBeLessThan(Math.min(...ends)); + for (const name of ['a', 'b', 'c']) expect(timestamp(`${name}-dep`)).toBeGreaterThanOrEqual(timestamp(`${name}-end`)); + expect(existsSync(join(f.directory, 'joined'))).toBe(true); + }, 120_000); + it('serializes the immutable prepared binding facts through the Node and CLI boundary', () => { const f = fixture(`const activity=f.on(webhook('github_pull_request',{action:'opened',repository:{id:7}}),{settle:'2m',idle:'1h',deadline:'1d',includeSelf:true});await activity.next();f.done('success');`); const result = f.run(); expect(result.status, result.stderr + result.stdout).toBe(4); diff --git a/packages/sdk/tests/authored-parallel-eligibility.test.ts b/packages/sdk/tests/authored-parallel-eligibility.test.ts index 08a7b1c6d..770362243 100644 --- a/packages/sdk/tests/authored-parallel-eligibility.test.ts +++ b/packages/sdk/tests/authored-parallel-eligibility.test.ts @@ -1,11 +1,34 @@ import { describe, expect, it } from 'vitest'; -import { parallelRunEligible } from '../src/authored-parallel-eligibility.js'; +import { parallelPinnedSourceEligible, parallelRunEligible } from '../src/authored-parallel-eligibility.js'; const body = (source: string): Function => new Function(`return (${source});`)() as Function; const eligible = (candidate: Function, moduleSource = `import { flow } from '@relayflows/surface';\nexport default flow('probe', ${Function.prototype.toString.call(candidate)});`) => parallelRunEligible(candidate, moduleSource); describe('parallel run admission', () => { + it('certifies the pinned standalone source while retaining the closed module grammar', () => { + const source = `import { flow } from '@relayflows/surface'; +export default flow('hosted', async f => { + const a = async () => { await f.run('A'); await f.run('a-dep'); }; + const b = async () => { await f.run('B'); await f.run('b-dep'); }; + await Promise.all([a(), b()]); + await f.run('join'); + f.done('success'); +});`; + const reformatted = body(`async (f) => { + const a = async () => { await f.run("A"); await f.run("a-dep"); }, + b = async () => { await f.run("B"); await f.run("b-dep"); }; + await Promise.all([a(), b()]); await f.run("join"); f.done("success"); + }`); + // Bun emits this kind of reformatted Function.toString body. The Node CLI + // keeps its exact body/source identity requirement. + expect(parallelRunEligible(reformatted, source)).toBe(false); + expect(parallelPinnedSourceEligible(source)).toBe(true); + expect(parallelPinnedSourceEligible(source.replace("import { flow }", "import { flow, webhook }"))).toBe(false); + expect(parallelPinnedSourceEligible(`${source}\nconst Promise = globalThis.Promise;`)).toBe(false); + expect(parallelPinnedSourceEligible(source.replace('Promise.all', 'Promise.race'))).toBe(false); + }); + it('certifies direct Promise.all and awaited branch continuations', () => { expect(eligible(body(`async f => { const outputs = await Promise.all([f.run('a'), f.run('b')]); diff --git a/packages/sdk/tests/authored-root.test.ts b/packages/sdk/tests/authored-root.test.ts index 621f9f3ae..6fd0dbfc1 100644 --- a/packages/sdk/tests/authored-root.test.ts +++ b/packages/sdk/tests/authored-root.test.ts @@ -185,6 +185,25 @@ describe('durable authored root', () => { expect(journal.peer.completions).toEqual([{ attempt: 1, reason: 'success' }]); }); + it('reports a serial admission reason only when operator diagnostics are enabled', async () => { + const loaded = await fixture(); + const journal = new RootJournal(); + journal.startStatus = outcome(journal.runId, 'completed', 'success'); + journal.entries = [completedEntry()]; + const previous = process.env['FLOWS_PARALLEL_DEBUG']; + const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true); + try { + process.env['FLOWS_PARALLEL_DEBUG'] = '1'; + await executeDurableAuthoredFlow(loaded, journal as unknown as JournalClient, undefined, + { dataDir: '/unused', admissionKey: 'operator-diagnostic' }); + expect(stderr).toHaveBeenCalledWith('[flows] parallel f.run admission: serial (source_certificate)\n'); + } finally { + stderr.mockRestore(); + if (previous === undefined) delete process.env['FLOWS_PARALLEL_DEBUG']; + else process.env['FLOWS_PARALLEL_DEBUG'] = previous; + } + }); + it('pins a certified Promise.all run body to parallel mode before dispatch', async () => { const body = async (f: Ctx) => { await Promise.all([f.run('printf a'), f.run('printf b')]); From 2d4ca4255a260a3fbab45575e5a5b533554af567 Mon Sep 17 00:00:00 2001 From: Miya Date: Sun, 18 Oct 2026 20:00:54 +0200 Subject: [PATCH 2/3] WIP: address hosted parallel review findings --- docs/SURFACE.md | 16 +- .../logs/review1-focused-tests.txt | 19 ++ .../logs/review1-typecheck-tests.txt | 3 + .../logs/review1-typecheck.txt | 3 + .../hosted-parallel-review1/cases-review1.mjs | 26 ++ evidence/hosted-parallel-review1/envelope.mjs | 16 ++ evidence/hosted-parallel-review1/h676.mjs | 233 ++++++++++++++++++ evidence/hosted-parallel-review1/s.mjs | 164 ++++++++++++ evidence/hosted-parallel-review1/s676.mjs | 182 ++++++++++++++ packages/sdk/src/authored-node-entry.ts | 7 +- packages/sdk/src/authored-node-runner.ts | 6 +- .../sdk/src/authored-parallel-eligibility.ts | 30 +++ packages/sdk/src/authored-root.ts | 16 +- packages/sdk/src/authored-verdict.ts | 7 +- .../sdk/tests/authored-node-result.test.ts | 15 +- .../sdk/tests/authored-node-runtime.test.ts | 64 +++++ .../authored-parallel-eligibility.test.ts | 24 +- .../tests/authored-parallel-run-live.test.ts | 4 +- packages/sdk/tests/authored-root.test.ts | 20 +- .../sdk/tests/authored-status-detail.test.ts | 4 +- 20 files changed, 828 insertions(+), 31 deletions(-) create mode 100644 evidence/hosted-parallel-2-0-53/logs/review1-focused-tests.txt create mode 100644 evidence/hosted-parallel-2-0-53/logs/review1-typecheck-tests.txt create mode 100644 evidence/hosted-parallel-2-0-53/logs/review1-typecheck.txt create mode 100644 evidence/hosted-parallel-review1/cases-review1.mjs create mode 100644 evidence/hosted-parallel-review1/envelope.mjs create mode 100644 evidence/hosted-parallel-review1/h676.mjs create mode 100644 evidence/hosted-parallel-review1/s.mjs create mode 100644 evidence/hosted-parallel-review1/s676.mjs diff --git a/docs/SURFACE.md b/docs/SURFACE.md index c143e224b..3ba3ca331 100644 --- a/docs/SURFACE.md +++ b/docs/SURFACE.md @@ -989,8 +989,10 @@ preload, or with an already replaced Promise or Array iterator intrinsic, also admits new roots in serial mode. Generated arrays such as `Array.from` use serial mode. Branches may not mutate shared values, await timers or other promises, or call `f.done`; completion belongs at the end of -the root body. The check is conservative: an unrecognized source form uses -serial mode even when its author knows it would be safe. In parallel mode, +the root body. TypeScript annotations on the body parameter, awaited values, +and branch return types are allowed when the type-stripped body loaded by Node +matches the certified source exactly. The check is conservative: an unrecognized +source form uses serial mode even when its author knows it would be safe. In parallel mode, each deterministic child uses its own journal connection because the daemon handles requests on one connection in order and `run.start` returns only after its command exits. An authored body admits at most eight @@ -1034,10 +1036,12 @@ failure and capacity rules. resume in serial compatibility mode, preserving their `kind-N` step ids and shared connection. Completed child effects remain bound to their original admission keys, including follow-ups after a `Promise.all` join. -New parallel roots carry a distinct root kind. A 2.0.50 or 2.0.51 CLI refuses one on -rollback, leaving it resumable by this or a newer CLI rather than replaying -parallel follow-ups with old `run-N` identities. New serial roots retain the -original root kind and serial IDs. An active pinned parallel root resumed in a +New 2.0.53 parallel roots carry the `parallel.v3` root kind. A 2.0.52 or older +CLI refuses one before dispatch on rollback, leaving it resumable by a 2.0.53 +or newer CLI. The 2.0.53 CLI can still resume earlier `parallel.v2` roots. +Cloud must keep a 2.0.53-capable artifact available for every active +`parallel.v3` root; rollback cannot resume those roots on 2.0.52. New serial +roots retain the original root kind and serial IDs. An active pinned parallel root resumed in a process with a Node preload or altered Promise or Array iterator intrinsic is refused before dispatch and remains resumable in a clean process. A completed root reports its journaled result without dispatching new work. diff --git a/evidence/hosted-parallel-2-0-53/logs/review1-focused-tests.txt b/evidence/hosted-parallel-2-0-53/logs/review1-focused-tests.txt new file mode 100644 index 000000000..b6ba29f43 --- /dev/null +++ b/evidence/hosted-parallel-2-0-53/logs/review1-focused-tests.txt @@ -0,0 +1,19 @@ + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + + ✓ tests/authored-node-result.test.ts (46 tests) 10ms +(node:52258) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) + ✓ tests/authored-parallel-eligibility.test.ts (12 tests) 62ms +(node:52248) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) +(node:52248) [FLOWS_ROOT_LEASE_LOST] Warning: authored root run_id=root-run attempt=1: lease_conflict: attempt has no active worker lease. Waiting for the kernel to retry it. + ✓ tests/authored-root.test.ts (42 tests) 493ms +(node:52265) ExperimentalWarning: SQLite is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) + ✓ tests/authored-status-detail.test.ts (23 tests) 212ms + + Test Files 4 passed (4) + Tests 123 passed (123) + Start at 19:54:21 + Duration 2.33s (transform 452ms, setup 23ms, collect 2.07s, tests 778ms, environment 0ms, prepare 148ms) diff --git a/evidence/hosted-parallel-2-0-53/logs/review1-typecheck-tests.txt b/evidence/hosted-parallel-2-0-53/logs/review1-typecheck-tests.txt new file mode 100644 index 000000000..23420b807 --- /dev/null +++ b/evidence/hosted-parallel-2-0-53/logs/review1-typecheck-tests.txt @@ -0,0 +1,3 @@ + +> @relayflows/sdk@2.0.52 typecheck:tests +> tsc -p tsconfig.tests.json diff --git a/evidence/hosted-parallel-2-0-53/logs/review1-typecheck.txt b/evidence/hosted-parallel-2-0-53/logs/review1-typecheck.txt new file mode 100644 index 000000000..547c4fb34 --- /dev/null +++ b/evidence/hosted-parallel-2-0-53/logs/review1-typecheck.txt @@ -0,0 +1,3 @@ + +> @relayflows/sdk@2.0.52 typecheck +> tsc --noEmit && tsc -p tsconfig.type-tests.json diff --git a/evidence/hosted-parallel-review1/cases-review1.mjs b/evidence/hosted-parallel-review1/cases-review1.mjs new file mode 100644 index 000000000..2b3e5df03 --- /dev/null +++ b/evidence/hosted-parallel-review1/cases-review1.mjs @@ -0,0 +1,26 @@ +// Keep the reviewer's s676.mjs source bytes unchanged; only update expected +// outcomes for the fixes requested after that review. +import scenarios from './s676.mjs'; + +export default scenarios.map(scenario => { + switch (scenario.name) { + case 'SUB1': + return { ...scenario, okIf: result => result.start === 'sah' + ? result.mode === 'parallel' && result.code !== 0 && result.effects === '' + : result.mode === 'serial' && result.code === 0 && [...result.effects].sort().join('') === 'XZpq' }; + case 'FG_edges': + return { ...scenario, okIf: result => result.start === 'sah' + ? result.mode === 'parallel' && result.code !== 0 && [...result.effects].sort().join('') === 'XZabc' + : result.mode === 'serial' && result.code === 0 && [...result.effects].sort().join('') === 'XZabc' }; + case 'RB_NATW': + case 'RB_N2': + return { ...scenario, okIf: result => result.mode === 'parallel' + && result.code1 !== 0 && result.effectsMid === result.effects0 + && result.code === 0 && scenario.okIf(result) }; + default: + if (scenario.name.startsWith('TS')) return { ...scenario, okIf: result => + result.mode === (result.start === 'sa52' ? 'serial' : 'parallel') + && result.code === 0 && scenario.expect(result) }; + return scenario; + } +}); diff --git a/evidence/hosted-parallel-review1/envelope.mjs b/evidence/hosted-parallel-review1/envelope.mjs new file mode 100644 index 000000000..00d7091d7 --- /dev/null +++ b/evidence/hosted-parallel-review1/envelope.mjs @@ -0,0 +1,16 @@ +// Apply the Cloud Bun-compatibility envelope (cloud packages/core/src/bootstrap/lib/relayflow-v2-executor.ts ~3912-3975) to an installed Surface. +import fs from 'node:fs'; import path from 'node:path'; +const root = process.argv[2]; +const m = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); +fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: m.name, version: m.version, private: true, type: 'module', types: './index.d.ts', + exports: { '.': './index.js', './runtime': './runtime.js', './triggers': './triggers/index.js', './triggers/*': './triggers/*.js' } })); +fs.writeFileSync(path.join(root, 'index.js'), "export * from './dist/index.js';\n"); +fs.writeFileSync(path.join(root, 'runtime.js'), "export { getFlowDefinition } from './dist/flow.js';\n"); +fs.writeFileSync(path.join(root, 'index.d.ts'), "export * from './dist/index.js';\n"); +fs.writeFileSync(path.join(root, 'runtime.d.ts'), "export { getFlowDefinition } from './dist/flow.js';\n"); +fs.mkdirSync(path.join(root, 'triggers'), { recursive: true }); +for (const e of fs.readdirSync(path.join(root, 'dist/triggers')).filter(n => /^[a-z0-9-]+\.js$/.test(n))) { + fs.writeFileSync(path.join(root, 'triggers', e), `export * from '../dist/triggers/${e}';\n`); + fs.writeFileSync(path.join(root, 'triggers', e.replace(/\.js$/, '.d.ts')), `export * from '../dist/triggers/${e}';\n`); +} +console.log('envelope applied', m.version); diff --git a/evidence/hosted-parallel-review1/h676.mjs b/evidence/hosted-parallel-review1/h676.mjs new file mode 100644 index 000000000..0c2285149 --- /dev/null +++ b/evidence/hosted-parallel-review1/h676.mjs @@ -0,0 +1,233 @@ +// Native macOS adaptation of the review-8 crash/resume harness for flows #676. +// Required env: REVIEW_STANDALONE, REVIEW_2052, REVIEW_SDK, REVIEW_NM, +// REVIEW_DAEMON, REVIEW_RUNS, FLOWS_AUTHORED_NODE. +// usage: node --no-warnings h676.mjs [--reps=6] [--conc=3] [--tag=x] [--sides=a:b/c:d] [--keep] +import fs from 'node:fs'; +import path from 'node:path'; +import { execFileSync, spawn } from 'node:child_process'; +import { createRequire } from 'node:module'; +import { DatabaseSync } from 'node:sqlite'; +globalThis.require = createRequire(import.meta.url); + +const args = process.argv.slice(2); +const opt = (k, d) => { const a = args.find(x => x.startsWith(`--${k}=`)); return a ? a.slice(k.length + 3) : d; }; +const file = args[0]; +const names = args[1]; +const reps = Number(opt('reps', '6')); +const conc = Number(opt('conc', '3')); +const tag = opt('tag', 't'); +const sidesOverride = opt('sides', undefined)?.split('/').map(p => p.split(':')); +const keep = args.includes('--keep'); +const DELAYS = [0, 60, 130, 220, 330, 450, 30, 180, 270, 400]; + +const required = name => { + const value = process.env[name]; + if (!value) throw new Error(`${name} is required`); + return value; +}; +const daemon = required('REVIEW_DAEMON'), nm = required('REVIEW_NM'); +const SIDES = { + sah: { cli: required('REVIEW_STANDALONE'), nm, daemon, standalone: true }, + sa52: { cli: required('REVIEW_2052'), nm, daemon, standalone: true }, + nb: { cli: path.join(required('REVIEW_SDK'), 'dist/cli.js'), nm, daemon, standalone: false }, +}; + +const sleep = ms => new Promise(r => setTimeout(r, ms)); +const read = p => { try { return fs.readFileSync(p, 'utf8'); } catch { return ''; } }; + +function start(side, argv, root, env = {}, nodeArgs = []) { + const s = SIDES[side]; + const child = spawn(s.standalone ? s.cli : process.execPath, + s.standalone ? argv : [...nodeArgs, '--experimental-transform-types', s.cli, ...argv], { + cwd: root, env: { ...process.env, ...env, RELAYFLOWD_BIN: s.daemon, + ...(s.standalone && nodeArgs.length ? { NODE_OPTIONS: nodeArgs.join(' ') } : {}) }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + let out = ''; + child.stdout.on('data', d => { out += d; }); + child.stderr.on('data', d => { out += d; }); + return { child, closed: new Promise(r => child.once('close', (code, signal) => r({ code, signal }))), get out() { return out; } }; +} + +function sweep(root) { + const targets = new Set(); + try { targets.add(JSON.parse(read(path.join(root, 'data/connection.json'))).pid); } catch {} + const rows = execFileSync('ps', ['-A', '-o', 'pid=,ppid=,command='], { encoding: 'utf8' }) + .split('\n').map(line => /^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(line)) + .filter(Boolean).map(match => ({ pid: Number(match[1]), ppid: Number(match[2]), cmd: match[3] })); + for (const row of rows) { + if (row.pid !== process.pid && row.cmd.includes(root)) targets.add(row.pid); + } + let changed = true; + while (changed) { changed = false; + for (const row of rows) if (!targets.has(row.pid) && targets.has(row.ppid)) { + targets.add(row.pid); changed = true; + } + } + for (const pid of targets) { try { if (pid) process.kill(pid, 'SIGKILL'); } catch {} } +} + +function journals(root) { + const dir = path.join(root, 'data/runs'); + if (!fs.existsSync(dir)) return []; + return fs.readdirSync(dir).filter(f => f.endsWith('.sqlite3')).sort().map(f => { + let db; + try { db = new DatabaseSync(path.join(dir, f), { readOnly: true }); } catch { return { runId: f.slice(0, -8), step: undefined, completed: 0 }; } + try { + if (!db.prepare("select name from sqlite_master where type='table' and name='entries'").get()) return { runId: f.slice(0, -8), step: undefined, completed: 0 }; + const sp = db.prepare("select payload from entries where entry_type='run.spawned' order by seq limit 1").get(); + const done = db.prepare("select count(*) as n from entries where entry_type='step.completed'").get().n; + const payload = sp ? JSON.parse(sp.payload) : undefined; + const spec = payload?.spec ?? payload?.payload?.spec ?? payload?.run?.spec; + return { runId: f.slice(0, -8), step: spec?.steps?.[0], completed: Number(done) }; + } finally { db.close(); } + }); +} + +function rootOf(js) { return js.find(j => j.step?.id === 'authored-root'); } +function modeOf(r) { + try { const m = JSON.parse(r.step.instruction); return m.executionMode ?? (m.identityVersion ? 'v2?' : 'absent'); } catch { return 'unknown'; } +} + +function replayCheck(js, rootId, expected = {}) { + const seen = new Map(); + const ids = []; + let replay = false; + const dups = []; + for (const j of js) { + if (j.runId === rootId || !j.step) continue; + const st = j.step; + const cmd = st.command ?? st.instruction ?? ''; + ids.push(`${st.id} c=${j.completed} ${String(cmd).slice(0, 70)}`); + if (!st.command) continue; + const ns = st.id.includes('--') ? st.id.slice(0, st.id.lastIndexOf('--')) : ''; + const key = `${ns}|${st.command}`; + const prev = seen.get(key) ?? []; + prev.push({ id: st.id, completed: j.completed }); + seen.set(key, prev); + const distinct = new Set(prev.map(p => p.id)); + if (distinct.size > (expected[st.command] ?? 1) && prev.some(p => p.completed > 0)) { + replay = true; dups.push(`${st.command.slice(0, 50)}: ${[...distinct].join(' & ')}`); + } + } + return { replay, ids, dups }; +} + +async function one(sc, startSide, resumeSide, i) { + const root = path.join(required('REVIEW_RUNS'), tag, sc.name, `${startSide}-${resumeSide}-${i}`); + fs.rmSync(root, { recursive: true, force: true }); + fs.mkdirSync(root, { recursive: true }); + const nmSide = sc.nmSide ?? startSide; + fs.writeFileSync(path.join(root, 'package.json'), '{"type":"module"}'); + if (sc.realNodeModules) fs.mkdirSync(path.join(root, 'node_modules'), { recursive: true }); + else fs.symlinkSync(SIDES[nmSide].nm, path.join(root, 'node_modules')); + fs.writeFileSync(path.join(root, 'flows.json'), JSON.stringify({ cli: path.join(root, 'adapter.mjs'), models: ['test-model'] })); + fs.writeFileSync(path.join(root, 'adapter.mjs'), '#!/usr/bin/env node\nprocess.exit(0);\n', { mode: 0o755 }); + for (const [f, src] of Object.entries(sc.files)) { + const t = path.join(root, f); + fs.mkdirSync(path.dirname(t), { recursive: true }); + fs.writeFileSync(t, src.replaceAll('', root).replaceAll('', SIDES[nmSide].nm)); + if (f.endsWith('adapter.mjs') || f.endsWith('.sh')) fs.chmodSync(t, 0o755); + } + for (const [link, target] of Object.entries(sc.symlinks ?? {})) { + fs.symlinkSync(target, path.join(root, link)); + } + if (sc.bluebird) { + const d = path.join(root, 'bb'); + fs.cpSync('/w/head/packages/sdk/node_modules/bluebird/js/release', d, { recursive: true }); + fs.writeFileSync(path.join(d, 'package.json'), '{"type":"commonjs"}'); + } + const env = Object.fromEntries(Object.entries(sc.env ?? {}).map(([k, v]) => [k, String(v).replaceAll('', root)])); + const renv = { ...env, ...Object.fromEntries(Object.entries(sc.resumeEnv ?? {}).map(([k, v]) => [k, String(v).replaceAll('', root)])) }; + const nargs = (sc.nodeArgs ?? []).map(a => a.replaceAll('', root)); + const rargs = (sc.resumeNodeArgs ?? sc.nodeArgs ?? []).map(a => a.replaceAll('', root)); + const entry = path.join(root, sc.entry ?? 'chain.flow.ts'); + const res = { s: sc.name, start: startSide, resume: resumeSide, i }; + const t0 = Date.now(); + const first = start(startSide, ['run', '--no-observer-link', '--json', ...(sc.extraArgs ?? []), '--data-dir', path.join(root, 'data'), entry, '--input', '{}'], root, env, nargs); + let early; + const deadline = Date.now() + 90_000; + while (!fs.existsSync(path.join(root, 'stalled'))) { + if (first.child.exitCode !== null || first.child.signalCode !== null) { early = await first.closed; break; } + if (Date.now() > deadline) { first.child.kill('SIGKILL'); early = { code: 'timeout' }; break; } + await sleep(15); + } + try { + if (early) { + res.fresh = true; res.code1 = early.code; res.code = early.code; + res.effects1 = res.effects = read(path.join(root, 'effects')); + res.out1 = first.out.slice(-600); + } else { + res.fresh = false; + await sleep(sc.delays?.[i] ?? DELAYS[i % DELAYS.length]); + first.child.kill('SIGKILL'); + await Promise.race([first.closed, sleep(2000)]); + if (!sc.keepDaemon) { sweep(root); await sleep(100); sweep(root); } + res.code1 = 'killed'; + res.effects1 = read(path.join(root, 'effects')); + fs.writeFileSync(path.join(root, 'release'), ''); + const r0 = rootOf(journals(root)); + if (!r0) throw new Error('no root journal'); + const resumed = start(resumeSide, ['resume', '--no-observer-link', '--json', ...(sc.extraArgs ?? []), '--data-dir', path.join(root, 'data'), r0.runId], root, renv, rargs); + const to = setTimeout(() => resumed.child.kill('SIGKILL'), 150_000); + const tr = Date.now(); const rr = await resumed.closed; clearTimeout(to); res.resumeMs = Date.now() - tr; + res.code = rr.code; res.out = resumed.out.slice(-600); + sweep(root); + if (sc.secondResume) { + res.effects0 = res.effects1; + res.effectsMid = read(path.join(root, 'effects')); + res.code1 = rr.code; res.effects1 = res.effectsMid; + const fin = start(sc.secondResume, ['resume', '--no-observer-link', '--json', ...(sc.extraArgs ?? []), '--data-dir', path.join(root, 'data'), r0.runId], root, env, nargs); + const to2 = setTimeout(() => fin.child.kill('SIGKILL'), 150_000); + const fr = await fin.closed; clearTimeout(to2); + res.code = fr.code; res.out2 = fin.out.slice(-400); + sweep(root); + } + res.effects = read(path.join(root, 'effects')); + } + const js = journals(root); + const r = rootOf(js); + res.mode = r ? modeOf(r) : 'noroot'; + const rc = replayCheck(js, r?.runId, sc.expectedCounts); + res.journalReplay = rc.replay; res.dups = rc.dups; res.ids = rc.ids; + const letters = [...res.effects].filter(c => /[A-Za-z]/.test(c)); + res.letterDup = new Set(letters).size !== letters.length; + res.replay = res.journalReplay || (res.letterDup && !sc.allowDupLetters); + if (sc.extraCheck) Object.assign(res, sc.extraCheck(root)); + const okDefault = r => r.code === 0 && !r.replay && (sc.noZ || r.effects.includes('Z')); + res.ok = sc.okIf ? (sc.okIf(res) && !res.replay) : sc.expect ? (sc.expect(res) && !res.replay && res.code === 0) : okDefault(res); + } catch (e) { + res.error = String(e?.stack ?? e).slice(0, 400); res.ok = false; + } finally { + try { first.child.kill('SIGKILL'); } catch {} + sweep(root); + res.ms = Date.now() - t0; + if (!keep && res.ok) fs.rmSync(root, { recursive: true, force: true }); + } + return res; +} + +const mod = await import(path.resolve(file)); +let scen = mod.default; +if (names !== 'all') { const want = names.split(','); scen = want.map(n => { const s = scen.find(x => x.name === n); if (!s) throw new Error(`no scenario ${n}`); return s; }); } +const jobs = []; +for (const sc of scen) for (const [a, b] of (sidesOverride ?? sc.sides)) for (let i = 0; i < reps; i++) jobs.push([sc, a, b, i]); +const results = []; +let next = 0; +await Promise.all(Array.from({ length: conc }, async () => { + while (next < jobs.length) { + const [sc, a, b, i] = jobs[next++]; + const r = await one(sc, a, b, i); + results.push(r); + console.log(JSON.stringify(r)); + } +})); +const groups = new Map(); +for (const r of results) { + const k = `${r.s} ${r.start}>${r.resume}`; + const g = groups.get(k) ?? { ok: 0, n: 0, replay: 0, fresh: 0, modes: new Set(), effects: new Set(), codes: new Set() }; + g.n++; if (r.ok) g.ok++; if (r.replay) g.replay++; if (r.fresh) g.fresh++; + g.modes.add(r.mode); g.effects.add(r.effects); g.codes.add(String(r.code)); + groups.set(k, g); +} +for (const [k, g] of groups) console.log(`SUMMARY ${k} ok=${g.ok}/${g.n} replay=${g.replay} fresh=${g.fresh} mode=${[...g.modes]} codes=${[...g.codes]} effects=${[...g.effects].join('|')}`); diff --git a/evidence/hosted-parallel-review1/s.mjs b/evidence/hosted-parallel-review1/s.mjs new file mode 100644 index 000000000..1de1d721f --- /dev/null +++ b/evidence/hosted-parallel-review1/s.mjs @@ -0,0 +1,164 @@ +// Scenario definitions for review 6 of flows#667. +const STALL = ` await f.run('test -f release || { touch stalled; sleep 120; }; printf Z >> effects', { timeout: '5m' }); + f.done('success');`; +const SURF = `import { flow } from '@relayflows/surface';`; +const flowSrc = (name, body, { fnExpr = false, header = '', pre = '' } = {}) => `${SURF} +${pre}export default flow('${name}'${header}, ${fnExpr ? 'async function(f)' : 'async f =>'} {${body} +${STALL} +}); +`; +const ADAPTER = `#!/usr/bin/env node +import fs from 'node:fs'; +if (process.argv[2] === 'auth') process.exit(0); +if (process.argv[2] === '--relayflows-adapter-v1') { + process.stdout.write('relayflows-agent-cli-v1\\n'); + let input = ''; + for await (const chunk of process.stdin) input += chunk; + if (input.trim() === '') process.exit(0); + const request = JSON.parse(input); + if (request.protocol !== 'relayflows-agent-cli-v1') process.exit(2); + process.stdout.write('relayflows-agent-cli-v1-execute\\n'); + fs.appendFileSync('/calls', 'x'); + process.stdout.write('{"message":"ok"}'); +} +`; +const HS = [['head', 'head'], ['base', 'base']]; +const S = []; +const add = (s) => S.push({ sides: HS, ...s }); + +// ---- review-5 fixtures, verbatim ---- +const P_BIND = `const Promise = { all: globalThis.Promise.all.bind(globalThis.Promise) };`; +const P_HAND = `const Promise = { all: (xs) => new globalThis.Promise((res, rej) => { const out = []; let n = xs.length; xs.forEach((x, i) => globalThis.Promise.resolve(x).then(v => { out[i] = v; if (--n === 0) res(out); }, rej)); }) };`; +const CHAIN = `export { default } from './impl.ts';\n`; +const TAIL5 = ` await f.run('printf X >> effects'); + await f.run('test -f release || { touch stalled; sleep 120; }; printf Z >> effects', { timeout: '5m' }); + f.done('success'); +}); +`; +const G3 = ` await Promise.all([f.run('sleep 0.5; printf A'), f.run('printf B'), f.run('sleep 0.1; printf C')]);\n`; +const NAT = ` const a = async () => { await f.run('sleep 0.4; printf A'); await f.run('printf a >> effects'); }; + const b = async () => { await f.run('printf B'); await f.run('printf b >> effects'); }; + const c = async () => { await f.run('sleep 0.2; printf C'); await f.run('printf c >> effects'); }; + await Promise.all([a(), b(), c()]);\n`; +const impl = (name, P, body) => `${SURF}\n${P}\nexport default flow('${name}', async f => {\n${body}${TAIL5}`; +const W2 = ` await f.run('sleep 0.2; printf W');\n`; +add({ name: 'Q5', files: { 'chain.flow.ts': CHAIN, 'impl.ts': impl('q5', P_BIND, G3) } }); +add({ name: 'Q5n', files: { 'chain.flow.ts': CHAIN, 'impl.ts': impl('q5n', P_BIND, NAT) } }); +add({ name: 'Q5h', files: { 'chain.flow.ts': CHAIN, 'impl.ts': impl('q5h', P_HAND, G3) } }); +add({ name: 'Q5w', files: { 'chain.flow.ts': CHAIN, 'impl.ts': impl('q5w', P_BIND, W2 + G3) } }); +add({ name: 'Q5wn', files: { 'chain.flow.ts': CHAIN, 'impl.ts': impl('q5wn', P_BIND, W2 + NAT) } }); +add({ name: 'Q5wh', files: { 'chain.flow.ts': CHAIN, 'impl.ts': impl('q5wh', P_HAND, W2 + G3) } }); +add({ name: 'Q6Pw', files: { + 'chain.flow.ts': `${SURF}\nimport './lib/patch.mjs';\nexport default flow('q6Pw', async f => {\n await f.run('sleep 0.3; printf W');\n${NAT}${TAIL5}`, + 'lib/patch.mjs': `const I = Function('return this')()['Pro' + 'mise']; +const intrinsicAll = I.all; +const t = setInterval(() => { if (I.all !== intrinsicAll) { clearInterval(t); I.all = function all(xs) { return intrinsicAll.call(I, xs); }; } }, 2); +t.unref(); +` } }); + +// ---- P-series (review 4) ---- +add({ name: 'P1', files: { 'chain.flow.ts': `${SURF}\n${P_BIND}\nexport default flow('p1', async f => {\n${G3}${TAIL5}` } }); +add({ name: 'P1h', files: { 'chain.flow.ts': `${SURF}\n${P_HAND}\nexport default flow('p1h', async f => {\n${G3}${TAIL5}` } }); +add({ name: 'P1c', files: { 'chain.flow.ts': `${SURF}\nclass Promise extends globalThis.Promise { static all(xs) { return globalThis.Promise.all.call(globalThis.Promise, xs); } }\nexport default flow('p1c', async f => {\n${G3}${TAIL5}` } }); +add({ name: 'P1n', files: { 'chain.flow.ts': `${SURF}\n${P_BIND}\nexport default flow('p1n', async f => {\n${NAT}${TAIL5}` } }); +add({ name: 'P1bn', files: { 'chain.flow.ts': `${SURF}\nimport Promise from './bb/bluebird.js';\nexport default flow('p1bn', async f => {\n${NAT}${TAIL5}` }, bluebird: true }); +add({ name: 'P2', files: { 'chain.flow.ts': flowSrc('p2', ` + const warm = async () => {}; + await Promise.all([warm(), warm()]); + await f.run('printf X >> effects');`) } }); + +// ---- prior matrix ---- +add({ name: 'A1', files: { 'chain.flow.ts': flowSrc('a1', ` + const left = async () => { + await Promise.all([f.run('sleep 0.3; printf A'), f.run('printf B')]); + await f.run('printf L >> effects'); + }; + const right = async () => { + await Promise.all([f.run('printf C'), f.run('sleep 0.3; printf D')]); + await f.run('printf R >> effects'); + }; + await Promise.all([left(), right()]);`) } }); +add({ name: 'B1', files: { 'chain.flow.ts': flowSrc('b1', ` + const left = async () => { + await f.run('printf A'); + await f.run('printf B'); + await f.run('printf L >> effects'); + }; + const right = async () => { + await Promise.all([f.run('printf C'), f.run('printf D')]); + await f.run('printf R >> effects'); + }; + await Promise.all([left(), right()]);`) } }); +const N2 = ` + const a = async () => { await f.run('sleep 0.3; printf A'); await f.run('printf a >> effects'); }; + const b = async () => { await f.run('sleep 0.1; printf B'); await f.run('printf b >> effects'); }; + const c = async () => { await f.run('sleep 0.2; printf C'); await f.run('printf c >> effects'); }; + await Promise.all([a(), b(), c()]);`; +add({ name: 'N2', files: { 'chain.flow.ts': flowSrc('n2', N2) } }); +add({ name: 'C1e', files: { 'chain.flow.ts': flowSrc('c1e', ` + const background = (async () => { await f.run('printf X >> effects'); })(); + await new Promise(resolve => setTimeout(resolve, 300)); + await f.run('printf Y >> effects'); + await background;`) } }); +add({ name: 'G1', files: { 'chain.flow.ts': flowSrc('g1', ` + await Promise.race([f.run('printf A >> effects'), f.run('sleep 0.3; printf B >> effects')]); + await f.run('printf G >> effects');`) } }); +const LLM_OUT = `const output = { type: 'object', properties: { message: { type: 'string' } }, required: ['message'] };`; +add({ name: 'H1', adapter: true, extraArgs: ['--local-agent'], files: { 'adapter.mjs': ADAPTER, 'chain.flow.ts': flowSrc('h1', ` + ${LLM_OUT} + const left = async () => { await f.llm('left', { output, model: 'test-model' }); await f.run('printf L >> effects'); }; + const right = async () => { await f.llm('right', { output, model: 'test-model' }); await f.run('printf R >> effects'); }; + await Promise.all([left(), right()]);`) } }); +add({ name: 'H2', adapter: true, extraArgs: ['--local-agent'], files: { 'adapter.mjs': ADAPTER, 'chain.flow.ts': flowSrc('h2', ` + const left = async () => { await f.agent('left', { task: 'left', model: 'test-model' }); await f.run('printf L >> effects'); }; + const right = async () => { await f.agent('right', { task: 'right', model: 'test-model' }); await f.run('printf R >> effects'); }; + await Promise.all([left(), right()]);`) } }); +add({ name: 'K4', adapter: true, extraArgs: ['--local-agent'], files: { 'adapter.mjs': ADAPTER, 'chain.flow.ts': flowSrc('k4', ` + const g = arguments[0]; + ${LLM_OUT} + const left = async () => { await g.llm('left', { output, model: 'test-model' }); await f.run('printf L >> effects'); }; + const right = async () => { await g.llm('right', { output, model: 'test-model' }); await f.run('printf R >> effects'); }; + await Promise.all([left(), right()]);`, { fnExpr: true }) } }); +add({ name: 'I1', files: { + 'child.flow.ts': `${SURF} +export default flow('child', async (f, input) => { + await Promise.all([f.run('printf first'), f.run('printf second')]); + await f.run('printf ' + input.token + ' >> effects'); + f.done('success'); +}); +`, + 'chain.flow.ts': flowSrc('i1', ` + const left = async () => { await f.dispatch('child', { token: 'x' }); await f.run('printf L >> effects'); }; + const right = async () => { await f.dispatch('child', { token: 'y' }); await f.run('printf R >> effects'); }; + await Promise.all([left(), right()]);`, { header: `, { use: ['./child.flow.ts'] }` }) } }); +add({ name: 'K1', files: { 'chain.flow.ts': flowSrc('k1', ` + const pause = () => sleep(300); + const late = async () => { await Promise.all([pause(), pause()]); await f.run('printf L >> effects'); }; + const fast = async () => { await f.run('sleep 0.2; printf A >> effects'); await f.run('sleep 0.2; printf B >> effects'); await f.run('printf C >> effects'); }; + await Promise.all([fast(), late()]);`, { pre: `import { setTimeout as sleep } from 'node:timers/promises';\n` }) } }); +add({ name: 'K2b', files: { 'chain.flow.ts': flowSrc('k2b', ` + const a = async () => { await f.run('sleep 0.3; printf A >> effects'); signal(); }; + const b = async () => { await f.run('sleep 0.1; printf B >> effects'); signal(); }; + const c = async () => { await Promise.all([ready(), ready()]); await f.run('printf C >> effects'); }; + await Promise.all([a(), b(), c()]);`, { pre: `let signal; const first = new Promise(r => { signal = r; });\nconst ready = async () => { await first; };\n` }) } }); +add({ name: 'K3', files: { 'chain.flow.ts': flowSrc('k3', ` + const out = []; + const a = async () => { out.push(await f.run('sleep 0.3; printf a')); }; + const b = async () => { out.push(await f.run('sleep 0.1; printf b')); }; + await Promise.all([a(), b()]); + await f.run('printf ' + out.join('') + ' >> effects');`) }, expect: r => /^(ab|ba)Z$/.test(r.effects) }); +add({ name: 'K5b', files: { 'chain.flow.ts': flowSrc('k5b', ` + const tree = async (d) => { if (d === 0) return; await Promise.all([tree(d - 1), tree(d - 1)]); }; + await f.run('printf A >> effects'); + await tree(12); + try { await f.run('printf B >> effects'); } catch (e) { void e; }`) }, expect: r => r.effects.includes('B') }); +add({ name: 'K13b', files: { 'chain.flow.ts': `${SURF} +export default flow('k13b', async f => { + const b = async () => { await f.run('sleep 0.4; printf B >> effects'); await f.run('printf C >> effects'); }; + const a = async () => { await f.run('sleep 0.1; printf A >> effects'); await f.run('test -f release || { touch stalled; sleep 120; }; printf Z >> effects', { timeout: '5m' }); f.done('success'); }; + await Promise.all([b(), a()]); +}); +` } }); + +export default S; +export { STALL, SURF, flowSrc, ADAPTER, HS, P_BIND, P_HAND, CHAIN, TAIL5, G3, NAT, impl, W2, add }; diff --git a/evidence/hosted-parallel-review1/s676.mjs b/evidence/hosted-parallel-review1/s676.mjs new file mode 100644 index 000000000..a596fe18d --- /dev/null +++ b/evidence/hosted-parallel-review1/s676.mjs @@ -0,0 +1,182 @@ +// Review-676 scenarios for flows#676 (head 64e7f9c2): hosted standalone (Bun parent + embedded Node child). +// Run with h676.mjs. Sides: sah = standalone head, sa52 = shipped 2.0.52 standalone, head = npm/Node head. +import { SURF, flowSrc, NAT, W2 } from './s.mjs'; +const N2 = ` + const a = async () => { await f.run('sleep 0.3; printf A'); await f.run('printf a >> effects'); }; + const b = async () => { await f.run('sleep 0.1; printf B'); await f.run('printf b >> effects'); }; + const c = async () => { await f.run('sleep 0.2; printf C'); await f.run('printf c >> effects'); }; + await Promise.all([a(), b(), c()]);`; +const S3 = process.env.MAC676 ? [['sah', 'sah'], ['nb', 'nb'], ['sa52', 'sa52']] : [['sah', 'sah'], ['head', 'head'], ['sa52', 'sa52']]; +const SA = [['sah', 'sah'], ['sa52', 'sa52']]; +const S = []; +const add = (s) => S.push({ sides: S3, ...s }); +const perm = (letters) => (e) => e.length === letters.length && [...letters].every(c => e.split(c).length - 1 === letters.split(c).length - 1); +const ok5 = r => perm('abcXZ')(r.effects); +const STALLCMD = `await f.run('test -f release || { touch stalled; sleep 120; }; printf Z >> effects', { timeout: '5m' });`; +const NATW = (name, param = 'f') => `${SURF} +export default flow('${name}', async ${param} => { +${W2}${NAT} await f.run('printf X >> effects'); + ${STALLCMD} + f.done('success'); +}); +`; + +// ---- 1. TypeScript-annotated certified shapes: Bun certifies from the TS source, npm compares +// against Node's type-stripped toString (annotations become spaces), so the routes differ. +add({ name: 'TS1param', files: { 'chain.flow.ts': NATW('ts1', '(f: any)') }, expect: ok5 }); +add({ name: 'TS2annot', files: { 'chain.flow.ts': `${SURF} +export default flow('ts2', async f => { + const w: string = await f.run('sleep 0.2; printf W'); +${NAT} await f.run('printf X >> effects'); + ${STALLCMD} + f.done('success'); +}); +` }, expect: ok5 }); +add({ name: 'TS3ret', files: { 'chain.flow.ts': `${SURF} +export default flow('ts3', async f => { +${W2} const a = async (): Promise => { await f.run('sleep 0.4; printf A'); await f.run('printf a >> effects'); }; + const b = async (): Promise => { await f.run('printf B'); await f.run('printf b >> effects'); }; + const c = async (): Promise => { await f.run('sleep 0.2; printf C'); await f.run('printf c >> effects'); }; + await Promise.all([a(), b(), c()]); + await f.run('printf X >> effects'); + ${STALLCMD} + f.done('success'); +}); +` }, expect: ok5 }); + +// ---- 2. Body substitution by the installed Surface (pinned by hash, not by authenticity). +// The certified .flow.ts writes abcXZ; the Surface's flow() registers a different parallel-shaped body +// that writes pqXZ. npm compares the executed body with the source (serial); the Bun parent never looks +// at the body and the Node child checks only its grammar. +const OTHER = `async f => { + await f.run('sleep 0.2; printf W'); + const p = async () => { await f.run('sleep 0.3; printf P'); await f.run('printf p >> effects'); }; + const q = async () => { await f.run('printf Q'); await f.run('printf q >> effects'); }; + await Promise.all([p(), q()]); + await f.run('printf X >> effects'); + ${STALLCMD} + f.done('success'); +}`; +const shimPkg = { name: '@relayflows/surface', version: '2.0.52', private: true, type: 'module', exports: { '.': './index.js', './runtime': './runtime.js' } }; +const shim = (index) => ({ + 'node_modules/@relayflows/surface/package.json': JSON.stringify(shimPkg), + 'node_modules/@relayflows/surface/index.js': index, + 'node_modules/@relayflows/surface/runtime.js': `export { getFlowDefinition } from '/@relayflows/surface/dist/flow.js';\n`, +}); +add({ name: 'SUB1', realNodeModules: true, files: { 'chain.flow.ts': NATW('sub1'), ...shim(`export * from '/@relayflows/surface/dist/index.js'; +import { flow as realFlow } from '/@relayflows/surface/dist/index.js'; +export function flow(name, _body) { return realFlow(name, ${OTHER}); } +`) }, expect: r => perm('pqXZ')(r.effects) }); + +// ---- 3. Child-only import-time patch (contrived): the Surface patches the Array iterator only under Node. +// npm sees it at admission (serial, completes); the Bun parent cannot see it (parallel); the child's +// pre-body intrinsic check then fails the root before any child. +const ITER = `const AP = Array.prototype; const iter = AP[Symbol.iterator]; +const P = Function('return this')()['Pro' + 'mise']; +Object.defineProperty(AP, Symbol.iterator, { value: function values() { + if (this.length === 3 && this.every(x => x instanceof P)) { + const st = new Error().stack; + if (/at (Promise|Function)\\.all \\(\\)/.test(st) && !/collectMembers/.test(st)) return iter.call([this[1]]); + } + return iter.call(this); +}, writable: true, configurable: true }); +`; +add({ name: 'CP1', realNodeModules: true, files: { 'chain.flow.ts': NATW('cp1'), ...shim(`if (!process.versions.bun) {\n${ITER}}\nexport * from '/@relayflows/surface/dist/index.js';\n`) }, expect: ok5 }); + +// ---- 4. TOCTOU: replace the .flow.ts after the Bun parent hashed/admitted it and before the Node child loads it. +// FLOWS_AUTHORED_NODE is a wrapper; its 2nd invocation is runAuthoredInNode's probe (after admission). +const WRAP = `#!/bin/sh +n=$(cat /probes 2>/dev/null || echo 0); n=$((n+1)); echo $n > /probes +if [ "$n" = "\${SWAP_AT:-2}" ] && [ ! -f /swapped ]; then cp /lib/evil.flow.ts /chain.flow.ts; touch /swapped; fi +exec "\${REAL_NODE:-/usr/local/bin/node}" "$@" +`; +const EVIL = `${SURF} +export default flow('toc1', async f => { + const e = async () => { await f.run('printf E >> effects'); }; + const v = async () => { await f.run('printf V >> effects'); }; + await Promise.all([e(), v()]); + ${STALLCMD} + f.done('success'); +}); +`; +S.push({ name: 'TOC1', sides: SA, env: { FLOWS_AUTHORED_NODE: '/lib/node-wrap.sh', REAL_NODE: process.env.REAL_NODE ?? '/usr/local/bin/node' }, + files: { 'chain.flow.ts': NATW('toc1'), 'lib/evil.flow.ts': EVIL, 'lib/node-wrap.sh': WRAP }, + okIf: r => r.fresh && r.code !== 0 && !/[EV]/.test(r.effects) }); + +// ---- 5. Symlinked entry: chain.flow.ts -> impl.ts (target not *.flow.ts) and -> real.flow.ts. +add({ name: 'SYM1', symlinks: { 'chain.flow.ts': 'impl.ts' }, files: { 'impl.ts': NATW('sym1') }, expect: ok5 }); +add({ name: 'SYM2', symlinks: { 'chain.flow.ts': 'real.flow.ts' }, files: { 'real.flow.ts': NATW('sym2') }, expect: ok5 }); + +// ---- 6. Forged result claims. The Surface wraps JSON.stringify in the Node child only, so the +// authenticated IPC result frame carries a mutated journalSteps claim. The durable journal is untouched. +const FORGES = { + none: 'void s;', + reorder: 'const t = s.pop(); s.reverse(); s.push(t);', + drop: 's.splice(0, 1);', + swap: 'const r0 = s[0].runId; s[0] = { ...s[0], runId: s[1].runId }; s[1] = { ...s[1], runId: r0 };', + fakeid: "s[1] = { ...s[1], id: 'run-after-' + 'a'.repeat(64) + '-1' };", + reason: "s[1] = { ...s[1], completionReason: 'timeout' };", + dupe: 's[1] = { ...s[0] };', + serialids: "for (let i = 0; i < s.length - 1; i++) s[i] = { ...s[i], id: 'run-' + (i + 1) };", + terminal: "m.result.completionReason = 'failure';", + gate: "s.splice(s.length - 1, 0, { id: s[0].id + '.gate', runId: s[0].runId, completionReason: 'success' });", + edges: "for (let i = 0; i < s.length - 1; i++) s[i] = { ...s[i], after: ['forged-step'] };", +}; +for (const [n, mutate] of Object.entries(FORGES)) { + S.push({ name: `FG_${n}`, sides: SA, realNodeModules: true, files: { 'release': '', 'chain.flow.ts': NATW(`fg-${n}`), ...shim(`if (!process.versions.bun) { + const stringify = JSON.stringify; + JSON.stringify = function (v, ...rest) { + if (v && v.type === 'result' && Array.isArray(v.result?.journalSteps)) { + const m = structuredClone(v); const s = m.result.journalSteps; ${mutate} + require_log(m); return stringify.call(this, m, ...rest); + } + return stringify.call(this, v, ...rest); + }; + const { appendFileSync } = await import('node:fs'); + function require_log(m) { appendFileSync('/forged', stringify(m.result.journalSteps.map(x => x.id + '|' + x.runId.slice(-6) + '|' + x.completionReason)) + '\\n'); } +} +export * from '/@relayflows/surface/dist/index.js'; +`) }, okIf: n === 'none' || n === 'reorder' || n === 'edges' ? (r => r.code === 0 && ok5(r)) : (r => r.code !== 0 && ok5(r)) }); +} + +// ---- 7. Kill windows inside the parallel group (not only at the stall marker). +add({ name: 'KM1', files: { 'chain.flow.ts': `${SURF} +export default flow('km1', async f => { + const a = async () => { await f.run('sleep 0.3; printf A'); await f.run('sleep 0.1; printf a >> effects'); }; + const b = async () => { await f.run('test -f release || touch stalled; sleep 0.6; printf B'); await f.run('printf b >> effects'); }; + const c = async () => { await f.run('sleep 0.2; printf C'); await f.run('sleep 0.2; printf c >> effects'); }; + await Promise.all([a(), b(), c()]); + await f.run('printf X >> effects'); + await f.run('printf Z >> effects'); + f.done('success'); +}); +` }, expect: ok5 }); +const POLLSTALL = `await f.run('test -f release || { touch stalled; i=0; while [ ! -f release ] && [ $i -lt 100 ]; do sleep 0.1; i=$((i+1)); done; }; printf Z >> effects', { timeout: '5m' });`; +add({ name: 'KD1', keepDaemon: true, files: { 'chain.flow.ts': NATW('kd1').replace(STALLCMD, POLLSTALL) }, expect: ok5 }); +add({ name: 'KD2', keepDaemon: true, files: { 'chain.flow.ts': `${SURF} +export default flow('kd2', async f => { + const a = async () => { await f.run('sleep 0.3; printf A'); await f.run('sleep 0.1; printf a >> effects'); }; + const b = async () => { await f.run('test -f release || touch stalled; sleep 0.6; printf B'); await f.run('printf b >> effects'); }; + const c = async () => { await f.run('sleep 0.2; printf C'); await f.run('sleep 0.2; printf c >> effects'); }; + await Promise.all([a(), b(), c()]); + await f.run('printf X >> effects'); + await f.run('printf Z >> effects'); + f.done('success'); +}); +` }, expect: ok5 }); + +// ---- 8. Upgrade (2.0.52 standalone root resumed by head standalone) and rollback (head root resumed by 2.0.52). +for (const [n, src, ok] of [['NATW', NATW('upn'), ok5], ['N2', flowSrc('upn2', N2), r => perm('abcZ')(r.effects)]]) { + S.push({ name: `UP_${n}`, sides: [['sa52', 'sah']], files: { 'chain.flow.ts': src }, expect: ok }); + S.push({ name: `RB_${n}`, sides: [['sah', 'sa52']], secondResume: 'sah', files: { 'chain.flow.ts': src }, + okIf: r => ok(r) && r.code === 0 }); +} + +// ---- 9. Embedded runner on Node 22.16.0 (the evidence's version) instead of PATH node. +S.push({ name: 'N2216', sides: [['sah', 'sah']], env: { FLOWS_AUTHORED_NODE: '/w/node2216/bin/node' }, files: { 'chain.flow.ts': NATW('n2216') }, expect: ok5 }); + +// ---- 10. Revert-proof fixture for the child-side module recheck: a module-level Promise binding +// (never certified) admitted parallel only by a forced Bun parent (mutants sam3 / sam4). +S.push({ name: 'FP1', sides: [['sam3', 'sam3'], ['sam4', 'sam4'], ['sah', 'sah']], files: { 'chain.flow.ts': NATW('fp1').replace(`${SURF}\n`, `${SURF}\nconst Promise = { all: globalThis.Promise.all.bind(globalThis.Promise) };\n`) }, expect: ok5 }); + +export default S; diff --git a/packages/sdk/src/authored-node-entry.ts b/packages/sdk/src/authored-node-entry.ts index 29ac55a99..b38936f25 100644 --- a/packages/sdk/src/authored-node-entry.ts +++ b/packages/sdk/src/authored-node-entry.ts @@ -2,6 +2,7 @@ import { FLOW_READ_BUDGET_MS, isReadInterruptionError } from './journal-read-pol import { Worker } from 'node:worker_threads'; import { createHash, createHmac } from 'node:crypto'; import { readFileSync, writeSync } from 'node:fs'; +import { pathToFileURL } from 'node:url'; import { JournalClient } from './journal-client.js'; import { executeAuthoredFlow } from './authored-flow-executor.js'; import { loadPinnedAuthoredSource } from './authored-source-authority.js'; @@ -81,9 +82,9 @@ try { if (createHash('sha256').update(bytes).digest('hex') !== request.metadata.sourceSha256) { throw new Error('authored root source authority mismatch'); } - const { parallelPinnedSourceEligible, parallelBodySyntaxEligible } = await import('./authored-parallel-eligibility.js'); - if (!parallelPinnedSourceEligible(bytes.toString('utf8')) - || !parallelBodySyntaxEligible(loaded.getDefinition(loaded.handle).body)) { + const { parallelLoadedBodyEligible } = await import('./authored-parallel-eligibility.js'); + if (!await parallelLoadedBodyEligible(loaded.getDefinition(loaded.handle).body, + bytes.toString('utf8'), pathToFileURL(request.metadata.flowPath).href)) { throw new AuthoredFlowExecutionError('step_failed', 'parallel f.run body lost its source certificate'); } } diff --git a/packages/sdk/src/authored-node-runner.ts b/packages/sdk/src/authored-node-runner.ts index 94d2c5c7e..957f7f6cb 100644 --- a/packages/sdk/src/authored-node-runner.ts +++ b/packages/sdk/src/authored-node-runner.ts @@ -374,7 +374,11 @@ export async function verifyAuthoredNodeResult( if (indexed.length !== count || indexed.some(step => { const matching = claimed.get(step.step); return matching === undefined || step.state !== 'completed' - || step.runId !== matching.runId || step.completionReason !== matching.completionReason; + || step.runId !== matching.runId || step.completionReason !== matching.completionReason + || step.label !== matching.label || step.afterTruncated !== matching.afterTruncated + || (step.after === undefined ? matching.after !== undefined + : !Array.isArray(step.after) || !Array.isArray(matching.after) || step.after.length !== matching.after.length + || step.after.some((id, index) => id !== matching.after?.[index])); })) invalid('parallel index'); } for (const [index, claimed] of [...ordered, ...gates].entries()) { diff --git a/packages/sdk/src/authored-parallel-eligibility.ts b/packages/sdk/src/authored-parallel-eligibility.ts index e9b0fa843..58ac7186b 100644 --- a/packages/sdk/src/authored-parallel-eligibility.ts +++ b/packages/sdk/src/authored-parallel-eligibility.ts @@ -17,6 +17,36 @@ export function parallelRunEligible(body: Function, moduleSource: string): boole return certifiedModuleBody(moduleSource, bodySource) !== undefined && parallelBodySyntaxEligible(body); } +/** + * Match the body Node actually loaded against the same transformed source its + * TypeScript loader serves. First certify the original bytes: transforming + * away a declaration must never make an unsafe module appear closed. + */ +export async function parallelLoadedBodyEligible( + body: Function, moduleSource: string, sourceUrl: string, +): Promise { + if (!parallelPinnedSourceEligible(moduleSource)) return false; + const { stripTypeScriptTypes } = await import('node:module') as unknown as { + stripTypeScriptTypes?: (source: string, options: { mode: 'transform'; sourceUrl: string }) => string; + }; + if (typeof stripTypeScriptTypes !== 'function') return false; + // The pinned loader suppresses this one Node warning on its own thread. + // Recomputing the text here happens on the main thread; suppress only this + // call's warning and restore the original writer before yielding again. + const emitWarning = process.emitWarning; + let transformed: string; + try { + process.emitWarning = ((warning: string | Error, ...rest: unknown[]) => { + if (rest[0] === 'ExperimentalWarning' && String(warning).startsWith('stripTypeScriptTypes')) return; + (emitWarning as (...args: unknown[]) => void).call(process, warning, ...rest); + }) as typeof process.emitWarning; + transformed = stripTypeScriptTypes(moduleSource, { mode: 'transform', sourceUrl }); + } finally { + process.emitWarning = emitWarning; + } + return parallelRunEligible(body, transformed); +} + /** * The compiled Bun CLI owns the root lease, but the pinned source is executed * by its embedded Node child. Bun reformats function toString output, so its diff --git a/packages/sdk/src/authored-root.ts b/packages/sdk/src/authored-root.ts index b716b2b6d..c8032300a 100644 --- a/packages/sdk/src/authored-root.ts +++ b/packages/sdk/src/authored-root.ts @@ -3,10 +3,11 @@ import { loadPinnedAuthoredSource } from './authored-source-authority.js'; import { assertAuthoredRuntimeAvailable, runAuthoredInNode } from './authored-node-runner.js'; import { createHash, randomUUID } from 'node:crypto'; import { readFile } from 'node:fs/promises'; +import { pathToFileURL } from 'node:url'; import { canonicalize } from './canonical.js'; import { compileSpec, toKernelSpec } from './compile.js'; import { executeAuthoredFlow, type AuthoredFlowExecutionResult, type AuthoredFlowSuspendedResult } from './authored-flow-executor.js'; -import { AUTHORED_PARALLEL_ROOT_KIND, AUTHORED_ROOT_KIND } from './authored-verdict.js'; +import { AUTHORED_PARALLEL_ROOT_KIND, AUTHORED_PARALLEL_ROOT_KIND_V3, AUTHORED_ROOT_KIND } from './authored-verdict.js'; import { loadAuthoredFlow, type LoadedAuthoredFlow, @@ -43,7 +44,7 @@ export interface AuthoredRootExtension { } export interface AuthoredRootMetadata { - readonly kind: typeof ROOT_KIND | typeof AUTHORED_PARALLEL_ROOT_KIND; + readonly kind: typeof ROOT_KIND | typeof AUTHORED_PARALLEL_ROOT_KIND | typeof AUTHORED_PARALLEL_ROOT_KIND_V3; readonly flowName: string; readonly flowPath: string; readonly sourceSha256: string; @@ -104,10 +105,10 @@ export async function executeDurableAuthoredFlow( // The parser is needed only for an authored run, never for an ordinary // CLI check or watch reload. Recheck intrinsics after the async import; // author code could have replaced them while it was loading. - const { parallelRunEligible, parallelPinnedSourceEligible } = await import('./authored-parallel-eligibility.js'); + const { parallelLoadedBodyEligible, parallelPinnedSourceEligible } = await import('./authored-parallel-eligibility.js'); parallelEligible = parallelPromiseIntrinsicsIntact() && (process.versions['bun'] === undefined - ? parallelRunEligible(definition.body, source.toString('utf8')) + ? await parallelLoadedBodyEligible(definition.body, source.toString('utf8'), pathToFileURL(loaded.sourcePath).href) : parallelPinnedSourceEligible(source.toString('utf8'))); if (!parallelEligible) parallelIneligibleReason = parallelPromiseIntrinsicsIntact() ? 'source_certificate' : 'runtime_intrinsics'; @@ -119,7 +120,7 @@ export async function executeDurableAuthoredFlow( } const executionMode = parallelEligible ? 'parallel' : 'serial'; const metadata: AuthoredRootMetadata = Object.freeze({ - kind: executionMode === 'parallel' ? AUTHORED_PARALLEL_ROOT_KIND : ROOT_KIND, + kind: executionMode === 'parallel' ? AUTHORED_PARALLEL_ROOT_KIND_V3 : ROOT_KIND, flowName: definition.name, flowPath: loaded.sourcePath, sourceSha256: sha256(source), @@ -661,8 +662,9 @@ function isRootMetadata(value: unknown): value is AuthoredRootMetadata { const root = value as Partial; const surface = root.surface as Partial | undefined; const sources = root.sources; - return (root.kind === ROOT_KIND || root.kind === AUTHORED_PARALLEL_ROOT_KIND) - && (root.kind !== AUTHORED_PARALLEL_ROOT_KIND + return (root.kind === ROOT_KIND || root.kind === AUTHORED_PARALLEL_ROOT_KIND + || root.kind === AUTHORED_PARALLEL_ROOT_KIND_V3) + && (root.kind === ROOT_KIND || (root.identityVersion === 2 && root.executionMode === 'parallel')) && typeof root.flowName === 'string' && typeof root.flowPath === 'string' && /^[a-f0-9]{64}$/.test(root.sourceSha256 ?? '') diff --git a/packages/sdk/src/authored-verdict.ts b/packages/sdk/src/authored-verdict.ts index 0a0731a83..55c40ed21 100644 --- a/packages/sdk/src/authored-verdict.ts +++ b/packages/sdk/src/authored-verdict.ts @@ -21,8 +21,10 @@ import type { JournalEvent } from './journal-reader.js'; /** The `instruction` discriminator `authored-root.ts` writes into the root step. */ export const AUTHORED_ROOT_KIND = 'relayflows.authored-root.v1'; -/** Older CLIs do not recognize this root and must refuse a parallel rollback. */ +/** Parallel roots admitted before 2.0.53; the 2.0.52 CLI recognizes these. */ export const AUTHORED_PARALLEL_ROOT_KIND = 'relayflows.authored-root.parallel.v2'; +/** Newly admitted parallel roots must be refused by the 2.0.52 CLI on rollback. */ +export const AUTHORED_PARALLEL_ROOT_KIND_V3 = 'relayflows.authored-root.parallel.v3'; export interface AuthoredVerdict { readonly reason: LoweredCompletionReason; @@ -79,7 +81,8 @@ export function declaresAuthoredRoot(spawned: JournalEvent): boolean { if (typeof step['instruction'] !== 'string') return false; try { const kind = record(JSON.parse(step['instruction']))?.['kind']; - return kind === AUTHORED_ROOT_KIND || kind === AUTHORED_PARALLEL_ROOT_KIND; + return kind === AUTHORED_ROOT_KIND || kind === AUTHORED_PARALLEL_ROOT_KIND + || kind === AUTHORED_PARALLEL_ROOT_KIND_V3; } catch { return false; } diff --git a/packages/sdk/tests/authored-node-result.test.ts b/packages/sdk/tests/authored-node-result.test.ts index 7d5f0030f..a697b64bb 100644 --- a/packages/sdk/tests/authored-node-result.test.ts +++ b/packages/sdk/tests/authored-node-result.test.ts @@ -48,7 +48,7 @@ describe('authored IPC result durable verification',()=>{ journalSteps:[ {id:'run-1',runId:'child-1',completionReason:'success'}, {id:'run-2',runId:'child-2',completionReason:'success'}, - {id:dependent,runId:'child-3',completionReason:'success'}, + {id:dependent,runId:'child-3',completionReason:'success',after:['run-1'],afterTruncated:true,label:'dependent'}, {id:'complete-4',runId:'child-4',completionReason:'success'}, ] }; const parallelMetadata = { ...metadata, identityVersion:2, executionMode:'parallel' } as AuthoredRootMetadata; @@ -59,8 +59,19 @@ describe('authored IPC result durable verification',()=>{ mocks.runGet.mockImplementation(async (run_id:string)=>({run_id,status:'completed', steps:{[runIds.get(run_id)!]:{state:'done'}}})); streamRecords = claimed.journalSteps.map(step => ({index:'relayflows.authored-step.v1', - step:step.id,runId:step.runId,state:'completed',completionReason:'success'})); + step:step.id,runId:step.runId,state:'completed',completionReason:'success', + ...(step.after === undefined ? {} : {after:step.after}), + ...(step.afterTruncated === undefined ? {} : {afterTruncated:step.afterTruncated}), + ...(step.label === undefined ? {} : {label:step.label})})); await expect(verifyAuthoredNodeResult(claimed,parallelMetadata,'root','socket')).resolves.toBeUndefined(); + for (const forged of [ + { after:['forged-step'] }, { afterTruncated:undefined }, { label:'forged-label' }, + ]) { + const altered = { ...claimed, journalSteps:claimed.journalSteps.map((step,index) => + index === 2 ? { ...step, ...forged } : step) }; + await expect(verifyAuthoredNodeResult(altered,parallelMetadata,'root','socket')) + .rejects.toThrow('no matching durable completion'); + } streamRecords[1] = {...streamRecords[1],runId:'unclaimed-child'}; await expect(verifyAuthoredNodeResult(claimed,parallelMetadata,'root','socket')) .rejects.toThrow('no matching durable completion'); diff --git a/packages/sdk/tests/authored-node-runtime.test.ts b/packages/sdk/tests/authored-node-runtime.test.ts index 9baf3e0a4..9c7a6bba2 100644 --- a/packages/sdk/tests/authored-node-runtime.test.ts +++ b/packages/sdk/tests/authored-node-runtime.test.ts @@ -122,6 +122,70 @@ export default flow('runtime-case', async f => { expect(existsSync(join(f.directory, 'joined'))).toBe(true); }, 120_000); + it.each([ + ['TS1param', 'async (f: any)', ''], + ['TS2annot', 'async f', `const w: string = await f.run('printf W >> effects');`], + ['TS3ret', 'async f', ''], + ])('admits %s as parallel in both standalone and Node CLIs', async (_name, signature, prelude) => { + const branchReturn = _name === 'TS3ret' ? ': Promise' : ''; + const source = `import { flow } from '@relayflows/surface'; +export default flow('typed-parallel', ${signature} => { + ${prelude} + const a = async ()${branchReturn} => { await f.run('sleep 0.2; printf A >> effects'); }; + const b = async ()${branchReturn} => { await f.run('sleep 0.1; printf B >> effects'); }; + const c = async ()${branchReturn} => { await f.run('sleep 0.15; printf C >> effects'); }; + await Promise.all([a(), b(), c()]); + await f.run('printf X >> effects'); + f.done('success'); +}); +`; + for (const side of ['standalone', 'node'] as const) { + const f = fixture(''); + writeFileSync(join(f.directory, 'case.flow.ts'), source); + const run = side === 'standalone' ? f.run() : spawnSync(process.execPath, + ['--experimental-transform-types', resolve('dist/cli.js'), 'run', 'case.flow.ts', '--input', '{}', ...f.flags], + { cwd: f.directory, env: f.env, encoding: 'utf8', timeout: 60_000 }); + expect(run.status, `${side}: ${run.stderr + run.stdout}`).toBe(0); + const report = JSON.parse(run.stdout) as { runId: string }; + const reader = new JournalClient(socketFor(join(f.directory, 'data'))); + await reader.connect(); await reader.hello('typed-parallel-test'); + try { + expect(await readAuthoredRootMetadata(reader, report.runId)) + .toMatchObject({ kind: 'relayflows.authored-root.parallel.v3', executionMode: 'parallel' }); + } finally { reader.close(); } + expect([...readFileSync(join(f.directory, 'effects'), 'utf8')].sort()) + .toEqual([...(_name === 'TS2annot' ? 'WABCX' : 'ABCX')].sort()); + } + }, 120_000); + + it('refuses a Surface-substituted body before standalone parallel effects', async () => { + const f = fixture(''); + writeFileSync(join(f.directory, 'case.flow.ts'), `import { flow } from '@relayflows/surface'; +export default flow('substituted', async f => { + const a = async () => { await f.run('printf A >> effects'); }; + const b = async () => { await f.run('printf B >> effects'); }; + await Promise.all([a(), b()]); + f.done('success'); +}); +`); + writeFileSync(join(f.directory, 'node_modules/@relayflows/surface/index.js'), ` +export * from './dist/index.js'; +import { flow as realFlow } from './dist/index.js'; +export function flow(name, _body) { + return realFlow(name, async f => { + const p = async () => { await f.run('printf p >> effects'); }; + const q = async () => { await f.run('printf q >> effects'); }; + await Promise.all([p(), q()]); + f.done('success'); + }); +} +`); + const run = f.run(); + expect(run.status, run.stderr + run.stdout).not.toBe(0); + expect(run.stderr + run.stdout).toContain('parallel f.run body lost its source certificate'); + expect(existsSync(join(f.directory, 'effects'))).toBe(false); + }, 120_000); + it('serializes the immutable prepared binding facts through the Node and CLI boundary', () => { const f = fixture(`const activity=f.on(webhook('github_pull_request',{action:'opened',repository:{id:7}}),{settle:'2m',idle:'1h',deadline:'1d',includeSelf:true});await activity.next();f.done('success');`); const result = f.run(); expect(result.status, result.stderr + result.stdout).toBe(4); diff --git a/packages/sdk/tests/authored-parallel-eligibility.test.ts b/packages/sdk/tests/authored-parallel-eligibility.test.ts index 770362243..9267a598c 100644 --- a/packages/sdk/tests/authored-parallel-eligibility.test.ts +++ b/packages/sdk/tests/authored-parallel-eligibility.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { parallelPinnedSourceEligible, parallelRunEligible } from '../src/authored-parallel-eligibility.js'; +import { stripTypeScriptTypes } from 'node:module'; +import { parallelLoadedBodyEligible, parallelPinnedSourceEligible, parallelRunEligible } from '../src/authored-parallel-eligibility.js'; const body = (source: string): Function => new Function(`return (${source});`)() as Function; const eligible = (candidate: Function, moduleSource = `import { flow } from '@relayflows/surface';\nexport default flow('probe', ${Function.prototype.toString.call(candidate)});`) => @@ -29,6 +30,27 @@ export default flow('hosted', async f => { expect(parallelPinnedSourceEligible(source.replace('Promise.all', 'Promise.race'))).toBe(false); }); + it('matches the Node-loaded body to transformed source, including TypeScript annotations', async () => { + const bodies = [ + `async (f: any) => { await Promise.all([f.run('A'), f.run('B')]); f.done('success'); }`, + `async f => { const w: string = await f.run('W'); + await Promise.all([f.run(w), f.run('B')]); f.done('success'); }`, + `async f => { const a = async (): Promise => { await f.run('A'); }; + const b = async (): Promise => { await f.run('B'); }; + await Promise.all([a(), b()]); f.done('success'); }`, + ]; + for (const candidate of bodies) { + const source = `import { flow } from '@relayflows/surface';\nexport default flow('typed', ${candidate});`; + const transformedBody = stripTypeScriptTypes(candidate, { mode: 'transform', sourceUrl: 'file:///typed.flow.ts' }); + const loaded = new Function(`return ${transformedBody}`)() as Function; + expect(parallelPinnedSourceEligible(source), candidate).toBe(true); + expect(await parallelLoadedBodyEligible(loaded, source, 'file:///typed.flow.ts'), candidate).toBe(true); + const substitute = body(`async f => { + await Promise.all([f.run('other-a'), f.run('other-b')]); f.done('success'); }`); + expect(await parallelLoadedBodyEligible(substitute, source, 'file:///typed.flow.ts'), candidate).toBe(false); + } + }); + it('certifies direct Promise.all and awaited branch continuations', () => { expect(eligible(body(`async f => { const outputs = await Promise.all([f.run('a'), f.run('b')]); diff --git a/packages/sdk/tests/authored-parallel-run-live.test.ts b/packages/sdk/tests/authored-parallel-run-live.test.ts index 8cc592a25..52322267c 100644 --- a/packages/sdk/tests/authored-parallel-run-live.test.ts +++ b/packages/sdk/tests/authored-parallel-run-live.test.ts @@ -540,7 +540,7 @@ export default flow('legacy-sequential-resume', async f => { ); it.skipIf(process.env.RELAYFLOWS_PREVIOUS_CLI === undefined)( - 'refuses a parallel v2 root under 2.0.50 and lets the new CLI resume it', async () => { + 'refuses a parallel v3 root under an older CLI and lets the new CLI resume it', async () => { const fixture = chainFixture(); closes.push(() => fixture.close()); writeFileSync(fixture.flowPath, `import { flow } from '@relayflows/surface'; @@ -562,7 +562,7 @@ export default flow('rollback-parallel-root', async f => { await reader.hello('rollback-root-kind-test'); try { expect(await readAuthoredRootMetadata(reader, rootRunId)) - .toMatchObject({ kind: 'relayflows.authored-root.parallel.v2', executionMode: 'parallel' }); + .toMatchObject({ kind: 'relayflows.authored-root.parallel.v3', executionMode: 'parallel' }); } finally { reader.close(); } cli.kill('SIGKILL'); await cliExited; diff --git a/packages/sdk/tests/authored-root.test.ts b/packages/sdk/tests/authored-root.test.ts index 6fd0dbfc1..6a81a4cb7 100644 --- a/packages/sdk/tests/authored-root.test.ts +++ b/packages/sdk/tests/authored-root.test.ts @@ -2,6 +2,9 @@ import { EventEmitter } from 'node:events'; import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; +import { stripTypeScriptTypes } from 'node:module'; +import { pathToFileURL } from 'node:url'; +import ts from 'typescript'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { flow, type Ctx } from '@relayflows/surface'; import { getFlowDefinition } from '@relayflows/surface/runtime'; @@ -205,13 +208,18 @@ describe('durable authored root', () => { }); it('pins a certified Promise.all run body to parallel mode before dispatch', async () => { - const body = async (f: Ctx) => { + const bodySource = `async f => { await Promise.all([f.run('printf a'), f.run('printf b')]); f.done('success'); - }; - const loaded = await fixture(false, 0, body); - await writeFile(loaded.sourcePath, - `import { flow } from '@relayflows/surface';\nexport default flow('flagship', ${Function.prototype.toString.call(body)});\n`); + }`; + const source = `import { flow } from '@relayflows/surface';\nexport default flow('flagship', ${bodySource});\n`; + const transformed = stripTypeScriptTypes(source, { mode: 'transform', sourceUrl: 'file:///flagship.flow.ts' }); + const file = ts.createSourceFile('flagship.js', transformed, ts.ScriptTarget.ES2022, true, ts.ScriptKind.JS); + const exported = file.statements[1] as ts.ExportAssignment; + const call = exported.expression as ts.CallExpression; + const loadedBody = new Function(`return ${call.arguments[1]!.getText(file)};`)() as (f: Ctx) => Promise; + const loaded = await fixture(false, 0, loadedBody); + await writeFile(loaded.sourcePath, source); const journal = new RootJournal(); journal.startStatus = outcome(journal.runId, 'completed', 'success'); journal.entries = [completedEntry()]; @@ -219,7 +227,7 @@ describe('durable authored root', () => { { dataDir: '/unused', admissionKey: 'certified-group' }); const step = (journal.starts[0]!.spec as { steps: Array<{ instruction: string }> }).steps[0]!; expect(JSON.parse(step.instruction)).toMatchObject({ - kind: 'relayflows.authored-root.parallel.v2', identityVersion: 2, executionMode: 'parallel', + kind: 'relayflows.authored-root.parallel.v3', identityVersion: 2, executionMode: 'parallel', }); }); diff --git a/packages/sdk/tests/authored-status-detail.test.ts b/packages/sdk/tests/authored-status-detail.test.ts index bc749f374..0d5dd3549 100644 --- a/packages/sdk/tests/authored-status-detail.test.ts +++ b/packages/sdk/tests/authored-status-detail.test.ts @@ -9,7 +9,7 @@ import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; -import { authoredVerdictOf, AUTHORED_PARALLEL_ROOT_KIND, AUTHORED_ROOT_KIND } from '../src/authored-verdict.js'; +import { authoredVerdictOf, AUTHORED_PARALLEL_ROOT_KIND, AUTHORED_PARALLEL_ROOT_KIND_V3, AUTHORED_ROOT_KIND } from '../src/authored-verdict.js'; import { canonicalize } from '../src/canonical.js'; import { parseStatusArgs, runStatus, type StatusOptions } from '../src/cli/status.js'; import type { JournalEvent } from '../src/journal-client.js'; @@ -89,6 +89,8 @@ describe('projecting the authored verdict out of a root journal', () => { .toEqual({ reason: 'step_failed', detail: FINDING }); expect(authoredVerdictOf(journal({ kind: AUTHORED_PARALLEL_ROOT_KIND, detail: FINDING }))) .toEqual({ reason: 'step_failed', detail: FINDING }); + expect(authoredVerdictOf(journal({ kind: AUTHORED_PARALLEL_ROOT_KIND_V3, detail: FINDING }))) + .toEqual({ reason: 'step_failed', detail: FINDING }); }); it('reads a verdict with no detail, and reports no detail key', () => { From b1e437573275d4a95574ff436e1adfdf487f767d Mon Sep 17 00:00:00 2001 From: Miya Date: Sun, 18 Oct 2026 20:14:21 +0200 Subject: [PATCH 3/3] fix: certify both native Node TypeScript loader modes --- .../desktop-report.txt | 581 ++++++++++++++++++ .../focused-reverts.py | 81 +++ .../hosted-parallel-review1/focused-types.py | 28 + .../logs/desktop-baseline.txt | 52 ++ .../logs/desktop-disk.txt | 4 + .../logs/desktop-focused.txt | 26 + .../logs/desktop-typecheck.txt | 6 + .../logs/f1-body-identity-restored.txt | 18 + .../logs/f1-body-identity-reverted.txt | 54 ++ .../logs/f3-type-identity-restored.txt | 19 + .../logs/f3-type-identity-reverted.txt | 90 +++ .../logs/f4-root-marker-restored.txt | 19 + .../logs/f4-root-marker-reverted.txt | 86 +++ .../logs/f5-step-metadata-restored.txt | 16 + .../logs/f5-step-metadata-reverted.txt | 71 +++ .../logs/wip-strip-mode-restored.txt | 19 + .../logs/wip-strip-mode-reverted.txt | 72 +++ packages/sdk/src/authored-node-entry.ts | 2 +- .../sdk/src/authored-parallel-eligibility.ts | 12 +- packages/sdk/src/authored-root.ts | 5 +- .../sdk/tests/authored-node-admission.test.ts | 39 ++ .../sdk/tests/authored-node-result.test.ts | 25 +- .../authored-parallel-eligibility.test.ts | 8 +- .../tests/authored-rollback-readback.test.ts | 39 ++ packages/sdk/tests/authored-root.test.ts | 26 +- 25 files changed, 1373 insertions(+), 25 deletions(-) create mode 100644 evidence/hosted-parallel-review1/desktop-report.txt create mode 100644 evidence/hosted-parallel-review1/focused-reverts.py create mode 100644 evidence/hosted-parallel-review1/focused-types.py create mode 100644 evidence/hosted-parallel-review1/logs/desktop-baseline.txt create mode 100644 evidence/hosted-parallel-review1/logs/desktop-disk.txt create mode 100644 evidence/hosted-parallel-review1/logs/desktop-focused.txt create mode 100644 evidence/hosted-parallel-review1/logs/desktop-typecheck.txt create mode 100644 evidence/hosted-parallel-review1/logs/f1-body-identity-restored.txt create mode 100644 evidence/hosted-parallel-review1/logs/f1-body-identity-reverted.txt create mode 100644 evidence/hosted-parallel-review1/logs/f3-type-identity-restored.txt create mode 100644 evidence/hosted-parallel-review1/logs/f3-type-identity-reverted.txt create mode 100644 evidence/hosted-parallel-review1/logs/f4-root-marker-restored.txt create mode 100644 evidence/hosted-parallel-review1/logs/f4-root-marker-reverted.txt create mode 100644 evidence/hosted-parallel-review1/logs/f5-step-metadata-restored.txt create mode 100644 evidence/hosted-parallel-review1/logs/f5-step-metadata-reverted.txt create mode 100644 evidence/hosted-parallel-review1/logs/wip-strip-mode-restored.txt create mode 100644 evidence/hosted-parallel-review1/logs/wip-strip-mode-reverted.txt create mode 100644 packages/sdk/tests/authored-node-admission.test.ts create mode 100644 packages/sdk/tests/authored-rollback-readback.test.ts diff --git a/evidence/hosted-parallel-review1/desktop-report.txt b/evidence/hosted-parallel-review1/desktop-report.txt new file mode 100644 index 000000000..94665f78b --- /dev/null +++ b/evidence/hosted-parallel-review1/desktop-report.txt @@ -0,0 +1,581 @@ +flows#676 desktop continuation from 2d4ca4255a260a3fbab45575e5a5b533554af567 + +F1/F3/F4/F5 code and focused verification are ready for review. Live verification +remains unfinished: the internal disk is below the required 10 GB build floor. +The prior WIP is preserved in ancestry. + +The WIP already added the child body/source identity check, parallel.v3 root kind, +and comparisons for after, afterTruncated and label. This continuation fixes npm +admission for Node's default strip loader as well as its opt-in transform loader. +Both paths retain exact body/source identity and the original closed source grammar. +The embedded child still accepts only the transform output its pinned loader serves. +An unavailable stripping mode cannot certify a body. + +F2 remains accepted as out of scope per ms-lead's review decision: a tampered, +hash-pinned Surface that patches intrinsics only in Node can fail closed before +effects. No F2 code change is included. + +Focused result: 142 tests passed. Per-mutant failing tests on revert (counts overlap): + F1 exact body identity in shared certifier: 2 + F3 type-stripped identity at npm admission: 7 + F4 new-root v3 marker emission: 8 + F5 durable edge/truncation/label comparison: 7 + Additional regression: reverting this continuation's strip-mode admission: 4 +Each mutant's source diff, complete failing output, byte-for-byte restoration hash, +and complete passing rerun follow below. These are focused mutations, not live +standalone revert proofs. In particular F1 does not exercise the child IPC entry +with a substituted Surface; that remains in the blocked SUB1 live fixture. + +The cached published 2.0.52 SDK readback test accepts v2 metadata as its positive +control and refuses v3 with journalRead as its only operation. This checks the old +resume function with a mock journal; it is not the RB_NATW/RB_N2 crash/resume run. +The native importer tests exercise TS1param/TS2annot/TS3ret with and without Node's +transform-types flag; the standalone execution matrix still needs the live gate. + +Focused type checking covers SDK source and all touched test files, including the +standalone tests, with noEmit. Small type-only repairs in existing test fixtures +allow those previously excluded tests to be checked without weakening assertions. + +Outstanding: once disk exceeds the floor, build the npm and standalone artifacts, +run the macOS review fixtures (including SUB1, TS1param/TS2annot/TS3ret, FG_edges, +RB_NATW/RB_N2), capture live revert evidence, and obtain independent review/CI. +No build, install or heavy/live tests were run by this desktop continuation. +PATH Node 26 lacks transform mode; the initial WIP baseline failure is preserved in +logs/desktop-baseline.txt. Verification below uses cached Node 22.22.2. + +Reproduce focused mutations from repo root: +REVIEW_NODE=/Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node FLOWS_2052_SDK=/tmp/ms-boot-2052-npm/node_modules/@relayflows/sdk python3 evidence/hosted-parallel-review1/focused-reverts.py + +Reproduce type checking from repo root: +REVIEW_NODE=/Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node python3 evidence/hosted-parallel-review1/focused-types.py + +The optional rollback test requires an existing unmodified @relayflows/sdk@2.0.52 +installation; FLOWS_2052_SDK names its package directory. No install is performed. + +Captured commands and complete outputs + +evidence/hosted-parallel-review1/logs/desktop-disk.txt +$ df -h /System/Volumes/Data +Filesystem Size Used Avail Capacity iused ifree %iused Mounted on +/dev/disk3s5 228Gi 194Gi 7.2Gi 97% 4.0M 76M 5% /System/Volumes/Data +exit_code=0 + +evidence/hosted-parallel-review1/logs/desktop-focused.txt +$ cd packages/sdk +$ FLOWS_2052_SDK=/tmp/ms-boot-2052-npm/node_modules/@relayflows/sdk /Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node node_modules/vitest/vitest.mjs run tests/authored-node-admission.test.ts tests/authored-rollback-readback.test.ts tests/authored-parallel-eligibility.test.ts tests/authored-node-result.test.ts tests/authored-root.test.ts tests/authored-status-detail.test.ts --maxWorkers=1 --minWorkers=1 + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + +(node:61856) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) +(node:61856) [FLOWS_ROOT_LEASE_LOST] Warning: authored root run_id=root-run attempt=1: lease_conflict: attempt has no active worker lease. Waiting for the kernel to retry it. + ✓ tests/authored-root.test.ts (49 tests) 555ms + ✓ tests/authored-node-result.test.ts (53 tests) 11ms +(node:61864) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) + ✓ tests/authored-parallel-eligibility.test.ts (13 tests) 59ms +(node:61868) ExperimentalWarning: SQLite is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) + ✓ tests/authored-status-detail.test.ts (23 tests) 431ms + ✓ tests/authored-rollback-readback.test.ts (1 test) 288ms + ✓ tests/authored-node-admission.test.ts (3 tests) 319ms + + Test Files 6 passed (6) + Tests 142 passed (142) + Start at 20:13:12 + Duration 3.56s (transform 408ms, setup 20ms, collect 1.36s, tests 1.66s, environment 0ms, prepare 118ms) + + +exit_code=0 + +evidence/hosted-parallel-review1/logs/desktop-typecheck.txt +Temporary .tsconfig-review676.json: +{"extends": "./tsconfig.tests.json", "include": ["src/**/*.ts", "tests/authored-node-admission.test.ts", "tests/authored-rollback-readback.test.ts", "tests/authored-parallel-eligibility.test.ts", "tests/authored-node-result.test.ts", "tests/authored-root.test.ts", "tests/authored-node-runtime.test.ts"]} +$ cd packages/sdk +$ /Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node node_modules/typescript/bin/tsc -p .tsconfig-review676.json --noEmit + +exit_code=0 + +evidence/hosted-parallel-review1/logs/f1-body-identity-reverted.txt +--- authored-parallel-eligibility.ts ++++ authored-parallel-eligibility.ts (reverted) +@@ -48,7 +48,7 @@ + } finally { + process.emitWarning = emitWarning; + } +- return parallelRunEligible(body, transformed); ++ return parallelBodySyntaxEligible(body); + } + + /** + +$ cd packages/sdk +$ FLOWS_2052_SDK=/tmp/ms-boot-2052-npm/node_modules/@relayflows/sdk /Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node node_modules/vitest/vitest.mjs run tests/authored-parallel-eligibility.test.ts --maxWorkers=1 --minWorkers=1 + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + +(node:61728) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) + ❯ tests/authored-parallel-eligibility.test.ts (13 tests | 2 failed) 62ms + × parallel run admission > matches the Node-loaded body to strip source, including TypeScript annotations 34ms + → async (f: any) => { await Promise.all([f.run('A'), f.run('B')]); f.done('success'); }: expected true to be false // Object.is equality + × parallel run admission > matches the Node-loaded body to transform source, including TypeScript annotations 5ms + → async (f: any) => { await Promise.all([f.run('A'), f.run('B')]); f.done('success'); }: expected true to be false // Object.is equality + +⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/authored-parallel-eligibility.test.ts > parallel run admission > matches the Node-loaded body to strip source, including TypeScript annotations + FAIL tests/authored-parallel-eligibility.test.ts > parallel run admission > matches the Node-loaded body to transform source, including TypeScript annotations +AssertionError: async (f: any) => { await Promise.all([f.run('A'), f.run('B')]); f.done('success'); }: expected true to be false // Object.is equality + +- Expected ++ Received + +- false ++ true + + ❯ tests/authored-parallel-eligibility.test.ts:50:110 + 48| const substitute = body(`async f => { + 49| await Promise.all([f.run('other-a'), f.run('other-b')]); f.don… + 50| expect(await parallelLoadedBodyEligible(substitute, source, 'fil… + | ^ + 51| } + 52| }); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯ + + Test Files 1 failed (1) + Tests 2 failed | 11 passed (13) + Start at 20:12:59 + Duration 700ms (transform 71ms, setup 10ms, collect 496ms, tests 62ms, environment 0ms, prepare 51ms) + + +exit_code=1 + +evidence/hosted-parallel-review1/logs/f1-body-identity-restored.txt +Restored byte-for-byte: authored-parallel-eligibility.ts +sha256=0f24a8da7b18cee3bae52415507332a99a109eb3c182c951d35b6ec2eab9bafb +$ cd packages/sdk +$ FLOWS_2052_SDK=/tmp/ms-boot-2052-npm/node_modules/@relayflows/sdk /Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node node_modules/vitest/vitest.mjs run tests/authored-parallel-eligibility.test.ts --maxWorkers=1 --minWorkers=1 + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + +(node:61736) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) + ✓ tests/authored-parallel-eligibility.test.ts (13 tests) 61ms + + Test Files 1 passed (1) + Tests 13 passed (13) + Start at 20:12:59 + Duration 447ms (transform 51ms, setup 9ms, collect 283ms, tests 61ms, environment 0ms, prepare 20ms) + + +exit_code=0 + +evidence/hosted-parallel-review1/logs/f3-type-identity-reverted.txt +--- authored-root.ts ++++ authored-root.ts (reverted) +@@ -105,13 +105,12 @@ + // The parser is needed only for an authored run, never for an ordinary + // CLI check or watch reload. Recheck intrinsics after the async import; + // author code could have replaced them while it was loading. +- const { parallelLoadedBodyEligible, parallelPinnedSourceEligible } = await import('./authored-parallel-eligibility.js'); ++ const { parallelRunEligible, parallelPinnedSourceEligible } = await import('./authored-parallel-eligibility.js'); + parallelEligible = parallelPromiseIntrinsicsIntact() + && (process.versions['bun'] === undefined + // Native imports strip by default, or transform with Node's opt-in + // flag. Require exact identity with one of those loader outputs. +- ? await parallelLoadedBodyEligible(definition.body, source.toString('utf8'), pathToFileURL(loaded.sourcePath).href, 'strip') +- || await parallelLoadedBodyEligible(definition.body, source.toString('utf8'), pathToFileURL(loaded.sourcePath).href, 'transform') ++ ? parallelRunEligible(definition.body, source.toString('utf8')) + : parallelPinnedSourceEligible(source.toString('utf8'))); + if (!parallelEligible) parallelIneligibleReason = parallelPromiseIntrinsicsIntact() + ? 'source_certificate' : 'runtime_intrinsics'; + +$ cd packages/sdk +$ FLOWS_2052_SDK=/tmp/ms-boot-2052-npm/node_modules/@relayflows/sdk /Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node node_modules/vitest/vitest.mjs run tests/authored-root.test.ts --maxWorkers=1 --minWorkers=1 + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + +(node:61746) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) +(node:61746) [FLOWS_ROOT_LEASE_LOST] Warning: authored root run_id=root-run attempt=1: lease_conflict: attempt has no active worker lease. Waiting for the kernel to retry it. + ❯ tests/authored-root.test.ts (49 tests | 7 failed) 529ms + × durable authored root > pins a certified 'plain' body loaded with 'transform' to parallel mode before dispatch 10ms + → expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS1param' body loaded with 'strip' to parallel mode before dispatch 4ms + → expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS1param' body loaded with 'transform' to parallel mode before dispatch 2ms + → expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS2annot' body loaded with 'strip' to parallel mode before dispatch 2ms + → expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS2annot' body loaded with 'transform' to parallel mode before dispatch 2ms + → expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS3ret' body loaded with 'strip' to parallel mode before dispatch 3ms + → expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS3ret' body loaded with 'transform' to parallel mode before dispatch 2ms + → expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + +⎯⎯⎯⎯⎯⎯⎯ Failed Tests 7 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'plain' body loaded with 'transform' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS1param' body loaded with 'strip' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS1param' body loaded with 'transform' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS2annot' body loaded with 'strip' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS2annot' body loaded with 'transform' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS3ret' body loaded with 'strip' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS3ret' body loaded with 'transform' to parallel mode before dispatch +AssertionError: expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + +- Expected ++ Received + + Object { +- "executionMode": "parallel", ++ "executionMode": "serial", + "identityVersion": 2, +- "kind": "relayflows.authored-root.parallel.v3", ++ "kind": "relayflows.authored-root.v1", + } + + ❯ tests/authored-root.test.ts:241:42 + 239| { dataDir: '/unused', admissionKey: 'certified-group' }); + 240| const step = (journal.starts[0]!.spec as { steps: Array<{ instruct… + 241| expect(JSON.parse(step.instruction)).toMatchObject({ + | ^ + 242| kind: 'relayflows.authored-root.parallel.v3', identityVersion: 2… + 243| }); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/7]⎯ + + Test Files 1 failed (1) + Tests 7 failed | 42 passed (49) + Start at 20:13:00 + Duration 1.97s (transform 439ms, setup 9ms, collect 1.33s, tests 529ms, environment 0ms, prepare 20ms) + + +exit_code=1 + +evidence/hosted-parallel-review1/logs/f3-type-identity-restored.txt +Restored byte-for-byte: authored-root.ts +sha256=5c8c80638f19cc034e310bb09a429ff9f529106f080f3e12ad2cbf1a1a76ea99 +$ cd packages/sdk +$ FLOWS_2052_SDK=/tmp/ms-boot-2052-npm/node_modules/@relayflows/sdk /Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node node_modules/vitest/vitest.mjs run tests/authored-root.test.ts --maxWorkers=1 --minWorkers=1 + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + +(node:61759) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) +(node:61759) [FLOWS_ROOT_LEASE_LOST] Warning: authored root run_id=root-run attempt=1: lease_conflict: attempt has no active worker lease. Waiting for the kernel to retry it. + ✓ tests/authored-root.test.ts (49 tests) 548ms + + Test Files 1 passed (1) + Tests 49 passed (49) + Start at 20:13:02 + Duration 1.56s (transform 352ms, setup 9ms, collect 905ms, tests 548ms, environment 0ms, prepare 20ms) + + +exit_code=0 + +evidence/hosted-parallel-review1/logs/f4-root-marker-reverted.txt +--- authored-root.ts ++++ authored-root.ts (reverted) +@@ -123,7 +123,7 @@ + } + const executionMode = parallelEligible ? 'parallel' : 'serial'; + const metadata: AuthoredRootMetadata = Object.freeze({ +- kind: executionMode === 'parallel' ? AUTHORED_PARALLEL_ROOT_KIND_V3 : ROOT_KIND, ++ kind: executionMode === 'parallel' ? AUTHORED_PARALLEL_ROOT_KIND : ROOT_KIND, + flowName: definition.name, + flowPath: loaded.sourcePath, + sourceSha256: sha256(source), + +$ cd packages/sdk +$ FLOWS_2052_SDK=/tmp/ms-boot-2052-npm/node_modules/@relayflows/sdk /Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node node_modules/vitest/vitest.mjs run tests/authored-root.test.ts --maxWorkers=1 --minWorkers=1 + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + +(node:61769) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) +(node:61769) [FLOWS_ROOT_LEASE_LOST] Warning: authored root run_id=root-run attempt=1: lease_conflict: attempt has no active worker lease. Waiting for the kernel to retry it. + ❯ tests/authored-root.test.ts (49 tests | 8 failed) 550ms + × durable authored root > pins a certified 'plain' body loaded with 'strip' to parallel mode before dispatch 38ms + → expected { executionMode: 'parallel', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'plain' body loaded with 'transform' to parallel mode before dispatch 10ms + → expected { executionMode: 'parallel', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS1param' body loaded with 'strip' to parallel mode before dispatch 5ms + → expected { executionMode: 'parallel', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS1param' body loaded with 'transform' to parallel mode before dispatch 4ms + → expected { executionMode: 'parallel', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS2annot' body loaded with 'strip' to parallel mode before dispatch 3ms + → expected { executionMode: 'parallel', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS2annot' body loaded with 'transform' to parallel mode before dispatch 7ms + → expected { executionMode: 'parallel', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS3ret' body loaded with 'strip' to parallel mode before dispatch 5ms + → expected { executionMode: 'parallel', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS3ret' body loaded with 'transform' to parallel mode before dispatch 4ms + → expected { executionMode: 'parallel', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + +⎯⎯⎯⎯⎯⎯⎯ Failed Tests 8 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'plain' body loaded with 'strip' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'plain' body loaded with 'transform' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS1param' body loaded with 'strip' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS1param' body loaded with 'transform' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS2annot' body loaded with 'strip' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS2annot' body loaded with 'transform' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS3ret' body loaded with 'strip' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS3ret' body loaded with 'transform' to parallel mode before dispatch +AssertionError: expected { executionMode: 'parallel', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + +- Expected ++ Received + + Object { + "executionMode": "parallel", + "identityVersion": 2, +- "kind": "relayflows.authored-root.parallel.v3", ++ "kind": "relayflows.authored-root.parallel.v2", + } + + ❯ tests/authored-root.test.ts:241:42 + 239| { dataDir: '/unused', admissionKey: 'certified-group' }); + 240| const step = (journal.starts[0]!.spec as { steps: Array<{ instruct… + 241| expect(JSON.parse(step.instruction)).toMatchObject({ + | ^ + 242| kind: 'relayflows.authored-root.parallel.v3', identityVersion: 2… + 243| }); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/8]⎯ + + Test Files 1 failed (1) + Tests 8 failed | 41 passed (49) + Start at 20:13:04 + Duration 1.56s (transform 353ms, setup 8ms, collect 911ms, tests 550ms, environment 0ms, prepare 20ms) + + +exit_code=1 + +evidence/hosted-parallel-review1/logs/f4-root-marker-restored.txt +Restored byte-for-byte: authored-root.ts +sha256=5c8c80638f19cc034e310bb09a429ff9f529106f080f3e12ad2cbf1a1a76ea99 +$ cd packages/sdk +$ FLOWS_2052_SDK=/tmp/ms-boot-2052-npm/node_modules/@relayflows/sdk /Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node node_modules/vitest/vitest.mjs run tests/authored-root.test.ts --maxWorkers=1 --minWorkers=1 + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + +(node:61783) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) +(node:61783) [FLOWS_ROOT_LEASE_LOST] Warning: authored root run_id=root-run attempt=1: lease_conflict: attempt has no active worker lease. Waiting for the kernel to retry it. + ✓ tests/authored-root.test.ts (49 tests) 639ms + + Test Files 1 passed (1) + Tests 49 passed (49) + Start at 20:13:06 + Duration 1.68s (transform 365ms, setup 9ms, collect 933ms, tests 639ms, environment 0ms, prepare 21ms) + + +exit_code=0 + +evidence/hosted-parallel-review1/logs/f5-step-metadata-reverted.txt +--- authored-node-runner.ts ++++ authored-node-runner.ts (reverted) +@@ -374,11 +374,7 @@ + if (indexed.length !== count || indexed.some(step => { + const matching = claimed.get(step.step); + return matching === undefined || step.state !== 'completed' +- || step.runId !== matching.runId || step.completionReason !== matching.completionReason +- || step.label !== matching.label || step.afterTruncated !== matching.afterTruncated +- || (step.after === undefined ? matching.after !== undefined +- : !Array.isArray(step.after) || !Array.isArray(matching.after) || step.after.length !== matching.after.length +- || step.after.some((id, index) => id !== matching.after?.[index])); ++ || step.runId !== matching.runId || step.completionReason !== matching.completionReason; + })) invalid('parallel index'); + } + for (const [index, claimed] of [...ordered, ...gates].entries()) { + +$ cd packages/sdk +$ FLOWS_2052_SDK=/tmp/ms-boot-2052-npm/node_modules/@relayflows/sdk /Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node node_modules/vitest/vitest.mjs run tests/authored-node-result.test.ts --maxWorkers=1 --minWorkers=1 + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + + ❯ tests/authored-node-result.test.ts (53 tests | 7 failed) 15ms + × authored IPC result durable verification > checks forged edges causal parallel claims against the durable root index 3ms + → promise resolved "undefined" instead of rejecting + × authored IPC result durable verification > checks missing edges causal parallel claims against the durable root index 0ms + → promise resolved "undefined" instead of rejecting + × authored IPC result durable verification > checks extra edge causal parallel claims against the durable root index 0ms + → promise resolved "undefined" instead of rejecting + × authored IPC result durable verification > checks missing truncation causal parallel claims against the durable root index 0ms + → promise resolved "undefined" instead of rejecting + × authored IPC result durable verification > checks false truncation causal parallel claims against the durable root index 0ms + → promise resolved "undefined" instead of rejecting + × authored IPC result durable verification > checks forged label causal parallel claims against the durable root index 0ms + → promise resolved "undefined" instead of rejecting + × authored IPC result durable verification > checks missing label causal parallel claims against the durable root index 0ms + → promise resolved "undefined" instead of rejecting + +⎯⎯⎯⎯⎯⎯⎯ Failed Tests 7 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/authored-node-result.test.ts > authored IPC result durable verification > checks forged edges causal parallel claims against the durable root index + FAIL tests/authored-node-result.test.ts > authored IPC result durable verification > checks missing edges causal parallel claims against the durable root index + FAIL tests/authored-node-result.test.ts > authored IPC result durable verification > checks extra edge causal parallel claims against the durable root index + FAIL tests/authored-node-result.test.ts > authored IPC result durable verification > checks missing truncation causal parallel claims against the durable root index + FAIL tests/authored-node-result.test.ts > authored IPC result durable verification > checks false truncation causal parallel claims against the durable root index + FAIL tests/authored-node-result.test.ts > authored IPC result durable verification > checks forged label causal parallel claims against the durable root index + FAIL tests/authored-node-result.test.ts > authored IPC result durable verification > checks missing label causal parallel claims against the durable root index +AssertionError: promise resolved "undefined" instead of rejecting + +- Expected: +[Error: rejected promise] + ++ Received: +undefined + + ❯ tests/authored-node-result.test.ts:79:128 + 77| const altered = { ...claimed, journalSteps:claimed.journalSteps.… + 78| index === 2 ? { ...step, ...forged } : step) }; + 79| await expect(verifyAuthoredNodeResult(altered as unknown as Auth… + | ^ + 80| .rejects.toThrow('no matching durable completion'); + 81| return; + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/7]⎯ + + Test Files 1 failed (1) + Tests 7 failed | 46 passed (53) + Start at 20:13:08 + Duration 272ms (transform 92ms, setup 9ms, collect 154ms, tests 15ms, environment 0ms, prepare 21ms) + + +exit_code=1 + +evidence/hosted-parallel-review1/logs/f5-step-metadata-restored.txt +Restored byte-for-byte: authored-node-runner.ts +sha256=11e0f0d1d2571b5793ed22668e10407533c79347c7aa100c400c232e0b2ee9bd +$ cd packages/sdk +$ FLOWS_2052_SDK=/tmp/ms-boot-2052-npm/node_modules/@relayflows/sdk /Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node node_modules/vitest/vitest.mjs run tests/authored-node-result.test.ts --maxWorkers=1 --minWorkers=1 + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + + ✓ tests/authored-node-result.test.ts (53 tests) 11ms + + Test Files 1 passed (1) + Tests 53 passed (53) + Start at 20:13:08 + Duration 261ms (transform 93ms, setup 9ms, collect 145ms, tests 11ms, environment 0ms, prepare 21ms) + + +exit_code=0 + +evidence/hosted-parallel-review1/logs/wip-strip-mode-reverted.txt +--- authored-root.ts ++++ authored-root.ts (reverted) +@@ -110,8 +110,7 @@ + && (process.versions['bun'] === undefined + // Native imports strip by default, or transform with Node's opt-in + // flag. Require exact identity with one of those loader outputs. +- ? await parallelLoadedBodyEligible(definition.body, source.toString('utf8'), pathToFileURL(loaded.sourcePath).href, 'strip') +- || await parallelLoadedBodyEligible(definition.body, source.toString('utf8'), pathToFileURL(loaded.sourcePath).href, 'transform') ++ ? await parallelLoadedBodyEligible(definition.body, source.toString('utf8'), pathToFileURL(loaded.sourcePath).href, 'transform') + : parallelPinnedSourceEligible(source.toString('utf8'))); + if (!parallelEligible) parallelIneligibleReason = parallelPromiseIntrinsicsIntact() + ? 'source_certificate' : 'runtime_intrinsics'; + +$ cd packages/sdk +$ FLOWS_2052_SDK=/tmp/ms-boot-2052-npm/node_modules/@relayflows/sdk /Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node node_modules/vitest/vitest.mjs run tests/authored-root.test.ts --maxWorkers=1 --minWorkers=1 + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + +(node:61815) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) +(node:61815) [FLOWS_ROOT_LEASE_LOST] Warning: authored root run_id=root-run attempt=1: lease_conflict: attempt has no active worker lease. Waiting for the kernel to retry it. + ❯ tests/authored-root.test.ts (49 tests | 4 failed) 515ms + × durable authored root > pins a certified 'plain' body loaded with 'strip' to parallel mode before dispatch 36ms + → expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS1param' body loaded with 'strip' to parallel mode before dispatch 4ms + → expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS2annot' body loaded with 'strip' to parallel mode before dispatch 3ms + → expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS3ret' body loaded with 'strip' to parallel mode before dispatch 4ms + → expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + +⎯⎯⎯⎯⎯⎯⎯ Failed Tests 4 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'plain' body loaded with 'strip' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS1param' body loaded with 'strip' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS2annot' body loaded with 'strip' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS3ret' body loaded with 'strip' to parallel mode before dispatch +AssertionError: expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + +- Expected ++ Received + + Object { +- "executionMode": "parallel", ++ "executionMode": "serial", + "identityVersion": 2, +- "kind": "relayflows.authored-root.parallel.v3", ++ "kind": "relayflows.authored-root.v1", + } + + ❯ tests/authored-root.test.ts:241:42 + 239| { dataDir: '/unused', admissionKey: 'certified-group' }); + 240| const step = (journal.starts[0]!.spec as { steps: Array<{ instruct… + 241| expect(JSON.parse(step.instruction)).toMatchObject({ + | ^ + 242| kind: 'relayflows.authored-root.parallel.v3', identityVersion: 2… + 243| }); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/4]⎯ + + Test Files 1 failed (1) + Tests 4 failed | 45 passed (49) + Start at 20:13:08 + Duration 1.51s (transform 348ms, setup 11ms, collect 888ms, tests 515ms, environment 0ms, prepare 20ms) + + +exit_code=1 + +evidence/hosted-parallel-review1/logs/wip-strip-mode-restored.txt +Restored byte-for-byte: authored-root.ts +sha256=5c8c80638f19cc034e310bb09a429ff9f529106f080f3e12ad2cbf1a1a76ea99 +$ cd packages/sdk +$ FLOWS_2052_SDK=/tmp/ms-boot-2052-npm/node_modules/@relayflows/sdk /Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node node_modules/vitest/vitest.mjs run tests/authored-root.test.ts --maxWorkers=1 --minWorkers=1 + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + +(node:61843) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) +(node:61843) [FLOWS_ROOT_LEASE_LOST] Warning: authored root run_id=root-run attempt=1: lease_conflict: attempt has no active worker lease. Waiting for the kernel to retry it. + ✓ tests/authored-root.test.ts (49 tests) 534ms + + Test Files 1 passed (1) + Tests 49 passed (49) + Start at 20:13:10 + Duration 1.55s (transform 357ms, setup 8ms, collect 913ms, tests 534ms, environment 0ms, prepare 20ms) + + +exit_code=0 diff --git a/evidence/hosted-parallel-review1/focused-reverts.py b/evidence/hosted-parallel-review1/focused-reverts.py new file mode 100644 index 000000000..20dbc4403 --- /dev/null +++ b/evidence/hosted-parallel-review1/focused-reverts.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Focused mutation evidence only; does not build or run the live review gate.""" +import difflib +import hashlib +import os +from pathlib import Path +import shlex +import subprocess + +root = Path(__file__).resolve().parents[2] +sdk = root / 'packages/sdk' +logs = Path(__file__).resolve().parent / 'logs' +node = os.environ['REVIEW_NODE'] +old_sdk = os.environ['FLOWS_2052_SDK'] + + +def test(tag, tests, prefix=''): + command = [node, 'node_modules/vitest/vitest.mjs', 'run', *tests, + '--maxWorkers=1', '--minWorkers=1'] + result = subprocess.run(command, cwd=sdk, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True) + transcript = (prefix + '$ cd packages/sdk\n$ FLOWS_2052_SDK=' + shlex.quote(old_sdk) + + ' ' + shlex.join(command) + '\n' + result.stdout + + '\nexit_code=' + str(result.returncode) + '\n') + (logs / (tag + '.txt')).write_text(transcript) + print(tag + ':\n' + '\n'.join(line for line in result.stdout.splitlines() + if 'Test Files ' in line or 'Tests ' in line) + '\nexit_code=' + str(result.returncode), flush=True) + return result.returncode + + +root_calls = """await parallelLoadedBodyEligible(definition.body, source.toString('utf8'), pathToFileURL(loaded.sourcePath).href, 'strip') + || await parallelLoadedBodyEligible(definition.body, source.toString('utf8'), pathToFileURL(loaded.sourcePath).href, 'transform')""" +edge_check = """ + || step.label !== matching.label || step.afterTruncated !== matching.afterTruncated + || (step.after === undefined ? matching.after !== undefined + : !Array.isArray(step.after) || !Array.isArray(matching.after) || step.after.length !== matching.after.length + || step.after.some((id, index) => id !== matching.after?.[index]))""" +mutations = [ + ('f1-body-identity', 'authored-parallel-eligibility.ts', [ + ('return parallelRunEligible(body, transformed);', 'return parallelBodySyntaxEligible(body);')], + ['tests/authored-parallel-eligibility.test.ts']), + ('f3-type-identity', 'authored-root.ts', [ + ('const { parallelLoadedBodyEligible, parallelPinnedSourceEligible }', + 'const { parallelRunEligible, parallelPinnedSourceEligible }'), + (root_calls, "parallelRunEligible(definition.body, source.toString('utf8'))")], + ['tests/authored-root.test.ts']), + ('f4-root-marker', 'authored-root.ts', [ + ("kind: executionMode === 'parallel' ? AUTHORED_PARALLEL_ROOT_KIND_V3 : ROOT_KIND,", + "kind: executionMode === 'parallel' ? AUTHORED_PARALLEL_ROOT_KIND : ROOT_KIND,")], + ['tests/authored-root.test.ts']), + ('f5-step-metadata', 'authored-node-runner.ts', [(edge_check, '')], + ['tests/authored-node-result.test.ts']), + ('wip-strip-mode', 'authored-root.ts', [ + (root_calls, "await parallelLoadedBodyEligible(definition.body, source.toString('utf8'), pathToFileURL(loaded.sourcePath).href, 'transform')")], + ['tests/authored-root.test.ts']), +] + +for tag, filename, changes, tests in mutations: + path = sdk / 'src' / filename + original = path.read_bytes() + before = hashlib.sha256(original).hexdigest() + mutant = original.decode() + for old, new in changes: + assert mutant.count(old) == 1, (tag, old) + mutant = mutant.replace(old, new) + diff = ''.join(difflib.unified_diff(original.decode().splitlines(True), + mutant.splitlines(True), fromfile=filename, tofile=filename + ' (reverted)')) + try: + path.write_text(mutant) + failed = test(tag + '-reverted', tests, diff + '\n') + finally: + path.write_bytes(original) + assert path.read_bytes() == original + restored = test(tag + '-restored', tests, + 'Restored byte-for-byte: ' + filename + '\nsha256=' + before + '\n') + assert failed != 0 and restored == 0, (tag, failed, restored) + +assert test('desktop-focused', [ + 'tests/authored-node-admission.test.ts', 'tests/authored-rollback-readback.test.ts', + 'tests/authored-parallel-eligibility.test.ts', 'tests/authored-node-result.test.ts', + 'tests/authored-root.test.ts', 'tests/authored-status-detail.test.ts']) == 0 diff --git a/evidence/hosted-parallel-review1/focused-types.py b/evidence/hosted-parallel-review1/focused-types.py new file mode 100644 index 000000000..bc450c718 --- /dev/null +++ b/evidence/hosted-parallel-review1/focused-types.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +"""Typecheck the source and touched tests without emitting build artifacts.""" +import json +import os +from pathlib import Path +import shlex +import subprocess + +root = Path(__file__).resolve().parents[2] +sdk = root / 'packages/sdk' +config = sdk / '.tsconfig-review676.json' +assert not config.exists() +config.write_text(json.dumps({'extends': './tsconfig.tests.json', 'include': [ + 'src/**/*.ts', 'tests/authored-node-admission.test.ts', + 'tests/authored-rollback-readback.test.ts', 'tests/authored-parallel-eligibility.test.ts', + 'tests/authored-node-result.test.ts', 'tests/authored-root.test.ts', + 'tests/authored-node-runtime.test.ts']})) +command = [os.environ['REVIEW_NODE'], 'node_modules/typescript/bin/tsc', '-p', config.name, '--noEmit'] +try: + result = subprocess.run(command, cwd=sdk, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + transcript = ('Temporary .tsconfig-review676.json:\n' + config.read_text() + + '\n$ cd packages/sdk\n$ ' + shlex.join(command) + '\n' + result.stdout + + '\nexit_code=' + str(result.returncode) + '\n') + (Path(__file__).parent / 'logs/desktop-typecheck.txt').write_text(transcript) + print(transcript) +finally: + config.unlink() +raise SystemExit(result.returncode) diff --git a/evidence/hosted-parallel-review1/logs/desktop-baseline.txt b/evidence/hosted-parallel-review1/logs/desktop-baseline.txt new file mode 100644 index 000000000..fce3a9d70 --- /dev/null +++ b/evidence/hosted-parallel-review1/logs/desktop-baseline.txt @@ -0,0 +1,52 @@ +$ cd packages/sdk +$ node node_modules/vitest/vitest.mjs run tests/authored-parallel-eligibility.test.ts tests/authored-node-result.test.ts tests/authored-root.test.ts tests/authored-status-detail.test.ts --maxWorkers=1 --minWorkers=1 + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + +(node:58685) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) +(node:58685) [FLOWS_ROOT_LEASE_LOST] Warning: authored root run_id=root-run attempt=1: lease_conflict: attempt has no active worker lease. Waiting for the kernel to retry it. + ❯ tests/authored-root.test.ts (42 tests | 1 failed) 451ms + × durable authored root > pins a certified Promise.all run body to parallel mode before dispatch 2ms + → The property 'options.mode' must be one of: 'strip'. Received 'transform' + ✓ tests/authored-node-result.test.ts (46 tests) 9ms +(node:58697) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) + ❯ tests/authored-parallel-eligibility.test.ts (12 tests | 1 failed) 18ms + × parallel run admission > matches the Node-loaded body to transformed source, including TypeScript annotations 1ms + → The property 'options.mode' must be one of: 'strip'. Received 'transform' + ✓ tests/authored-status-detail.test.ts (23 tests) 193ms + +⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/authored-parallel-eligibility.test.ts > parallel run admission > matches the Node-loaded body to transformed source, including TypeScript annotations +TypeError: The property 'options.mode' must be one of: 'strip'. Received 'transform' + ❯ tests/authored-parallel-eligibility.test.ts:44:31 + 42| for (const candidate of bodies) { + 43| const source = `import { flow } from '@relayflows/surface';\nexp… + 44| const transformedBody = stripTypeScriptTypes(candidate, { mode: … + | ^ + 45| const loaded = new Function(`return ${transformedBody}`)() as Fu… + 46| expect(parallelPinnedSourceEligible(source), candidate).toBe(tru… + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯ + + FAIL tests/authored-root.test.ts > durable authored root > pins a certified Promise.all run body to parallel mode before dispatch +TypeError: The property 'options.mode' must be one of: 'strip'. Received 'transform' + ❯ tests/authored-root.test.ts:216:25 + 214| }`; + 215| const source = `import { flow } from '@relayflows/surface';\nexpor… + 216| const transformed = stripTypeScriptTypes(source, { mode: 'transfor… + | ^ + 217| const file = ts.createSourceFile('flagship.js', transformed, ts.Sc… + 218| const exported = file.statements[1] as ts.ExportAssignment; + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯ + + Test Files 2 failed | 2 passed (4) + Tests 2 failed | 121 passed (123) + Start at 20:05:52 + Duration 2.85s (transform 474ms, setup 22ms, collect 1.68s, tests 671ms, environment 0ms, prepare 120ms) + + +exit_code=1 diff --git a/evidence/hosted-parallel-review1/logs/desktop-disk.txt b/evidence/hosted-parallel-review1/logs/desktop-disk.txt new file mode 100644 index 000000000..4eb376976 --- /dev/null +++ b/evidence/hosted-parallel-review1/logs/desktop-disk.txt @@ -0,0 +1,4 @@ +$ df -h /System/Volumes/Data +Filesystem Size Used Avail Capacity iused ifree %iused Mounted on +/dev/disk3s5 228Gi 194Gi 7.2Gi 97% 4.0M 76M 5% /System/Volumes/Data +exit_code=0 diff --git a/evidence/hosted-parallel-review1/logs/desktop-focused.txt b/evidence/hosted-parallel-review1/logs/desktop-focused.txt new file mode 100644 index 000000000..c33597144 --- /dev/null +++ b/evidence/hosted-parallel-review1/logs/desktop-focused.txt @@ -0,0 +1,26 @@ +$ cd packages/sdk +$ FLOWS_2052_SDK=/tmp/ms-boot-2052-npm/node_modules/@relayflows/sdk /Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node node_modules/vitest/vitest.mjs run tests/authored-node-admission.test.ts tests/authored-rollback-readback.test.ts tests/authored-parallel-eligibility.test.ts tests/authored-node-result.test.ts tests/authored-root.test.ts tests/authored-status-detail.test.ts --maxWorkers=1 --minWorkers=1 + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + +(node:61856) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) +(node:61856) [FLOWS_ROOT_LEASE_LOST] Warning: authored root run_id=root-run attempt=1: lease_conflict: attempt has no active worker lease. Waiting for the kernel to retry it. + ✓ tests/authored-root.test.ts (49 tests) 555ms + ✓ tests/authored-node-result.test.ts (53 tests) 11ms +(node:61864) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) + ✓ tests/authored-parallel-eligibility.test.ts (13 tests) 59ms +(node:61868) ExperimentalWarning: SQLite is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) + ✓ tests/authored-status-detail.test.ts (23 tests) 431ms + ✓ tests/authored-rollback-readback.test.ts (1 test) 288ms + ✓ tests/authored-node-admission.test.ts (3 tests) 319ms + + Test Files 6 passed (6) + Tests 142 passed (142) + Start at 20:13:12 + Duration 3.56s (transform 408ms, setup 20ms, collect 1.36s, tests 1.66s, environment 0ms, prepare 118ms) + + +exit_code=0 diff --git a/evidence/hosted-parallel-review1/logs/desktop-typecheck.txt b/evidence/hosted-parallel-review1/logs/desktop-typecheck.txt new file mode 100644 index 000000000..f3f3769d6 --- /dev/null +++ b/evidence/hosted-parallel-review1/logs/desktop-typecheck.txt @@ -0,0 +1,6 @@ +Temporary .tsconfig-review676.json: +{"extends": "./tsconfig.tests.json", "include": ["src/**/*.ts", "tests/authored-node-admission.test.ts", "tests/authored-rollback-readback.test.ts", "tests/authored-parallel-eligibility.test.ts", "tests/authored-node-result.test.ts", "tests/authored-root.test.ts", "tests/authored-node-runtime.test.ts"]} +$ cd packages/sdk +$ /Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node node_modules/typescript/bin/tsc -p .tsconfig-review676.json --noEmit + +exit_code=0 diff --git a/evidence/hosted-parallel-review1/logs/f1-body-identity-restored.txt b/evidence/hosted-parallel-review1/logs/f1-body-identity-restored.txt new file mode 100644 index 000000000..2c85d6570 --- /dev/null +++ b/evidence/hosted-parallel-review1/logs/f1-body-identity-restored.txt @@ -0,0 +1,18 @@ +Restored byte-for-byte: authored-parallel-eligibility.ts +sha256=0f24a8da7b18cee3bae52415507332a99a109eb3c182c951d35b6ec2eab9bafb +$ cd packages/sdk +$ FLOWS_2052_SDK=/tmp/ms-boot-2052-npm/node_modules/@relayflows/sdk /Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node node_modules/vitest/vitest.mjs run tests/authored-parallel-eligibility.test.ts --maxWorkers=1 --minWorkers=1 + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + +(node:61736) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) + ✓ tests/authored-parallel-eligibility.test.ts (13 tests) 61ms + + Test Files 1 passed (1) + Tests 13 passed (13) + Start at 20:12:59 + Duration 447ms (transform 51ms, setup 9ms, collect 283ms, tests 61ms, environment 0ms, prepare 20ms) + + +exit_code=0 diff --git a/evidence/hosted-parallel-review1/logs/f1-body-identity-reverted.txt b/evidence/hosted-parallel-review1/logs/f1-body-identity-reverted.txt new file mode 100644 index 000000000..b9e5f3e54 --- /dev/null +++ b/evidence/hosted-parallel-review1/logs/f1-body-identity-reverted.txt @@ -0,0 +1,54 @@ +--- authored-parallel-eligibility.ts ++++ authored-parallel-eligibility.ts (reverted) +@@ -48,7 +48,7 @@ + } finally { + process.emitWarning = emitWarning; + } +- return parallelRunEligible(body, transformed); ++ return parallelBodySyntaxEligible(body); + } + + /** + +$ cd packages/sdk +$ FLOWS_2052_SDK=/tmp/ms-boot-2052-npm/node_modules/@relayflows/sdk /Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node node_modules/vitest/vitest.mjs run tests/authored-parallel-eligibility.test.ts --maxWorkers=1 --minWorkers=1 + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + +(node:61728) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) + ❯ tests/authored-parallel-eligibility.test.ts (13 tests | 2 failed) 62ms + × parallel run admission > matches the Node-loaded body to strip source, including TypeScript annotations 34ms + → async (f: any) => { await Promise.all([f.run('A'), f.run('B')]); f.done('success'); }: expected true to be false // Object.is equality + × parallel run admission > matches the Node-loaded body to transform source, including TypeScript annotations 5ms + → async (f: any) => { await Promise.all([f.run('A'), f.run('B')]); f.done('success'); }: expected true to be false // Object.is equality + +⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/authored-parallel-eligibility.test.ts > parallel run admission > matches the Node-loaded body to strip source, including TypeScript annotations + FAIL tests/authored-parallel-eligibility.test.ts > parallel run admission > matches the Node-loaded body to transform source, including TypeScript annotations +AssertionError: async (f: any) => { await Promise.all([f.run('A'), f.run('B')]); f.done('success'); }: expected true to be false // Object.is equality + +- Expected ++ Received + +- false ++ true + + ❯ tests/authored-parallel-eligibility.test.ts:50:110 + 48| const substitute = body(`async f => { + 49| await Promise.all([f.run('other-a'), f.run('other-b')]); f.don… + 50| expect(await parallelLoadedBodyEligible(substitute, source, 'fil… + | ^ + 51| } + 52| }); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯ + + Test Files 1 failed (1) + Tests 2 failed | 11 passed (13) + Start at 20:12:59 + Duration 700ms (transform 71ms, setup 10ms, collect 496ms, tests 62ms, environment 0ms, prepare 51ms) + + +exit_code=1 diff --git a/evidence/hosted-parallel-review1/logs/f3-type-identity-restored.txt b/evidence/hosted-parallel-review1/logs/f3-type-identity-restored.txt new file mode 100644 index 000000000..4a8666349 --- /dev/null +++ b/evidence/hosted-parallel-review1/logs/f3-type-identity-restored.txt @@ -0,0 +1,19 @@ +Restored byte-for-byte: authored-root.ts +sha256=5c8c80638f19cc034e310bb09a429ff9f529106f080f3e12ad2cbf1a1a76ea99 +$ cd packages/sdk +$ FLOWS_2052_SDK=/tmp/ms-boot-2052-npm/node_modules/@relayflows/sdk /Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node node_modules/vitest/vitest.mjs run tests/authored-root.test.ts --maxWorkers=1 --minWorkers=1 + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + +(node:61759) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) +(node:61759) [FLOWS_ROOT_LEASE_LOST] Warning: authored root run_id=root-run attempt=1: lease_conflict: attempt has no active worker lease. Waiting for the kernel to retry it. + ✓ tests/authored-root.test.ts (49 tests) 548ms + + Test Files 1 passed (1) + Tests 49 passed (49) + Start at 20:13:02 + Duration 1.56s (transform 352ms, setup 9ms, collect 905ms, tests 548ms, environment 0ms, prepare 20ms) + + +exit_code=0 diff --git a/evidence/hosted-parallel-review1/logs/f3-type-identity-reverted.txt b/evidence/hosted-parallel-review1/logs/f3-type-identity-reverted.txt new file mode 100644 index 000000000..867c5245e --- /dev/null +++ b/evidence/hosted-parallel-review1/logs/f3-type-identity-reverted.txt @@ -0,0 +1,90 @@ +--- authored-root.ts ++++ authored-root.ts (reverted) +@@ -105,13 +105,12 @@ + // The parser is needed only for an authored run, never for an ordinary + // CLI check or watch reload. Recheck intrinsics after the async import; + // author code could have replaced them while it was loading. +- const { parallelLoadedBodyEligible, parallelPinnedSourceEligible } = await import('./authored-parallel-eligibility.js'); ++ const { parallelRunEligible, parallelPinnedSourceEligible } = await import('./authored-parallel-eligibility.js'); + parallelEligible = parallelPromiseIntrinsicsIntact() + && (process.versions['bun'] === undefined + // Native imports strip by default, or transform with Node's opt-in + // flag. Require exact identity with one of those loader outputs. +- ? await parallelLoadedBodyEligible(definition.body, source.toString('utf8'), pathToFileURL(loaded.sourcePath).href, 'strip') +- || await parallelLoadedBodyEligible(definition.body, source.toString('utf8'), pathToFileURL(loaded.sourcePath).href, 'transform') ++ ? parallelRunEligible(definition.body, source.toString('utf8')) + : parallelPinnedSourceEligible(source.toString('utf8'))); + if (!parallelEligible) parallelIneligibleReason = parallelPromiseIntrinsicsIntact() + ? 'source_certificate' : 'runtime_intrinsics'; + +$ cd packages/sdk +$ FLOWS_2052_SDK=/tmp/ms-boot-2052-npm/node_modules/@relayflows/sdk /Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node node_modules/vitest/vitest.mjs run tests/authored-root.test.ts --maxWorkers=1 --minWorkers=1 + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + +(node:61746) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) +(node:61746) [FLOWS_ROOT_LEASE_LOST] Warning: authored root run_id=root-run attempt=1: lease_conflict: attempt has no active worker lease. Waiting for the kernel to retry it. + ❯ tests/authored-root.test.ts (49 tests | 7 failed) 529ms + × durable authored root > pins a certified 'plain' body loaded with 'transform' to parallel mode before dispatch 10ms + → expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS1param' body loaded with 'strip' to parallel mode before dispatch 4ms + → expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS1param' body loaded with 'transform' to parallel mode before dispatch 2ms + → expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS2annot' body loaded with 'strip' to parallel mode before dispatch 2ms + → expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS2annot' body loaded with 'transform' to parallel mode before dispatch 2ms + → expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS3ret' body loaded with 'strip' to parallel mode before dispatch 3ms + → expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS3ret' body loaded with 'transform' to parallel mode before dispatch 2ms + → expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + +⎯⎯⎯⎯⎯⎯⎯ Failed Tests 7 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'plain' body loaded with 'transform' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS1param' body loaded with 'strip' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS1param' body loaded with 'transform' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS2annot' body loaded with 'strip' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS2annot' body loaded with 'transform' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS3ret' body loaded with 'strip' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS3ret' body loaded with 'transform' to parallel mode before dispatch +AssertionError: expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + +- Expected ++ Received + + Object { +- "executionMode": "parallel", ++ "executionMode": "serial", + "identityVersion": 2, +- "kind": "relayflows.authored-root.parallel.v3", ++ "kind": "relayflows.authored-root.v1", + } + + ❯ tests/authored-root.test.ts:241:42 + 239| { dataDir: '/unused', admissionKey: 'certified-group' }); + 240| const step = (journal.starts[0]!.spec as { steps: Array<{ instruct… + 241| expect(JSON.parse(step.instruction)).toMatchObject({ + | ^ + 242| kind: 'relayflows.authored-root.parallel.v3', identityVersion: 2… + 243| }); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/7]⎯ + + Test Files 1 failed (1) + Tests 7 failed | 42 passed (49) + Start at 20:13:00 + Duration 1.97s (transform 439ms, setup 9ms, collect 1.33s, tests 529ms, environment 0ms, prepare 20ms) + + +exit_code=1 diff --git a/evidence/hosted-parallel-review1/logs/f4-root-marker-restored.txt b/evidence/hosted-parallel-review1/logs/f4-root-marker-restored.txt new file mode 100644 index 000000000..8f6b668ab --- /dev/null +++ b/evidence/hosted-parallel-review1/logs/f4-root-marker-restored.txt @@ -0,0 +1,19 @@ +Restored byte-for-byte: authored-root.ts +sha256=5c8c80638f19cc034e310bb09a429ff9f529106f080f3e12ad2cbf1a1a76ea99 +$ cd packages/sdk +$ FLOWS_2052_SDK=/tmp/ms-boot-2052-npm/node_modules/@relayflows/sdk /Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node node_modules/vitest/vitest.mjs run tests/authored-root.test.ts --maxWorkers=1 --minWorkers=1 + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + +(node:61783) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) +(node:61783) [FLOWS_ROOT_LEASE_LOST] Warning: authored root run_id=root-run attempt=1: lease_conflict: attempt has no active worker lease. Waiting for the kernel to retry it. + ✓ tests/authored-root.test.ts (49 tests) 639ms + + Test Files 1 passed (1) + Tests 49 passed (49) + Start at 20:13:06 + Duration 1.68s (transform 365ms, setup 9ms, collect 933ms, tests 639ms, environment 0ms, prepare 21ms) + + +exit_code=0 diff --git a/evidence/hosted-parallel-review1/logs/f4-root-marker-reverted.txt b/evidence/hosted-parallel-review1/logs/f4-root-marker-reverted.txt new file mode 100644 index 000000000..bd1134d1d --- /dev/null +++ b/evidence/hosted-parallel-review1/logs/f4-root-marker-reverted.txt @@ -0,0 +1,86 @@ +--- authored-root.ts ++++ authored-root.ts (reverted) +@@ -123,7 +123,7 @@ + } + const executionMode = parallelEligible ? 'parallel' : 'serial'; + const metadata: AuthoredRootMetadata = Object.freeze({ +- kind: executionMode === 'parallel' ? AUTHORED_PARALLEL_ROOT_KIND_V3 : ROOT_KIND, ++ kind: executionMode === 'parallel' ? AUTHORED_PARALLEL_ROOT_KIND : ROOT_KIND, + flowName: definition.name, + flowPath: loaded.sourcePath, + sourceSha256: sha256(source), + +$ cd packages/sdk +$ FLOWS_2052_SDK=/tmp/ms-boot-2052-npm/node_modules/@relayflows/sdk /Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node node_modules/vitest/vitest.mjs run tests/authored-root.test.ts --maxWorkers=1 --minWorkers=1 + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + +(node:61769) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) +(node:61769) [FLOWS_ROOT_LEASE_LOST] Warning: authored root run_id=root-run attempt=1: lease_conflict: attempt has no active worker lease. Waiting for the kernel to retry it. + ❯ tests/authored-root.test.ts (49 tests | 8 failed) 550ms + × durable authored root > pins a certified 'plain' body loaded with 'strip' to parallel mode before dispatch 38ms + → expected { executionMode: 'parallel', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'plain' body loaded with 'transform' to parallel mode before dispatch 10ms + → expected { executionMode: 'parallel', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS1param' body loaded with 'strip' to parallel mode before dispatch 5ms + → expected { executionMode: 'parallel', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS1param' body loaded with 'transform' to parallel mode before dispatch 4ms + → expected { executionMode: 'parallel', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS2annot' body loaded with 'strip' to parallel mode before dispatch 3ms + → expected { executionMode: 'parallel', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS2annot' body loaded with 'transform' to parallel mode before dispatch 7ms + → expected { executionMode: 'parallel', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS3ret' body loaded with 'strip' to parallel mode before dispatch 5ms + → expected { executionMode: 'parallel', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS3ret' body loaded with 'transform' to parallel mode before dispatch 4ms + → expected { executionMode: 'parallel', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + +⎯⎯⎯⎯⎯⎯⎯ Failed Tests 8 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'plain' body loaded with 'strip' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'plain' body loaded with 'transform' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS1param' body loaded with 'strip' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS1param' body loaded with 'transform' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS2annot' body loaded with 'strip' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS2annot' body loaded with 'transform' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS3ret' body loaded with 'strip' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS3ret' body loaded with 'transform' to parallel mode before dispatch +AssertionError: expected { executionMode: 'parallel', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + +- Expected ++ Received + + Object { + "executionMode": "parallel", + "identityVersion": 2, +- "kind": "relayflows.authored-root.parallel.v3", ++ "kind": "relayflows.authored-root.parallel.v2", + } + + ❯ tests/authored-root.test.ts:241:42 + 239| { dataDir: '/unused', admissionKey: 'certified-group' }); + 240| const step = (journal.starts[0]!.spec as { steps: Array<{ instruct… + 241| expect(JSON.parse(step.instruction)).toMatchObject({ + | ^ + 242| kind: 'relayflows.authored-root.parallel.v3', identityVersion: 2… + 243| }); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/8]⎯ + + Test Files 1 failed (1) + Tests 8 failed | 41 passed (49) + Start at 20:13:04 + Duration 1.56s (transform 353ms, setup 8ms, collect 911ms, tests 550ms, environment 0ms, prepare 20ms) + + +exit_code=1 diff --git a/evidence/hosted-parallel-review1/logs/f5-step-metadata-restored.txt b/evidence/hosted-parallel-review1/logs/f5-step-metadata-restored.txt new file mode 100644 index 000000000..53d7fd684 --- /dev/null +++ b/evidence/hosted-parallel-review1/logs/f5-step-metadata-restored.txt @@ -0,0 +1,16 @@ +Restored byte-for-byte: authored-node-runner.ts +sha256=11e0f0d1d2571b5793ed22668e10407533c79347c7aa100c400c232e0b2ee9bd +$ cd packages/sdk +$ FLOWS_2052_SDK=/tmp/ms-boot-2052-npm/node_modules/@relayflows/sdk /Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node node_modules/vitest/vitest.mjs run tests/authored-node-result.test.ts --maxWorkers=1 --minWorkers=1 + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + + ✓ tests/authored-node-result.test.ts (53 tests) 11ms + + Test Files 1 passed (1) + Tests 53 passed (53) + Start at 20:13:08 + Duration 261ms (transform 93ms, setup 9ms, collect 145ms, tests 11ms, environment 0ms, prepare 21ms) + + +exit_code=0 diff --git a/evidence/hosted-parallel-review1/logs/f5-step-metadata-reverted.txt b/evidence/hosted-parallel-review1/logs/f5-step-metadata-reverted.txt new file mode 100644 index 000000000..f821a1eb9 --- /dev/null +++ b/evidence/hosted-parallel-review1/logs/f5-step-metadata-reverted.txt @@ -0,0 +1,71 @@ +--- authored-node-runner.ts ++++ authored-node-runner.ts (reverted) +@@ -374,11 +374,7 @@ + if (indexed.length !== count || indexed.some(step => { + const matching = claimed.get(step.step); + return matching === undefined || step.state !== 'completed' +- || step.runId !== matching.runId || step.completionReason !== matching.completionReason +- || step.label !== matching.label || step.afterTruncated !== matching.afterTruncated +- || (step.after === undefined ? matching.after !== undefined +- : !Array.isArray(step.after) || !Array.isArray(matching.after) || step.after.length !== matching.after.length +- || step.after.some((id, index) => id !== matching.after?.[index])); ++ || step.runId !== matching.runId || step.completionReason !== matching.completionReason; + })) invalid('parallel index'); + } + for (const [index, claimed] of [...ordered, ...gates].entries()) { + +$ cd packages/sdk +$ FLOWS_2052_SDK=/tmp/ms-boot-2052-npm/node_modules/@relayflows/sdk /Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node node_modules/vitest/vitest.mjs run tests/authored-node-result.test.ts --maxWorkers=1 --minWorkers=1 + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + + ❯ tests/authored-node-result.test.ts (53 tests | 7 failed) 15ms + × authored IPC result durable verification > checks forged edges causal parallel claims against the durable root index 3ms + → promise resolved "undefined" instead of rejecting + × authored IPC result durable verification > checks missing edges causal parallel claims against the durable root index 0ms + → promise resolved "undefined" instead of rejecting + × authored IPC result durable verification > checks extra edge causal parallel claims against the durable root index 0ms + → promise resolved "undefined" instead of rejecting + × authored IPC result durable verification > checks missing truncation causal parallel claims against the durable root index 0ms + → promise resolved "undefined" instead of rejecting + × authored IPC result durable verification > checks false truncation causal parallel claims against the durable root index 0ms + → promise resolved "undefined" instead of rejecting + × authored IPC result durable verification > checks forged label causal parallel claims against the durable root index 0ms + → promise resolved "undefined" instead of rejecting + × authored IPC result durable verification > checks missing label causal parallel claims against the durable root index 0ms + → promise resolved "undefined" instead of rejecting + +⎯⎯⎯⎯⎯⎯⎯ Failed Tests 7 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/authored-node-result.test.ts > authored IPC result durable verification > checks forged edges causal parallel claims against the durable root index + FAIL tests/authored-node-result.test.ts > authored IPC result durable verification > checks missing edges causal parallel claims against the durable root index + FAIL tests/authored-node-result.test.ts > authored IPC result durable verification > checks extra edge causal parallel claims against the durable root index + FAIL tests/authored-node-result.test.ts > authored IPC result durable verification > checks missing truncation causal parallel claims against the durable root index + FAIL tests/authored-node-result.test.ts > authored IPC result durable verification > checks false truncation causal parallel claims against the durable root index + FAIL tests/authored-node-result.test.ts > authored IPC result durable verification > checks forged label causal parallel claims against the durable root index + FAIL tests/authored-node-result.test.ts > authored IPC result durable verification > checks missing label causal parallel claims against the durable root index +AssertionError: promise resolved "undefined" instead of rejecting + +- Expected: +[Error: rejected promise] + ++ Received: +undefined + + ❯ tests/authored-node-result.test.ts:79:128 + 77| const altered = { ...claimed, journalSteps:claimed.journalSteps.… + 78| index === 2 ? { ...step, ...forged } : step) }; + 79| await expect(verifyAuthoredNodeResult(altered as unknown as Auth… + | ^ + 80| .rejects.toThrow('no matching durable completion'); + 81| return; + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/7]⎯ + + Test Files 1 failed (1) + Tests 7 failed | 46 passed (53) + Start at 20:13:08 + Duration 272ms (transform 92ms, setup 9ms, collect 154ms, tests 15ms, environment 0ms, prepare 21ms) + + +exit_code=1 diff --git a/evidence/hosted-parallel-review1/logs/wip-strip-mode-restored.txt b/evidence/hosted-parallel-review1/logs/wip-strip-mode-restored.txt new file mode 100644 index 000000000..ce009501f --- /dev/null +++ b/evidence/hosted-parallel-review1/logs/wip-strip-mode-restored.txt @@ -0,0 +1,19 @@ +Restored byte-for-byte: authored-root.ts +sha256=5c8c80638f19cc034e310bb09a429ff9f529106f080f3e12ad2cbf1a1a76ea99 +$ cd packages/sdk +$ FLOWS_2052_SDK=/tmp/ms-boot-2052-npm/node_modules/@relayflows/sdk /Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node node_modules/vitest/vitest.mjs run tests/authored-root.test.ts --maxWorkers=1 --minWorkers=1 + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + +(node:61843) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) +(node:61843) [FLOWS_ROOT_LEASE_LOST] Warning: authored root run_id=root-run attempt=1: lease_conflict: attempt has no active worker lease. Waiting for the kernel to retry it. + ✓ tests/authored-root.test.ts (49 tests) 534ms + + Test Files 1 passed (1) + Tests 49 passed (49) + Start at 20:13:10 + Duration 1.55s (transform 357ms, setup 8ms, collect 913ms, tests 534ms, environment 0ms, prepare 20ms) + + +exit_code=0 diff --git a/evidence/hosted-parallel-review1/logs/wip-strip-mode-reverted.txt b/evidence/hosted-parallel-review1/logs/wip-strip-mode-reverted.txt new file mode 100644 index 000000000..b62af6d82 --- /dev/null +++ b/evidence/hosted-parallel-review1/logs/wip-strip-mode-reverted.txt @@ -0,0 +1,72 @@ +--- authored-root.ts ++++ authored-root.ts (reverted) +@@ -110,8 +110,7 @@ + && (process.versions['bun'] === undefined + // Native imports strip by default, or transform with Node's opt-in + // flag. Require exact identity with one of those loader outputs. +- ? await parallelLoadedBodyEligible(definition.body, source.toString('utf8'), pathToFileURL(loaded.sourcePath).href, 'strip') +- || await parallelLoadedBodyEligible(definition.body, source.toString('utf8'), pathToFileURL(loaded.sourcePath).href, 'transform') ++ ? await parallelLoadedBodyEligible(definition.body, source.toString('utf8'), pathToFileURL(loaded.sourcePath).href, 'transform') + : parallelPinnedSourceEligible(source.toString('utf8'))); + if (!parallelEligible) parallelIneligibleReason = parallelPromiseIntrinsicsIntact() + ? 'source_certificate' : 'runtime_intrinsics'; + +$ cd packages/sdk +$ FLOWS_2052_SDK=/tmp/ms-boot-2052-npm/node_modules/@relayflows/sdk /Users/khaliqgant/.local/share/mise/installs/node/22.22.2/bin/node node_modules/vitest/vitest.mjs run tests/authored-root.test.ts --maxWorkers=1 --minWorkers=1 + + RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/hosted-parallel/packages/sdk + +(node:61815) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) +(node:61815) [FLOWS_ROOT_LEASE_LOST] Warning: authored root run_id=root-run attempt=1: lease_conflict: attempt has no active worker lease. Waiting for the kernel to retry it. + ❯ tests/authored-root.test.ts (49 tests | 4 failed) 515ms + × durable authored root > pins a certified 'plain' body loaded with 'strip' to parallel mode before dispatch 36ms + → expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS1param' body loaded with 'strip' to parallel mode before dispatch 4ms + → expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS2annot' body loaded with 'strip' to parallel mode before dispatch 3ms + → expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + × durable authored root > pins a certified 'TS3ret' body loaded with 'strip' to parallel mode before dispatch 4ms + → expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + +⎯⎯⎯⎯⎯⎯⎯ Failed Tests 4 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'plain' body loaded with 'strip' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS1param' body loaded with 'strip' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS2annot' body loaded with 'strip' to parallel mode before dispatch + FAIL tests/authored-root.test.ts > durable authored root > pins a certified 'TS3ret' body loaded with 'strip' to parallel mode before dispatch +AssertionError: expected { executionMode: 'serial', …(9) } to match object { …(3) } +(11 matching properties omitted from actual) + +- Expected ++ Received + + Object { +- "executionMode": "parallel", ++ "executionMode": "serial", + "identityVersion": 2, +- "kind": "relayflows.authored-root.parallel.v3", ++ "kind": "relayflows.authored-root.v1", + } + + ❯ tests/authored-root.test.ts:241:42 + 239| { dataDir: '/unused', admissionKey: 'certified-group' }); + 240| const step = (journal.starts[0]!.spec as { steps: Array<{ instruct… + 241| expect(JSON.parse(step.instruction)).toMatchObject({ + | ^ + 242| kind: 'relayflows.authored-root.parallel.v3', identityVersion: 2… + 243| }); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/4]⎯ + + Test Files 1 failed (1) + Tests 4 failed | 45 passed (49) + Start at 20:13:08 + Duration 1.51s (transform 348ms, setup 11ms, collect 888ms, tests 515ms, environment 0ms, prepare 20ms) + + +exit_code=1 diff --git a/packages/sdk/src/authored-node-entry.ts b/packages/sdk/src/authored-node-entry.ts index b38936f25..e50b79376 100644 --- a/packages/sdk/src/authored-node-entry.ts +++ b/packages/sdk/src/authored-node-entry.ts @@ -84,7 +84,7 @@ try { } const { parallelLoadedBodyEligible } = await import('./authored-parallel-eligibility.js'); if (!await parallelLoadedBodyEligible(loaded.getDefinition(loaded.handle).body, - bytes.toString('utf8'), pathToFileURL(request.metadata.flowPath).href)) { + bytes.toString('utf8'), pathToFileURL(request.metadata.flowPath).href, 'transform')) { throw new AuthoredFlowExecutionError('step_failed', 'parallel f.run body lost its source certificate'); } } diff --git a/packages/sdk/src/authored-parallel-eligibility.ts b/packages/sdk/src/authored-parallel-eligibility.ts index 58ac7186b..6038847c6 100644 --- a/packages/sdk/src/authored-parallel-eligibility.ts +++ b/packages/sdk/src/authored-parallel-eligibility.ts @@ -18,16 +18,17 @@ export function parallelRunEligible(body: Function, moduleSource: string): boole } /** - * Match the body Node actually loaded against the same transformed source its + * Match the body Node actually loaded against the same type-stripped source its * TypeScript loader serves. First certify the original bytes: transforming * away a declaration must never make an unsafe module appear closed. */ export async function parallelLoadedBodyEligible( body: Function, moduleSource: string, sourceUrl: string, + mode: 'strip' | 'transform', ): Promise { if (!parallelPinnedSourceEligible(moduleSource)) return false; const { stripTypeScriptTypes } = await import('node:module') as unknown as { - stripTypeScriptTypes?: (source: string, options: { mode: 'transform'; sourceUrl: string }) => string; + stripTypeScriptTypes?: (source: string, options: { mode: 'strip' | 'transform'; sourceUrl: string }) => string; }; if (typeof stripTypeScriptTypes !== 'function') return false; // The pinned loader suppresses this one Node warning on its own thread. @@ -40,7 +41,10 @@ export async function parallelLoadedBodyEligible( if (rest[0] === 'ExperimentalWarning' && String(warning).startsWith('stripTypeScriptTypes')) return; (emitWarning as (...args: unknown[]) => void).call(process, warning, ...rest); }) as typeof process.emitWarning; - transformed = stripTypeScriptTypes(moduleSource, { mode: 'transform', sourceUrl }); + transformed = stripTypeScriptTypes(moduleSource, { mode, sourceUrl }); + } catch { + // A loader mode unavailable in this Node version cannot certify a body. + return false; } finally { process.emitWarning = emitWarning; } @@ -52,7 +56,7 @@ export async function parallelLoadedBodyEligible( * by its embedded Node child. Bun reformats function toString output, so its * in-memory body cannot be compared byte-for-byte with the source. Certify * the closed source module instead; the Node child loads those hash-pinned - * bytes and independently checks the body's grammar before any child starts. + * bytes and checks exact body/source identity and grammar before any child starts. */ export function parallelPinnedSourceEligible(moduleSource: string): boolean { const bodySource = certifiedModuleBody(moduleSource); diff --git a/packages/sdk/src/authored-root.ts b/packages/sdk/src/authored-root.ts index c8032300a..983e4c99a 100644 --- a/packages/sdk/src/authored-root.ts +++ b/packages/sdk/src/authored-root.ts @@ -108,7 +108,10 @@ export async function executeDurableAuthoredFlow( const { parallelLoadedBodyEligible, parallelPinnedSourceEligible } = await import('./authored-parallel-eligibility.js'); parallelEligible = parallelPromiseIntrinsicsIntact() && (process.versions['bun'] === undefined - ? await parallelLoadedBodyEligible(definition.body, source.toString('utf8'), pathToFileURL(loaded.sourcePath).href) + // Native imports strip by default, or transform with Node's opt-in + // flag. Require exact identity with one of those loader outputs. + ? await parallelLoadedBodyEligible(definition.body, source.toString('utf8'), pathToFileURL(loaded.sourcePath).href, 'strip') + || await parallelLoadedBodyEligible(definition.body, source.toString('utf8'), pathToFileURL(loaded.sourcePath).href, 'transform') : parallelPinnedSourceEligible(source.toString('utf8'))); if (!parallelEligible) parallelIneligibleReason = parallelPromiseIntrinsicsIntact() ? 'source_certificate' : 'runtime_intrinsics'; diff --git a/packages/sdk/tests/authored-node-admission.test.ts b/packages/sdk/tests/authored-node-admission.test.ts new file mode 100644 index 000000000..19abee3a3 --- /dev/null +++ b/packages/sdk/tests/authored-node-admission.test.ts @@ -0,0 +1,39 @@ +import { spawnSync } from 'node:child_process'; +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { expect, it } from 'vitest'; +import { parallelLoadedBodyEligible, parallelPinnedSourceEligible } from '../src/authored-parallel-eligibility.js'; + +const candidates = [ + ['TS1param', `async (f: any) => { + await Promise.all([f.run('A'), f.run('B')]); f.done('success'); }`], + ['TS2annot', `async f => { const w: string = await f.run('W'); + await Promise.all([f.run(w), f.run('B')]); f.done('success'); }`], + ['TS3ret', `async f => { + const a = async (): Promise => { await f.run('A'); }; + const b = async (): Promise => { await f.run('B'); }; + await Promise.all([a(), b()]); f.done('success'); }`], +]; + +// Use the real native importer, outside Vitest's TypeScript transform, to pin +// both loader outputs that npm admission must recognize by exact identity. +it.each(candidates)('certifies native Node imports of %s with and without transform-types', async (_name, candidate) => { + const directory = mkdtempSync(join(tmpdir(), 'authored-node-admission-')); + try { + const path = join(directory, 'body.mts'); + writeFileSync(path, `export default ${candidate};`); + const source = `import { flow } from '@relayflows/surface';\nexport default flow('typed', ${candidate});`; + expect(parallelPinnedSourceEligible(source)).toBe(true); + for (const flags of [[], ['--experimental-transform-types']]) { + const child = spawnSync(process.execPath, [...flags, '--input-type=module', '-e', + 'const {default: body} = await import(process.argv[1]); console.log(Function.prototype.toString.call(body));', + pathToFileURL(path).href], { encoding: 'utf8' }); + expect(child.status, child.stderr).toBe(0); + const loaded = new Function(`return (${child.stdout});`)() as Function; + const mode = flags.length === 0 ? 'strip' : 'transform'; + expect(await parallelLoadedBodyEligible(loaded, source, pathToFileURL(path).href, mode), flags.join(' ')).toBe(true); + } + } finally { rmSync(directory, { recursive: true, force: true }); } +}); diff --git a/packages/sdk/tests/authored-node-result.test.ts b/packages/sdk/tests/authored-node-result.test.ts index a697b64bb..2650e62ed 100644 --- a/packages/sdk/tests/authored-node-result.test.ts +++ b/packages/sdk/tests/authored-node-result.test.ts @@ -42,7 +42,16 @@ beforeEach(()=>{ })); }); describe('authored IPC result durable verification',()=>{ - it('accepts causal parallel ids only when every claimed child matches the durable root index', async () => { + it.each([ + ['matching', undefined], + ['forged edges', { after: ['forged-step'] }], + ['missing edges', { after: undefined }], + ['extra edge', { after: ['run-1', 'forged-step'] }], + ['missing truncation', { afterTruncated: undefined }], + ['false truncation', { afterTruncated: false }], + ['forged label', { label: 'forged-label' }], + ['missing label', { label: undefined }], + ] as const)('checks %s causal parallel claims against the durable root index', async (_name, forged) => { const dependent = `run-after-${'a'.repeat(64)}-1`; const claimed: AuthoredFlowExecutionResult = { rootRunId:'root', name:'example', completionReason:'success', journalSteps:[ @@ -64,13 +73,12 @@ describe('authored IPC result durable verification',()=>{ ...(step.afterTruncated === undefined ? {} : {afterTruncated:step.afterTruncated}), ...(step.label === undefined ? {} : {label:step.label})})); await expect(verifyAuthoredNodeResult(claimed,parallelMetadata,'root','socket')).resolves.toBeUndefined(); - for (const forged of [ - { after:['forged-step'] }, { afterTruncated:undefined }, { label:'forged-label' }, - ]) { + if (forged !== undefined) { const altered = { ...claimed, journalSteps:claimed.journalSteps.map((step,index) => index === 2 ? { ...step, ...forged } : step) }; - await expect(verifyAuthoredNodeResult(altered,parallelMetadata,'root','socket')) + await expect(verifyAuthoredNodeResult(altered as unknown as AuthoredFlowExecutionResult,parallelMetadata,'root','socket')) .rejects.toThrow('no matching durable completion'); + return; } streamRecords[1] = {...streamRecords[1],runId:'unclaimed-child'}; await expect(verifyAuthoredNodeResult(claimed,parallelMetadata,'root','socket')) @@ -84,8 +92,9 @@ describe('authored IPC result durable verification',()=>{ }); it('accepts only a declared, durably settled agent timeout', async () => { - const claimed = result(); - claimed.journalSteps[0]!.completionReason = 'timeout'; + const base = result(); + const claimed: AuthoredFlowExecutionResult = { ...base, journalSteps: base.journalSteps.map((step, index) => + index === 0 ? { ...step, completionReason: 'timeout' } : step) }; const step = { id: 'run-1', type: 'agent', timeout_ms: 1000 }; records.set('child-1', [ { entry_type: 'run.spawned', payload: { spec: { name: 'example/run-1', steps: [step] } } }, @@ -120,7 +129,7 @@ describe('authored IPC result durable verification',()=>{ ['a gate whose child run has no durable completion', [{id:'run-1.gate',runId:'child-missing',completionReason:'success'}]], ] as const)('refuses %s',async(_label,gateSteps)=>{ const base=result(); - const claimed:AuthoredFlowExecutionResult={...base,journalSteps:[base.journalSteps[0]!,...gateSteps,base.journalSteps[1]!]}; + const claimed={...base,journalSteps:[base.journalSteps[0]!,...gateSteps,base.journalSteps[1]!]} as unknown as AuthoredFlowExecutionResult; records.set('child-gate',entries(':','run-1.gate')); mocks.runGet.mockImplementation(async (run_id:string)=>{ if(run_id==='child-missing') return {run_id,status:'running',steps:{}}; diff --git a/packages/sdk/tests/authored-parallel-eligibility.test.ts b/packages/sdk/tests/authored-parallel-eligibility.test.ts index 9267a598c..51906517b 100644 --- a/packages/sdk/tests/authored-parallel-eligibility.test.ts +++ b/packages/sdk/tests/authored-parallel-eligibility.test.ts @@ -30,7 +30,7 @@ export default flow('hosted', async f => { expect(parallelPinnedSourceEligible(source.replace('Promise.all', 'Promise.race'))).toBe(false); }); - it('matches the Node-loaded body to transformed source, including TypeScript annotations', async () => { + it.each(['strip', 'transform'] as const)('matches the Node-loaded body to %s source, including TypeScript annotations', async mode => { const bodies = [ `async (f: any) => { await Promise.all([f.run('A'), f.run('B')]); f.done('success'); }`, `async f => { const w: string = await f.run('W'); @@ -41,13 +41,13 @@ export default flow('hosted', async f => { ]; for (const candidate of bodies) { const source = `import { flow } from '@relayflows/surface';\nexport default flow('typed', ${candidate});`; - const transformedBody = stripTypeScriptTypes(candidate, { mode: 'transform', sourceUrl: 'file:///typed.flow.ts' }); + const transformedBody = stripTypeScriptTypes(candidate, { mode, sourceUrl: 'file:///typed.flow.ts' }); const loaded = new Function(`return ${transformedBody}`)() as Function; expect(parallelPinnedSourceEligible(source), candidate).toBe(true); - expect(await parallelLoadedBodyEligible(loaded, source, 'file:///typed.flow.ts'), candidate).toBe(true); + expect(await parallelLoadedBodyEligible(loaded, source, 'file:///typed.flow.ts', mode), candidate).toBe(true); const substitute = body(`async f => { await Promise.all([f.run('other-a'), f.run('other-b')]); f.done('success'); }`); - expect(await parallelLoadedBodyEligible(substitute, source, 'file:///typed.flow.ts'), candidate).toBe(false); + expect(await parallelLoadedBodyEligible(substitute, source, 'file:///typed.flow.ts', mode), candidate).toBe(false); } }); diff --git a/packages/sdk/tests/authored-rollback-readback.test.ts b/packages/sdk/tests/authored-rollback-readback.test.ts new file mode 100644 index 000000000..4ebb8c8d6 --- /dev/null +++ b/packages/sdk/tests/authored-rollback-readback.test.ts @@ -0,0 +1,39 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { expect, it } from 'vitest'; +import { AUTHORED_PARALLEL_ROOT_KIND, AUTHORED_PARALLEL_ROOT_KIND_V3 } from '../src/authored-verdict.js'; + +// Optional compatibility check against an existing, unmodified published SDK. +// No daemon, install, source loading or authored effects are needed here. +const oldSdk = process.env['FLOWS_2052_SDK']; +it.skipIf(!oldSdk)('2.0.52 reads v2 but refuses v3 before any resume operation', async () => { + expect(JSON.parse(readFileSync(join(oldSdk!, 'package.json'), 'utf8')).version).toBe('2.0.52'); + const old = await import(/* @vite-ignore */ pathToFileURL(join(oldSdk!, 'dist/authored-root.js')).href); + const surface = { packageName: '@relayflows/surface', version: '2.0.52', + packageSha256: 'a'.repeat(64), runtimeSha256: 'b'.repeat(64) }; + const metadata = { kind: AUTHORED_PARALLEL_ROOT_KIND_V3, flowName: 'rollback', flowPath: '/unused.flow.ts', + sourceSha256: 'c'.repeat(64), surface, + sources: [{ path: '/unused.flow.ts', sourceSha256: 'c'.repeat(64), surface }], + extensions: [], inputPresent: false, + identityVersion: 2, executionMode: 'parallel' }; + const operations: string[] = []; + const journal = new Proxy({}, { get(_target, key) { + if (key === 'journalRead') return async () => { + operations.push('journalRead'); + return { entries: [{ entry_type: 'run.spawned', payload: { spec: { steps: [{ + id: 'authored-root', instruction: JSON.stringify(metadata), + surfaces: { streams: [{ stream: 'authored-root-rollback' }] }, + }] } } }] }; + }; + return () => { operations.push(String(key)); throw new Error(`unexpected operation: ${String(key)}`); }; + } }); + // Positive control: the old runtime recognizes the same metadata as v2. + metadata.kind = AUTHORED_PARALLEL_ROOT_KIND; + await expect(old.readAuthoredRootMetadata(journal, 'root')).resolves.toMatchObject({ kind: metadata.kind }); + metadata.kind = AUTHORED_PARALLEL_ROOT_KIND_V3; + operations.length = 0; + await expect(old.resumeDurableAuthoredFlow('root', journal, { dataDir: '/unused' })) + .rejects.toThrow('authored root journal has malformed authority metadata'); + expect(operations).toEqual(['journalRead']); +}); diff --git a/packages/sdk/tests/authored-root.test.ts b/packages/sdk/tests/authored-root.test.ts index 6a81a4cb7..59f9db3a6 100644 --- a/packages/sdk/tests/authored-root.test.ts +++ b/packages/sdk/tests/authored-root.test.ts @@ -207,13 +207,25 @@ describe('durable authored root', () => { } }); - it('pins a certified Promise.all run body to parallel mode before dispatch', async () => { - const bodySource = `async f => { + it.each([ + ['plain', 'async f', ''], + ['TS1param', 'async (f: any)', ''], + ['TS2annot', 'async f', "const w: string = await f.run('printf W');"], + ['TS3ret', 'async f', ''], + ].flatMap(([name, signature, prelude]) => (['strip', 'transform'] as const) + .map(mode => ({ name, signature, prelude, mode }))))( + 'pins a certified $name body loaded with $mode to parallel mode before dispatch', async ({ name, signature, prelude, mode }) => { + const branchType = name === 'TS3ret' ? ': Promise' : ''; + const bodySource = `${signature} => { + ${prelude} + const a = async ()${branchType} => { await f.run('printf A'); }; + const b = async ()${branchType} => { await f.run('printf B'); }; + await Promise.all([a(), b()]); await Promise.all([f.run('printf a'), f.run('printf b')]); f.done('success'); }`; const source = `import { flow } from '@relayflows/surface';\nexport default flow('flagship', ${bodySource});\n`; - const transformed = stripTypeScriptTypes(source, { mode: 'transform', sourceUrl: 'file:///flagship.flow.ts' }); + const transformed = stripTypeScriptTypes(source, { mode, sourceUrl: 'file:///flagship.flow.ts' }); const file = ts.createSourceFile('flagship.js', transformed, ts.ScriptTarget.ES2022, true, ts.ScriptKind.JS); const exported = file.statements[1] as ts.ExportAssignment; const call = exported.expression as ts.CallExpression; @@ -366,7 +378,7 @@ describe('durable authored root', () => { { dataDir: '/unused', admissionKey: 'with-detail' }, ); - expect(result.completionDetail).toBe('review found 1 P2: review.clean was not created'); + expect(result).toMatchObject({ completionDetail: 'review found 1 P2: review.clean was not created' }); expect(journal.peer.outputs.at(-1)).toMatchObject({ name: 'flagship', completionReason: 'step_failed', @@ -424,8 +436,8 @@ describe('durable authored root', () => { { dataDir: '/unused', admissionKey: 'legacy-no-detail' }, ); - expect(result.completionReason).toBe('step_failed'); - expect(result.completionDetail).toBeUndefined(); + expect(result).toMatchObject({ completionReason: 'step_failed' }); + expect('completionDetail' in result ? result.completionDetail : undefined).toBeUndefined(); expect('completionDetail' in result).toBe(false); }); @@ -806,7 +818,7 @@ describe('durable authored root', () => { async function fixture( fails = false, delayMs = 0, - body?: Parameters[1], + body?: Parameters[2], ): Promise { const directory = await mkdtemp(join(tmpdir(), 'authored-root-')); directories.push(directory);