Skip to content

feat(surface): add unpublished authored contract foundation - #134

Merged
kjgbot merged 10 commits into
mainfrom
feat/v2-surface-package
Sep 4, 2026
Merged

feat(surface): add unpublished authored contract foundation#134
kjgbot merged 10 commits into
mainfrom
feat/v2-surface-package

Conversation

@kjgbot

@kjgbot kjgbot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add an unpublished contract foundation for @relayflows/surface: immutable authored-flow handles, closed context/completion types, and a private runtime bridge
  • keep handle provenance in a module-private WeakMap; genuine handles expose no symbol/key from which another accepted handle can be forged
  • keep every authored run/llm/agent root operation in an executor-owned lifecycle independent of author promise handlers
  • recheck terminal state when an operation starts, retain root and derived-chain rejection, refuse direct/manual chains, and leave forgotten work unstarted
  • preserve the real internal awaited-f.run lowering through JournalClient and step.completed, while keeping executeAuthoredFlow absent from the SDK root export
  • keep the repository-owned package gate unchanged: it builds/tests surface, typechecks regressions and SDK, packs a tarball, installs a clean consumer, and proves Node plus TypeScript consumption

This PR does not publish a package, ship direct .flow.ts execution, implement a durable authored root, or close issue #132's “ship @relayflows/surface” item. Independent child journals are not crash-safe authored resume. v1 remains unchanged/default; kernel source and vocabulary are unchanged.

Exact pushed head: 5f2c0b9a22a7cab916980d49b5992f3cab041761

Refs #132

Literal red before repair (d830d027843b64da27eca3b2f805ddc9b33ac058)

Reflectable handle provenance

$ ./node_modules/.bin/vitest run tests/flow.test.ts -t 'refuses malformed and forged handles' --reporter=verbose; rc=$?; printf 'HANDLE_PROVENANCE_RED_EXIT=%s\n' "$rc"; exit 0

 RUN  v2.1.9 /Users/khaliqgant/AgentWorkforce/flows-132-surface-wt/surface

 × tests/flow.test.ts > flow > refuses malformed and forged handles at the runtime boundary
   → expected [ …(1) ] to deeply equal []

- Expected
+ Received

- Array []
+ Array [
+   Symbol(@relayflows/surface.authored-definition.v1),
+ ]

 Test Files  1 failed (1)
      Tests  1 failed | 5 skipped (6)
HANDLE_PROVENANCE_RED_EXIT=1

Terminal/lifecycle and rejection-chain failures

$ ./node_modules/.bin/vitest run tests/authored-flow.test.ts -t 'rechecks terminal state|refuses manually chained|retains root operation failures|captures a rejected derived' --reporter=dot; rc=$?; printf 'DETERMINISTIC_LIFECYCLE_RED_EXIT=%s\n' "$rc"; exit 0

 ❯ tests/authored-flow.test.ts (11 tests | 4 failed | 7 skipped) 421ms
   × authored flow journal executor > rechecks terminal state when a precreated lazy step first starts
   × authored flow journal executor > refuses manually chained work even when it settles before the body returns
   × authored flow journal executor > retains root operation failures even when a derived rejection handler consumes them
   × authored flow journal executor > captures a rejected derived callback instead of leaking unhandled success
Unhandled Rejection Error: derived callback exploded
 Test Files  1 failed (1)
      Tests  4 failed | 7 skipped (11)
     Errors  1 error
DETERMINISTIC_LIFECYCLE_RED_EXIT=1

An additional red pinned the zero-journal requirement for forgotten work while the body remains open:

$ ./node_modules/.bin/vitest run tests/authored-flow.test.ts -t 'refuses forgotten work even when the body remains open long enough to settle it' --reporter=verbose; rc=$?; printf 'FORGOTTEN_SETTLED_RED_EXIT=%s\n' "$rc"; exit 0
 × tests/authored-flow.test.ts > authored flow journal executor > refuses forgotten work even when the body remains open long enough to settle it 607ms
   → expected [ { version: '0.1.0', …(2) } ] to have a length of +0 but got 1
 Test Files  1 failed (1)
      Tests  1 failed | 11 skipped (12)
FORGOTTEN_SETTLED_RED_EXIT=1

Literal green at pushed head

Focused lifecycle/type gate

$ ./node_modules/.bin/tsc --noEmit && ./node_modules/.bin/vitest run tests/authored-flow.test.ts --reporter=dot
 ✓ tests/authored-flow.test.ts (12 tests) 1013ms
   ✓ authored flow journal executor > refuses manually chained work even when it settles before the body returns 312ms
   ✓ authored flow journal executor > retains root operation failures even when a derived rejection handler consumes them 313ms

 Test Files  1 passed (1)
      Tests  12 passed (12)

The 12 cases include all three verbs for precreated-after-done, manual start, and consumed root rejection; a forgotten delayed body; a multi-link derived rejection; awaited positive lowering; raw headers; closed completion types; and synchronous unsupported verbs.

Packed package provenance

$ bun pm pack --destination "$pack_dir" --quiet
$ cd "$consumer_dir" && bun init -y >/dev/null && bun add "$tarball" >/dev/null
$ node --input-type=module <<'NODE'
import { flow } from '@relayflows/surface';
import { getFlowDefinition } from '@relayflows/surface/runtime';
const genuine = flow('packed-genuine', async () => undefined);
const symbols = Object.getOwnPropertySymbols(genuine);
if (symbols.length !== 0) throw new Error(`reflectable provenance symbols=${symbols.length}`);
const forged = Object.freeze({ name: 'packed-forgery' });
let refused = false;
try { getFlowDefinition(forged); } catch (error) {
  refused = error instanceof TypeError
    && error.message === 'expected an @relayflows/surface flow handle';
}
if (!refused) throw new Error('packed runtime accepted reflected forgery');
console.log(`PACKED_PROVENANCE_OK symbols=${symbols.length} forgery=refused genuine=${getFlowDefinition(genuine).name}`);
NODE
$ bun run build
$ tsc
/tmp/pr134-packed-provenance.o5TJ2g/relayflows-surface-0.1.0.tgz
Resolving dependencies
Resolved, downloaded and extracted [1]
Saved lockfile
PACKED_PROVENANCE_OK symbols=0 forgery=refused genuine=packed-genuine
PACKED_CONSUMER_GREEN_EXIT=0 pack=/tmp/pr134-packed-provenance.o5TJ2g consumer=/tmp/pr134-packed-consumer.jsrxj5

Real daemon boundary

The probe starts exact-tree kernel/target/debug/relayflowd, uses JournalClient, executes the named authored cases, counts the daemon's SQLite journals, and asserts the post-done marker is absent.

$ live_data=$(mktemp -d /tmp/pr134-lifecycle-exact.XXXXXX)
$ marker_path="$live_data/post-done-effect"
$ kernel/target/debug/relayflowd --data-dir "$live_data" serve >"$live_data/relayflowd.log" 2>&1 &
$ for probe_try in {1..100}; do test -S "$live_data/relayflowd.sock" && break; sleep 0.05; done
$ AUTHORED_SOCKET="$live_data/relayflowd.sock" MARKER_PATH="$marker_path" node --input-type=module <<'NODE'
import { flow } from './sdk/node_modules/@relayflows/surface/dist/index.js';
import { executeAuthoredFlow } from './sdk/dist/authored-flow-executor.js';
import { JournalClient } from './sdk/dist/index.js';
const client = new JournalClient(process.env.AUTHORED_SOCKET, { requestTimeoutMs: 5000 });
await client.connect();
await client.hello('pr134-lifecycle-exact');
async function refuse(label, code, handle) {
  try { await executeAuthoredFlow(handle, client); throw new Error(`${label}: unexpected success`); }
  catch (error) { if (error?.code !== code) throw error; console.log(`${label}=REFUSED code=${error.code}`); }
}
try {
  for (const [label, handle] of [
    ['precreated-run', flow('precreated-run', async f => { const p=f.run(`touch ${process.env.MARKER_PATH}`); f.done('success'); await p; })],
    ['precreated-llm', flow('precreated-llm', async f => { const p=f.llm`no`; f.done('success'); await p; })],
    ['precreated-agent', flow('precreated-agent', async f => { const p=f.agent('w',{task:'no'}); f.done('success'); await p; })],
  ]) await refuse(label, 'operation_after_completion', handle);
  for (const [label, handle] of [
    ['forgotten-run', flow('forgotten-run', async f => { f.run(`touch ${process.env.MARKER_PATH}`); await new Promise(r=>setTimeout(r,100)); f.done('success'); })],
    ['forgotten-llm', flow('forgotten-llm', async f => { f.llm`no`; f.done('success'); })],
    ['forgotten-agent', flow('forgotten-agent', async f => { f.agent('w',{task:'no'}); f.done('success'); })],
  ]) await refuse(label, 'unawaited_step', handle);
  await refuse('manual-run','unawaited_step',flow('manual-run',async f=>{f.run('printf manual').then(()=>undefined);await new Promise(r=>setTimeout(r,100));f.done('success');}));
  await refuse('manual-llm','unsupported_verb',flow('manual-llm',async f=>{f.llm`no`.then(undefined,()=>undefined);await new Promise(r=>setTimeout(r,100));f.done('success');}));
  await refuse('manual-agent','unsupported_verb',flow('manual-agent',async f=>{f.agent('w',{task:'no'}).then(undefined,()=>undefined);await new Promise(r=>setTimeout(r,100));f.done('success');}));
  await refuse('consumed-run','step_failed',flow('consumed-run',async f=>{f.run('false').then(undefined,()=>0);await new Promise(r=>setTimeout(r,100));f.done('success');}));
  await refuse('consumed-llm','unsupported_verb',flow('consumed-llm',async f=>{f.llm`no`.then(undefined,()=>0);await new Promise(r=>setTimeout(r,100));f.done('success');}));
  await refuse('consumed-agent','unsupported_verb',flow('consumed-agent',async f=>{f.agent('w',{task:'no'}).then(undefined,()=>0);await new Promise(r=>setTimeout(r,100));f.done('success');}));
  await refuse('derived-chain','operation_callback_failed',flow('derived-chain',async f=>{f.run('printf source').then(()=>1).then(()=>{throw new Error('chain exploded')});await new Promise(r=>setTimeout(r,100));f.done('success');}));
  const positive=await executeAuthoredFlow(flow('positive',async f=>{const out=await f.run('printf awaited-ok');if(out!=='awaited-ok')throw new Error(out);f.done('success');}),client);
  console.log(`AWAITED_POSITIVE=SUCCESS steps=${positive.journalSteps.length}`);
} finally { client.close(); }
NODE
$ marker_state=absent; test ! -e "$marker_path" || marker_state=present
$ journal_files=$(find "$live_data/runs" -maxdepth 1 -type f -name '*.sqlite3' | wc -l | tr -d ' ')
$ printf 'POST_DONE_MARKER=%s JOURNAL_FILES=%s EXPECTED=5\n' "$marker_state" "$journal_files"
$ test "$marker_state" = absent && test "$journal_files" = 5
precreated-run=REFUSED code=operation_after_completion
precreated-llm=REFUSED code=operation_after_completion
precreated-agent=REFUSED code=operation_after_completion
forgotten-run=REFUSED code=unawaited_step
forgotten-llm=REFUSED code=unawaited_step
forgotten-agent=REFUSED code=unawaited_step
manual-run=REFUSED code=unawaited_step
manual-llm=REFUSED code=unsupported_verb
manual-agent=REFUSED code=unsupported_verb
consumed-run=REFUSED code=step_failed
consumed-llm=REFUSED code=unsupported_verb
consumed-agent=REFUSED code=unsupported_verb
derived-chain=REFUSED code=operation_callback_failed
AWAITED_POSITIVE=SUCCESS steps=2
POST_DONE_MARKER=absent JOURNAL_FILES=5 EXPECTED=5
REAL_DAEMON_EXACT_GREEN_EXIT=0

The five journals are exactly manual run, failed consumed run, derived-chain source run, awaited positive run, and its terminal marker. Precreated/forgotten cases create none.

Repository-owned packed consumer gate

$ NPM_CONFIG_USERCONFIG=/dev/null bash scripts/surface-package-gate.sh
 ✓ tests/flow.test.ts (6 tests) 21ms
 Test Files  1 passed (1)
      Tests  6 passed (6)
$ tsc -p ../regressions/tsconfig.json
> @relayflows/sdk@0.1.0 typecheck
> tsc --noEmit
PACKED_RUNTIME_OK name=packed-runtime-consumer completionReason=success
PACKED_RUNTIME_REFUSAL_OK invalidHeaders=9 forgedHandle=refused
PACKED_TYPESCRIPT_OK
 ✓ tests/authored-flow.test.ts (12 tests) 892ms
 Test Files  1 passed (1)
      Tests  12 passed (12)

Full SDK and kernel regressions

$ ./node_modules/.bin/tsc && node scripts/make-cli-executable.mjs && ./node_modules/.bin/vitest run --exclude tests/live-kernel.test.ts --reporter=dot --maxWorkers=1 --minWorkers=1
 Test Files  17 passed (17)
      Tests  232 passed (232)
   Duration  33.08s
$ RELAYFLOWD_BIN=/Users/khaliqgant/AgentWorkforce/flows-132-surface-wt/kernel/target/debug/relayflowd RELAYFLOWS_ALLOW_ANALYZER_SKIP=1 ./node_modules/.bin/vitest run tests/live-kernel.test.ts --reporter=dot --maxWorkers=1 --minWorkers=1
LIVE_KERNEL relayflowd=/Users/khaliqgant/AgentWorkforce/flows-132-surface-wt/kernel/target/debug/relayflowd
LIVE_ANALYZER ready: claude -p --model claude-haiku-4-5-20251001 round-trip OK
LIVE_KERNEL kill -9 pid=93187 run=01M1HVB57B1TWSVZXY9F4HDN93 while step=two state=Running
 ✓ tests/live-kernel.test.ts (17 tests) 58852ms
 Test Files  1 passed (1)
      Tests  17 passed (17)
$ CARGO_TARGET_DIR=/tmp/pr134-repair-r2-cargo /Users/khaliqgant/.cargo/bin/cargo test --locked --workspace --manifest-path kernel/Cargo.toml --quiet
test result: ok. 22 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
test result: ok. 19 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
test result: ok. 26 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
test result: ok. 17 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Commit and remote identity

$ git show --format='%H%n%P%n%s' --no-patch HEAD
5f2c0b9a22a7cab916980d49b5992f3cab041761
d830d027843b64da27eca3b2f805ddc9b33ac058
fix(surface): close authored operation lifecycle

$ git ls-remote origin refs/heads/feat/v2-surface-package refs/pull/134/head
5f2c0b9a22a7cab916980d49b5992f3cab041761 refs/heads/feat/v2-surface-package
5f2c0b9a22a7cab916980d49b5992f3cab041761 refs/pull/134/head

@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 5fb61521-eac4-4186-a855-6147c22ea8fc

📥 Commits

Reviewing files that changed from the base of the PR and between 5f2c0b9 and 4f85c4e.

📒 Files selected for processing (25)
  • .github/workflows/cloud-runtime-artifact.yml
  • docs/SURFACE.md
  • ops/pr134-lifecycle-repair-evidence.md
  • ops/probes/pr134-repair-0903/aggregate-membership.mjs
  • ops/probes/pr134-repair-0903/combinators.mjs
  • ops/probes/pr134-repair-0903/gate-cost.mjs
  • ops/probes/pr134-repair-0903/harness.mjs
  • ops/probes/pr134-repair-0903/negatives.mjs
  • ops/probes/pr134-repair-0903/p0-derived-race.mjs
  • ops/probes/pr134-repair-0903/p1a-authoring.mjs
  • ops/probes/pr134-repair-0903/promise-all-semantics.mjs
  • ops/probes/pr134-repair-0903/verb-output-fields.mjs
  • ops/reviews/20260902-2035-pr134-structure.md
  • ops/reviews/20260903-pr134-repair-0903.md
  • sdk/package.json
  • sdk/src/authored-flow-error.ts
  • sdk/src/authored-flow-executor.ts
  • sdk/src/authored-flow-lifecycle.ts
  • sdk/src/authored-flow-operation.ts
  • sdk/src/authored-promise-graph.ts
  • sdk/src/index.ts
  • sdk/tests/authored-flow-lifecycle-executor.test.ts
  • sdk/tests/authored-flow-operation.test.ts
  • sdk/tsconfig.tests.json
  • surface/README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • surface/README.md

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change adds the @relayflows/surface authoring package, authored-flow journal execution in the SDK, packed-package validation, CI coverage, and regression integration updates.

Changes

Surface authoring contract

Layer / File(s) Summary
Surface package and flow contract
surface/*, README.md
Adds typed flow, context, cloud, completion, and step contracts. Flow registration validates inputs, freezes definitions, and rejects forged handles.
Authored-flow journal execution
sdk/src/authored-flow*.ts, sdk/src/index.ts, sdk/package.json
Adds operation tracking, typed execution errors, promise graph lifecycle control, journal lowering, runtime definition access, and public SDK exports for the authored-flow APIs.
Authored-flow tests and probes
sdk/tests/*, ops/probes/pr134-repair-0903/*, ops/reviews/*, ops/pr134-lifecycle-repair-evidence.md
Adds lifecycle and executor tests, probe scripts, and review evidence for authored-flow behavior, Promise combinators, and output lowering.
Packed package validation and CI
scripts/surface-package-gate.sh, .github/workflows/surface-package.yml, .github/workflows/cloud-runtime-artifact.yml
Builds and tests the surface package, validates packed runtime and type contracts, and integrates the checks into CI.
Regression and harness vocabulary updates
regressions/*, docs/SURFACE.md
Routes regression typechecking through the surface source alias, updates flow results to success, and documents success and canceled outcomes plus the authored-flow contract.
Surface package docs and layout
surface/README.md, README.md
Documents the new surface package in the repository layout and in the package README.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: ⚪ Minimal · up to 5f2c0

The PR adds an unpublished authored-flow contract foundation with validated headers, closed completion types, and explicit refusal of unsupported or unawaited work. No actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant FlowFixture
  participant executeAuthoredFlow
  participant AuthoredFlowLifecycle
  participant AuthoredFlowOperation
  participant JournalClient
  participant JournalServer
  FlowFixture->>executeAuthoredFlow: provide FlowHandle
  executeAuthoredFlow->>AuthoredFlowLifecycle: validate authored body and scope
  executeAuthoredFlow->>AuthoredFlowOperation: start awaited run step
  AuthoredFlowOperation->>JournalClient: submit lowered run specification
  JournalClient->>JournalServer: request run and journal
  JournalServer-->>JournalClient: return completion and journal data
  JournalClient-->>executeAuthoredFlow: return journal output
  executeAuthoredFlow-->>FlowFixture: return frozen execution result
Loading

Poem

A rabbit reads the flow,
The surface steps now know their place,
The journal keeps the trail,
The tests hop close and guard the gate,
Success lands soft and clear.


Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

@kjgbot kjgbot changed the title feat(surface): ship v2 authoring package feat(surface): add v2 authoring foundation and runtime bridge Sep 2, 2026
@kjgbot kjgbot changed the title feat(surface): add v2 authoring foundation and runtime bridge feat(surface): add unpublished authored run-to-journal slice Sep 2, 2026
@kjgbot kjgbot changed the title feat(surface): add unpublished authored run-to-journal slice feat(surface): add unpublished authored contract foundation Sep 2, 2026

@kjgbot kjgbot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Structure/RFC blocking findings from exact reviewed head d830d027843b64da27eca3b2f805ddc9b33ac058. These comments are deliberately pinned to that historical PR commit; they are not a claim about later head e8699407…. Full literal evidence: ops/reviews/20260902-2035-pr134-structure.md.

Comment thread sdk/src/authored-flow-executor.ts Outdated
Comment thread sdk/src/authored-flow-executor.ts Outdated
Comment thread surface/src/flow.ts Outdated
kjgbot pushed a commit that referenced this pull request Sep 3, 2026
The derived-chain gate sampled which failures had already landed. It skipped
every promise that had not settled, so the same program passed or failed on
how many microtask ticks the failure took: ten `await null`s, or any real
derived I/O, cleared the window and a flow recorded terminal success while
work derived from a step had thrown.

Widening the window cannot fix that; there is no safe tick count. What is
timing-independent is whether derived work was still in flight when the body
returned: work the author awaited is settled at that instant in every timing,
and work the author did not await is pending in every timing. The gate now
reads the in-flight set before it awaits anything and refuses on it
(`unsettled_derived_work`). That also makes the settled set complete, so
inspecting settled outcomes stops being a sample and becomes a total answer
over a closed set.

The same escape existed through Promise.allSettled, Promise.any and
Promise.race, which no earlier review had demonstrated. A combinator resolves
its aggregate from inside the reaction of one of its members, so the aggregate
is not downstream of any member by `trigger` — only `Promise.all` was covered,
and only because it is registered by name. Aggregates now inherit attribution
from the context that resolves them, which covers every combinator without
intercepting any of them.

Also repaired, all measured:

- The reachability predicate refused ordinary authoring. `trigger` and
  `resolutionCause` do not connect an async function's resumption context to
  the context it suspended from, so a walk from `done()` reached only the last
  await's lineage and `const steps = [f.run(a), f.run(b)]; for (const s of
  steps) await s;` reported run-1 unawaited. The init-time `executionAsyncId()`
  is that missing edge, and it is a fact the runtime reports rather than a
  widened approximation.

- The gate was quadratic in process-wide promise count: 5 000 awaits -> 1803 ms,
  30 000 -> 94 700 ms on a 25 ms body. Attribution is now eager and O(1) per
  promise, and only promises created inside the flow's own async scope are
  tracked. 30 000 -> 34 ms, and 140 007 unrelated process promises retained -> 0.
  A performance assertion pins the 30 000 case under 1 000 ms.

- `Promise.all` is still intercepted, because a combinator's aggregate has no
  runtime edge to its non-final members and every alternative reduces to
  callback identity inference. It no longer changes what `Promise.all` does:
  `Promise.all(5)` rejects instead of resolving `[]`, `Promise.all(null)`
  returns a rejected promise instead of throwing synchronously, and `name` is
  `all`. The interception is now DISCLOSED in docs/SURFACE.md with its reason
  and its process-wide scope, together with the one documented limit of the
  contract: a derived chain created inside a timer that fires after the body
  returns does not exist yet and cannot be observed.

- `close()` releases every tracked map, not only the promise handles.

Three things a reviewer should not mistake for noise:

- sdk/tsconfig.tests.json is WIDENED here, and the four type errors fixed in
  sdk/tests/authored-flow-operation.test.ts were NOT introduced by this change.
  Main's new typecheck:tests gate included only src and typed-output.test.ts;
  tsconfig.json excludes tests/ and vitest does not typecheck, so #134's test
  files were type-checked by nothing. Three of the four errors are pre-existing
  at 59c062c. `lib` is raised to ES2024.Promise in that gate config only, for
  Promise.withResolvers in the forgery tests; the SDK's own tsconfig stays on
  ES2022.

- Fire-and-forget async work outstanding at done() is now refused even when it
  would have succeeded. That is a deliberate tightening under covenant 2 and is
  documented; awaited work of any shape is unaffected.

- The 140 lines this branch deletes from main are all #134's own intent, and
  the biggest block is regressions/surface.d.ts, whose own header asked to be
  deleted once the real surface shipped. The repair report carries the full
  attribution table, and all 51 of #136's files this branch does not touch are
  blob-identical to 990093b.

The promise-graph observation moves to its own module; the lifecycle keeps the
operation-facing contract. Probes for every claim in the repair report are
committed under ops/probes/pr134-repair-0903/ and run with plain node —
including the kernel-spec assertion that `output` still LOWERS to a
json_schema gate, which validateSpec and `flows check` cannot see.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
@kjgbot
kjgbot force-pushed the feat/v2-surface-package branch from 59c062c to 311b18c Compare September 3, 2026 12:24
kjgbot pushed a commit that referenced this pull request Sep 3, 2026
…review still blocked

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
@kjgbot

kjgbot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

PR #134 independent signoff — Codex — 2026-09-04

VERDICT: PASSED

Reviewed only exact head c4941e13b7b2fa93c088416a4153fa093719fc79. I made no product fix, did not resolve review threads, and did not push or merge. The only product-code mutation below was the required test mutation; its source file was restored byte-for-byte.

Reviewed head and thread state

$ git rev-parse HEAD
c4941e13b7b2fa93c088416a4153fa093719fc79

The requested GraphQL query returned the three threads as still administratively unresolved:

PRRT_kwDOUF0ysM6ezlZD isResolved=false  sdk/src/authored-flow-executor.ts
PRRT_kwDOUF0ysM6ezlZN isResolved=false  sdk/src/authored-flow-executor.ts
PRRT_kwDOUF0ysM6ezlZS isResolved=false  surface/src/flow.ts

The conclusions below are based on current code plus fresh execution, not on the unresolved flag or prior review prose.

P1 — a pre-created lazy step can start after done()

CLOSED. sdk/src/authored-flow-executor.ts supplies every lazy run, llm, and agent operation with a start-time assertOperationAllowed(...) callback. AuthoredFlowOperation.begin() in sdk/src/authored-flow-operation.ts invokes that callback immediately before transitioning to running and calling the journal-backed start function. Thus creation-time permission is not treated as permission to start later.

The focused test executes all three pre-created verbs, calls done('success'), then awaits the operation. It asserts operation_after_completion for each and asserts that the fake journal received no new run:

$ cd sdk && ./node_modules/.bin/vitest run tests/authored-flow.test.ts -t 'rechecks terminal state when a precreated lazy step first starts'

 RUN  v2.1.9 /Users/khaliqgant/AgentWorkforce/flows-pr134-signoff-wt/sdk

 ✓ tests/authored-flow.test.ts (12 tests | 11 skipped) 5ms

 Test Files  1 passed (1)
      Tests  1 passed | 11 skipped (12)

P1 — settled state loses the settlement outcome

CLOSED. sdk/src/authored-flow-operation.ts retains the root operation rejection in rootFailureRecorded/rootFailure through an observer attached to the root promise. verifyAuthoredOperations() waits for settlement and then throws the retained root failure before considering whether a user's derived rejection handler consumed it. User code such as .then(undefined, () => 'consumed') cannot erase the root result.

The focused test executes the original escape shape for failing run, unsupported llm, and unsupported agent, waits until each rejection has settled, calls done('success'), and expects the original step_failed or unsupported_verb outcome:

$ cd sdk && ./node_modules/.bin/vitest run tests/authored-flow.test.ts -t 'retains root operation failures even when a derived rejection handler consumes them'

 RUN  v2.1.9 /Users/khaliqgant/AgentWorkforce/flows-pr134-signoff-wt/sdk

 ✓ tests/authored-flow.test.ts (12 tests | 11 skipped) 318ms
   ✓ authored flow journal executor > retains root operation failures even when a derived rejection handler consumes them 314ms

 Test Files  1 passed (1)
      Tests  1 passed | 11 skipped (12)

P2 — a unique-Symbol definition property is reflectively extractable

CLOSED. surface/src/flow.ts stores definitions only in the module-private WeakMap<object, AuthoredFlowDefinition> definitions. A handle is the frozen public object { name }; it has no definition-bearing symbol property. getFlowDefinition() accepts only identities present in the private map.

The focused test explicitly runs Object.getOwnPropertySymbols(genuine) and expects [], then attempts both the legacy Symbol.for(...) forgery and a reflected-symbol forgery and expects both to be refused:

$ cd surface && ./node_modules/.bin/vitest run tests/flow.test.ts -t 'refuses malformed and forged handles at the runtime boundary'

 RUN  v2.1.9 /Users/khaliqgant/AgentWorkforce/flows-pr134-signoff-wt/surface

 ✓ tests/flow.test.ts (6 tests | 5 skipped) 2ms

 Test Files  1 passed (1)
      Tests  1 passed | 5 skipped (6)

Required combinator mutation

The committed source hash before mutation was:

$ cd sdk && sha256sum src/authored-flow-lifecycle.ts
081787dc17727e379bfb790ca747ac33bc0be0ab627f0fb01573e3d203c2c201  src/authored-flow-lifecycle.ts

I changed only this line temporarily:

-const COMBINATORS = ['all', 'allSettled', 'any', 'race'] as const;
+const COMBINATORS = ['all'] as const;

The mutated hash differed, proving the mutation was applied:

$ sha256sum src/authored-flow-lifecycle.ts
edde33ae3689b49a7f61432003c1add70629c54757e72ce1464d0635969b641b  src/authored-flow-lifecycle.ts

The lifecycle suite failed 9 tests, precisely on the omitted combinators. Four failures were false success (a derived failure escaped) and five were false unawaited_step refusals:

$ ./node_modules/.bin/vitest run tests/authored-flow-lifecycle-executor.test.ts

 RUN  v2.1.9 /Users/khaliqgant/AgentWorkforce/flows-pr134-signoff-wt/sdk

 ❯ tests/authored-flow-lifecycle-executor.test.ts (27 tests | 9 failed) 550ms
   × authored flow lifecycle through the journal executor > refuses a deferred derived failure behind an aggregate: allSettled resolved by an unrelated member 27ms
     → promise resolved "{ …(3) }" instead of rejecting
   × authored flow lifecycle through the journal executor > refuses a deferred derived failure behind an aggregate: allSettled with the step declared second 19ms
     → promise resolved "{ …(3) }" instead of rejecting
   × authored flow lifecycle through the journal executor > refuses a deferred derived failure behind an aggregate: race resolved by an unrelated member 3ms
     → promise resolved "{ …(3) }" instead of rejecting
   × authored flow lifecycle through the journal executor > refuses a deferred derived failure behind an aggregate: any resolved by an unrelated member 2ms
     → promise resolved "{ …(3) }" instead of rejecting
   × authored flow lifecycle through the journal executor > preserves authoring: await Promise.allSettled over two steps 3ms
     → unawaited_step: flow "accepted-await Promise.allSettled over two steps" returned with unawaited steps: run-1 (f.run)
   × authored flow lifecycle through the journal executor > preserves authoring: await Promise.allSettled over five steps 2ms
     → unawaited_step: flow "accepted-await Promise.allSettled over five steps" returned with unawaited steps: run-1 (f.run), run-2 (f.run), run-3 (f.run), run-4 (f.run)
   × authored flow lifecycle through the journal executor > preserves authoring: await Promise.race over two steps 1ms
     → unawaited_step: flow "accepted-await Promise.race over two steps" returned with unawaited steps: run-2 (f.run)
   × authored flow lifecycle through the journal executor > preserves authoring: await Promise.any over two steps 1ms
     → unawaited_step: flow "accepted-await Promise.any over two steps" returned with unawaited steps: run-2 (f.run)
   × authored flow lifecycle through the journal executor > preserves authoring: await an aggregate mixing a step and an unrelated promise 17ms
     → unawaited_step: flow "accepted-await an aggregate mixing a step and an unrelated promise" returned with unawaited steps: run-1 (f.run)

 Test Files  1 failed (1)
      Tests  9 failed | 18 passed (27)

After restoring the four-element list, the source hash exactly matched the pre-mutation hash and the same focused suite passed:

$ sha256sum src/authored-flow-lifecycle.ts
081787dc17727e379bfb790ca747ac33bc0be0ab627f0fb01573e3d203c2c201  src/authored-flow-lifecycle.ts

$ ./node_modules/.bin/vitest run tests/authored-flow-lifecycle-executor.test.ts

 RUN  v2.1.9 /Users/khaliqgant/AgentWorkforce/flows-pr134-signoff-wt/sdk

 ✓ tests/authored-flow-lifecycle-executor.test.ts (27 tests) 523ms

 Test Files  1 passed (1)
      Tests  27 passed (27)

Mutation result: PASS. Narrowing the list is strongly detected; the contract is enforced.

Build and full-suite gate

The requested SDK build completed with exit 0 and no diagnostics:

$ cd sdk && ./node_modules/.bin/tsc && node scripts/make-cli-executable.mjs
[no output; exit 0]

An unpinned run initially selected the most recently modified global toolchain binary at /Users/khaliqgant/.relayflows-toolchain/target/1346384442/debug/relayflowd. Path-key proof showed that binary belongs to /Users/khaliqgant/AgentWorkforce/flows-pr139-rebase-wt, not this review worktree:

$ printf '%s' /Users/khaliqgant/AgentWorkforce/flows-pr134-signoff-wt | cksum
1107369837 55
$ printf '%s' /Users/khaliqgant/AgentWorkforce/flows-pr139-rebase-wt | cksum
1346384442 54

That unrelated binary produced live-kernel protocol mismatches. I therefore built the kernel at the reviewed checkout and pinned the final SDK run to its exact worktree-keyed artifact. The final run still printed the documented backlog-picker ENOENT race during a passing backlog test; per the assignment, it is not treated as a #134 regression. The complete gate passed:

$ cd kernel && PATH="$HOME/.cargo/bin:$PATH" RUSTUP_TOOLCHAIN=stable sh ../ops/cargo.sh build
   Compiling relayflowd-core v0.1.0 (/Users/khaliqgant/AgentWorkforce/flows-pr134-signoff-wt/kernel/relayflowd-core)
   Compiling relayflowd-journal v0.1.0 (/Users/khaliqgant/AgentWorkforce/flows-pr134-signoff-wt/kernel/relayflowd-journal)
   Compiling relayflowd v0.1.0 (/Users/khaliqgant/AgentWorkforce/flows-pr134-signoff-wt/kernel/relayflowd)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 11.42s

$ cd sdk && RELAYFLOWD_BIN=/Users/khaliqgant/.relayflows-toolchain/target/1107369837/debug/relayflowd ./node_modules/.bin/vitest run

 RUN  v2.1.9 /Users/khaliqgant/AgentWorkforce/flows-pr134-signoff-wt/sdk

stdout | tests/live-kernel.test.ts
LIVE_KERNEL relayflowd=/Users/khaliqgant/.relayflows-toolchain/target/1107369837/debug/relayflowd
LIVE_KERNEL flows=/Users/khaliqgant/AgentWorkforce/flows-pr134-signoff-wt/sdk/dist/cli.js

Error: ENOENT: no such file or directory, open '.relayflow/backlog-picker-entry.json'

 ✓ tests/authored-flow-lifecycle-executor.test.ts (27 tests) 510ms
 ✓ tests/authored-flow.test.ts (12 tests) 837ms
 ✓ tests/live-kernel.test.ts (21 tests) 49314ms

 Test Files  25 passed | 1 skipped (26)
      Tests  432 passed | 3 skipped (435)

No unresolved thread remains live in the reviewed code.

kjgbot pushed a commit that referenced this pull request Sep 3, 2026
The derived-chain gate sampled which failures had already landed. It skipped
every promise that had not settled, so the same program passed or failed on
how many microtask ticks the failure took: ten `await null`s, or any real
derived I/O, cleared the window and a flow recorded terminal success while
work derived from a step had thrown.

Widening the window cannot fix that; there is no safe tick count. What is
timing-independent is whether derived work was still in flight when the body
returned: work the author awaited is settled at that instant in every timing,
and work the author did not await is pending in every timing. The gate now
reads the in-flight set before it awaits anything and refuses on it
(`unsettled_derived_work`). That also makes the settled set complete, so
inspecting settled outcomes stops being a sample and becomes a total answer
over a closed set.

The same escape existed through Promise.allSettled, Promise.any and
Promise.race, which no earlier review had demonstrated. A combinator resolves
its aggregate from inside the reaction of one of its members, so the aggregate
is not downstream of any member by `trigger` — only `Promise.all` was covered,
and only because it is registered by name. Aggregates now inherit attribution
from the context that resolves them, which covers every combinator without
intercepting any of them.

Also repaired, all measured:

- The reachability predicate refused ordinary authoring. `trigger` and
  `resolutionCause` do not connect an async function's resumption context to
  the context it suspended from, so a walk from `done()` reached only the last
  await's lineage and `const steps = [f.run(a), f.run(b)]; for (const s of
  steps) await s;` reported run-1 unawaited. The init-time `executionAsyncId()`
  is that missing edge, and it is a fact the runtime reports rather than a
  widened approximation.

- The gate was quadratic in process-wide promise count: 5 000 awaits -> 1803 ms,
  30 000 -> 94 700 ms on a 25 ms body. Attribution is now eager and O(1) per
  promise, and only promises created inside the flow's own async scope are
  tracked. 30 000 -> 34 ms, and 140 007 unrelated process promises retained -> 0.
  A performance assertion pins the 30 000 case under 1 000 ms.

- `Promise.all` is still intercepted, because a combinator's aggregate has no
  runtime edge to its non-final members and every alternative reduces to
  callback identity inference. It no longer changes what `Promise.all` does:
  `Promise.all(5)` rejects instead of resolving `[]`, `Promise.all(null)`
  returns a rejected promise instead of throwing synchronously, and `name` is
  `all`. The interception is now DISCLOSED in docs/SURFACE.md with its reason
  and its process-wide scope, together with the one documented limit of the
  contract: a derived chain created inside a timer that fires after the body
  returns does not exist yet and cannot be observed.

- `close()` releases every tracked map, not only the promise handles.

Three things a reviewer should not mistake for noise:

- sdk/tsconfig.tests.json is WIDENED here, and the four type errors fixed in
  sdk/tests/authored-flow-operation.test.ts were NOT introduced by this change.
  Main's new typecheck:tests gate included only src and typed-output.test.ts;
  tsconfig.json excludes tests/ and vitest does not typecheck, so #134's test
  files were type-checked by nothing. Three of the four errors are pre-existing
  at 59c062c. `lib` is raised to ES2024.Promise in that gate config only, for
  Promise.withResolvers in the forgery tests; the SDK's own tsconfig stays on
  ES2022.

- Fire-and-forget async work outstanding at done() is now refused even when it
  would have succeeded. That is a deliberate tightening under covenant 2 and is
  documented; awaited work of any shape is unaffected.

- The 140 lines this branch deletes from main are all #134's own intent, and
  the biggest block is regressions/surface.d.ts, whose own header asked to be
  deleted once the real surface shipped. The repair report carries the full
  attribution table, and all 51 of #136's files this branch does not touch are
  blob-identical to 990093b.

The promise-graph observation moves to its own module; the lifecycle keeps the
operation-facing contract. Probes for every claim in the repair report are
committed under ops/probes/pr134-repair-0903/ and run with plain node —
including the kernel-spec assertion that `output` still LOWERS to a
json_schema gate, which validateSpec and `flows check` cannot see.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
@kjgbot
kjgbot force-pushed the feat/v2-surface-package branch from c4941e1 to 817db37 Compare September 3, 2026 23:25
@kjgbot

kjgbot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main @ 3725025c4941e1817db37

The PR had gone CONFLICTING as main moved. One conflict, in .github/workflows/cloud-runtime-artifact.yml: main added a Test kernel step (#153) and this branch adds Build authoring surface at the same point. Both kept, with the surface build before Install SDK dependencies, since the SDK reaches it through a file: dependency.

The Codex signoff still carries

The independent signoff above was performed at c4941e13 and returned PASSED. Rather than assume a rebase preserves it, I hashed the four files its conclusions rest on, before and after:

081787dc17727e37  sdk/src/authored-flow-lifecycle.ts
7f1f6c3a4e2014b8  sdk/src/authored-flow-executor.ts
bd4075df7275379a  sdk/src/authored-flow-operation.ts
8487d735a93dca36  surface/src/flow.ts

Identical at both heads. 081787dc… also matches the pre-mutation hash recorded in the signoff, which independently confirms the COMBINATORS mutation was restored byte-for-byte. So the three threads' CLOSED findings and the 9-test mutation result apply unchanged at 817db37; only the workflow file differs.

Suites at the rebased head

kernel:  sh ops/cargo.sh test --workspace   → 130 passed, 0 failed
SDK:     vitest run                          → 526 passed, 3 skipped, 0 failed (27 files)

One correction to method worth recording: my first SDK run here reported 2 failed files, all Cannot find module '@relayflows/surface'. That was my harness, not the branch — I had copied a node_modules whose surface/dist was empty. After bun install --frozen-lockfile --ignore-scripts && bun run build in surface/, exactly as the step this PR adds does, the suite is clean. Worth noting because it is the same failure CI would show if that build step were ever dropped.

Blocked on #154, not on this PR

This cannot go green yet. The rebase necessarily carries main's current Test kernel step, which invokes ops/cargo.sh and dies on a runner at rustup could not choose a version of cargo — my bug from #153, fixed in #154. #134's CI will fail for that reason and no other until #154 lands, at which point this branch needs a trivial re-resolve of the same three workflow lines.

Not merging: green CI is part of the bar and it is not reachable tonight.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

kjgbot pushed a commit that referenced this pull request Sep 4, 2026
The derived-chain gate sampled which failures had already landed. It skipped
every promise that had not settled, so the same program passed or failed on
how many microtask ticks the failure took: ten `await null`s, or any real
derived I/O, cleared the window and a flow recorded terminal success while
work derived from a step had thrown.

Widening the window cannot fix that; there is no safe tick count. What is
timing-independent is whether derived work was still in flight when the body
returned: work the author awaited is settled at that instant in every timing,
and work the author did not await is pending in every timing. The gate now
reads the in-flight set before it awaits anything and refuses on it
(`unsettled_derived_work`). That also makes the settled set complete, so
inspecting settled outcomes stops being a sample and becomes a total answer
over a closed set.

The same escape existed through Promise.allSettled, Promise.any and
Promise.race, which no earlier review had demonstrated. A combinator resolves
its aggregate from inside the reaction of one of its members, so the aggregate
is not downstream of any member by `trigger` — only `Promise.all` was covered,
and only because it is registered by name. Aggregates now inherit attribution
from the context that resolves them, which covers every combinator without
intercepting any of them.

Also repaired, all measured:

- The reachability predicate refused ordinary authoring. `trigger` and
  `resolutionCause` do not connect an async function's resumption context to
  the context it suspended from, so a walk from `done()` reached only the last
  await's lineage and `const steps = [f.run(a), f.run(b)]; for (const s of
  steps) await s;` reported run-1 unawaited. The init-time `executionAsyncId()`
  is that missing edge, and it is a fact the runtime reports rather than a
  widened approximation.

- The gate was quadratic in process-wide promise count: 5 000 awaits -> 1803 ms,
  30 000 -> 94 700 ms on a 25 ms body. Attribution is now eager and O(1) per
  promise, and only promises created inside the flow's own async scope are
  tracked. 30 000 -> 34 ms, and 140 007 unrelated process promises retained -> 0.
  A performance assertion pins the 30 000 case under 1 000 ms.

- `Promise.all` is still intercepted, because a combinator's aggregate has no
  runtime edge to its non-final members and every alternative reduces to
  callback identity inference. It no longer changes what `Promise.all` does:
  `Promise.all(5)` rejects instead of resolving `[]`, `Promise.all(null)`
  returns a rejected promise instead of throwing synchronously, and `name` is
  `all`. The interception is now DISCLOSED in docs/SURFACE.md with its reason
  and its process-wide scope, together with the one documented limit of the
  contract: a derived chain created inside a timer that fires after the body
  returns does not exist yet and cannot be observed.

- `close()` releases every tracked map, not only the promise handles.

Three things a reviewer should not mistake for noise:

- sdk/tsconfig.tests.json is WIDENED here, and the four type errors fixed in
  sdk/tests/authored-flow-operation.test.ts were NOT introduced by this change.
  Main's new typecheck:tests gate included only src and typed-output.test.ts;
  tsconfig.json excludes tests/ and vitest does not typecheck, so #134's test
  files were type-checked by nothing. Three of the four errors are pre-existing
  at 59c062c. `lib` is raised to ES2024.Promise in that gate config only, for
  Promise.withResolvers in the forgery tests; the SDK's own tsconfig stays on
  ES2022.

- Fire-and-forget async work outstanding at done() is now refused even when it
  would have succeeded. That is a deliberate tightening under covenant 2 and is
  documented; awaited work of any shape is unaffected.

- The 140 lines this branch deletes from main are all #134's own intent, and
  the biggest block is regressions/surface.d.ts, whose own header asked to be
  deleted once the real surface shipped. The repair report carries the full
  attribution table, and all 51 of #136's files this branch does not touch are
  blob-identical to 990093b.

The promise-graph observation moves to its own module; the lifecycle keeps the
operation-facing contract. Probes for every claim in the repair report are
committed under ops/probes/pr134-repair-0903/ and run with plain node —
including the kernel-spec assertion that `output` still LOWERS to a
json_schema gate, which validateSpec and `flows check` cannot see.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
@kjgbot
kjgbot force-pushed the feat/v2-surface-package branch from 817db37 to 4fa4ff5 Compare September 4, 2026 06:12
kjgbot added 8 commits September 4, 2026 10:14
Replace the regression-only ambient declaration with a real @relayflows/surface package and point the dormant in-repo flows at its source contract. Keep execution and compiler concerns behind the journal-backed runtime.

Refs #132

Session-Id: 01a0627a-c11f-7850-9667-c638320d25f4

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
Session-Id: 01a062b4-562d-7143-9296-dd34cc65251f

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
Session-Id: 01a062b4-562d-7143-9296-dd34cc65251f

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
Session-Id: 01a062b4-562d-7143-9296-dd34cc65251f

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
Session-Id: 01a062b4-562d-7143-9296-dd34cc65251f

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
Session-Id: 01a062b7-2aef-7772-8225-7cb00ee311dd

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
Session-Id: 01a062b7-2aef-7772-8225-7cb00ee311dd

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
Session-Id: 01a062d6-d0fb-7060-b9a7-57031d858ea9

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
kjgbot added 2 commits September 4, 2026 10:14
The derived-chain gate sampled which failures had already landed. It skipped
every promise that had not settled, so the same program passed or failed on
how many microtask ticks the failure took: ten `await null`s, or any real
derived I/O, cleared the window and a flow recorded terminal success while
work derived from a step had thrown.

Widening the window cannot fix that; there is no safe tick count. What is
timing-independent is whether derived work was still in flight when the body
returned: work the author awaited is settled at that instant in every timing,
and work the author did not await is pending in every timing. The gate now
reads the in-flight set before it awaits anything and refuses on it
(`unsettled_derived_work`). That also makes the settled set complete, so
inspecting settled outcomes stops being a sample and becomes a total answer
over a closed set.

The same escape existed through Promise.allSettled, Promise.any and
Promise.race, which no earlier review had demonstrated. A combinator resolves
its aggregate from inside the reaction of one of its members, so the aggregate
is not downstream of any member by `trigger` — only `Promise.all` was covered,
and only because it is registered by name. Aggregates now inherit attribution
from the context that resolves them, which covers every combinator without
intercepting any of them.

Also repaired, all measured:

- The reachability predicate refused ordinary authoring. `trigger` and
  `resolutionCause` do not connect an async function's resumption context to
  the context it suspended from, so a walk from `done()` reached only the last
  await's lineage and `const steps = [f.run(a), f.run(b)]; for (const s of
  steps) await s;` reported run-1 unawaited. The init-time `executionAsyncId()`
  is that missing edge, and it is a fact the runtime reports rather than a
  widened approximation.

- The gate was quadratic in process-wide promise count: 5 000 awaits -> 1803 ms,
  30 000 -> 94 700 ms on a 25 ms body. Attribution is now eager and O(1) per
  promise, and only promises created inside the flow's own async scope are
  tracked. 30 000 -> 34 ms, and 140 007 unrelated process promises retained -> 0.
  A performance assertion pins the 30 000 case under 1 000 ms.

- `Promise.all` is still intercepted, because a combinator's aggregate has no
  runtime edge to its non-final members and every alternative reduces to
  callback identity inference. It no longer changes what `Promise.all` does:
  `Promise.all(5)` rejects instead of resolving `[]`, `Promise.all(null)`
  returns a rejected promise instead of throwing synchronously, and `name` is
  `all`. The interception is now DISCLOSED in docs/SURFACE.md with its reason
  and its process-wide scope, together with the one documented limit of the
  contract: a derived chain created inside a timer that fires after the body
  returns does not exist yet and cannot be observed.

- `close()` releases every tracked map, not only the promise handles.

Three things a reviewer should not mistake for noise:

- sdk/tsconfig.tests.json is WIDENED here, and the four type errors fixed in
  sdk/tests/authored-flow-operation.test.ts were NOT introduced by this change.
  Main's new typecheck:tests gate included only src and typed-output.test.ts;
  tsconfig.json excludes tests/ and vitest does not typecheck, so #134's test
  files were type-checked by nothing. Three of the four errors are pre-existing
  at 59c062c. `lib` is raised to ES2024.Promise in that gate config only, for
  Promise.withResolvers in the forgery tests; the SDK's own tsconfig stays on
  ES2022.

- Fire-and-forget async work outstanding at done() is now refused even when it
  would have succeeded. That is a deliberate tightening under covenant 2 and is
  documented; awaited work of any shape is unaffected.

- The 140 lines this branch deletes from main are all #134's own intent, and
  the biggest block is regressions/surface.d.ts, whose own header asked to be
  deleted once the real surface shipped. The repair report carries the full
  attribution table, and all 51 of #136's files this branch does not touch are
  blob-identical to 990093b.

The promise-graph observation moves to its own module; the lifecycle keeps the
operation-facing contract. Probes for every claim in the repair report are
committed under ops/probes/pr134-repair-0903/ and run with plain node —
including the kernel-spec assertion that `output` still LOWERS to a
json_schema gate, which validateSpec and `flows check` cannot see.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
The previous revision claimed adoption-from-the-resolving-context "covers
every combinator, present and future". It covers whichever member happens to
resolve the aggregate. An aggregate is derived from EVERY member, but the
runtime supplies an edge to only one: the aggregate is resolved inside the
reaction of whichever member settled last (all, allSettled) or first (race,
any). Inferring membership from that edge is sufficient, never necessary, and
it failed in both directions.

Signoff at 311b18c found both halves from the same line:

  P0  Promise.allSettled([step, unrelated]) where `unrelated` settles last
      orphans the aggregate, so a handled-and-forgotten derived rejection
      escapes and complete-* is lowered with result "success".
  P1  await Promise.allSettled([a, b]) refused every member except the last
      to settle -- authoring docs/SURFACE.md explicitly supports.

Membership cannot be recovered from the promise graph, so it is recorded
where the combinator is called and the member list is in hand: all four
intrinsics are intercepted rather than Promise.all alone.
adoptFromResolvingContext remains as a best-effort fallback for aggregates
built by hand, no longer as the mechanism.

Why the tests could not have caught it: every combinator row used a
single-member aggregate, `Promise.allSettled([step])`, which is always
resolved by the step itself. That shape cannot exhibit "resolved by a
different member" BY CONSTRUCTION -- the rows would have passed however the
mechanism was written. They now use multi-member aggregates that vary the
resolver, in both directions, across all four combinators.

MUTATION-VERIFIED. Reverting COMBINATORS to ['all'] (the pre-fix state) fails
exactly six tests, and they are both halves of the defect:

  x allSettled resolved by an unrelated member  -> promise resolved instead of rejecting
  x allSettled with the step declared second    -> promise resolved instead of rejecting
  x race resolved by an unrelated member        -> promise resolved instead of rejecting
  x any resolved by an unrelated member         -> promise resolved instead of rejecting
  x await Promise.allSettled over two steps     -> unawaited_step: run-1
  x await Promise.allSettled over five steps    -> unawaited_step: run-1..run-4

  pre  sha256 081787dc17727e379bfb790ca747ac33bc0be0ab627f0fb01573e3d203c2c201
  post sha256 edde33ae3689b49a7f61432003c1add70629c54757e72ce1464d0635969b641b  (file changed: asserted before the run)
  restored     081787dc17727e379bfb790ca747ac33bc0be0ab627f0fb01573e3d203c2c201  (byte-for-byte)

Spec transparency preserved and widened to all four: Symbol.iterator read
exactly once as the intrinsic does, a non-iterable handed to the intrinsic so
it produces the specified rejected promise rather than resolving [] or
throwing synchronously, `this` honoured for subclasses, name/length matching.

Gates: tsc --noEmit 0; tsc -p tsconfig.tests.json 0; full SDK suite
432 passed / 3 skipped / 0 failed with RELAYFLOWD_BIN pinned to this
worktree's build; lifecycle executor suite 27/27.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
@kjgbot
kjgbot force-pushed the feat/v2-surface-package branch from 4fa4ff5 to 4f85c4e Compare September 4, 2026 08:14
@kjgbot

kjgbot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Merging: green CI, independent signoff, and the three threads resolved

CI green at 4f85c4elinux-x64-artifact pass 7m32s, packed-consumer pass. The full kernel suite and all 26 SDK files now actually run, which they did not when this PR was opened.

Its first run after the rebase failed on cli-hn-monitor.test.ts > terminates (exit 1) when the worker emits an error asynchronously (expected +0 to be 1). I did not attribute that to this PR: locally it passes 16/16 across 3 runs, and #157's run passed the same test. Re-run, it passes. An exit-code race, not this branch.

The Codex signoff carries to this head, proven rather than assumed. It returned PASSED at c4941e13; this head is 4f85c4e after two rebases. So I hashed every one of the PR's own non-workflow files at both heads:

PR touches 57 non-workflow files
files differing from the signed-off head: 0

Including the four the signoff's conclusions rest on:

081787dc17727e37  sdk/src/authored-flow-lifecycle.ts
7f1f6c3a4e2014b8  sdk/src/authored-flow-executor.ts
bd4075df7275379a  sdk/src/authored-flow-operation.ts
8487d735a93dca36  surface/src/flow.ts

081787dc… also matches the pre-mutation hash recorded in the signoff, which independently confirms the COMBINATORS mutation was restored byte-for-byte. Only .github/ differs between the two heads.

Threads resolved. All three were mine, written at the older head d830d027, and an independent reviewer proved each CLOSED by execution — not by reading:

  • terminal guard on a pre-created lazy step → assertOperationAllowed runs at start, not creation; focused test passes
  • settled state losing the outcome → root failure retained via rootFailureRecorded; a derived .then(undefined, …) cannot erase it
  • reflectively-readable SymbolObject.getOwnPropertySymbols(genuine) returns []; both forgery paths refused

Plus the mutation that matters: narrowing COMBINATORS to ['all'] fails 9 tests, four of them multi-member aggregates resolved by an unrelated member. That is the allSettled attribution concern, and it is bound by tests rather than by assertion.

I resolved them on that evidence rather than my own, since I wrote the fix.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

@kjgbot
kjgbot merged commit e9321d2 into main Sep 4, 2026
4 of 5 checks passed
kjgbot pushed a commit that referenced this pull request Sep 4, 2026
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
kjgbot added a commit that referenced this pull request Sep 4, 2026
* feat(surface): ship v2 authoring package

Replace the regression-only ambient declaration with a real @relayflows/surface package and point the dormant in-repo flows at its source contract. Keep execution and compiler concerns behind the journal-backed runtime.

Refs #132

Session-Id: 01a0627a-c11f-7850-9667-c638320d25f4

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* feat(surface): add runtime bridge and package gate

Session-Id: 01a062b4-562d-7143-9296-dd34cc65251f

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* feat(surface): lower authored run steps through journal

Session-Id: 01a062b4-562d-7143-9296-dd34cc65251f

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(surface): fail closed at authored boundary

Session-Id: 01a062b4-562d-7143-9296-dd34cc65251f

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(surface): close authored operation lifecycle

Session-Id: 01a062b4-562d-7143-9296-dd34cc65251f

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* test(surface): reproduce authored lifecycle escapes

Session-Id: 01a062b7-2aef-7772-8225-7cb00ee311dd

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(surface): enforce authored lifecycle provenance

Session-Id: 01a062b7-2aef-7772-8225-7cb00ee311dd

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* docs(review): record PR 134 structure findings

Session-Id: 01a062d6-d0fb-7060-b9a7-57031d858ea9

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(sdk): close the derived-work settlement race in the authored gate

The derived-chain gate sampled which failures had already landed. It skipped
every promise that had not settled, so the same program passed or failed on
how many microtask ticks the failure took: ten `await null`s, or any real
derived I/O, cleared the window and a flow recorded terminal success while
work derived from a step had thrown.

Widening the window cannot fix that; there is no safe tick count. What is
timing-independent is whether derived work was still in flight when the body
returned: work the author awaited is settled at that instant in every timing,
and work the author did not await is pending in every timing. The gate now
reads the in-flight set before it awaits anything and refuses on it
(`unsettled_derived_work`). That also makes the settled set complete, so
inspecting settled outcomes stops being a sample and becomes a total answer
over a closed set.

The same escape existed through Promise.allSettled, Promise.any and
Promise.race, which no earlier review had demonstrated. A combinator resolves
its aggregate from inside the reaction of one of its members, so the aggregate
is not downstream of any member by `trigger` — only `Promise.all` was covered,
and only because it is registered by name. Aggregates now inherit attribution
from the context that resolves them, which covers every combinator without
intercepting any of them.

Also repaired, all measured:

- The reachability predicate refused ordinary authoring. `trigger` and
  `resolutionCause` do not connect an async function's resumption context to
  the context it suspended from, so a walk from `done()` reached only the last
  await's lineage and `const steps = [f.run(a), f.run(b)]; for (const s of
  steps) await s;` reported run-1 unawaited. The init-time `executionAsyncId()`
  is that missing edge, and it is a fact the runtime reports rather than a
  widened approximation.

- The gate was quadratic in process-wide promise count: 5 000 awaits -> 1803 ms,
  30 000 -> 94 700 ms on a 25 ms body. Attribution is now eager and O(1) per
  promise, and only promises created inside the flow's own async scope are
  tracked. 30 000 -> 34 ms, and 140 007 unrelated process promises retained -> 0.
  A performance assertion pins the 30 000 case under 1 000 ms.

- `Promise.all` is still intercepted, because a combinator's aggregate has no
  runtime edge to its non-final members and every alternative reduces to
  callback identity inference. It no longer changes what `Promise.all` does:
  `Promise.all(5)` rejects instead of resolving `[]`, `Promise.all(null)`
  returns a rejected promise instead of throwing synchronously, and `name` is
  `all`. The interception is now DISCLOSED in docs/SURFACE.md with its reason
  and its process-wide scope, together with the one documented limit of the
  contract: a derived chain created inside a timer that fires after the body
  returns does not exist yet and cannot be observed.

- `close()` releases every tracked map, not only the promise handles.

Three things a reviewer should not mistake for noise:

- sdk/tsconfig.tests.json is WIDENED here, and the four type errors fixed in
  sdk/tests/authored-flow-operation.test.ts were NOT introduced by this change.
  Main's new typecheck:tests gate included only src and typed-output.test.ts;
  tsconfig.json excludes tests/ and vitest does not typecheck, so #134's test
  files were type-checked by nothing. Three of the four errors are pre-existing
  at 59c062c. `lib` is raised to ES2024.Promise in that gate config only, for
  Promise.withResolvers in the forgery tests; the SDK's own tsconfig stays on
  ES2022.

- Fire-and-forget async work outstanding at done() is now refused even when it
  would have succeeded. That is a deliberate tightening under covenant 2 and is
  documented; awaited work of any shape is unaffected.

- The 140 lines this branch deletes from main are all #134's own intent, and
  the biggest block is regressions/surface.d.ts, whose own header asked to be
  deleted once the real surface shipped. The repair report carries the full
  attribution table, and all 51 of #136's files this branch does not touch are
  blob-identical to 990093b.

The promise-graph observation moves to its own module; the lifecycle keeps the
operation-facing contract. Probes for every claim in the repair report are
committed under ops/probes/pr134-repair-0903/ and run with plain node —
including the kernel-spec assertion that `output` still LOWERS to a
json_schema gate, which validateSpec and `flows check` cannot see.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(surface): make aggregate membership exact, not resolution-inferred

The previous revision claimed adoption-from-the-resolving-context "covers
every combinator, present and future". It covers whichever member happens to
resolve the aggregate. An aggregate is derived from EVERY member, but the
runtime supplies an edge to only one: the aggregate is resolved inside the
reaction of whichever member settled last (all, allSettled) or first (race,
any). Inferring membership from that edge is sufficient, never necessary, and
it failed in both directions.

Signoff at 311b18c found both halves from the same line:

  P0  Promise.allSettled([step, unrelated]) where `unrelated` settles last
      orphans the aggregate, so a handled-and-forgotten derived rejection
      escapes and complete-* is lowered with result "success".
  P1  await Promise.allSettled([a, b]) refused every member except the last
      to settle -- authoring docs/SURFACE.md explicitly supports.

Membership cannot be recovered from the promise graph, so it is recorded
where the combinator is called and the member list is in hand: all four
intrinsics are intercepted rather than Promise.all alone.
adoptFromResolvingContext remains as a best-effort fallback for aggregates
built by hand, no longer as the mechanism.

Why the tests could not have caught it: every combinator row used a
single-member aggregate, `Promise.allSettled([step])`, which is always
resolved by the step itself. That shape cannot exhibit "resolved by a
different member" BY CONSTRUCTION -- the rows would have passed however the
mechanism was written. They now use multi-member aggregates that vary the
resolver, in both directions, across all four combinators.

MUTATION-VERIFIED. Reverting COMBINATORS to ['all'] (the pre-fix state) fails
exactly six tests, and they are both halves of the defect:

  x allSettled resolved by an unrelated member  -> promise resolved instead of rejecting
  x allSettled with the step declared second    -> promise resolved instead of rejecting
  x race resolved by an unrelated member        -> promise resolved instead of rejecting
  x any resolved by an unrelated member         -> promise resolved instead of rejecting
  x await Promise.allSettled over two steps     -> unawaited_step: run-1
  x await Promise.allSettled over five steps    -> unawaited_step: run-1..run-4

  pre  sha256 081787dc17727e379bfb790ca747ac33bc0be0ab627f0fb01573e3d203c2c201
  post sha256 edde33ae3689b49a7f61432003c1add70629c54757e72ce1464d0635969b641b  (file changed: asserted before the run)
  restored     081787dc17727e379bfb790ca747ac33bc0be0ab627f0fb01573e3d203c2c201  (byte-for-byte)

Spec transparency preserved and widened to all four: Symbol.iterator read
exactly once as the intrinsic does, a non-iterable handed to the intrinsic so
it produces the specified rejected promise rather than resolving [] or
throwing synchronously, `this` honoured for subclasses, name/length matching.

Gates: tsc --noEmit 0; tsc -p tsconfig.tests.json 0; full SDK suite
432 passed / 3 skipped / 0 failed with RELAYFLOWD_BIN pinned to this
worktree's build; lifecycle executor suite 27/27.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

---------

Co-authored-by: kjgbot <kjgbot@agentrelay.dev>
Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
kjgbot pushed a commit that referenced this pull request Sep 4, 2026
The branch shipped unresolved markers in authored-flow-executor.ts and
authored-flow.test.ts (both citing 0987e38), so it did not compile:
TS1185: Merge conflict marker encountered.

Executor: keep main's authored-operation lifecycle from #134 -- runBody,
stopAuthoredOperations, verifyAuthoredOperations -- and thread this branch's
input through it, rather than the bare await that would have deleted the
lifecycle wholesale.

Tests: the two sides are DIFFERENT tests, not rival versions of one. HEAD holds
main's merged lifecycle and refusal coverage; the branch adds direct-input,
sibling-ordering and explicit-completion cases. Kept both, and merged the import
so FlowHeader and Ctx are both available.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
kjgbot added a commit that referenced this pull request Sep 4, 2026
* feat(cli): run authored flows with direct input

Session-Id: 01a062de-3f73-7621-8286-72efe45639aa

Session-Id: 01a062de-3f73-7621-8286-72efe45639aa

Session-Id: 6cae47a0-1263-4c8b-bfaa-bd5ffc72e08e
(cherry picked from commit c4aaf12)

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* ops(review): record PR 140 repair findings

Session-Id: 01a062de-3f73-7621-8286-72efe45639aa

Session-Id: 01a062de-3f73-7621-8286-72efe45639aa

Session-Id: 6cae47a0-1263-4c8b-bfaa-bd5ffc72e08e
(cherry picked from commit 5d2e41c)

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(cli): execute direct flows through journal runtime

Session-Id: 01a062de-3f73-7621-8286-72efe45639aa

Session-Id: 6cae47a0-1263-4c8b-bfaa-bd5ffc72e08e
(cherry picked from commit 6384600)

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(sdk): resolve the committed conflict markers this branch carried

The branch shipped unresolved markers in authored-flow-executor.ts and
authored-flow.test.ts (both citing 0987e38), so it did not compile:
TS1185: Merge conflict marker encountered.

Executor: keep main's authored-operation lifecycle from #134 -- runBody,
stopAuthoredOperations, verifyAuthoredOperations -- and thread this branch's
input through it, rather than the bare await that would have deleted the
lifecycle wholesale.

Tests: the two sides are DIFFERENT tests, not rival versions of one. HEAD holds
main's merged lifecycle and refusal coverage; the branch adds direct-input,
sibling-ordering and explicit-completion cases. Kept both, and merged the import
so FlowHeader and Ctx are both available.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* test(sdk): restore main's authored-flow suite rather than guess the interleave

The branch's committed markers in authored-flow.test.ts could not be resolved
mechanically. Measured, not assumed: four of the six regions have
brace_delta=2 and paren_delta=1 on the incoming side -- unbalanced fragments
whose closing braces live in shared trailing context. The two sides interleave,
so neither 'take one side' nor 'concatenate both' produces valid syntax, and my
first attempt at the latter orphaned braces (esbuild: Expected "finally").

Taking main's file whole guarantees its merged lifecycle and refusal coverage
survives intact -- the property that matters most, since silently dropping
shipped tests is the failure this stack keeps hitting.

The branch's own feature stays covered: direct-input.test.ts is separate and
passes. The four supplementary cases it added inside authored-flow.test.ts
(direct input into a journal-backed body, sibling ordering before the join,
explicit completion after journal-backed steps, and an it.each table) are NOT
in this commit and should be re-added by someone who knows their intended
bodies.

Kernel 142 passed, SDK 649 passed, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(surface): cast at the definitions store, where Input turns invariant

The authoring package stopped compiling once flow() became generic over Input:

  src/flow.ts(67,27): error TS2345: Argument of type
  'AuthoredFlowDefinition<Input>' is not assignable to parameter of type
  'AuthoredFlowDefinition<unknown>'.

One WeakMap holds definitions for many input types, and `body` puts Input in a
parameter position, so the type is invariant -- a definition parameterised over
the author's Input is not assignable to the map's default parameterisation even
though getFlowDefinition<Input> recovers exactly that type on the way out. Cast
once at the storage boundary, with the reason recorded there.

CI caught this, not my local run: I had piped `bun run build` to /dev/null and
echoed success without checking its exit code, so a failing surface build looked
green. Every gate in this commit was re-run with its exit code asserted.

surface build exit=0, surface 7 passed, sdk tsc exit=0, SDK 649 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

---------

Co-authored-by: kjgbot <kjgbot@agentrelay.dev>
kjgbot added a commit that referenced this pull request Sep 4, 2026
* feat(cli): run authored flows with direct input

Session-Id: 01a062de-3f73-7621-8286-72efe45639aa

Session-Id: 01a062de-3f73-7621-8286-72efe45639aa

Session-Id: 6cae47a0-1263-4c8b-bfaa-bd5ffc72e08e
(cherry picked from commit c4aaf12)

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* ops(review): record PR 140 repair findings

Session-Id: 01a062de-3f73-7621-8286-72efe45639aa

Session-Id: 01a062de-3f73-7621-8286-72efe45639aa

Session-Id: 6cae47a0-1263-4c8b-bfaa-bd5ffc72e08e
(cherry picked from commit 5d2e41c)

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(cli): execute direct flows through journal runtime

Session-Id: 01a062de-3f73-7621-8286-72efe45639aa

Session-Id: 6cae47a0-1263-4c8b-bfaa-bd5ffc72e08e
(cherry picked from commit 6384600)

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(sdk): resolve the committed conflict markers this branch carried

The branch shipped unresolved markers in authored-flow-executor.ts and
authored-flow.test.ts (both citing 0987e38), so it did not compile:
TS1185: Merge conflict marker encountered.

Executor: keep main's authored-operation lifecycle from #134 -- runBody,
stopAuthoredOperations, verifyAuthoredOperations -- and thread this branch's
input through it, rather than the bare await that would have deleted the
lifecycle wholesale.

Tests: the two sides are DIFFERENT tests, not rival versions of one. HEAD holds
main's merged lifecycle and refusal coverage; the branch adds direct-input,
sibling-ordering and explicit-completion cases. Kept both, and merged the import
so FlowHeader and Ctx are both available.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* test(sdk): restore main's authored-flow suite rather than guess the interleave

The branch's committed markers in authored-flow.test.ts could not be resolved
mechanically. Measured, not assumed: four of the six regions have
brace_delta=2 and paren_delta=1 on the incoming side -- unbalanced fragments
whose closing braces live in shared trailing context. The two sides interleave,
so neither 'take one side' nor 'concatenate both' produces valid syntax, and my
first attempt at the latter orphaned braces (esbuild: Expected "finally").

Taking main's file whole guarantees its merged lifecycle and refusal coverage
survives intact -- the property that matters most, since silently dropping
shipped tests is the failure this stack keeps hitting.

The branch's own feature stays covered: direct-input.test.ts is separate and
passes. The four supplementary cases it added inside authored-flow.test.ts
(direct input into a journal-backed body, sibling ordering before the join,
explicit completion after journal-backed steps, and an it.each table) are NOT
in this commit and should be re-added by someone who knows their intended
bodies.

Kernel 142 passed, SDK 649 passed, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(surface): cast at the definitions store, where Input turns invariant

The authoring package stopped compiling once flow() became generic over Input:

  src/flow.ts(67,27): error TS2345: Argument of type
  'AuthoredFlowDefinition<Input>' is not assignable to parameter of type
  'AuthoredFlowDefinition<unknown>'.

One WeakMap holds definitions for many input types, and `body` puts Input in a
parameter position, so the type is invariant -- a definition parameterised over
the author's Input is not assignable to the map's default parameterisation even
though getFlowDefinition<Input> recovers exactly that type on the way out. Cast
once at the storage boundary, with the reason recorded there.

CI caught this, not my local run: I had piped `bun run build` to /dev/null and
echoed success without checking its exit code, so a failing surface build looked
green. Every gate in this commit was re-run with its exit code asserted.

surface build exit=0, surface 7 passed, sdk tsc exit=0, SDK 649 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

---------

Co-authored-by: kjgbot <kjgbot@agentrelay.dev>
Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
kjgbot pushed a commit that referenced this pull request Sep 5, 2026
…aked test daemons

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
kjgbot pushed a commit that referenced this pull request Sep 6, 2026
…e escape live

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
kjgbot pushed a commit that referenced this pull request Sep 6, 2026
…" claim

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
kjgbot pushed a commit that referenced this pull request Sep 6, 2026
Main carries the four-combinator interception by a different commit, verified
50/50 with 8 rows red under mutation. Records why is-ancestor was the wrong
check: it answers whether a commit landed, not whether the defect is still real.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
kjgbot pushed a commit that referenced this pull request Sep 8, 2026
…s stale

#134 and #139 both merged four days ago and the allSettled fix is on main in
refactored form — two of my checks gave false negatives (stale path, grep for
the old branch's identifiers) before I confirmed the behaviour. Verified the
docs disclosure the code comment claims rather than trusting it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR
kjgbot pushed a commit that referenced this pull request Sep 8, 2026
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant