feat(surface): add unpublished authored contract foundation - #134
Conversation
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (25)
🚧 Files skipped from review as they are similar to previous changes (1)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe change adds the ChangesSurface authoring contract
Estimated code review effort: 4 (Complex) | ~45 minutes Merge Risk: ⚪ Minimal · up to 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
Poem
Note 🎁 Summarized by CodeRabbit FreeYour 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 |
kjgbot
left a comment
There was a problem hiding this comment.
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.
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
59c062c to
311b18c
Compare
…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
PR #134 independent signoff — Codex — 2026-09-04VERDICT: PASSED Reviewed only exact head Reviewed head and thread stateThe requested GraphQL query returned the three threads as still administratively unresolved: 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
|
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
c4941e1 to
817db37
Compare
Rebased onto
|
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
817db37 to
4fa4ff5
Compare
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
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
4fa4ff5 to
4f85c4e
Compare
Merging: green CI, independent signoff, and the three threads resolvedCI green at Its first run after the rebase failed on The Codex signoff carries to this head, proven rather than assumed. It returned PASSED at Including the four the signoff's conclusions rest on:
Threads resolved. All three were mine, written at the older head
Plus the mutation that matters: narrowing I resolved them on that evidence rather than my own, since I wrote the fix. 🤖 Generated with Claude Code |
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
* 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
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
* 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>
* 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
…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
…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
…" 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
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
…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
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR
Summary
@relayflows/surface: immutable authored-flow handles, closed context/completion types, and a private runtime bridgeWeakMap; genuine handles expose no symbol/key from which another accepted handle can be forgedrun/llm/agentroot operation in an executor-owned lifecycle independent of author promise handlersf.runlowering throughJournalClientandstep.completed, while keepingexecuteAuthoredFlowabsent from the SDK root exportThis PR does not publish a package, ship direct
.flow.tsexecution, 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:
5f2c0b9a22a7cab916980d49b5992f3cab041761Refs #132
Literal red before repair (
d830d027843b64da27eca3b2f805ddc9b33ac058)Reflectable handle provenance
Terminal/lifecycle and rejection-chain failures
An additional red pinned the zero-journal requirement for forgotten work while the body remains open:
Literal green at pushed head
Focused lifecycle/type gate
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
Real daemon boundary
The probe starts exact-tree
kernel/target/debug/relayflowd, usesJournalClient, executes the named authored cases, counts the daemon's SQLite journals, and asserts the post-donemarker is absent.The five journals are exactly manual
run, failed consumedrun, derived-chain sourcerun, awaited positiverun, and its terminal marker. Precreated/forgotten cases create none.Repository-owned packed consumer gate
Full SDK and kernel regressions
Commit and remote identity