From cd2f47778d09e974dd6b673837755045e43e6a2b Mon Sep 17 00:00:00 2001 From: Relayflow Lead Date: Sat, 29 Aug 2026 16:17:14 -0400 Subject: [PATCH 1/2] test: pin the dispatch-ordering contract that gate 2 turns on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A run started with no worker parks with its step in `runnable`. Attaching a worker afterwards does NOT re-drive it — attach_worker (server/session.rs) registers the worker and nothing revisits parked runs. `run.resume` is the primitive that picks it back up. Measured, not assumed: after start, no worker: status=running steps=analyze-story=runnable attach alone: NO_DISPATCH after run.resume: DISPATCHED step=analyze-story This is why gate 2 could not be shown executing: the HN demo submits events while nothing is attached, so every woken run parks and stays parked. The kernel executes it fine — nothing was resuming it. Either attach the worker before submitting, or resume afterwards. This is a characterization test, not a regression test: it passes on main and asserts BOTH directions, so it fails if attach ever starts re-driving parked runs or if resume stops dispatching. I am not claiming it was seen to fail — the behaviour it pins is current behaviour, deliberately. Two wrong diagnoses preceded this and are worth recording so they are not repeated: first that the Covenant 2 attach preflight and validate_agent_pins deadlocked for a surface-less agent step (they do not — such a step dispatches when the worker attaches first), and then that attach failing to re-drive was itself a kernel defect (it is not — resume is the mechanism). The first was caught only by reverting the fix and finding the test still passed. Verified: sdk 182 passed, tsc clean. Co-Authored-By: Claude Fable 5 --- sdk/tests/live-kernel.test.ts | 55 +++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/sdk/tests/live-kernel.test.ts b/sdk/tests/live-kernel.test.ts index d644af591..3cc2ea44a 100644 --- a/sdk/tests/live-kernel.test.ts +++ b/sdk/tests/live-kernel.test.ts @@ -7,6 +7,7 @@ import { readdirSync, rmSync, writeFileSync, + readFileSync, } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; @@ -170,6 +171,60 @@ steps: expect(completed.stderr).not.toContain('protocol_error'); }); + it('needs run.resume to reach a worker that attached after the run parked', async () => { + // The contract that cost the most time to establish, so it is pinned here. + // + // A run started with no worker parks with its step in `runnable`. Attaching + // a worker afterwards does NOT re-drive it — `attach_worker` + // (server/session.rs) registers the worker and nothing revisits parked + // runs. That is deliberate, not a defect: the run is driven by whoever + // started it, and `run.resume` is the primitive that picks it back up. + // + // This matters for gate 2. The HN demo submits events while nothing is + // attached, so every woken run parks and stays parked — not because the + // kernel cannot execute it, but because nothing resumes it. Either attach + // the worker BEFORE submitting, or resume afterwards. + const dataDir = temporaryDirectory('flows-live-resume-'); + await startDaemon(dataDir); + // Start from a canonical spec rather than compiling yaml: this test is + // about dispatch ordering, not about the authoring surface. + const spec = JSON.parse( + readFileSync(join(TESTDATA, 'hn-monitor.spec.canonical.json'), 'utf8'), + ) as Parameters[0]; + + const starter = await connectClient(dataDir); + await starter.hello('live-resume-starter'); + const started = await starter.runStart(spec); + const runId = started.run_id; + await new Promise((resolve) => setTimeout(resolve, 1500)); + + const worker = await connectClient(dataDir); + await worker.hello('live-resume-worker'); + const dispatched = eventOnce(worker, 'step.dispatch'); + await worker.workerAttach('live-resume-agent', ['agent'], { + workspace: [{ surface: 'repo', revision_id: 'rev-a' }], + streams: [], + }); + + // Attaching alone must not rescue the parked run. + const passive = await Promise.race([ + dispatched, + new Promise((resolve) => setTimeout(() => resolve(null), 4000)), + ]); + expect(passive, 'attaching a worker must not by itself re-drive a parked run').toBeNull(); + + // run.resume is what does. + await starter.runResume(runId); + const resumed = await Promise.race([ + dispatched, + new Promise((resolve) => setTimeout(() => resolve(null), 8000)), + ]); + expect(resumed, 'run.resume must dispatch the parked step to the attached worker').not.toBeNull(); + + worker.close(); + starter.close(); + }, 45_000); + it('reports a real manual-recovery NeedsHuman state as parked', async () => { const dataDir = temporaryDirectory('flows-live-human-'); await startDaemon(dataDir); From d057ece71ff19887f065a6963d7b5b620eef48c1 Mon Sep 17 00:00:00 2001 From: Relayflow Lead Date: Sat, 29 Aug 2026 16:24:37 -0400 Subject: [PATCH 2/2] test: do not assert that attach leaves a parked run stranded (PR #36 review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review was right. Asserting `passive === null` made today's behaviour a contract, which would block a kernel that re-elects parked steps when a worker attaches — a change that may well be the right one. The test now records that as an observation and passes either way: if attach alone dispatches, the requirement is already met and the test returns; if it does not, run.resume must be able to pick the run up. What is pinned is the requirement — a parked run can always be got to a late-attaching worker — not the mechanism. Verified: sdk live-kernel 8 passed, tsc clean. Co-Authored-By: Claude Fable 5 --- sdk/tests/live-kernel.test.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/sdk/tests/live-kernel.test.ts b/sdk/tests/live-kernel.test.ts index 3cc2ea44a..2f2b0777b 100644 --- a/sdk/tests/live-kernel.test.ts +++ b/sdk/tests/live-kernel.test.ts @@ -171,7 +171,7 @@ steps: expect(completed.stderr).not.toContain('protocol_error'); }); - it('needs run.resume to reach a worker that attached after the run parked', async () => { + it('can always get a parked run to a late-attaching worker', async () => { // The contract that cost the most time to establish, so it is pinned here. // // A run started with no worker parks with its step in `runnable`. Attaching @@ -206,14 +206,24 @@ steps: streams: [], }); - // Attaching alone must not rescue the parked run. + // Observation, deliberately NOT an assertion: today, attaching alone does + // not rescue the parked run. Review pushed back on asserting that (PR #36) + // and was right — pinning it would freeze a design decision that is still + // open, and block a future kernel that re-elects parked steps on attach. + // Either behaviour is acceptable here; what must hold is the line below. const passive = await Promise.race([ dispatched, new Promise((resolve) => setTimeout(() => resolve(null), 4000)), ]); - expect(passive, 'attaching a worker must not by itself re-drive a parked run').toBeNull(); + if (passive !== null) { + // A kernel that re-drives on attach has satisfied the real requirement + // already — the step reached a worker. Nothing further to prove. + worker.close(); + starter.close(); + return; + } - // run.resume is what does. + // Otherwise run.resume must be able to pick it up. await starter.runResume(runId); const resumed = await Promise.race([ dispatched,