Skip to content

[Fix] Live transcript converges to persisted envelopes on every history refetch - #37

Merged
daniel-lxs merged 1 commit into
developfrom
fix/live-transcript-converges-to-envelopes
Jul 9, 2026
Merged

[Fix] Live transcript converges to persisted envelopes on every history refetch#37
daniel-lxs merged 1 commit into
developfrom
fix/live-transcript-converges-to-envelopes

Conversation

@daniel-lxs

Copy link
Copy Markdown
Member

What problem this solves

Root-cause fix for the live-session transcript corruption from the steering test pass (tasks 190holbb4xm2k, 11nl0xsy05oyl on openmote): transcripts collapsing to the initial prompt + latest answer after a reload, blank renders mid-session, steered messages rendering out of chronological order, and missed live replies staying missing — always self-resolving once the sandbox slept (historical rendering was always correct, and server envelope data was always intact).

Root cause

The live page reconstructed history through two different code paths:

  • Initial load: loadAcpEnvelopes — the envelope-aware builder the historical view uses (always correct).
  • Every subsequent refetch: _mergeAcpHistory patched the in-memory list envelope-by-envelope through the live-event handler, and skipped any envelope whose id was already present.

The skip-by-id rule means any drift in the live in-memory state (missed socket events, replacements at stale indexes, ordering skew) could never be corrected by a refetch — it persisted until the sandbox slept and the page fell back to the historical loader.

What changed

  • _mergeAcpHistory now rebuilds the transcript from the fetched envelopes with the same loadAcpEnvelopes builder, then carries over only messages the fetch doesn't cover yet: optimistic sends and live-only events, matched via logical event id, user clientMessageId, toolCallId, or the originating event timestamp. Every refetch — which fires on every socket (re)connect — is now a convergence point to the server's authoritative transcript.
  • Duplicate live/persisted twins resolve to the persisted message (its id is the durable anchor reloads and the historical view use). One existing test expectation updated accordingly.
  • An identical refetch returns the exact previous references (no re-render churn).

Provability

The new steered-task-history.client.test.ts suite (7 tests) includes a drift-healing case — content corrupted under a matching envelope id — that fails against the previous merge implementation (verified: 1 failed / 6 passed on the old code) and passes with the rebuild. Also covered: collapse-recovery to full transcript, carried-over optimistic + unpersisted live messages, timestamp ordering for envelopes the socket missed, and the no-change fast path.

Validation

Full src/app/(sandbox)/task/ suite green (483 tests, incl. the 64-case store suite), pnpm lint:fast + check-types:fast clean.

🤖 Generated with Claude Code

…ry refetch

The live task page reconstructed history through two different code
paths: initial load used the envelope loader (loadAcpEnvelopes, the same
builder the always-correct historical view uses), but every subsequent
refetch patched the in-memory list envelope-by-envelope through the
live-event handler, skipping any envelope whose id was already present.
Once the live state drifted - missed socket events, replacements applied
at stale positions, ordering skew - the drift persisted until the
sandbox slept, matching the corruption observed on staging (transcripts
collapsing to the prompt plus latest answer, blank renders, steered
messages out of order).

_mergeAcpHistory now rebuilds the transcript from the fetched envelopes
with the same loader, then carries over only messages the fetch does not
cover yet (optimistic sends and unpersisted live events, matched by
logical event id, clientMessageId, tool call id, or originating event
timestamp). Every refetch is a convergence point to server truth, and an
identical refetch keeps the previous references so nothing re-renders.

The new steered-task regression suite includes a drift-healing case that
fails against the previous incremental merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@roomote-roomote-v1

roomote-roomote-v1 Bot commented Jul 9, 2026

Copy link
Copy Markdown

2 issues outstanding. See task

  • apps/web/src/app/(sandbox)/task/[taskId]/hooks/use-sandbox-store.ts:852 preserves stale todo state when server history clears todos: rebuilt.todos.length > 0 ? rebuilt.todos : state.todos keeps the old list even when the rebuilt history's latest plan is intentionally empty, so a refetch cannot converge the sidebar todo state to server truth. Use rebuilt todos directly when history includes a plan/todowrite result, including empty arrays.
  • apps/web/src/app/(sandbox)/task/[taskId]/hooks/use-sandbox-store.ts:854-865 calls acpService.setMessages(...) immediately after loadAcpEnvelopes(...), which resets the protocol service and discards pending request/user lookup state populated from history. After a refetch over a pending request_user_input, a subsequent live request_user_input_response formats with request === null, so secret answers are no longer masked by formatRequestUserInputResponseText and can render raw in the transcript; preserve or rebuild those service maps instead of clearing them.

@daniel-lxs
daniel-lxs merged commit 28577e5 into develop Jul 9, 2026
daniel-lxs added a commit that referenced this pull request Jul 9, 2026
…ion panel fixes

Three issues from the openmote reviews of #33 and #37:

- The history rebuild no longer resets the protocol service's auxiliary
  lookups: rebindMessages re-points only the id/position indexes, so the
  pending request_user_input map populated from history survives a
  refetch and a subsequent live response still masks secret answers.
- Todos now converge to the fetched history whenever it carries plan
  state, including an intentionally empty list, instead of falling back
  to stale live todos.
- A taskAborted status (cancel or terminal session error) drops the
  client-side pending user input requests, matching the worker-side
  clear, so an aborted question can no longer stay answerable and
  swallow free-text sends.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
daniel-lxs added a commit that referenced this pull request Jul 9, 2026
…ion panel fixes (#44)

* [Fix] Address review findings on the transcript convergence and question panel fixes

Three issues from the openmote reviews of #33 and #37:

- The history rebuild no longer resets the protocol service's auxiliary
  lookups: rebindMessages re-points only the id/position indexes, so the
  pending request_user_input map populated from history survives a
  refetch and a subsequent live response still masks secret answers.
- Todos now converge to the fetched history whenever it carries plan
  state, including an intentionally empty list, instead of falling back
  to stale live todos.
- A taskAborted status (cancel or terminal session error) drops the
  client-side pending user input requests, matching the worker-side
  clear, so an aborted question can no longer stay answerable and
  swallow free-text sends.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [Fix] Address second-round review findings on the merge reconciliation

- The history rebuild now derives the trailing turn-completion decision
  from the live task status (falling back to what the transcript already
  shows), instead of always marking it - merging while a task is running
  no longer flips the active trailing assistant message to completed.
- todowrite plan state arriving through ToolCall/ToolCallUpdate history
  envelopes now counts as authoritative plan history for todo
  convergence.
- Live terminal tool results are matched by terminality rather than
  id/tool key: a persisted pending tool_call no longer swallows the live
  result (which can sit under the same envelope id after an in-place
  update), and the superseded pending call row is dropped instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [Fix] Match live tool results against persisted terminality, not result kind

A persisted in-progress ToolCallUpdate also rebuilds as a (partial)
tool_result, so kind alone let it swallow a live terminal result and
leave the transcript stuck on stale in-progress output. Coverage now
requires a non-partial persisted result for live terminal results, and
carried terminal results supersede both pending tool_call and partial
tool_result rebuilt rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@mrubens
mrubens deleted the fix/live-transcript-converges-to-envelopes branch July 12, 2026 04:19
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