Repository navigation
feat(runtime-host): bridge event waits to Goal continuation - #6147
Conversation
Astro-Han
left a comment
There was a problem hiding this comment.
Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.
First review of 30c291f96bcc0aaed730a36a6a3e40cd4983e951, one commit on merge-base db455a1661ec (current main 368f5c9a3d4a). I read the diff against the PR3 plan in #5926 and checked the storage bridge, both providers, schema v15, the Root admission seam, the Host bridge and Goal gating, and the Runtime yield. No prior review findings.
Verdict: I found no verified P0–P2 defect. Atomicity, provider parity, single-successor and migration behaviour hold up. Below are three P3 items: an epoch collision, a test that hangs on Node 22, and an untested lost-acknowledgement replay path.
What I verified
- Atomicity and provider parity. Register, settle, queue and admit are pure functions in
event-wait-bridge.ts. SQLite runs each one inside a single operational-DB write transaction, and Root admission writes the Goal/wait linkage in the same transaction that insertscore_root_turn_admissions. The Memory provider runs them inside oneauthority.write. Ordering is identical in both: existing admission → rejection → source checks → joint write.- The only parity difference is how a second undelivered wait is rejected. Memory checks for it explicitly; SQLite relies on the partial unique index
workflow_event_waits_one_active_session. Both reject, but the error type differs. - Rollback at each stage and SQLite SIGKILL at each window are tested.
- The only parity difference is how a second undelivered wait is rejected. Memory checks for it explicitly; SQLite relies on the partial unique index
- At most one effective successor.
- Result vs timeout: resolving at or after the deadline is rejected,
expiredis terminal, and the PR2 CAS commit picks one winner. - Once queued, the pending entry is frozen: same turn/run/message IDs and provenance digest, and a conflicting re-queue throws.
- Admission requires a settled gate, no
currentExecution, no priordelivery, and a matching digest. In the same transaction it clears the gate and setsdelivery.admitted. - A replayed
admitRootTurnreturnsexistingbefore reaching the bridge block. - The late-queue-acknowledgement race is covered, and
#adoptWaitAuthorityignores stale snapshots. - Goal control shares the Session admission gate with Root admission, so a pause cannot interleave with the
admitted()adoption.
- Result vs timeout: resolving at or after the deadline is rejected,
- Gating and resources.
- A source Turn that does not end with a durable
event_waityield ends up pausing the Goal, which suppresses the wait; it does not replay. - Observation authorization reuses PR2's flight and cooldown accounting.
- The bridge's own timer is unref'd and stopped on drain.
- A source Turn that does not end with a durable
- Schema. v14 → v15 runs only when the registered version is older; newer versions are refused. The target-schema check uses the same migration function. Indexes exclude delivered rows from active ownership and delivery scans. Portable copies pause linked Goals with a new generation; same-root backups keep the linkage.
- Protocol.
turn-origin, the runtime invocation root authority and the root execution descriptor each gain an optionaleventWait, and older strict decoders reject it. The epoch bump is therefore warranted.
Findings
-
P3: protocol epoch collision. This PR moves
RUNTIME_HOST_COMPATIBILITY_EPOCHfrom 222 to 223. #6115, #6091 and #6124 also declare 223. Whichever merges later needs to rebase to the next free epoch and keep its comment line. -
P3: test hangs deterministically on Node 22.
late queue acknowledgement cannot restore a gate after Root admissionhangs on Node 22.23.2 (enginesallows^22.19.0). It fails 3/3 in isolation, and the runner then cancels the remaining 22 tests ingoal-root-authority.test.js.- It stalls at
await admissionAccess.promise. The second authorization depends on the bridge's 1 s rescan timer, which isunref()'d (goal-event-waits.ts:391), so Node 22's event loop empties first. - Adding a ref'd keep-alive around that await makes the whole file pass (45/45).
- CI's
testjob runs only Node 24, where it passes in about 2 s. - Production is unaffected. A fix would be to drive the rescan through the fixture's injected clock, or to call
reconcile()explicitly before awaiting.
-
P3 (unreproduced suspicion): in-process lost acknowledgement at Root admission.
- Suppose
admitRootTurncommits but its acknowledgement is lost, and the Goal lane retriesadmitTurnwith the same frozen identity. The retry takes the existing-admission branch ofRootTurnCoordinator, which runs theadmitExecutiongate after the fact. - By then the gate has been cleared and
delivery.admittedis set.authorizeAdmissiontherefore returnsfalse, and the retry fails withHostedRootAdmissionGateErrorinstead of adopting the durable admission. - Nothing is duplicated. The likely outcome is an errored Goal Turn or a Host drain, then a restart that closes the admitted successor without a Run, so the wait result is never acted on.
- Only the storage-level replay is tested (Memory,
event-wait-bridge.test.ts). - Suggested fix: have
authorizeAdmissionaccept a wait whosedeliveryisadmittedwith exactly the pending turn/run/message IDs, and add a Host-level test.
- Suppose
Tests run locally. Fresh worktree, Node 22.23.2, npm ci + build, then the PR's 14 focused files run serially: 766 tests, 743 pass, 0 fail, 23 cancelled. All 23 cancellations come from finding 2; with the keep-alive the affected file passes 45/45. I did not run the full suite.
CI and mergeability. All checks on the head pass, including test (Node 24, 15m) and installed-CLI validation on Node 22.19/24. GitHub reports the PR as mergeable; BLOCKED is only the pending required review.
| // Increment when the same protocol version no longer guarantees safe Client-Host | ||
| // interoperability. Mismatches are rejected before domain commands are admitted. | ||
| export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 222 as const; | ||
| export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 223 as const; |
| } | ||
| }); | ||
|
|
||
| test('late queue acknowledgement cannot restore a gate after Root admission', { |
There was a problem hiding this comment.
P3: on Node 22.23.2 (allowed by engines ^22.19.0) this test hangs at await admissionAccess.promise every time. The second authorization depends on the bridge's unref'd 1 s rescan timer, so the event loop empties first, and the runner then cancels the remaining 22 tests in this file. Adding a ref'd keep-alive around that await makes the file pass 45/45. CI only runs it on Node 24. Consider driving the rescan through the fixture clock, or calling reconcile() explicitly.
| }); | ||
| if ( | ||
| !wait || | ||
| wait.record.delivery || |
There was a problem hiding this comment.
P3 (unreproduced): suppose a Root admission commits but its acknowledgement is lost, and admitTurn is retried with the same frozen identity. The existing-admission path runs this gate again, but the admission transaction has already cleared activeEventWait and set delivery.admitted. This check then returns false, the retry fails with HostedRootAdmissionGateError, and the durable admission is never adopted. Consider accepting delivery.kind === 'admitted' when its turn/run/message IDs match pending.execution/userMessageId, and add a Host-level test for this window.
30c291f to
5386b1d
Compare
|
Updated branch: Drafts for the three review threads; not posted to GitHub. Protocol epochThanks for flagging the merge-order dependency. I rebased onto Node 22 test lifetimeConfirmed and fixed. On Node 22.23.2, the original test exits while awaiting The original test still reproduces the cancellation against the rebased production code. The updated complete Lost admission acknowledgementI added Host-level regression cases for both Memory and SQLite, injecting an error after the real These confirm the existing failure boundary: the Host requests drain, the delivery remains durably admitted, and no successor Run starts. An explicit retry with the same identity is rejected by I therefore kept the event-wait gate unchanged: allowing |
Astro-Han
left a comment
There was a problem hiding this comment.
Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.
Incremental re-review of 5386b1dfc0bb318ef90b3cac99c7294ccd8c74ed. The previous review was at 30c291f96bcc. The branch was rebased from merge-base db455a1661ec onto 6772a3456223, which brings in 24 main commits, and now has two commits: the rebased feature commit cdc7a68d6 and a new test-only commit 5386b1dfc.
What changed. git range-diff shows the feature commit differs only in rebase context: an import line in runtime-invocation.ts, surrounding lines in ai-sdk-turn.ts, and the protocol epoch moving from 223 to 225, placed above main's 224 comment block. No production logic changed. The new commit only touches goal-root-authority.test.ts. It wraps the late-queue barrier in withTimeout, adds an afterRootAdmission fixture hook, adds a Memory and a SQLite test for a lost Root-admission acknowledgement, and makes fixture cleanup run in finally.
Prior findings
- P3, epoch collision at 223: superseded, but the collision is back at 225 (see below).
- P3, the late-queue test hanging on Node 22: fixed.
withTimeoutuses a ref'd timer, which keeps the event loop alive while the unref'd bridge rescan reaches the barrier. On Node 22.23.2,goal-root-authority.test.jsnow passes 47/47, three runs in a row (before the fix: 23 cancelled). - P3, in-process lost acknowledgement at Root admission: resolved by design. The author chose to fail closed instead of adopting the durable admission. When the acknowledgement is lost,
RootAdmissionOwnerpoisons the Session. A same-identity retry is rejected with "identity changed within one Host Epoch" before the execution gate runs, nothing starts, and the Host drains. This matches the existing crash-after-admission contract and is now tested for both Memory and SQLite. Both tests pass locally. What happens to the Goal after restart is still not tested end to end, but I'm not raising that as a finding.
New findings
- P3: protocol epoch collision at 225. Main is at 224, and this PR now claims 225. Open PRs #6163 and #6091 also declare
RUNTIME_HOST_COMPATIBILITY_EPOCH = 225(#6115 still declares the stale 223). Whichever merges later needs to move to the next free epoch and keep its comment line. Separately, the new 225 comment sits above main's 224 block, and that block still says "223 is used by ... open PRs #6115 and #6091". That note is now stale and could be cleaned up when the epoch is re-checked at merge time.
I found no P0–P2 issues in the increment.
Tests run locally. Fresh worktree, Node 22.23.2, npm ci plus builds of core through runtime-host.
goal-root-authority,protocolandevent-wait-managerrun serially in one invocation: 395 tests, 394 pass, 1 fail. The failure wasexpire: peer Retry-After releases the dispatch reservation without consuming another attempt. That test already exists on main and is not changed by this PR, and it passed 3/3 when run alone, so I treat it as a load-related flake unrelated to this PR.- Storage
event-wait-bridgeandevent-wait-authority: 88/88 pass.
CI and mergeability. All checks on 5386b1df pass, including test (15m28s) and installed-CLI validation on Node 22.19 and 24. GitHub reports the PR as MERGEABLE; the BLOCKED state only reflects the pending required review.
Astro-Han
left a comment
There was a problem hiding this comment.
Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.
I reviewed the 50-file change at 5386b1dfc0bb318ef90b3cac99c7294ccd8c74ed, including the Goal continuation coordinator, Root admission/recovery, both storage providers, workflow migration, and typed transcript provenance. The change registers an explicit wait, settles its source Turn, freezes a successor, and consumes that successor in the Root admission transaction. WaitForEvent remains internal: production adapters, catalogs, ShellRun integration, and UI are not enabled here.
No verified P0–P2 findings. Registration/settlement/delivery preserve Goal control generations and budget checks. Ordinary evaluation and waiting timers remain gated while an explicit wait is outstanding; cancellation, revocation, and newer user work do not authorize a stale successor. I checked the joint transitions in packages/storage/src/event-wait-bridge.ts, their SQLite/Memory implementations, and the admission and recovery integration under packages/runtime-host/src/server/.
The lost-acknowledgement behavior deliberately favors avoiding duplicate work over automatic progress. The existing Root authority rejects another admission in the same Host epoch and requests Host drain after a committed admission loses its acknowledgement. I additionally reopened a SQLite snapshot into fresh Host/Root/Goal objects: an admitted successor without a Run was closed as app_restarted, the Goal became paused, and explicit user resume completed through a fresh ordinary Goal Turn without redelivering the old wait. This requires Host restart and user resume; it is not transparent automatic recovery. Separately, queued resolved and expired results survived reopening and executed exactly one successor with their original Turn/Run/message IDs and provenance. Documenting this recovery procedure alongside the internal bridge would help future adapters expose the behavior clearly.
Validation on Node 22.23.2: 770 tests passed across the 14 focused files, with no skips or cancellations. These exercise both providers, failed transactions, storage SIGKILL/reopen boundaries, bounded observation, permission/control races, accounting, existing Goal behavior, and real Root admission with fake adapters/models. Six additional checks covered the three fresh-Host recovery scenarios above and workflow 14 → 15 upgrades of existing waiting/resolved/expired records; Goal/wait facts and source cooldowns were preserved. Fresh dependency installation/patches, normal root build:test, workspace typechecks, production renderer build and output checks, lint, formatting, ASF headers, renderer architecture, surface inventory, and protocol guard passed.
P3 — coordinate compatibility epoch 225 before merging (packages/runtime-host/src/protocol/index.ts:111). Main is currently 224, but open #6091 (752020ce) and #6163 (591c08bc) also declare 225 for different protocol changes. The first merge may use 225; later changes must rebase and take the next unused epoch. The PR body's older 222 → 223 migration note also needs refreshing.
The exact head remains OPEN/MERGEABLE, and CI workflow 39218667755 succeeded. A trial merge with current main was clean; I did not build that merged tree. I did not run full Electron, production adapters/providers, the complete workspace test suite, or exhaustive Host-process crash interleavings. The additional recovery checks used durable SQLite snapshots, not process-kill testing of the full Host.
Register and settle explicit waits through joint Goal authority operations, then deliver frozen continuations through atomic Root admission. Keep observation bounded and the production tool hidden. Generated-by: OpenAI Codex
Keep the late-queue authorization wait alive on Node 22 with a bounded signal wait. Exercise lost storage acknowledgements through the real Root admission path for Memory and SQLite, retaining the existing fencing and cleanup contract. Generated-by: OpenAI Codex
5386b1d to
7fe6963
Compare
Astro-Han
left a comment
There was a problem hiding this comment.
Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.
Incremental review of 5386b1dfc0bb318ef90b3cac99c7294ccd8c74ed → 7fe6963f6da7fd9964536978dbf7056c74efbe29. No new verified P0–P2 findings. I compared the rewritten two-commit series with git range-diff and checked the actual file differences. Both heads retain the same merge base, 6772a3456; this update did not incorporate newer main commits. The second patch is identical. The first changes only recovery documentation and protocol comments: two files, 12 additions/two deletions. All executable Goal, event-wait, Root-admission and storage code, and all tests, remain byte-for-byte unchanged; the protocol module differs only in standalone comments.
The new recovery instructions in packages/runtime/README.md:79 match the existing behavior: queued successors retain frozen identities, while uncertain admission drains the Host and fences the Session. After restart, an admitted Goal Turn without a Run is closed as app_restarted and the Goal pauses; explicit user resume starts a fresh continuation without redelivering the old wait. Both Memory and SQLite lost-acknowledgement cases passed again. My earlier fresh-Host recovery and migration probes remain historical evidence at 5386b1dfc, rather than tests newly rerun here. Production event sources and the public WaitForEvent tool remain outside this PR's enabled surface.
P3 — epoch 225 still overlaps other open protocol changes. Current main 622a4cb045b067aa93c798a598fc124646199f3e still declares epoch 224, so 225 has not been consumed on main. This head uses 225 at packages/runtime-host/src/protocol/index.ts:111; open #6091 at 752020ce418d55a406943ebfe556ed019d142fb1 and #6163 at 591c08bc2bc365f85320eab3b7133486ae2448cb also declare 225 for their distinct changes. Re-check after the first incompatible change lands and give later changes the next unused epoch. The guard currently passes 224 → 225. The PR description now states that transition correctly; the previous stale epoch description is fixed.
Fresh dependency installation/patches, normal root build:test, all workspace typechecks including the four Desktop configurations, formatting/lint for the changed protocol file, and git diff --check passed. Five focused test files covering Goal/Root admission, protocol decoding, event-wait management and the storage bridge/authority passed 483/483, with no skips or cancellations. CI 39253103634, CLI package validation and dependency audit succeeded at this exact head.
The head is OPEN/MERGEABLE and a trial merge with current main was clean. I did not build that merged tree, repeat the complete local suite or earlier custom recovery probes, or test Electron, real production adapters/providers, or exhaustive Host-process crashes. This follow-up confirms the unchanged execution logic and the documentation update within those limits.
|
Thanks for the incremental review and the focused test rerun. Agreed on coordinating the epoch at merge time. I checked main at The update at |
Summary
PR2 persists event-wait observations but does not resume Goal execution. This implements the PR3 execution-bridge plan: a Turn registers a wait, ends normally and releases execution resources; bounded observation makes no work-model or Goal-evaluator calls; a result or timeout can then admit at most one effective successor through existing Root admission.
WaitForEventremains internal and absent from production catalogs; production adapters, the ShellRun entry point and UI remain in PR4.Refs #5926 and #5920. Builds on #6057 and #6113.
Verification
Final source:
7fe6963f6da7fd9964536978dbf7056c74efbe29. The latest follow-up changes only recovery documentation and protocol comments; executable behavior and tests are unchanged from5386b1dfc.git diff --check, and the protocol epoch guard against main21f1cc1c2(224 → 225). Normal commit hooks passed.5386b1dfcpassed 1,053/1,053 focused Node 24 tests and 47/47 Goal Root tests on Node 22. CI 39218667755 passed on that earlier head; this is not a claim about CI on the new head.The first sandboxed 483-test run could not initialize storage authority locks; the unchanged command passed outside the sandbox. No fresh full-workspace test run, full Electron run, production-adapter test or exhaustive Host-process crash test was performed for this follow-up. SQLite kill/reopen and consistent-snapshot tests establish their respective storage/recovery boundaries, not full Host-process crash coverage.
Migration and recovery
turn.startpermissions remain unchanged. Re-check the epoch against main before merging if another incompatible protocol change lands first.app_restartedand pauses the Goal; explicit user resume starts a fresh continuation without redelivering the old wait. This is not transparent retry of an uncertain execution. The internal bridge README now documents this procedure.AI use
Tool(s) and scope: OpenAI Codex assisted with implementation, regression tests, review and documentation. Both commits retain
Generated-by: OpenAI Codex; retain attribution in the final squash commit.Checklist
Does this PR entail a change in behavior?