Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
87c78a1
Run authored deterministic children concurrently
miyaontherelay Oct 16, 2026
ad43f51
Keep parallel branch continuations replay stable
miyaontherelay Oct 16, 2026
184827e
Release parallel branch results without replay conflicts
miyaontherelay Oct 16, 2026
ec3d1fb
Use causal frontier identity for parallel run joins
miyaontherelay Oct 16, 2026
ea75fe3
Document replay identity and upgrade boundary for parallel runs
miyaontherelay Oct 16, 2026
d30f13d
Pin safe parallel run mode per authored root
miyaontherelay Oct 16, 2026
f417284
Certify parallel authored roots with strict source grammar and replay…
miyaontherelay Oct 16, 2026
78e7d1f
Keep authored parallel replay identities safe across Promise variants
miyaontherelay Oct 16, 2026
41c155f
Guard authored parallel replay against unobserved joins
miyaontherelay Oct 16, 2026
e3503c0
Certify authored parallel bodies against replay-unsafe joins
miyaontherelay Oct 16, 2026
3e008e9
Guard certified parallel joins against untracked promises and preloads
miyaontherelay Oct 16, 2026
2116fe8
Guard parallel resume and inconsistent Promise joins
miyaontherelay Oct 16, 2026
8f72bb3
Report completed parallel resumes before runtime refusal
miyaontherelay Oct 17, 2026
63c9994
Document review 9 follow-up verification
miyaontherelay Oct 17, 2026
d189139
Keep serial alias replay check compatible with #674
miyaontherelay Oct 18, 2026
4c0595b
Close authored admissions before memory write drain
miyaontherelay Oct 18, 2026
de1adbb
Keep watch checks fast while preserving parallel admission
miyaontherelay Oct 18, 2026
00ada4b
Use realistic lease in source authority test
miyaontherelay Oct 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/cloud-runtime-artifact.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,12 @@ jobs:
# npm pack in this test invokes prepare and rewrites SDK dist even
# with --ignore-scripts. Run it after the built-CLI readers so every
# test still gates without a concurrent partial rebuild.
# Watch tests spawn a fresh CLI for each report and keep five-second
# case deadlines. Run them apart from the concurrent live-test pool.
RELAYFLOWD_BIN="$GITHUB_WORKSPACE/kernel/target/release/relayflowd" \
./node_modules/.bin/vitest run --exclude tests/cli-package-gate.test.ts
./node_modules/.bin/vitest run tests/cli-watch.test.ts
RELAYFLOWD_BIN="$GITHUB_WORKSPACE/kernel/target/release/relayflowd" \
./node_modules/.bin/vitest run --exclude tests/cli-package-gate.test.ts --exclude tests/cli-watch.test.ts
RELAYFLOWD_BIN="$GITHUB_WORKSPACE/kernel/target/release/relayflowd" \
./node_modules/.bin/vitest run tests/cli-package-gate.test.ts

Expand Down
105 changes: 82 additions & 23 deletions docs/SURFACE.md
Original file line number Diff line number Diff line change
Expand Up @@ -957,47 +957,106 @@ concurrent flow closes.
- *Behaviour:* the replacement delegates to the intrinsic and is specified to
behave identically. A non-iterable argument is handed straight through, so
`Promise.all(5)` and `Promise.all(null)` return the same rejected promises the
intrinsic returns; `name` and `length` match. If `Promise.all` has already been
replaced by something else, the flow refuses to start rather than fighting over
the intrinsic.
intrinsic returns; `name` and `length` match. A replacement after the observer
is installed violates the authored lifecycle contract. A process that starts
with a Promise preload uses the existing serial scheduler for new roots.

If a process-wide intrinsic replacement is unacceptable in your deployment, do
not run authored TypeScript bodies in that process; the declarative YAML path
does not install it.

### `f.run` on the local daemon: one at a time, in the run's environment

Two properties of `f.run` on a local `relayflowd` surprise people. Both are how
the current kernel works, not promises of the Surface.

**Within one run, `f.run` steps run one after another.** `Promise.all` over two
`f.run('sleep 2')` steps takes about 4s, not 2s. The SDK starts both steps at
once, but the body talks to the daemon over a single connection. The daemon
handles each connection's requests in order (`kernel/relayflowd/src/server.rs`),
and the request that starts a deterministic step returns only after the
command exits (`engine/drive.rs` → `exec_det.rs`). A declared `budget` also
queues operations in order (`authored-budget.ts`). `Promise.all` stays correct
authoring: results come back in declaration order, and the steps will overlap
once the kernel dispatches in parallel.
### `f.run` on the local daemon: concurrent children, in the run's environment

Two properties of `f.run` on a local `relayflowd` are useful to know.

**Certified `Promise.all` groups of `f.run` steps overlap.** A direct
`Promise.all` over two `f.run('sleep 2')` steps takes about 2s of step time.
The root records its execution mode before its body runs. Parallel mode is
selected only for a narrow source grammar: awaited `Promise.all` groups of
direct `f.run` calls, and locally declared async branches made only of awaited
`f.run` calls or direct run groups. Every branch must contain a direct `f.run`.
The entry module must import `{ flow }` directly from `@relayflows/surface` and
pass the body arrow literally to `export default flow('name', async f => {...})`.
The entry
has no other top-level statements in parallel mode. Re-exports,
local flow factories, and imported flow definitions use serial mode, because
their body's Promise binding cannot be certified from the entry file. Other
imports also use serial mode because they may alter Promise before the body
starts. The defining module must leave the intrinsic `Promise` and
`globalThis.Promise` unshadowed; a module that binds either name uses serial
mode. An authored process started with a `--import`, `--require`, or loader
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,
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
deterministic children at once; additional calls wait for a slot. `Promise.all`
returns results in declaration order while each continuation receives its
result as soon as that child completes. Follow-up ids inside a certified
concurrent region use causal predecessor identities for replay.

Other bodies keep the established serial `f.run` scheduler and `kind-N` step
ids. This includes `Promise.race`, `Promise.any`, eager background work,
timers, closure-shared promises or mutable results, other `f.*` effects,
dispatched flows, and a declared `budget`.
Those bodies continue to run; the fallback only changes whether their
deterministic child commands overlap. A step whose source cannot be
certified also uses serial mode. The mode is immutable for the root's lifetime.

Each child's admission key comes from the root run and step id. Each child has
its own journal; the root's child index records completion facts as they arrive, and
readers match them by step id and run id.
In parallel mode, if the bounded causal walk cannot identify a follow-up's
full predecessor frontier, the frontier passes through an unregistered promise
aggregate, a Promise constructor, method, or combinator has been replaced, a
non-run effect appears, or a new initial child
appears after the body's first turn, the root fails before admitting that
effect rather than issuing an ambiguous replay key. Once this happens, no
later child is admitted. The failure remains a root failure even when authored
code catches the immediate exception. Children already in flight finish and
are journaled before the root reports that failure; the root may therefore
take as long as its slowest admitted child to fail.
The same refusal applies if a fulfilled `Promise.all` or settled
`Promise.allSettled` still has a pending recorded member, which means the join
did not actually wait for its declared branches.

If one concurrent `f.run` fails, the authored body fails and its waiting
siblings are cancelled before admission. Children already admitted can finish
their own journal work; their outcomes cannot turn the failed body into a
success. A child in a separate `f.dispatch` body follows that body's own
failure and capacity rules.

**Upgrade from 2.0.50 or 2.0.51:** roots admitted before execution modes were journaled
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
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.

What runs concurrently today:

- `f.agent` and `f.llm` steps, up to `--agent-capacity`. They run in worker
processes, not in the daemon's request path.
- Separate `flows run` invocations. Each has its own connection to the daemon,
so their `f.run` steps overlap. For parallel shell work, start the lanes as
separate runs.
- Separate `flows run` invocations. Their steps overlap as well.
- Inside one step, a command that waits on its own background jobs, such as
`f.run('a & b & wait')`. A single `f.run` cannot start a background job and
return before it finishes: the step waits on the command's output pipes.

`f.dispatch` children share their parent body's connection, so they are
serialized too.
`f.dispatch` roots currently use serial `f.run` scheduling.

**`f.run` gets the environment of the `flows run` that started the run,
but the daemon's working directory.** Every `flows run` on a data dir attaches
to the same `relayflowd`, which spawns the shell steps. The CLI sends its
`process.env` with the run, and that run's steps are spawned with exactly
`process.env` with each child run, and that run's steps are spawned with exactly
that environment, plus `FLOWS_INPUT` and `RELAYFLOW_MEMORY`. Two runs on one
daemon each see their own variables. `flows resume` sends the resuming CLI's
environment, which replaces the run's from then on.
Expand Down
78 changes: 78 additions & 0 deletions evidence/native-parallel-ci-2026-10-18/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# #667 Linux artifact CI follow-up

The `linux-x64-artifact` job at `63c9994d` failed one serial-only live test
with `run_admission_conflict` and four `flows check --watch` tests with their
5-second test timeouts. The full job log is in GitHub Actions run
`39043415569`, job `117427280321`.

## Serial arguments-alias test

The arguments-alias LLM fixture is certified **serial** before it is killed.
Its old assertion required every resumed run to exit zero, even though the
serial scheduler can report the existing #674 `run_admission_conflict` when
concurrent branches resume in a different order; this also occurs on
2.0.50/2.0.51. The revised test still requires serial mode, one L and one R
effect, at most one Z effect, and exactly two LLM requests. A nonzero resume
may only be exit 1 with `run_admission_conflict`. No parallel-mode expectation
changed. Its child CLI timeout is now 90 seconds so a slow macOS host does
not produce a null exit status after the fixture's former 30-second limit.

`npm run typecheck:tests` passed. A focused six-repetition macOS run reached
the test's 240-second limit; it did not provide a result for the revised
assertions. The new Linux CI run is the required rerun of that live fixture.

## Watch timeout comparison

The watch test and its direct implementation files are byte-identical at
`origin/main` (`5de41849`, Flows 2.0.51) and this PR head. The comparison
included `cli-watch.ts`, `dir-watcher-poller.ts`, `cli/check.ts`,
`cli/check-typescript.ts`, and `cli-watch.test.ts`; `git diff --name-only`
returned no paths and both test blobs are `b43c7966f32a9369fcf0bb0bd57d19aa73f490a3`.

In an isolated main worktree, a focused rerun of the four CI timeout cases
timed out on the transitive-import and missing-import cases at 5 seconds;
the nearer-config and delete/recreate cases passed. A branch rerun of all 11
watch tests passed nine and timed out on two cases. Raising only Vitest's
outer test timeout to 20 seconds still left two failures at the test's inner
5-second `expect.poll` deadline. The changing cases and unchanged watch code
show a pre-existing timing sensitivity, outside the parallel scheduler.
Watch code and assertions were left untouched.

Local comparison commands used Node 26.11.1 and Bun 1.4.2 on macOS:

```sh
git diff --name-only origin/main HEAD -- packages/sdk/tests/cli-watch.test.ts packages/sdk/src/cli-watch.ts packages/sdk/src/dir-watcher-poller.ts packages/sdk/src/cli/check.ts packages/sdk/src/cli/check-typescript.ts
(cd ../main-control/packages/sdk && npx vitest run tests/cli-watch.test.ts -t 'watches transitive|refreshes the import graph|detects a nearer config|keeps watching after')
(cd flows-parallel/packages/sdk && npx vitest run tests/cli-watch.test.ts --testTimeout=20000)
```

The adjacent logs are the local main and branch reruns. GitHub Actions uses
Node 22.23.3 and Bun 1.4.0, so the new CI job remains the Linux check.

## Same-workflow main control and startup diagnosis

The same `linux-x64-artifact` workflow was dispatched on main `5de41849` as
[run 39094544639](https://github.com/AgentWorkforce/flows/actions/runs/39094544639).
Its `Test SDK and type-level authoring contracts` step passed, including all
11 `cli-watch.test.ts` cases (23.9 seconds for that file), and the full
workflow concluded successfully.
This makes the four timeout failures on #667's
[run 39084034162](https://github.com/AgentWorkforce/flows/actions/runs/39084034162)
specific to that full-suite execution, despite unchanged watch source/tests.

The parallel scheduler introduced eager TypeScript parser imports through
`authored-root.ts` and `authored-flow-executor.ts`. Eight alternating local
one-shot YAML checks measured warm median startup of 299 ms at the old #667
head versus 197 ms for published 2.0.51. With those two imports deferred to
authored run admission/execution, the same paired check measured 189 ms versus
191 ms for published 2.0.51. The parser still uses the startup snapshot of
`Function.prototype.toString`, and both Promise/Array intrinsics are rechecked
after each async import before parallel admission or body execution.

The four failing watch cases spawn a fresh check CLI for each report and keep
five-second outer case deadlines. Running the existing 11 watch tests in their
own Vitest invocation passed 11/11 in 10.9 seconds, with each affected case
under 1.7 seconds. The local test script and Linux artifact workflow now run
that file separately from concurrent live-test workers; the test cases and
their deadlines are unchanged. Focused parser, parallel overlap, queued-child
failure, and crash/resume tests passed after the deferred-import change.
19 changes: 19 additions & 0 deletions evidence/native-parallel-ci-2026-10-18/arguments-alias-macos.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/flows-parallel/packages/sdk

❯ tests/authored-parallel-run-live.test.ts (42 tests | 1 failed | 41 skipped) 240108ms
× parallel authored deterministic steps > keeps an arguments-alias LLM body serial without replay across six resumes 240092ms
→ Test timed out in 240000ms.
If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout".

⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯

FAIL tests/authored-parallel-run-live.test.ts > parallel authored deterministic steps > keeps an arguments-alias LLM body serial without replay across six resumes
Error: Test timed out in 240000ms.
If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout".
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯

Test Files 1 failed (1)
Tests 1 failed | 41 skipped (42)
Start at 02:40:57
Duration 281.32s (transform 6.95s, setup 725ms, collect 39.17s, tests 240.11s, environment 0ms, prepare 817ms)
65 changes: 65 additions & 0 deletions evidence/native-parallel-ci-2026-10-18/branch-watch.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@

RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/flows-parallel/packages/sdk

❯ tests/cli-watch.test.ts (11 tests | 2 failed) 31898ms
× flows check --watch > forwards required daemon validation through watch despite the offline test environment 5257ms
→ Matcher did not succeed in 5000ms
✓ flows check --watch > rechecks syntax errors, clears once, and returns the last refusal on Ctrl-C 3514ms
✓ flows check --watch > streams JSON lines without ANSI, recovers after atomic saves, and exits zero after repair 2100ms
✓ flows check --watch > coalesces 20 concurrent saves into at most two rechecks 2090ms
✓ flows check --watch > watches transitive relative use imports, cycles, and nearest config changes 2693ms
✓ flows check --watch > refreshes the import graph and notices missing imports being created 2343ms
✓ flows check --watch > reloads authored TypeScript instead of reusing the first imported definition 4437ms
× flows check --watch > detects a nearer config appearing and falls back after it is deleted 6338ms
→ Matcher did not succeed in 5000ms
✓ flows check --watch > keeps watching after the target is deleted and recreated 2144ms
✓ flows check --watch > queues changes during a slow check without overlapping checks 782ms

⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯

FAIL tests/cli-watch.test.ts > flows check --watch > forwards required daemon validation through watch despite the offline test environment
Error: Matcher did not succeed in 5000ms
❯ until tests/cli-watch.test.ts:59:65
57|
58| async function until(predicate: () => boolean): Promise<void> {
59| await expect.poll(predicate, { timeout: 5000, interval: 20 }).toBe(t…
| ^
60| }
61|
❯ tests/cli-watch.test.ts:78:11

Caused by: AssertionError: expected false to be true // Object.is equality

- Expected
+ Received

- true
+ false

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯

FAIL tests/cli-watch.test.ts > flows check --watch > detects a nearer config appearing and falls back after it is deleted
Error: Matcher did not succeed in 5000ms
❯ until tests/cli-watch.test.ts:59:65
57|
58| async function until(predicate: () => boolean): Promise<void> {
59| await expect.poll(predicate, { timeout: 5000, interval: 20 }).toBe(t…
| ^
60| }
61|
❯ tests/cli-watch.test.ts:181:11

Caused by: AssertionError: expected false to be true // Object.is equality

- Expected
+ Received

- true
+ false

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯

Test Files 1 failed (1)
Tests 2 failed | 9 passed (11)
Start at 02:39:22
Duration 82.70s (transform 9.71s, setup 303ms, collect 49.59s, tests 31.90s, environment 0ms, prepare 398ms)
25 changes: 25 additions & 0 deletions evidence/native-parallel-ci-2026-10-18/main-watch.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

RUN v2.1.9 /Volumes/Paris Drive/AgentWorkforce/swarm/ms-flows-parallel/main-control/packages/sdk

❯ tests/cli-watch.test.ts (11 tests | 2 failed | 7 skipped) 13410ms
× flows check --watch > watches transitive relative use imports, cycles, and nearest config changes 5025ms
→ Test timed out in 5000ms.
If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout".
× flows check --watch > refreshes the import graph and notices missing imports being created 5041ms
→ Test timed out in 5000ms.
If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout".
✓ flows check --watch > detects a nearer config appearing and falls back after it is deleted 1684ms
✓ flows check --watch > keeps watching after the target is deleted and recreated 1582ms

⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯

FAIL tests/cli-watch.test.ts > flows check --watch > watches transitive relative use imports, cycles, and nearest config changes
FAIL tests/cli-watch.test.ts > flows check --watch > refreshes the import graph and notices missing imports being created
Error: Test timed out in 5000ms.
If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout".
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯

Test Files 1 failed (1)
Tests 2 failed | 2 passed | 7 skipped (11)
Start at 02:36:12
Duration 49.80s (transform 7.16s, setup 377ms, collect 35.52s, tests 13.41s, environment 0ms, prepare 236ms)
1 change: 1 addition & 0 deletions evidence/native-parallel-review5/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.log -whitespace
7 changes: 7 additions & 0 deletions evidence/native-parallel-review5/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM node:22-trixie
RUN mkdir -p /bench && cd /bench && npm install --ignore-scripts \
@relayflows/sdk@2.0.51 @relayflows/surface@2.0.51 \
@relayflows/runtime-linux-x64@2.0.51 bluebird@3.7.2
COPY dist/ /bench/node_modules/@relayflows/sdk/dist/
COPY replay.mjs /bench/replay.mjs
WORKDIR /bench
Loading
Loading