Skip to content

fix(web): derive thread runtime phase from canonical turn state - #1772

Merged
chuks-qua merged 4 commits into
mainfrom
feat/canonical-runtime-phase
Sep 29, 2026
Merged

chuks-qua merged 4 commits into
mainfrom
feat/canonical-runtime-phase

Conversation

@chuks-qua

@chuks-qua chuks-qua commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

What

Stacked on #1771. Runtime state is now canonical-owned once correlation is proven.

  • AgentTurn gains an optional executionId; the reducer stamps it from routing.executionId and rejects a payload value that disagrees, so snapshots and replayed history both carry the identity.
  • New getCanonicalRuntimeTurn correlates the latest canonical turn to record.turnExecutionId. A matching terminal clears stale running; a mismatch (older turn, or an optimistic send whose turnStarted has not landed) falls back to legacy. finalizing is never overridden.
  • reconcileCanonicalRuntime syncs runtimePhase and runningThreadIds inside both canonical apply points (push events and reconnect recoveries), so composer, project tree, and every runningThreadIds reader converge on the same truth.

The narrative projection (getCanonicalLifecycleTurn) keeps its child-only gate, so parent chat rendering is unchanged until the narrative migration lands.

Why

Two writers owned the running flag. Legacy agent.event terminal events pass ownership guards that can silently drop them, leaving composer and tree saying "streaming" after the turn is done. Canonical turn status is persisted server-side with revisions and gap recovery, so it is the right authority.

Evidence

  • New src/__tests__/canonical-runtime-reconcile.test.ts: correlated terminal clears stale running, canonical running claims an idle record, optimistic window survives, mismatched identity is ignored, interrupted maps correctly.
  • Regression: 206 lifecycle/store tests, 431 contract tests, 192 server canonical tests, 15 agent-model tests pass. tsc --noEmit clean for apps/web and packages/agent-model.

Review notes

  • This is PR 2 of the stack; base is fix/ws-liveness-heartbeat and must retarget to main after PR 1 merges.
  • Live lanes and perf probes per the plan doc are still outstanding.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

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.
…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.
@chuks-qua
chuks-qua merged commit 5f7b19b into main Sep 29, 2026
6 checks passed
@chuks-qua
chuks-qua deleted the feat/canonical-runtime-phase branch September 29, 2026 20:37
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.

1 participant