Skip to content

Release v0.51.309 — Release JY (#3763 — replay restored live tool cards on reconnect, fixes #3707)#3766

Merged
nesquena-hermes merged 2 commits into
masterfrom
release/stage-a5b
Jun 7, 2026
Merged

Release v0.51.309 — Release JY (#3763 — replay restored live tool cards on reconnect, fixes #3707)#3766
nesquena-hermes merged 2 commits into
masterfrom
release/stage-a5b

Conversation

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Release v0.51.309 — Release JY (#3763, fixes #3707)

Part of the #3400 live-to-final epic — the correct post-#3401 fix for #3707 (proper successor to the closed #3724, whose pre-refactor mechanism no longer existed). Re-gated fresh on current master after the author settled.

Fixed

  • fix(streaming): replay restored live tool cards on reconnect #3763 (@franksong2702) — switching back to a long-running session during reconnect no longer drops the live tool cards. The restore-success + reconnect path now replays persisted live tool calls (was: only the !restoredLiveTurn fallback), so the Worklog isn't empty until a later SSE event. Dedup is handled two ways: skipUnkeyedRestoredDuplicates skips an unkeyed persisted tool when the restored snapshot already shows tool rows, and appendLiveToolCard() + the new liveToolReplayId() both key on the full 5-alias set (tid/id/tool_call_id/tool_use_id/call_id) so keyed cards replace-by-data-live-tid instead of duplicating. Both replay sites pass the session/stream ownership guard.

Gates (re-gated fresh on master after v0.51.302→308 shipped under it)

  • Full suite: 8163 passed, 0 failed
  • Codex: SAFE TO SHIP — verified restored rows keep data-live-tid through the outerHTML restore so keyed replay replaces rather than duplicates
  • Opus: SAFE TO SHIP — confirmed keyed-replace, skip-unkeyed safety, fallback preserved, ordering+ownership intact (one transient self-healing edge noted: a memory-only snapshot straddling this deploy under the old 4-alias keying, resolves on next state save)
  • ESLint runtime + scope-undef + ruff + browser-smoke: CLEAN
  • Revert-guard: origin/master is ancestor

Closes #3763, fixes #3707.

nesquena-hermes added 2 commits June 7, 2026 03:54
…ixes #3707)

Post-#3401 (#3400 live-to-final epic) recovery residual. When a running session
is restored from its in-memory live-turn snapshot and then reattached to the SSE
stream, the restore-success path skipped replaying persisted live tool calls,
leaving restored live text/thinking but an EMPTY Worklog until a later SSE event
or the final render rebuilt the turn.

- Extract the persisted-tool-card replay into replayPersistedLiveToolCards()
  (reads S.toolCalls or INFLIGHT[sid].toolCalls); run it on restoredLiveTurn &&
  didReconnect, not only the !restoredLiveTurn fallback.
- Dedup safety: restore-success replay passes {skipUnkeyedRestoredDuplicates:true}
  — when the restored snapshot already has .tool-card-row rows, an UNKEYED
  persisted tool is skipped to avoid a duplicate; keyed cards still replay and
  appendLiveToolCard's tid-dedup replaces the correct restored row.
- appendLiveToolCard() and the new liveToolReplayId() both key on
  tid||id||tool_call_id||tool_use_id||call_id (consistent 5-alias set), so the
  dedup covers all known id shapes.
- Both replay sites pass {sessionId, streamId} so the ownership guard applies.
- Regression coverage: restore-success+reconnect replays tools; unkeyed-restored
  duplicates skipped; all-id-alias dedup; prior ordering invariants preserved.

Correct post-#3401 fix for #3707 (supersedes the closed #3724).

Co-authored-by: franksong2702 <[email protected]>
@nesquena-hermes nesquena-hermes merged commit a20ef5e into master Jun 7, 2026
11 checks passed
@nesquena-hermes nesquena-hermes deleted the release/stage-a5b branch June 7, 2026 04:11
@greptile-apps

greptile-apps Bot commented Jun 7, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a bug (#3707) where switching back to a long-running session during reconnect would restore live text and thinking but leave the Worklog empty until a later SSE event. The root cause was that the restore-success + reconnect path inside loadSession skipped the persisted live tool card replay that the fallback path already performed.

  • sessions.js: Extracts the per-tool-call replay loop into a replayPersistedLiveToolCards helper with an opts.skipUnkeyedRestoredDuplicates flag, adds a didReconnect boolean, and calls the helper in the restoredLiveTurn && didReconnect case so reconnected sessions repopulate their Worklog immediately. Unkeyed tools are skipped when the restored snapshot already contains .tool-card-row elements to avoid double-painting.
  • ui.js: Expands the tid alias lookup in appendLiveToolCard from tc.tid only to the full 5-alias set (tid/id/tool_call_id/tool_use_id/call_id), matching the new liveToolReplayId helper so keyed data-live-tid replacement works regardless of which alias the persisted tool record carries.
  • Tests: Three new/updated structural tests verify the replay ordering, the skip-unkeyed-duplicate guard, and the expanded alias set in appendLiveToolCard.

Confidence Score: 4/5

Safe to merge — the reconnect replay path is correctly gated by both restoredLiveTurn and didReconnect, appendLiveToolCard ownership guard prevents stale-session repaints, and keyed deduplication replaces rather than duplicates cards.

The logic for the restore-success + reconnect path is correct and well-covered by new structural tests. The one notable thing is a dead ternary branch inside replayPersistedLiveToolCards — the fallback read of INFLIGHT[sid].toolCalls is never reached because S.toolCalls is always an array at that point. It does not affect behaviour, but it leaves a confusing alternate read path in the closure.

static/sessions.js — the replayPersistedLiveToolCards helper fallback branch is dead code worth cleaning up.

Important Files Changed

Filename Overview
static/sessions.js Extracts live-tool replay into a replayPersistedLiveToolCards helper and adds a didReconnect flag so the restore-success + reconnect path now replays persisted tool cards with dedup; fallback branch inside the helper is dead code since S.toolCalls is always an array at call time.
static/ui.js Expands the tid lookup in appendLiveToolCard from only tc.tid to all five known tool-id aliases, aligning it with the new liveToolReplayId helper so keyed replace-by-data-live-tid works for any alias variant.
tests/test_inflight_stream_reuse.py Adds two new structural tests covering the restore-success + reconnect replay path and the unkeyed-duplicate-skip logic; updates existing test to match the refactored replayPersistedLiveToolCards call site.
tests/test_regressions.py Updates regression test search string from the old inline appendLiveToolCard(tc) to const replayPersistedLiveToolCards=(opts)=> to track the helper definition, preserving the ordering invariant assertion.
tests/test_ui_tool_call_cleanup.py Adds a static assertion that appendLiveToolCard uses the full 5-alias tid expression rather than only tc.tid.
CHANGELOG.md Adds a v0.51.309 entry describing the fix for dropped live tool cards on reconnect.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[loadSession - INFLIGHT branch] --> B[S.activeStreamId = activeStreamId]
    B --> C[define liveToolReplayId + replayPersistedLiveToolCards]
    C --> D{INFLIGHT reattach?}
    D -- yes --> E[attachLiveStream reconnecting:true\ndidReconnect = true]
    D -- no --> F[didReconnect = false]
    E --> G[syncTopbar / renderMessages]
    F --> G
    G --> H{restoreLiveTurnHtmlForSession available?}
    H -- yes + hasStructuredLiveState --> I{hasCurrentWorklogContent?}
    I -- yes --> J[restoredLiveTurn = true]
    I -- no --> K[restoredLiveTurn = restoreHTML]
    H -- yes + no structured state --> K
    H -- no --> L[restoredLiveTurn = false]
    J --> M{restoredLiveTurn AND didReconnect?}
    K --> M
    L --> M
    M -- yes --> N[replayPersistedLiveToolCards\nskipUnkeyedRestoredDuplicates:true\nkeyed cards replace via data-live-tid]
    M -- no --> O{!restoredLiveTurn?}
    N --> P[done]
    O -- yes --> Q[clearLiveToolCards\nplaceLiveToolCardsHost\nensureLiveWorklogShell\nreplayPersistedLiveToolCards]
    O -- no --> P
    Q --> P
Loading

Reviews (1): Last reviewed commit: "docs(changelog): stamp v0.51.309 — Relea..." | Re-trigger Greptile

Comment thread static/sessions.js
Comment on lines +966 to +968
const liveToolCalls=Array.isArray(S.toolCalls)
? S.toolCalls
: (Array.isArray(INFLIGHT[sid]&&INFLIGHT[sid].toolCalls)?INFLIGHT[sid].toolCalls:[]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 The fallback branch of the ternary is unreachable: S.toolCalls is assigned INFLIGHT[sid].toolCalls||[] at line 953 (always an Array), so Array.isArray(S.toolCalls) is always true when the helper is called. The second operand — re-reading INFLIGHT[sid].toolCalls through the closure — is never evaluated. Simplifying removes the dead read and makes the intent clearer.

Suggested change
const liveToolCalls=Array.isArray(S.toolCalls)
? S.toolCalls
: (Array.isArray(INFLIGHT[sid]&&INFLIGHT[sid].toolCalls)?INFLIGHT[sid].toolCalls:[]);
const liveToolCalls=S.toolCalls||[];

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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.

bug(streaming): restored live tool cards re-stripped on reconnect after session switch (#3668 residual)

1 participant