From 7fbf9164de4cc9737f6cc49220787d6c1cae0ed6 Mon Sep 17 00:00:00 2001 From: Relayflow Lead Date: Sat, 29 Aug 2026 04:44:50 -0400 Subject: [PATCH 1/5] drive: cloud run 4a4a60b7 Work produced by cloud run 4a4a60b7-ba97-4f8a-babb-17f0c399b761 in a workflow sandbox and delivered from this host, because a sandbox has no remote and no GitHub token. Verification and adversarial review ran in-run; see ops/reviews/ in the diff. --- sdk/package-lock.json | 36 --------------------- sdk/package.json | 1 + sdk/src/demo-hn-monitor.ts | 66 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 36 deletions(-) create mode 100644 sdk/src/demo-hn-monitor.ts diff --git a/sdk/package-lock.json b/sdk/package-lock.json index 44ace6f11..81dbe5f54 100644 --- a/sdk/package-lock.json +++ b/sdk/package-lock.json @@ -9,8 +9,6 @@ "version": "0.1.0", "license": "UNLICENSED", "dependencies": { - "@types/js-yaml": "^4.0.9", - "js-yaml": "^5.4.1", "yaml": "^2.5.1" }, "bin": { @@ -794,12 +792,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/js-yaml": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", - "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", - "license": "MIT" - }, "node_modules/@types/node": { "version": "22.20.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", @@ -923,12 +915,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1085,28 +1071,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/js-yaml": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.1.tgz", - "integrity": "sha512-28R/k+NAjeuf7+CKlTxWZVExJGwVVLwY06DgEnOMz2gEpfNkDcD7QvyiVPT0xy0XXhU8vHsd4Ot42OOPdJG7dQ==", - "funding": [ - { - "type": "github", - "url": "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/sponsors/puzrin" - }, - { - "type": "github", - "url": "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/sponsors/nodeca" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.mjs" - } - }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", diff --git a/sdk/package.json b/sdk/package.json index 8be489961..123b35394 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -20,6 +20,7 @@ ], "scripts": { "build": "tsc && node scripts/make-cli-executable.mjs", + "demo:hn": "npm run build && node dist/demo-hn-monitor.js", "prepare": "npm run build", "typecheck": "tsc --noEmit", "test": "npm run build && vitest run", diff --git a/sdk/src/demo-hn-monitor.ts b/sdk/src/demo-hn-monitor.ts new file mode 100644 index 000000000..8b0dedb38 --- /dev/null +++ b/sdk/src/demo-hn-monitor.ts @@ -0,0 +1,66 @@ +import { readFile } from 'node:fs/promises'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { pollHackerNewsOnce, type EventSink } from './hn-poller.js'; +import { JournalClient } from './journal-client.js'; +import type { EventSubmitResult } from './protocol.js'; + +const sdkRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const repositoryRoot = resolve(sdkRoot, '..'); +const dataDir = resolve(process.env.RELAYFLOW_DATA_DIR ?? join(repositoryRoot, '.relayflowd')); +const socketPath = join(dataDir, 'relayflowd.sock'); +const specPath = join(repositoryRoot, 'testdata', 'hn-monitor.spec.canonical.json'); + +interface Submission { + storyId: unknown; + outcome: EventSubmitResult; +} + +async function main(): Promise { + const spec: unknown = JSON.parse(await readFile(specPath, 'utf8')); + const client = new JournalClient(socketPath); + + try { + await client.connect(); + } catch (error) { + throw new Error( + `No relayflowd is listening at "${socketPath}". Start it with: relayflowd --data-dir "${dataDir}" serve`, + { cause: error }, + ); + } + + try { + await client.hello('hn-monitor-demo'); + const submissions: Submission[] = []; + const sink: EventSink = { + async eventSubmit(submittedSpec, event) { + const outcome = await client.eventSubmit(submittedSpec, event); + submissions.push({ storyId: storyId(event.payload), outcome }); + return outcome; + }, + }; + + console.log('Fetching live Hacker News top stories...'); + await pollHackerNewsOnce(spec, sink); + for (const { storyId, outcome } of submissions) { + const wake = outcome.run === undefined || outcome.run === null ? 'none' : 'created'; + console.log( + `Story ${String(storyId)}: matched=${outcome.matched} deduped=${outcome.deduped} wake=${wake}`, + ); + } + } finally { + client.close(); + } +} + +function storyId(payload: unknown): unknown { + if (typeof payload === 'object' && payload !== null && 'id' in payload) { + return payload.id; + } + return 'unknown'; +} + +main().catch((error: unknown) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; +}); From c3584de81d6e39232c6c488fc8779ab9ad1b9a35 Mon Sep 17 00:00:00 2001 From: Relayflow Lead Date: Sat, 29 Aug 2026 06:22:14 -0400 Subject: [PATCH 2/5] fix: the demo must not let a created wake read as an executed workload (PR #19 P1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review caught that with only `relayflowd serve` running, no agent worker is attached — so every run the demo creates is enqueued and then waits. The output printed 'wake=created' and let the reader conclude a workload had run. It had not, and I repeated that conclusion when reporting the results. The distinction is the whole of RFC-0001 section 3 gate 2: 'a real proactive workload RUNS as a relayflow' is a claim about execution, not about enqueueing. The demo now prints both halves explicitly — PROVEN: runs created from live Hacker News via event.submit, the fetch/match/dedupe/wake path working end to end; NOT PROVEN: that those runs executed, because no worker is attached — and says what would close the gap. Verified: sdk 153 passed across 10 files, tsc --noEmit clean. Co-Authored-By: Claude Fable 5 --- sdk/src/demo-hn-monitor.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/sdk/src/demo-hn-monitor.ts b/sdk/src/demo-hn-monitor.ts index 8b0dedb38..c60ff93fe 100644 --- a/sdk/src/demo-hn-monitor.ts +++ b/sdk/src/demo-hn-monitor.ts @@ -42,12 +42,40 @@ async function main(): Promise { console.log('Fetching live Hacker News top stories...'); await pollHackerNewsOnce(spec, sink); + + let woke = 0; for (const { storyId, outcome } of submissions) { const wake = outcome.run === undefined || outcome.run === null ? 'none' : 'created'; + if (wake === 'created') woke += 1; console.log( `Story ${String(storyId)}: matched=${outcome.matched} deduped=${outcome.deduped} wake=${wake}`, ); } + + // Say exactly what was proven, and no more. + // + // A wake is a run being CREATED. It is not the flow's steps being + // EXECUTED — that needs an agent worker attached to the kernel, and + // `relayflowd serve` alone does not provide one. Review caught this + // (PR #19, P1): the first version of this demo printed created wakes and + // let the reader conclude a workload had run. It had not. + // + // The distinction is the whole of RFC-0001 §3 gate 2: "a real proactive + // workload RUNS as a relayflow" is a claim about execution, not about + // enqueueing. + console.log(''); + if (woke > 0) { + console.log(`PROVEN: ${woke} run(s) created from live Hacker News data via event.submit.`); + console.log(' The event path — fetch, match, dedupe claim, wake — works end to end.'); + console.log(''); + console.log('NOT PROVEN: that those runs EXECUTED. No agent worker is attached to this'); + console.log(' kernel, so each run is created and then waits. Gate 2 asks whether a'); + console.log(' workload RUNS as a relayflow; this shows it is woken, not that it ran.'); + console.log(' Attach a worker and re-run to close that gap.'); + } else { + console.log('No runs were created. Either every story was already claimed (dedupe working'); + console.log('as intended on a repeat poll), or nothing matched the subscription.'); + } } finally { client.close(); } From 064b1d2c46e3d2260ee8cfb416ab0fa45e80e8b8 Mon Sep 17 00:00:00 2001 From: Relayflow Lead Date: Sat, 29 Aug 2026 15:21:59 -0400 Subject: [PATCH 3/5] fix: stop reverting main's lockfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The branch was cut before those 36 lines landed on main, so rebasing carried a deletion of them as if it were an intentional change. package.json only adds a demo:hn script — no dependency changed, so the lockfile should be byte-identical to main. Restored and regenerated to confirm it is. Co-Authored-By: Claude Fable 5 --- sdk/package-lock.json | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/sdk/package-lock.json b/sdk/package-lock.json index 81dbe5f54..44ace6f11 100644 --- a/sdk/package-lock.json +++ b/sdk/package-lock.json @@ -9,6 +9,8 @@ "version": "0.1.0", "license": "UNLICENSED", "dependencies": { + "@types/js-yaml": "^4.0.9", + "js-yaml": "^5.4.1", "yaml": "^2.5.1" }, "bin": { @@ -792,6 +794,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.20.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", @@ -915,6 +923,12 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1071,6 +1085,28 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/js-yaml": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.1.tgz", + "integrity": "sha512-28R/k+NAjeuf7+CKlTxWZVExJGwVVLwY06DgEnOMz2gEpfNkDcD7QvyiVPT0xy0XXhU8vHsd4Ot42OOPdJG7dQ==", + "funding": [ + { + "type": "github", + "url": "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/sponsors/puzrin" + }, + { + "type": "github", + "url": "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/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", From 7c4108b32f2352c38fba01b660bed8efcf30f31f Mon Sep 17 00:00:00 2001 From: Relayflow Lead Date: Sat, 29 Aug 2026 15:32:30 -0400 Subject: [PATCH 4/5] fix: the demo observes execution state instead of asserting it (PR #19 P1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous fix separated 'a run was created' from 'a run executed', which was the right distinction, but it then hardcoded the sentence 'No agent worker is attached to this kernel'. That is an assumption, not an observation — it would have printed the same false claim the moment someone attached a worker. Now the demo calls run.get on a run it actually created and prints the real per-step states, then draws its conclusion from them. Choosing the predicate mattered more than expected. A run nobody works reports status=running while its steps sit in 'runnable' — ready, with nothing to claim them. So neither the run status nor 'state !== pending' is evidence of execution; only a step reaching 'running' or 'done' is. My first version used '!== pending' and would have declared success on exactly the case this fix exists to catch. Verified against live Hacker News with relayflowd serve and no worker: PROVEN: 5 run(s) created from live Hacker News data via event.submit. Observed run 01M17G141YPVSFQG2RH48SAQK4: status=running, steps: analyze-story=runnable NOT PROVEN: that those runs EXECUTED. No step reached running or done... Note the run says 'running' while its only step is 'runnable' — the exact false positive the step-level predicate avoids. The positive branch is NOT proven by a live run. Attaching a probe agent worker succeeded but no step.dispatch arrived within 15s, so I could not observe the ALSO PROVEN path firing. Reporting that rather than claiming a green I did not see. Verified: sdk 179 passed (13 files), tsc clean. Co-Authored-By: Claude Fable 5 --- sdk/src/demo-hn-monitor.ts | 41 +++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/sdk/src/demo-hn-monitor.ts b/sdk/src/demo-hn-monitor.ts index c60ff93fe..16b75aafc 100644 --- a/sdk/src/demo-hn-monitor.ts +++ b/sdk/src/demo-hn-monitor.ts @@ -44,9 +44,14 @@ async function main(): Promise { await pollHackerNewsOnce(spec, sink); let woke = 0; + const createdRunIds: string[] = []; for (const { storyId, outcome } of submissions) { const wake = outcome.run === undefined || outcome.run === null ? 'none' : 'created'; - if (wake === 'created') woke += 1; + if (wake === 'created') { + woke += 1; + const runId = typeof outcome.run === 'string' ? outcome.run : (outcome.run as { run_id?: string })?.run_id; + if (runId) createdRunIds.push(runId); + } console.log( `Story ${String(storyId)}: matched=${outcome.matched} deduped=${outcome.deduped} wake=${wake}`, ); @@ -68,10 +73,36 @@ async function main(): Promise { console.log(`PROVEN: ${woke} run(s) created from live Hacker News data via event.submit.`); console.log(' The event path — fetch, match, dedupe claim, wake — works end to end.'); console.log(''); - console.log('NOT PROVEN: that those runs EXECUTED. No agent worker is attached to this'); - console.log(' kernel, so each run is created and then waits. Gate 2 asks whether a'); - console.log(' workload RUNS as a relayflow; this shows it is woken, not that it ran.'); - console.log(' Attach a worker and re-run to close that gap.'); + + // Do not ASSERT that nothing executed — ask the kernel and report what it + // says. The first version of this block hardcoded "no agent worker is + // attached", which would have been a false statement the moment someone + // attached one. Evidence is captured, not narrated. + const observed = createdRunIds[0]; + if (observed === undefined) { + console.log('NOT PROVEN: that those runs EXECUTED — no run id came back to inspect.'); + } else { + const snapshot = await client.runGet(observed); + const steps = Object.entries(snapshot.steps); + // A run nobody works sits in `runnable` — ready, with no worker to + // claim it. So "not pending" is NOT evidence of execution; only a step + // that reached `running` or `done` proves a worker picked it up. + const executed = steps.filter(([, step]) => step.state === 'running' || step.state === 'done').length; + const stateCounts = steps.map(([id, step]) => `${id}=${step.state}`).join(' '); + console.log(`Observed run ${observed}: status=${snapshot.status}, steps: ${stateCounts}`); + if (executed === 0) { + console.log(''); + console.log('NOT PROVEN: that those runs EXECUTED. No step reached running or done,'); + console.log(' which is what a created-but-unworked run looks like: `relayflowd serve`'); + console.log(' alone attaches no agent worker. Gate 2 asks whether a workload RUNS'); + console.log(' as a relayflow; this shows it is woken, not that it ran.'); + console.log(' Attach a worker and re-run to close that gap.'); + } else { + console.log(''); + console.log(`ALSO PROVEN: execution happened — ${String(executed)} step(s) reached`); + console.log(' running or done, so a worker claimed this run. That is gate 2 proper.'); + } + } } else { console.log('No runs were created. Either every story was already claimed (dedupe working'); console.log('as intended on a repeat poll), or nothing matched the subscription.'); From 925b4a08edd21e977a21dc953567a5316b61e683 Mon Sep 17 00:00:00 2001 From: Relayflow Lead Date: Sat, 29 Aug 2026 16:42:51 -0400 Subject: [PATCH 5/5] fix: the demo's guidance on closing the execution gap was wrong MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It said 'Attach a worker and re-run to close that gap.' Attaching a worker afterwards does NOT close it: a run that found no worker parks, and nothing revisits parked runs. Measured on the live kernel — attach-then-submit dispatches, submit-then-attach does not until run.resume is called. So the demo now states the ordering requirement, which is the part that is easy to get wrong and that cost real time to establish: To close it, the worker must be attached BEFORE these events are submitted — attaching afterwards does not re-drive a parked run. Already parked? Call run.resume on it once a worker is attached. Re-run against live Hacker News to confirm the output is right, not just that it compiles: 5 runs created, analyze-story=runnable, NOT PROVEN reported with the corrected guidance. Verified: sdk 184 passed (13 files), tsc clean. Rebased onto main; diff is package.json +1 and demo-hn-monitor.ts, no reverts. Co-Authored-By: Claude Fable 5 --- sdk/src/demo-hn-monitor.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sdk/src/demo-hn-monitor.ts b/sdk/src/demo-hn-monitor.ts index 16b75aafc..3cfff5587 100644 --- a/sdk/src/demo-hn-monitor.ts +++ b/sdk/src/demo-hn-monitor.ts @@ -96,7 +96,14 @@ async function main(): Promise { console.log(' which is what a created-but-unworked run looks like: `relayflowd serve`'); console.log(' alone attaches no agent worker. Gate 2 asks whether a workload RUNS'); console.log(' as a relayflow; this shows it is woken, not that it ran.'); - console.log(' Attach a worker and re-run to close that gap.'); + // Ordering matters, and the obvious advice is wrong. A run that + // finds no worker parks; attaching one AFTERWARDS does not re-drive + // it, because nothing revisits parked runs. Measured on the live + // kernel: attach-then-submit dispatches, submit-then-attach does not + // until run.resume is called. + console.log(' To close it, the worker must be attached BEFORE these events are'); + console.log(' submitted — attaching afterwards does not re-drive a parked run.'); + console.log(' Already parked? Call run.resume on it once a worker is attached.'); } else { console.log(''); console.log(`ALSO PROVEN: execution happened — ${String(executed)} step(s) reached`);