From dad0a3554deef3ff5edd8da5b1c9ffac009c594e Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Thu, 25 Jun 2026 19:14:23 +0000 Subject: [PATCH 1/4] fix(#4927): durable fallback for tool output/diff body on cold reload In the transparent stream, terminal output and patch diffs intermittently vanished after a cold/paginated reload. A settled tool row renders its body from `snippet`, but the raw-assistant-envelope 'derived' rebuild only joined the result by tool id via resultsByTid (role:tool messages); when that join missed (id mismatch, recovery-rebuilt turn) the snippet was empty, and #4622's live enrichment only helps when S.toolCalls still matches (empty on cold load). Build a persistedSnippetByTid lookup from the durable session.tool_calls compact summary (which carries a bounded result snippet per tid) and use it as a fallback in all three derived-push sites, so the persisted record is the reliable source and the live join becomes a fast-path, not the only path that shows the body. Part of #4925. --- static/ui.js | 19 +++++- tests/test_issue4927_durable_tool_snippet.py | 62 ++++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 tests/test_issue4927_durable_tool_snippet.py diff --git a/static/ui.js b/static/ui.js index 744f1584..0f6d3274 100644 --- a/static/ui.js +++ b/static/ui.js @@ -11808,6 +11808,21 @@ function renderMessages(options){ // Without this step CLI-origin sessions reload with empty tool cards. const resultsByTid={}; const fallbackToolSources=[]; + // Durable fallback: the persisted compact summary (session.tool_calls, built + // by _extract_tool_calls_from_messages) carries a bounded result `snippet` + // keyed by tid. On a cold/paginated load where the role:tool result-message + // join below misses (id mismatch, recovery-rebuilt turn), use this so the + // terminal output / diff body still renders instead of vanishing (#4927). + const persistedSnippetByTid={}; + try{ + const persisted=(S.session&&Array.isArray(S.session.tool_calls))?S.session.tool_calls:[]; + persisted.forEach(tc=>{ + if(!tc||typeof tc!=='object') return; + const ptid=tc.tid||tc.id||tc.tool_call_id||tc.call_id||''; + const psnip=tc.snippet||tc.result||tc.output||tc.preview||''; + if(ptid&&psnip&&!persistedSnippetByTid[ptid]) persistedSnippetByTid[ptid]=String(psnip); + }); + }catch(e){} S.messages.forEach((m,rawIdx)=>{ if(!m) return; // OpenAI / Hermes CLI format: role=tool with tool_call_id @@ -11873,7 +11888,7 @@ function renderMessages(options){ try{ args=JSON.parse(fn.arguments||'{}'); }catch(e){} const tid=tc.id||tc.call_id||''; const patchSnippet=_cliPatchSnippetFromArgs(name,args); - const resultSnippet=resultsByTid[tid]||''; + const resultSnippet=resultsByTid[tid]||persistedSnippetByTid[tid]||''; let argsSnap=_toolArgsSnapshot(args); derived.push(copyLiveToolMetadata({ name, @@ -11900,7 +11915,7 @@ function renderMessages(options){ } const tid=tc.tid||tc.id||tc.tool_call_id||tc.call_id||''; const patchSnippet=_cliPatchSnippetFromArgs(name,args); - const resultSnippet=resultsByTid[tid]||tc.snippet||tc.preview||''; + const resultSnippet=resultsByTid[tid]||tc.snippet||tc.preview||persistedSnippetByTid[tid]||''; const argsSnap=_toolArgsSnapshot(args); derived.push(copyLiveToolMetadata({ name, diff --git a/tests/test_issue4927_durable_tool_snippet.py b/tests/test_issue4927_durable_tool_snippet.py new file mode 100644 index 00000000..4bbaf521 --- /dev/null +++ b/tests/test_issue4927_durable_tool_snippet.py @@ -0,0 +1,62 @@ +"""Regression coverage for #4927 — terminal output / patch diffs must survive a +cold reload in the transparent activity stream. + +A settled tool row renders its output/diff from `snippet`. There are two rebuild +paths: the persisted compact summary (`session.tool_calls`, which HAS a bounded +`snippet`) and the raw-assistant-envelope `derived` rebuild (which joins the +result by tool id via `resultsByTid`). On a cold/paginated load the +`resultsByTid` join can miss (id mismatch, recovery-rebuilt turn) — and #4622's +live enrichment only helps when `S.toolCalls` still matches (empty on cold +load). So the derived build must fall back to the persisted `session.tool_calls` +snippet by tid, making the durable record the reliable source. + +This is a source-structure guard: the derived rebuild is inlined in +`renderMessages`, so we assert the fallback wiring is present rather than +extracting a standalone function. +""" +from __future__ import annotations + +import re +from pathlib import Path + +UI_JS = (Path(__file__).parent.parent / "static" / "ui.js").read_text(encoding="utf-8") + + +def _slice_derived_rebuild() -> str: + """Return the renderMessages fallback-rebuild region (resultsByTid block).""" + start = UI_JS.index("const resultsByTid={};") + # The region runs through the _partial_tool_calls derived push; bound it + # generously so both derived-push sites are included. + return UI_JS[start:start + 6000] + + +def test_persisted_snippet_lookup_is_built_from_session_tool_calls(): + """A tid->snippet lookup must be built from S.session.tool_calls.""" + region = _slice_derived_rebuild() + assert "persistedSnippetByTid" in region, "no persisted-snippet fallback lookup" + # It must be sourced from the durable persisted summary. + assert re.search(r"S\.session\s*&&\s*Array\.isArray\(S\.session\.tool_calls\)", region), \ + "persistedSnippetByTid must be built from S.session.tool_calls" + # It must key by tid and capture the snippet. + assert re.search(r"persistedSnippetByTid\[\s*ptid\s*\]\s*=", region), \ + "persisted lookup must be keyed by tid" + + +def test_derived_result_snippet_falls_back_to_persisted(): + """The derived OpenAI-format build must use persistedSnippetByTid as a + fallback when the live result-message join (resultsByTid) misses.""" + region = _slice_derived_rebuild() + # The primary OpenAI-format derived push. + assert "resultsByTid[tid]||persistedSnippetByTid[tid]" in region, ( + "derived resultSnippet must fall back to the persisted snippet by tid " + "(#4927) so cold-load tool output/diffs don't vanish" + ) + + +def test_partial_tool_calls_path_also_falls_back(): + """The _partial_tool_calls derived path must also use the persisted fallback.""" + region = _slice_derived_rebuild() + assert "resultsByTid[tid]||tc.snippet||tc.preview||persistedSnippetByTid[tid]" in region, ( + "the partial-tool-calls derived path must also fall back to the " + "persisted snippet (#4927)" + ) From 0caa57d8b701e35cb695bfd45016c3dff4d0c1e7 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Thu, 25 Jun 2026 19:23:31 +0000 Subject: [PATCH 2/4] fix(#4927): cover the Anthropic tool_use derived push with the persisted fallback Codex gate: the third derived-push site (Anthropic tool_use content-array branch, ui.js) still read resultsByTid[tid]||'' without the persisted fallback, so Anthropic-format sessions could still cold-reload with an empty tool body. Add the persistedSnippetByTid fallback there too; now all derived result-snippet reads (OpenAI top-level, Anthropic tool_use, partial) chain to the durable persisted snippet. Added a guard test asserting EVERY derived resultSnippet read includes the fallback. --- static/ui.js | 2 +- tests/test_issue4927_durable_tool_snippet.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/static/ui.js b/static/ui.js index 0f6d3274..58635ada 100644 --- a/static/ui.js +++ b/static/ui.js @@ -11935,7 +11935,7 @@ function renderMessages(options){ const args=p.input||{}; const tid=p.id||''; const patchSnippet=_cliPatchSnippetFromArgs(name,args); - const resultSnippet=resultsByTid[tid]||''; + const resultSnippet=resultsByTid[tid]||persistedSnippetByTid[tid]||''; const argsSnap=_toolArgsSnapshot(args); derived.push(copyLiveToolMetadata({ name, diff --git a/tests/test_issue4927_durable_tool_snippet.py b/tests/test_issue4927_durable_tool_snippet.py index 4bbaf521..6973b1cc 100644 --- a/tests/test_issue4927_durable_tool_snippet.py +++ b/tests/test_issue4927_durable_tool_snippet.py @@ -60,3 +60,20 @@ def test_partial_tool_calls_path_also_falls_back(): "the partial-tool-calls derived path must also fall back to the " "persisted snippet (#4927)" ) + + +def test_all_derived_result_snippet_reads_have_fallback(): + """Every derived-push result-snippet READ (OpenAI top-level, Anthropic + tool_use content-array, and partial) must include the persisted fallback — + a missed branch (e.g. the Anthropic content-array push) would still + cold-reload with an empty body (#4927 gate).""" + region = _slice_derived_rebuild() + # Find every `const resultSnippet=resultsByTid[tid]...` read and assert each + # one chains to persistedSnippetByTid. + reads = re.findall(r"const resultSnippet=resultsByTid\[tid\][^;]*;", region) + assert reads, "no resultSnippet reads found in the derived rebuild" + for r in reads: + assert "persistedSnippetByTid[tid]" in r, ( + f"derived resultSnippet read missing the persisted fallback (#4927): {r}" + ) + From 4030dca989fcdde3213d358a504931998b5b0f34 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Thu, 25 Jun 2026 19:29:06 +0000 Subject: [PATCH 3/4] fix(#4927): persist loaded session.tool_calls so the cold-load fallback has a source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex gate (the key one): the persistedSnippetByTid fallback read S.session.tool_calls, but loadSession() keeps the messages=0 session object (tool_calls []), and the real messages=1 summary arrives only as the sessionToolCalls argument to _syncToolCallsForLoadedMessages() — never copied onto S.session. So the fallback source was empty on exactly the cold-load path it repairs. Persist sessionToolCalls onto S.session.tool_calls in _syncToolCallsForLoadedMessages (covering _ensureMessagesLoaded, older-message paging, and full-history loaders) so renderMessages builds a populated persistedSnippetByTid. + guard test. --- static/sessions.js | 7 +++++++ tests/test_issue4927_durable_tool_snippet.py | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/static/sessions.js b/static/sessions.js index 25fc5a33..6de1fe0a 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -2200,6 +2200,13 @@ function _syncToolCallsForLoadedMessages(messages, sessionToolCalls){ // During active streaming, skip — clearing S.toolCalls would lose Activity // and the renderMessages fallback is blocked by S.busy=true. if(S.busy||S.activeStreamId) return; + // Persist the loaded compact tool summary onto S.session so the renderMessages + // derived rebuild can use it as a durable per-tid snippet fallback on cold + // load (#4927). loadSession keeps the messages=0 session object (tool_calls + // []), and the messages=1 summary arrives only as this argument — without + // copying it across, the fallback source is empty exactly on the cold-load + // path it's meant to repair. + if(S.session&&Array.isArray(sessionToolCalls)) S.session.tool_calls=sessionToolCalls.map(tc=>({...tc})); const hasMessageToolMetadata=msgs.some(m=>{ if(!m) return false; const hasTc=Array.isArray(m.tool_calls)&&m.tool_calls.length>0; diff --git a/tests/test_issue4927_durable_tool_snippet.py b/tests/test_issue4927_durable_tool_snippet.py index 6973b1cc..a787813d 100644 --- a/tests/test_issue4927_durable_tool_snippet.py +++ b/tests/test_issue4927_durable_tool_snippet.py @@ -20,6 +20,7 @@ import re from pathlib import Path UI_JS = (Path(__file__).parent.parent / "static" / "ui.js").read_text(encoding="utf-8") +SESSIONS_JS = (Path(__file__).parent.parent / "static" / "sessions.js").read_text(encoding="utf-8") def _slice_derived_rebuild() -> str: @@ -77,3 +78,15 @@ def test_all_derived_result_snippet_reads_have_fallback(): f"derived resultSnippet read missing the persisted fallback (#4927): {r}" ) + +def test_loaded_session_tool_calls_persisted_onto_session(): + """#4927 gate: the cold-load path must persist the messages=1 compact tool + summary onto S.session.tool_calls, or the renderMessages fallback source is + empty on exactly the cold-load path it repairs (loadSession keeps the + messages=0 object whose tool_calls is []).""" + start = SESSIONS_JS.index("function _syncToolCallsForLoadedMessages(") + region = SESSIONS_JS[start:start + 1500] + assert "S.session.tool_calls=sessionToolCalls" in region.replace(" ", ""), ( + "_syncToolCallsForLoadedMessages must copy the loaded sessionToolCalls " + "onto S.session.tool_calls so the derived-rebuild fallback has a source" + ) From c9c3d8b8e9855c966715a23bd16b9652b59bd641 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Thu, 25 Jun 2026 19:41:51 +0000 Subject: [PATCH 4/4] Release XP (v0.51.660): durable tool output/diff body on cold reload (#4927, completes #4925) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42f930a7..10a02c07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] +## [v0.51.660] — 2026-06-25 — Release XP (terminal output and diffs survive a reload) + +### Fixed + +- **Terminal command output and patch/edit diffs no longer intermittently vanish after a reload in the transparent activity stream.** A settled tool card renders its output/diff from a `snippet`, but on a cold or paginated reload the card could rebuild from the raw assistant message envelope — which carries the command but not the result — and the join to the separate result message would sometimes miss (id mismatch, recovery-rebuilt turn), leaving the body empty. The rebuild now falls back to the durable per-tool snippet from the persisted session summary across all tool formats (OpenAI, Anthropic `tool_use`, and partial), and the loaded summary is now carried onto the session object so that fallback source is populated on cold load. The live in-memory enrichment becomes a fast path rather than the only path that shows the body. (#4927, completes #4925) + ## [v0.51.659] — 2026-06-25 — Release XO (one-click extension install works with no setup) ### Fixed