fix(streaming): replay restored live tool cards on reconnect#3763
fix(streaming): replay restored live tool cards on reconnect#3763franksong2702 wants to merge 1 commit into
Conversation
|
| Filename | Overview |
|---|---|
| static/sessions.js | Core fix: adds replayPersistedLiveToolCards() helper and the restoredLiveTurn&&didReconnect replay path; liveToolCalls fallback branch inside the helper is unreachable dead code. |
| static/ui.js | Expands tid resolution in appendLiveToolCard to include all common id aliases (tool_call_id, tool_use_id, call_id); one-line change, correct and well-tested. |
| tests/test_inflight_stream_reuse.py | Adds three new targeted tests for the reconnect replay path; the re.sub whitespace-stripped block check adequately verifies syntactic nesting of the replay call. |
| tests/test_regressions.py | Updates the S.activeStreamId-before-replay invariant test to anchor on the helper function definition rather than a call site; invariant still holds but proxy is weaker. |
| tests/test_ui_tool_call_cleanup.py | Adds assertion verifying the full id-alias chain in appendLiveToolCard; straightforward test addition. |
| CHANGELOG.md | Adds user-visible changelog entry for the reconnect tool card fix; accurate and appropriately scoped. |
Sequence Diagram
sequenceDiagram
participant U as User (returns to session)
participant LS as loadSession()
participant DOM as DOM / liveAssistantTurn
participant ALS as attachLiveStream()
participant ATC as appendLiveToolCard()
U->>LS: loadSession(sid)
LS->>LS: "S.activeStreamId = activeStreamId"
LS->>LS: define replayPersistedLiveToolCards()
alt "INFLIGHT[sid].reattach && activeStreamId"
LS->>LS: "didReconnect = true"
LS->>ALS: "attachLiveStream(sid, activeStreamId, {reconnecting:true})"
end
LS->>LS: renderMessages()
alt hasStructuredLiveState
alt hasCurrentWorklogContent
LS->>LS: "restoredLiveTurn = true"
else
LS->>DOM: restoreLiveTurnHtmlForSession(sid)
DOM-->>LS: restoredLiveTurn (element or falsy)
end
else
LS->>DOM: restoreLiveTurnHtmlForSession(sid)
DOM-->>LS: restoredLiveTurn (element or falsy)
end
alt "restoredLiveTurn && didReconnect"
Note over LS,ATC: NEW PATH
LS->>DOM: getElementById('liveAssistantTurn')
DOM-->>LS: hasRestoredLiveToolRows?
loop each tc in S.toolCalls
alt "skipUnkeyed && hasRows && !liveToolReplayId(tc)"
LS->>LS: skip duplicate
else
LS->>ATC: "appendLiveToolCard(tc, {sessionId, streamId})"
ATC->>DOM: replaceWith or appendChild
end
end
else not restoredLiveTurn
Note over LS,ATC: EXISTING FALLBACK PATH
LS->>DOM: clearLiveToolCards()
loop each tc in S.toolCalls
LS->>ATC: "appendLiveToolCard(tc, {sessionId, streamId})"
end
end
Reviews (4): Last reviewed commit: "fix(streaming): replay restored live too..." | Re-trigger Greptile
c9aa748 to
1892e39
Compare
|
Follow-up on the Greptile duplicate-risk note:
Updated local verification:
|
1892e39 to
f031c0c
Compare
f031c0c to
7cecb79
Compare
Review — looks good; holding briefly while you iterateThanks @franksong2702 — this is the correct post-#3401 fix for #3707 (and the right successor to the closed #3724, whose pre-refactor What I verified (on your latest commit, rebased onto master)
Gate results (combined diff, current master)
One optional, non-blocking nit (worth folding in if you push again)
Solid work — this is in good shape. Holding only to avoid releasing mid-iteration. 🙏 |
…ds on reconnect, fixes #3707) (#3766) * fix(streaming): replay restored live tool cards on reconnect (#3763, fixes #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]> * docs(changelog): stamp v0.51.309 — Release JY (stage-a5b #3763) --------- Co-authored-by: nesquena-hermes <[email protected]>
Adds a server-side run-journal live snapshot (_run_journal_live_snapshot) returned in GET /api/session as runtime_journal_snapshot, so a FRESH client (another device, or a tab with no in-memory snapshot) opening an in-progress session immediately sees the already-streamed assistant text + tool cards rebuilt from the server. Composes with the existing _replay_run_journal cursor path (seeds lastRunJournalSeq so replay resumes from the snapshot cutoff, not duplicating it) and keys tool cards by the same 5 id aliases (tid/id/tool_call_id/tool_use_id/call_id) as nesquena#3763 so SSE replay replaces rather than duplicates snapshot cards. Payload values truncated; redaction test added. Co-authored-by: t3chn0pr13st <technopriest@live.ru>
Thinking Path
#3707 is a post-#3401 live-to-final recovery residual. When a running session is restored from the in-memory live turn snapshot and then reattached to the SSE stream, the restore-success path could skip replaying persisted live tool calls. That left users with restored live text/thinking but missing tool cards until a later SSE event or final render rebuilt the turn.
The smallest safe fix is to keep the existing fallback replay behavior, but also replay persisted live tool cards after a successful restore when the session is reconnecting.
What Changed
replayPersistedLiveToolCards()helper inside theloadSession()INFLIGHT path.restoredLiveTurn && didReconnect, not only on the fallback path.sessionIdandstreamIdintoappendLiveToolCard().id,tool_call_id,tool_use_id,call_id), not onlytid.Why It Matters
Users can switch away from a long-running live session and come back during reconnect. The page should rebuild the same live Worklog they had before switching away. Without this, the turn can look like it lost tool history even though the in-flight state still knows about those tool calls.
Fixes #3707.
Verification
node --check static/messages.js static/ui.js static/sessions.jsgit diff --checkuv run --python 3.12 --with pytest --with pytest-timeout --with pytest-asyncio --with pyyaml --with cryptography python -m pytest tests/test_inflight_stream_reuse.py tests/test_ui_tool_call_cleanup.py tests/test_issue3668_prompt_card_reshow_on_switch.py tests/test_regressions.py -q126 passed, 1 skippedpython3 scripts/scope_undef_gate.pyeslintis not available on PATH.Risks / Follow-ups
tids fromupsertLiveToolCall().scope_undef_gate.pyrun could not execute without eslint; CI should run the actual gate.Contract Routing
Model Used
AI assisted: OpenAI GPT-5 Codex in coordinator/worker mode, with local shell verification and GitHub CLI checks.