Repository navigation
refactor(agents)!: replace agent.event replay with canonical recovery - #1779
Merged
Merged
Conversation
A half-open socket answers nothing and never fires close, so every recovery mechanism (reconnect, hydration, subscription replay) waited forever while the UI showed stale streaming state. A 30s interval now issues the existing app.version RPC with a 10s deadline; an unanswered probe closes the socket locally and the established onclose ladder owns recovery. agent.send gains a 20s timeout so a composer submit cannot park forever on a dead socket. The wedged-socket test now answers the liveness probe, since a socket that ignores it is exactly what the watchdog closes.
The reducer now writes routing.executionId onto the stored turn so clients can correlate a canonical turn to the local runtime without threading the event envelope through state. A payload-supplied executionId that disagrees with routing is rejected as a routing conflict.
Composer, project tree, and running-thread indicators now follow the canonical replica once a turn correlates by execution identity, so a dropped legacy terminal event can no longer strand runningThreadIds. The optimistic window before turnStarted and the finalizing phase stay on the legacy path until a canonical turn proves correlation. The narrative projection keeps its existing child-only lifecycle gate.
The relay opened one socket and never recovered: a dropped connection silently degraded the renderer to WebSocket-only delivery for the rest of the session. Reconnect in the main process with bounded exponential backoff, reset on successful connect, and stop when the window dies or the relay is torn down. The renderer re-suppresses push channels as frames resume, so no reconnect handshake is needed.
No caller remains: consumers read runningThreadIds directly.
Parent narrative recovery wrote items directly into canonical_agent_items, so subscribed replicas only learned about them through snapshots. Each mutation now commits an item.recorded envelope inside the existing elapsed-bounded batch writer, discards become narrativeRecoveryDiscarded tombstones, and the semantic writer flushes the envelopes through its buffered publication channel. The web store reruns recoverParentNarrative when a canonical batch touches narrative items, letting parent tool calls and segments reach the chat through the canonical stream.
Numbered livePublication intents are the enriched, sanitized events the agent.event broadcast sends today. Recording each one as a canonical publication.recorded envelope inside the same operation transaction makes the canonical stream carry the same renderer-facing payload, so replay and gap recovery deliver identical effects without the legacy channel. The reducer treats the envelope as a passthrough: identity and dedup bookkeeping apply, but no thread or item state mutates. On the client, canonical pushes and reconnect deltas dispatch the embedded event through handleAgentEvent with its publicationId, so the existing stable publication cursor accepts whichever copy arrives first and drops the other. Envelopes committed by unbuffered operation paths (control, stage-terminal, assistant text, terminal batches) queue on the writer and flush after the outer transaction commits, matching the buffered flush used by live operations.
Live publication intents embed provider events before the public enrichment stage, so canonical copies would persist raw tool inputs that the legacy wire path strips. Sanitize the embedded event the same way AgentEventPublicationService does before recording publication.recorded.
Goals, provider-unavailable notices, post-terminal hooks, control replies, and materialized assistant messages bypassed canonical durability and reached clients only through fire-and-forget agent.event broadcasts. Route them through recordSynthesizedPublications, which commits publication.recorded envelopes on the shared thread-scoped publication head so replay and reconnect recovery deliver them. Synthesized events carry no execution identity, so they publish under a reserved sentinel execution with persistCheckpoint disabled, and the event store seeds accepted sequences from prior events when no checkpoint exists. The client publication cursor dedups on the thread-scoped publicationId alone since turnExecutionId never participated in reserve. The legacy agent.event copy still emits with the same publicationId so whichever copy arrives first wins while the client migrates.
…hase The runtime reconcile stamps runtimePhase from a correlated canonical turn but never correlated the record's execution identity, so a canonically owned "running" phase made getCanonicalLifecycleTurn treat the provider-owned child turn as a local execution and suppress its projection. A later terminal canonical event could then no longer clear the stamped phase either. reconcileCanonicalRuntime now stamps turnExecutionId from the claiming turn's executionId on live claims, and the lifecycle gate only treats runtimePhase as locally owned when the canonical runtime turn is not the latest turn.
The bounded agent.event journal, subscription cursors, and client-side sequence acceptance duplicated authority the canonical channel already owns. A dropped or reordered legacy event could leave composer, project tree, and chat disagreeing on whether a thread was still streaming. - Server publishes only agent.canonical; subscription-scoped replay is gone. - push.setThreadSubscriptions takes canonical revisions and returns canonicalRecoveries (delta or snapshot) instead of journal cursors. - Renderer drops the agent.event listener, event sequence/epoch fields, and background-drop preflight; publicationId dedup still covers replays. - AgentEvent loses sequence/epoch; publicationId remains the receipt key. - Recovery retention accounts for persisted narrative items again instead of serialized envelope overhead. BREAKING CHANGE: the agent.event channel and the cursors, hydrationRequiredThreadIds, and replayedThrough fields are removed from the subscription contract; reconnect recovery now runs through canonical revisions.
# Conflicts: # apps/web/src/stores/threadStore.ts
chuks-qua
changed the base branch from
feat/canonical-synthesized-publications
to
main
September 29, 2026 22:12
The merged signature drops the agent.event publish dep; the tests still passed a stale enriched-event argument and asserted a removed broadcast. They now exercise only the status transitions the function still owns.
This was referenced Oct 6, 2026
chuks-qua
added a commit
that referenced
this pull request
Oct 6, 2026
* fix(verify-mcode): read live turn events from agent.canonical Since #1779 the server publishes live turn events only on the agent.canonical push channel. The runtime live harness and the composer-queue proof still listened for agent.event, so every runtime live scenario timed out waiting for a terminal event even when the assistant reply was saved. canonicalFrameToLiveEvents reads the publication.recorded payloads of accepted, saved, and recovery frames and applies the web client's publication route. Callers record each event ID once because saved and recovery frames repeat accepted events. A stopped turn now publishes only ended with an interrupted outcome, so ended counts as completion only with a completed outcome and counts as a stop otherwise. * fix(verify-mcode): wait for runtime readiness after the OpenCode resume restart agent:up returns after it writes ports.json, before the server answers health. The resume proof called health with a 15-second budget right after agent:up and failed when startup took longer. On a loaded host the server took up to 15.9 seconds to open its listener, Vite once took 19.9 seconds, and one restart also failed the agent:ready CLI's fixed 30-second budget. Call agentReady in-process, bounded by the proof deadline.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Deletes the legacy
agent.eventtransport end to end: the subscription-scoped channel, the bounded server journal and cursor replay, the renderer listener, and the client-side sequence/epoch acceptance machinery.agent.canonicalis now the only renderer-facing live event channel, and reconnect recovery runs through canonical revisions (deltaorsnapshot) instead of journal cursors.Stacked on #1778. Two commits:
fix(web)repairs a regression from fix(web): derive thread runtime phase from canonical turn state #1772: canonical runtime reconcile stampedruntimePhasewithout correlatingturnExecutionId, so a canonically ownedrunningphase suppressed the provider-owned child lifecycle and could strandrunningThreadIds. Reconcile now stamps the execution identity on live claims and the lifecycle gate distinguishes canonical-owned from locally-owned phases.refactor(agents)!removes the legacy path:publishAgentEvent, the journal,cursors/hydrationRequiredThreadIds/replayedThroughin the subscription contract,sequence/epochonAgentEvent, and the client's drop-preflight and cursor state.Why
Composer, project tree, and chat could disagree on whether a thread was active: two transports carried overlapping authority, and a dropped or reordered legacy event left the UI stuck in streaming. One canonical channel with revision-based recovery removes the duplicated authority;
publicationIddedup still covers replays.UI Changes
None. No user-visible UI changes; event delivery semantics are internal.
Config Changes
None.
Review Notes
agent.eventchannel and the subscriptioncursors,hydrationRequiredThreadIds, andreplayedThroughfields are removed; clients must use canonical revisions.agent.listRunningstays the runtime snapshot source; startup reconciliation (TurnRecoveryService.reconcileOnStartup) makes the runtime registry authoritative before hydration.Depends on #1778
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.