Skip to content

fix(engine): replay backlog to all certified sessions on node reconnect - #445

Merged
khaliqgant merged 4 commits into
mainfrom
fix/node-reconnect-delivery-replay
Sep 19, 2026
Merged

khaliqgant merged 4 commits into
mainfrom
fix/node-reconnect-delivery-replay

Conversation

@AgentRelayBot

@AgentRelayBot AgentRelayBot commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Deliveries queued while a node's delivery socket is down were never replayed on reconnect — observed live: a GitHub webhook event landed in an agent's channel while its broker socket was down, and after the socket recovered the event sat durable-but-unsent until a new event arrived.

Root cause: inventory.sync replay on a cursor-negotiated reconnect was scoped to newly-routed agents only, so deliveries queued for already-ready agents had nothing to re-mark them due. This change makes reconnect replay every session the inventory.sync certifies, with the replay scope taken from the connection's negotiated delivery mode — connection-scoped readiness (providerDeliveryReadinessMode()) rather than node_providers.capabilities, which heartbeatNode rewrites wholesale and could silently un-flush the backlog.

  • Legacy sessions keep register-time replay without a redundant inventory flush
  • Registered cursor capability is preserved across heartbeat roster refreshes
  • Compatibility fallback retained for older/out-of-process registry contracts

Test plan

  • 3 new conformance tests — all verified to fail against pre-fix code:
    • replay backlog when heartbeat roster omits cursor capability
    • no re-flush when heartbeat adds cursor capability to an immediate connection
    • registered cursor advertisement kept out of persisted heartbeat capability updates
  • npx vitest run in packages/engine: 99 files, 1,151 tests passed
  • Engine typecheck + lint clean
  • Full repo npx turbo test: 18/18 tasks

Known limits (documented, not introduced here)

  • Transport remains at-least-once; the broker's monotonic-seq gate is the duplicate filter. This PR does not claim exactly-once.
  • A half-open duplicate socket can mark a row delivered into a dead socket (replay still recovers it).
  • Provider-less brokers synthesize a connection-based instance id, so reconnects can be classified as duplicate instances until the old incumbent goes stale.
  • Cloud Durable Object socket-owner behavior not exercised directly.

Review evidence: FLOW_NOTES.md + REVIEW_VERDICT.json on this branch (fresh-eyes Codex review, approved at HEAD).

Generated with Devin


Note

Medium Risk
Changes core node reconnect delivery replay and heartbeat capability merging; behavior is well covered by new conformance tests but affects at-least-once message delivery to brokers.

Overview
Fixes stranded mailbox rows when a broker node reconnects after its delivery socket was down: messages queued during the outage were not replayed on inventory.sync if the socket owner already reported those agents as delivery-ready.

inventory.sync replay scope now follows the connection’s negotiated delivery mode instead of readiness/routing transitions. On cursor-negotiated reconnects (no register-time flush), replay covers every identity in the certified inventory (reconciledAgentIds). Legacy immediate-delivery connections still only replay identities newly routed on that sync, avoiding a duplicate flush after node.register.

Handshake stability: replay mode is read from the live connection via optional NodeConnectionRegistry.providerDeliveryReadinessMode() (implemented on the in-process adapter). node.heartbeat roster updates merge capabilities with withRegisteredProtocolCapabilities() so heartbeats cannot drop or add relay:delivery-cursor-v1 and silently change replay behavior. Superseded connections get undefined from the registry and do not drain backlog on stale syncs.

Conformance tests cover outage replay, ordering/dedupe after ACK, heartbeat roster edge cases, and stale-connection certification. Root and engine changelogs add an [Unreleased - Patch] fix entry; review notes and trajectory artifacts are included in the branch.

Reviewed by Cursor Bugbot for commit c8c70fc. Bugbot is set up for automated code reviews on this repo. Configure here.

Review in cubic

agentrelaybot and others added 3 commits September 18, 2026 22:02
…nnect

A node whose delivery socket dropped got nothing back after reconnecting:
messages fanned out during the outage stayed queued in the mailbox until
their TTL and the agents were never woken.

A cursor-negotiated `node.register` deliberately does not replay (PR #443:
no identity is cursor-ready yet), so `inventory.sync` is the node's only
reconnect-replay trigger. It scoped that replay to identities whose delivery
readiness or provider routing *changed* during the sync, which is empty
whenever the socket owner already reports the certified sessions as
delivery-ready — an owner whose ready-set is keyed per node+provider rather
than per connection, or a registry that omits the optional readiness hooks,
where the shared helper defaults to ready. The drain then ran with an empty
scope and returned 0. The in-process adapter resets its ready-set on every
new connection, which is why no in-tree test caught it.

Replay the full certified set on a cursor-negotiated connection instead,
matching `agent.register` / `agent.recover` and the documented contract that
an `inventory.sync` certifies sessions that retained their cursors and may
replay. Legacy immediate-delivery connections are unchanged: `node.register`
already flushed the node to them, so only newly routed identities replay and
the register-time flush is not duplicated. The handshake mode is recovered
from the engine's own registration record through a new
`providerAdvertisesDeliveryCursor()` helper, which also replaces the two
duplicated inline capability lookups in agent register/recover.

Dedupe, ordering and gating are unchanged: the cumulative delivery cursor
still suppresses acked rows, pages still drain oldest-first under a bounded
high-water mark, and every frame is still gated on per-identity delivery
readiness.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ated mode

`inventory.sync` inferred the cursor handshake from the provider's persisted
capabilities, which `node.heartbeat` rewrites whenever a roster snapshot rides
along. A heartbeat that advertised only spawn capacity demoted a live
cursor-gated connection to the legacy branch, so a certification that had just
readied the identities replayed nothing and the outage backlog stayed queued
until its TTL; the inverse promoted an immediate connection and re-sent the
frames `node.register` had already flushed.

Recover the mode from the connection it was negotiated on: the registry now
exposes `providerDeliveryReadinessMode()`, the per-connection mode
`node.register` configures. Registries on the older contract fall back to the
persisted advertisement, which a heartbeat can no longer change — capabilities
negotiated at registration (`relay:delivery-cursor-v1`) are carried over a
heartbeat roster refresh instead of being replaced by it, so the fallback
answers for the registration rather than the latest snapshot. Either fix alone
holds the behaviour; out-of-process socket owners get the second for free.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 33 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 2848a8b8-4866-491c-ac0d-e589b11873c9

📥 Commits

Reviewing files that changed from the base of the PR and between b634354 and c8c70fc.

📒 Files selected for processing (12)
  • .agentworkforce/trajectories/compacted/compact_1akx23pm5dm3_2026-09-19.json
  • .agentworkforce/trajectories/compacted/compact_1akx23pm5dm3_2026-09-19.md
  • .agentworkforce/trajectories/compacted/compact_yy5pk8tn1xrh_2026-09-19.json
  • .agentworkforce/trajectories/compacted/compact_yy5pk8tn1xrh_2026-09-19.md
  • CHANGELOG.md
  • FLOW_NOTES.md
  • REVIEW_VERDICT.json
  • packages/engine/CHANGELOG.md
  • packages/engine/src/__tests__/conformance/delivery.test.ts
  • packages/engine/src/adapters/node/realtime.ts
  • packages/engine/src/engine/node.ts
  • packages/engine/src/ports/realtime.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment thread packages/engine/src/engine/node.ts Outdated
Comment on lines +277 to +279
const mode = registry.providerDeliveryReadinessMode?.(workspaceId, nodeId, providerName, connectionId);
if (mode !== undefined) return mode === 'agent_scoped';
return providerAdvertisesDeliveryCursor(db, workspaceId, nodeId, providerName);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Stale inventory replays to replacement connection

A superseded connection's queued inventory.sync can use the replacement registration's persisted cursor capability. The stale connection ID returns no mode, but persistent readiness can still permit replay. Frames reach the replacement before it certifies those sessions.

Learn more

providerDeliveryReadinessMode distinguishes a registry without mode support from a stale or unbound connection. Its contract returns undefined for a connection ID that is no longer current. Falling back in both cases loses that distinction and reads capabilities written by whichever registration now owns the provider.

Example: Connection B begins node.register while connection A still owns the provider. An inventory.sync arriving on A queues behind B and captures A's provider name. B then supersedes A and persists its cursor capability. When A's sync runs, the mode lookup rejects A's connection ID, but the fallback classifies the sync from B's capability. A remote owner whose ready-set survives reconnect can send A-certified deliveries to B before B certifies those sessions.

Recommended fix: Use the database fallback only when providerDeliveryReadinessMode is absent. When the method exists and returns undefined, treat the frame as non-cursor-gated and do not apply full certified replay.

Suggested change
const mode = registry.providerDeliveryReadinessMode?.(workspaceId, nodeId, providerName, connectionId);
if (mode !== undefined) return mode === 'agent_scoped';
return providerAdvertisesDeliveryCursor(db, workspaceId, nodeId, providerName);
if (registry.providerDeliveryReadinessMode) {
const mode = registry.providerDeliveryReadinessMode(workspaceId, nodeId, providerName, connectionId);
return mode === 'agent_scoped';
}
return providerAdvertisesDeliveryCursor(db, workspaceId, nodeId, providerName);

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in c8c70fc: when providerDeliveryReadinessMode exists, an undefined result (stale/superseded connection) now means not-cursor-gated — the persisted capabilities fallback is only used when the API itself is absent. New conformance test 'does not let a stale connection certify a replacement connection''s backlog' covers exactly this path and fails without the fix.

…cursor

A superseded connection's queued inventory.sync could inherit the
replacement registration's persisted cursor advertisement: the registry
mode lookup returns undefined for a connection that no longer owns the
provider, but the fallback then read capabilities written by whichever
registration owns it now. With an out-of-process owner whose ready-set
survives reconnect, the stale certification replayed the outage backlog
to sessions the replacement never certified.

When providerDeliveryReadinessMode exists its undefined is the answer;
the persisted-capability fallback now only serves registries that cannot
answer at all.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@khaliqgant
khaliqgant merged commit e8161db into main Sep 19, 2026
9 checks passed
@khaliqgant
khaliqgant deleted the fix/node-reconnect-delivery-replay branch September 19, 2026 18:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants