feat(notices): subscription-aware resources/updated for the notice inbox (#99 stage 4) - #376
Conversation
🦋 Changeset detectedLatest commit: 98d57ea The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f2d5c63803
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…e send signalAvailability accepts expectedRevision (compare-and-swap) and the inbox signaller now claims the receipt against the revision it computed eligibility from before sending resources/updated, retrying a bounded number of unrelated revision races. Two server processes over one durable store can no longer both spend a notice's single signal, and a transport failure after the claim leaves the notice pending and readable instead of freeing a duplicate. Addresses the Codex P2 review on #376.
|
Addressed the P2 review: |
…e send signalAvailability accepts expectedRevision (compare-and-swap) and the inbox signaller now claims the receipt against the revision it computed eligibility from before sending resources/updated, retrying a bounded number of unrelated revision races. Two server processes over one durable store can no longer both spend a notice's single signal, and a transport failure after the claim leaves the notice pending and readable instead of freeing a duplicate. Addresses the Codex P2 review on #376.
0b8363b to
d1a7949
Compare
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d1a7949340
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
|
Codex Review: Didn't find any major issues. Nice work! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
…e send signalAvailability accepts expectedRevision (compare-and-swap) and the inbox signaller now claims the receipt against the revision it computed eligibility from before sending resources/updated, retrying a bounded number of unrelated revision races. Two server processes over one durable store can no longer both spend a notice's single signal, and a transport failure after the claim leaves the notice pending and readable instead of freeing a duplicate. Addresses the Codex P2 review on #376.
078d40d to
669fe2f
Compare
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 669fe2f58c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…end it only on success Codex P2 on #376: signalAvailability() incremented the budget before the resources/updated send, so a failed send still cost a retry slot and the receipt no longer meant the protocol write succeeded. The ledger gains reserveAvailability()/releaseAvailability(): the signaller holds a slot by compare-and-swap, finalizes it into the receipt only after send() resolves, and releases it when send() rejects. Holds expire after AGENT_NOTICE_AVAILABILITY_RESERVATION_TTL_MS so a crashed holder cannot starve a notice.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4665444574
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…uring pending sends Codex P2 x2 on #376: - A send that succeeded but whose signalAvailability() commit failed was deduplicated only in memory, so a restarted signaller could resend it once the abandoned hold lapsed. Owed receipts are now retried with the same idempotency key before any later observation spends, and on close(). - A protocol write pending longer than the 30s TTL let another process take the hold and send too. The holder now renews under its key while send() is pending; the reducer refuses a foreign key while a hold is live and refuses a lapsed holder's late renewal once another key has taken over.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 964c947cdb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… on their own cadence Codex P2 x2 on #376: - signalAvailability({ reservationKey }) records a receipt only on notices that key still holds; a fresh commit that recorded nothing rejects with the new AgentNoticeError code 'reservation-lost'. A stale holder whose send completes after a takeover therefore cannot push a budget-one notice to count 2. Idempotent replays are recognised by revision and never misreported as lost. - Owed receipts are retried on the renewal cadence by a timer that also renews their hold, so a server that receives no further render past the TTL neither lapses nor loses a wire-successful send. A lost hold drops the owed receipt and is reported once.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 38dbe5ef05
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…mits over Codex P2 on #376: the ownership check read the head, then dispatched without a guard, so a takeover between the two left a refused receipt reported as recorded. signalAvailability({ reservationKey }) now dispatches with expectedRevision set to the revision it judged, re-reading on revision-conflict a bounded number of times; an idempotent replay is recognised from the store's replayed flag, never by revision arithmetic.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f1545d0356
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…e send signalAvailability accepts expectedRevision (compare-and-swap) and the inbox signaller now claims the receipt against the revision it computed eligibility from before sending resources/updated, retrying a bounded number of unrelated revision races. Two server processes over one durable store can no longer both spend a notice's single signal, and a transport failure after the claim leaves the notice pending and readable instead of freeing a duplicate. Addresses the Codex P2 review on #376.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8ad53a1018
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…box (#99 stage 4) Generated workspace-durable MCP servers now hold their own handle on the notice store their Flight worker mounts (createGeneratedNoticeRuntime), register resources/subscribe and resources/unsubscribe for the inbox URI only, advertise resources.subscribe exactly when that wiring is active, and after every completed render send at most one notifications/resources/updated to the subscribed connection for its newly eligible pending notices, recording the signal through signalAvailability as an availability receipt (never delivery). Eligibility is recipient-matched, honours nextAttemptAt, is bounded per notice by retryBudget across restarts, and never re-triggers on exposure or availability receipts, so subscribed clients cannot loop. Subscribing fails closed when the durable store is unreadable; volatile lifetimes stay in the worker's heap and advertise no subscribe capability.
…e send signalAvailability accepts expectedRevision (compare-and-swap) and the inbox signaller now claims the receipt against the revision it computed eligibility from before sending resources/updated, retrying a bounded number of unrelated revision races. Two server processes over one durable store can no longer both spend a notice's single signal, and a transport failure after the claim leaves the notice pending and readable instead of freeing a duplicate. Addresses the Codex P2 review on #376.
An unsubscribe that overlaps an observation parked on the durable store now wins: the observation yields before claiming any budget or sending, and the unsubscribe resolves (and the MCP request is acknowledged) only after in-flight observations settle, so a client is never signalled after its unsubscribe succeeded. Subscribe and unsubscribe share the observation queue.
…end it only on success Codex P2 on #376: signalAvailability() incremented the budget before the resources/updated send, so a failed send still cost a retry slot and the receipt no longer meant the protocol write succeeded. The ledger gains reserveAvailability()/releaseAvailability(): the signaller holds a slot by compare-and-swap, finalizes it into the receipt only after send() resolves, and releases it when send() rejects. Holds expire after AGENT_NOTICE_AVAILABILITY_RESERVATION_TTL_MS so a crashed holder cannot starve a notice.
…uring pending sends Codex P2 x2 on #376: - A send that succeeded but whose signalAvailability() commit failed was deduplicated only in memory, so a restarted signaller could resend it once the abandoned hold lapsed. Owed receipts are now retried with the same idempotency key before any later observation spends, and on close(). - A protocol write pending longer than the 30s TTL let another process take the hold and send too. The holder now renews under its key while send() is pending; the reducer refuses a foreign key while a hold is live and refuses a lapsed holder's late renewal once another key has taken over.
… on their own cadence Codex P2 x2 on #376: - signalAvailability({ reservationKey }) records a receipt only on notices that key still holds; a fresh commit that recorded nothing rejects with the new AgentNoticeError code 'reservation-lost'. A stale holder whose send completes after a takeover therefore cannot push a budget-one notice to count 2. Idempotent replays are recognised by revision and never misreported as lost. - Owed receipts are retried on the renewal cadence by a timer that also renews their hold, so a server that receives no further render past the TTL neither lapses nor loses a wire-successful send. A lost hold drops the owed receipt and is reported once.
…mits over Codex P2 on #376: the ownership check read the head, then dispatched without a guard, so a takeover between the two left a refused receipt reported as recorded. signalAvailability({ reservationKey }) now dispatches with expectedRevision set to the revision it judged, re-reading on revision-conflict a bounded number of times; an idempotent replay is recognised from the store's replayed flag, never by revision arithmetic.
… its send was in flight Codex P2 on #376: an acknowledgement (or expiry/withdrawal) landing between the wire send and receipt finalization left the notice out of the pending/attempted filter, so the reducer no-op'd and the ledger reported the receipt as recorded while nothing was written. Ownership now decides alone: transitionAvailability records the receipt for any state when the key still holds the slot (state unchanged), and the ledger judges reservation-lost by the hold, not by the state.
…umer-facing summary for #376
…ready spent Codex P2 on #376: after a lapsed holder's slot was taken over and the takeover's receipt cleared the hold, the old holder's renewal found an empty slot and re-created its reservation, letting its stale receipt push a budget-one notice to count 2. The reservation reducer now treats an empty (or lapsed foreign) slot whose availability count has reached retryBudget as not free for any key but the current holder. Also re-imports RenderRouteContext in the test harness after the rebase onto main.
…ceipts before the host closes Codex P2s on #376: a client repeating resources/subscribe without unsubscribing replaced the subscription and cleared its signalled set, so a retryBudget > 1 notice was re-sent to a connection that never lapsed; and the generated server closed the host (which owns or shares the durable store) before the signaller's final drain, so a receipt owed for a wire-successful send failed against a closed ledger. subscribe() is now idempotent for the same principal, and the signaller closes before the host.
…lace migration Codex P1 on #376: version 1 journaled availability-signalled over a terminal notice as a no-op, and the version-2 reducer now records it, so replaying a version-1 journal disagreed with its materialized head and the sqlite driver rejected the store as corrupt. agentNoticeStateDefinition is now version 2 (AGENT_NOTICE_STATE_VERSION) with an identity migration: a version-1 store is rebased on its head on first open, keeping every notice.
…tive changeset Codex P2 on #376: settled() awaited the inbox observation in finally, so a subscriber whose resources/updated write wedged (and whose hold is renewed for as long as it takes) held every completed tool, resource, prompt, and event response hostage. The observation is now scheduled at render completion and not awaited; the signaller serializes it and never rejects. Tests poll for the notification instead of sleeping. Codex P1: changeset summary rewritten in imperative form.
…st teardown fails Codex P2 on #376: a rejecting notices.close() skipped the bound protocol close, leaving the transport and its listeners open after server.close() failed. The protocol close now runs from an outer finally; the teardown error still surfaces once it has.
…; tear down notices and host even when the event runtime close fails Codex P2s on #376: signalAvailability({ reservationKey, expectedRevision }) prechecked the pin against the head before dispatching, so a retry of a receipt whose commit landed but whose response was lost could never reach the store's idempotency replay; the guard is now applied only by the store, which resolves the idempotency key first. And a rejecting events.close() skipped the signaller drain and host close; both now run from a finally, with the protocol close still outermost.
…, coalesce observations - renewWhile awaits a renewal still pending at the ledger before the hold is released or finalized, so a late renewal cannot re-create an orphan hold that blocks the slot for a whole TTL - the signaller's close() races pending sends against shutdown: an unsettled resources/updated write is abandoned (outcome unknown, hold left to lapse) so a subscriber that stopped reading cannot wedge server teardown - the generated server keeps one observation in flight and at most one owed instead of queueing a detached observation per completed render
8ad53a1 to
473f963
Compare
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 473f963627
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… drain a deadline - observations race each ledger call (read, reserve, release, receipt commit, store open) against closing, so a ledger that never answers cannot pin the serialized queue and, behind it, server teardown; an abandoned commit keeps its receipt owed instead of inferring either way - close() drains owed receipts and closes the store within closeTimeoutMs (default 5s), the one bounded chance to land a receipt before the process goes - subscribe() awaiting a wedged store rejects with aborted at close
|
@codex review |
|
Codex Review: Didn't find any major issues. Can't wait for the next one! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
#428) Document the user-facing behaviour that landed on main after the docsite PR (#384) merged, in both locales: - install: `agent-bundle install cursor --mode local|marketplace` and the Doctor findings for Cursor hook registration and marketplace staging (AB7322-AB7324) (#414) - project structure: per-host feature sets for src/commands and src/rules, with AB4907/AB4908 and AB4927/AB4928 (#427) - testing: the script-dispatch and workbench-surface proof levels, eleven in all, and the routed cli-tool template (#398) - notices reference: inbox and resources/updated wiring gated on the host's noticeDelivery advertisement (#412, #376)
Summary
Closes the last stage-4 item on #99: subscription-aware
notifications/resources/updatedemission from generated stateful MCP servers.@agent-bundle/runtime/notices:createNoticeInboxSignaller({ store })— one connection's subscription to the reserved inbox resource (AGENT_NOTICE_INBOX_URI). After every completed render,observe(send)reads the ledger and, when the subscriber has newly eligible pending notices, sends exactly oneresources/updatedand records it viasignalAvailability()as an availability receipt (never delivery). Eligibility is recipient-matched against the subscriber's observed identity, honoursnextAttemptAt, is bounded per notice byretryBudget(durable across restarts), and never re-triggers on exposure/availability receipts — a subscribed client cannot be driven into a refetch loop. Concurrent observations serialise; a failed send records nothing and retries on the next render (no timer).@agent-bundle/runtime/mount:createGeneratedNoticeRuntime({ driver, lifetime })andGeneratedRuntimeState.noticeLedger()give the server process its own handle on the durable store its Flight worker mounts, without evaluating the project state definition a second time.agent-bundleserver runtime:resources/subscribe/unsubscribehandlers for the inbox URI only (other resources reject with InvalidParams),resources.subscribeadvertised exactly when the wiring is present, subscribe fails closed (InternalError) when the store is unreadable, post-render observation on tool/resource/prompt renders and event-runtime renders, store closed with the server.createSqliteStateDriverover the same$AGENT_BUNDLE_PLUGIN_ROOT/artifact anchor as the worker and pass the signaller; volatile lifetimes (worker heap) stay unwired and advertise no subscribe capability; stateless entries unchanged.availabilityreceipt next toexposure.packages/rsc-runtime/README.md,docs/entry-conventions.md) and changeset for both packages.Evidence
packages/rsc-runtime/tests/notices-resource-updated.test.ts(10 tests): one signal per newly eligible set, none unsubscribed / non-matching, exposure + availability receipts do not re-signal,retryBudgethonoured across a restarted signaller,nextAttemptAtdeferral, expired/withdrawn/attempted/acknowledged/future-created skipped, concurrency serialised, failed send records nothing then retries, fail-closed store.packages/rsc-runtime/tests/mount.test.ts(+5):noticeLedger()shares the request-scope store, request lifetime fails typed,createGeneratedNoticeRuntimeopens once/closes before driver, shared-driver worker+server model, fail-closed open.packages/agent-bundle/tests/projection/mcp-in-memory.test.ts(+1, real SDK client over the real generated server): s1 subscribes, s2 does not; publish → exactly oneresources/updatedon s1, none on s2; inbox read showsavailability.count 1+exposure.count 1, statepending; unrelated renders and non-matching notices produce no signal; unsubscribe stops delivery; non-inbox subscribe rejected; volatile state advertises nosubscribeand has no handler.packages/agent-bundle/tests/generated-route-server.test.ts(+1, built artifact over stdio): durable project → server advertisesresources.subscribe: true, host-scoped notice reaches the subscribed stdio client exactly once, availability recorded, re-read does not re-signal, unsubscribe stops delivery.packages/agent-bundle/tests/entry-shell.test.ts: durable entry contains the wiring; volatile/stateless entries contain none of it.pnpm typecheck✓,pnpm lint0/0 ✓,pnpm test:projection64/64 ✓,pnpm test:unit2678 passed with 3 load-induced 5s timeouts that pass in isolation (37/37),pnpm test:route-unit34/35 with one load-induced timeout that passes in isolation (5/5),pnpm test:integration:run938/939 — the one failure is the pre-existing Codex host-install-prooflogointerface-field expectation (from feat(codex): interface fields, registered MCP apps, and policy/env capability evidence (#188 S1) #352, unrelated, only runs where the codex CLI is installed).Test plan