From 3009c0bf71db2fa4a2bcc7c49b58ce7cd781c967 Mon Sep 17 00:00:00 2001 From: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com> Date: Mon, 25 May 2026 08:28:12 +0200 Subject: [PATCH 1/2] fix(chat): keep compression tip selected in sidebar --- CHANGELOG.md | 4 +++ static/sessions.js | 7 +++++ tests/test_session_lineage_collapse.py | 42 ++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42919130..80a976a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ## [Unreleased] +### Fixed + +- Sidebar compression lineage collapse now prefers the current continuation tip over a preserved parent snapshot when both rows share the same backend segment count. This keeps reloads after context compression from reopening the older parent transcript and making the active conversation appear to disappear. + ## [v0.51.134] — 2026-05-25 — Release DF (stage-batch16 — single-PR Windows path defaults) ### Fixed diff --git a/static/sessions.js b/static/sessions.js index 83d8c5c4..e25cf812 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -2817,6 +2817,13 @@ function _collapseSessionLineageForSidebar(sessions){ if(bSeg||aSeg){ if(bSeg!==aSeg) return bSeg-aSeg; } + // Preserved pre-compression parents can share the same backend segment + // count as the continuation. Prefer the non-snapshot tip before falling + // back to timestamps, otherwise a recently-polled parent reopens the + // older transcript and makes the active continuation look lost. + const bSnapshot=!!(b&&b.pre_compression_snapshot); + const aSnapshot=!!(a&&a.pre_compression_snapshot); + if(bSnapshot!==aSnapshot) return aSnapshot-bSnapshot; return _sessionTimestampMs(b)-_sessionTimestampMs(a); }); const chosen=sorted[0]; diff --git a/tests/test_session_lineage_collapse.py b/tests/test_session_lineage_collapse.py index 6fba937c..2550375c 100644 --- a/tests/test_session_lineage_collapse.py +++ b/tests/test_session_lineage_collapse.py @@ -253,6 +253,48 @@ console.log(JSON.stringify(collapsed)); +def test_sidebar_lineage_collapse_prefers_current_tip_over_same_segment_snapshot(): + """A preserved parent snapshot can share the child's backend segment count. + + Loading/polling the parent refreshes its timestamp, but the collapsed row + must still open the non-snapshot continuation tip. Otherwise a reload after + compression jumps back to the older parent transcript and looks like the + active conversation disappeared. + """ + js = SESSIONS_JS_PATH.read_text(encoding="utf-8") + source = f""" +const src = {js!r}; +function extractFunc(name) {{ + const re = new RegExp('function\\\\s+' + name + '\\\\s*\\\\('); + const start = src.search(re); + if (start < 0) throw new Error(name + ' not found'); + let i = src.indexOf('{{', start); + let depth = 1; i++; + while (depth > 0 && i < src.length) {{ + if (src[i] === '{{') depth++; + else if (src[i] === '}}') depth--; + i++; + }} + return src.slice(start, i); +}} +eval(extractFunc('_sessionTimestampMs')); +eval(extractFunc('_isChildSession')); +eval(extractFunc('_sessionLineageKey')); +eval(extractFunc('_collapseSessionLineageForSidebar')); +const sessions = [ + {{session_id:'parent', title:'Duplicate Assistant Text Blocks', message_count:64, updated_at:300, last_message_at:300, pre_compression_snapshot:true, _lineage_root_id:'parent', _compression_segment_count:2}}, + {{session_id:'child', title:'Duplicate Assistant Text Blocks', parent_session_id:'parent', message_count:86, updated_at:200, last_message_at:200, _lineage_root_id:'parent', _compression_segment_count:2}}, +]; +const collapsed = _collapseSessionLineageForSidebar(sessions); +console.log(JSON.stringify(collapsed)); +""" + collapsed = json.loads(_run_node(source)) + assert [row["session_id"] for row in collapsed] == ["child"] + assert collapsed[0]["_lineage_collapsed_count"] == 2 + assert [seg["session_id"] for seg in collapsed[0]["_lineage_segments"]] == ["child", "parent"] + + + def test_sidebar_attaches_child_sessions_to_collapsed_hidden_parent_lineage(): js = SESSIONS_JS_PATH.read_text(encoding="utf-8") source = f""" From 9e74072bf3c38e2ddb254e883de1073bdb0a3ea9 Mon Sep 17 00:00:00 2001 From: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com> Date: Mon, 25 May 2026 08:47:53 +0200 Subject: [PATCH 2/2] fix(chat): resolve stale compression parent routes --- CHANGELOG.md | 1 + static/sessions.js | 37 +++++++++++++++++++++++ tests/test_session_lineage_collapse.py | 41 ++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80a976a6..0f55376f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ ### Fixed - Sidebar compression lineage collapse now prefers the current continuation tip over a preserved parent snapshot when both rows share the same backend segment count. This keeps reloads after context compression from reopening the older parent transcript and making the active conversation appear to disappear. +- Reloading a stale `/session/` compression URL now resolves to the visible continuation tip from the sidebar payload instead of reopening the archived parent snapshot. ## [v0.51.134] — 2026-05-25 — Release DF (stage-batch16 — single-PR Windows path defaults) diff --git a/static/sessions.js b/static/sessions.js index e25cf812..c04d7a52 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -543,6 +543,10 @@ async function newSession(flash, options={}){ async function loadSession(sid){ const opts = arguments[1] || {}; + if(!opts.skipLineageResolve && typeof _resolveSessionIdFromSidebarLineage==='function'){ + const resolvedSid=_resolveSessionIdFromSidebarLineage(sid); + if(resolvedSid&&resolvedSid!==sid) sid=resolvedSid; + } const forceReload = !!opts.force; const currentSid = S.session ? S.session.session_id : null; // Clicking the already-open session in the sidebar is a no-op. Reloading it @@ -2640,6 +2644,39 @@ function _sessionLineageContainsSession(s, sid){ return false; } +function _resolveSessionIdFromSidebarLineage(sid){ + sid=String(sid||'').trim(); + if(!sid||!Array.isArray(_allSessions)||!_allSessions.length) return sid||null; + const visibleRows=_collapseSessionLineageForSidebar(_allSessions).filter(row=>row&&!_isChildSession(row)); + if(visibleRows.some(row=>row&&row.session_id===sid)) return sid; + const candidates=[]; + for(const row of visibleRows){ + if(!row||!row.session_id) continue; + if(row.session_source==='fork'||row.relationship_type==='child_session') continue; + const lineageLike=!!( + row._lineage_key||row._lineage_root_id||row.lineage_root_id|| + row._compression_segment_count||row.pre_compression_snapshot|| + (Array.isArray(row._lineage_segments)&&row._lineage_segments.length>1) + ); + if(!lineageLike) continue; + const key=_sidebarLineageKeyForRow(row); + if(key===sid||row.parent_session_id===sid||row._lineage_root_id===sid||row.lineage_root_id===sid||_sessionLineageContainsSession(row,sid)){ + candidates.push(row); + } + } + if(!candidates.length) return sid; + candidates.sort((a,b)=>{ + const bSeg=Number(b&&b._compression_segment_count||b&&b._lineage_collapsed_count||0); + const aSeg=Number(a&&a._compression_segment_count||a&&a._lineage_collapsed_count||0); + if(bSeg!==aSeg) return bSeg-aSeg; + const bSnapshot=!!(b&&b.pre_compression_snapshot); + const aSnapshot=!!(a&&a.pre_compression_snapshot); + if(bSnapshot!==aSnapshot) return aSnapshot-bSnapshot; + return _sessionTimestampMs(b)-_sessionTimestampMs(a); + }); + return candidates[0].session_id||sid; +} + function _sessionSegmentCount(s){ if(!s) return 0; const counts=[]; diff --git a/tests/test_session_lineage_collapse.py b/tests/test_session_lineage_collapse.py index 2550375c..680d77fb 100644 --- a/tests/test_session_lineage_collapse.py +++ b/tests/test_session_lineage_collapse.py @@ -295,6 +295,47 @@ console.log(JSON.stringify(collapsed)); +def test_direct_parent_restore_resolves_to_visible_compression_tip(): + """A stale /session/ URL should reopen the visible continuation tip. + + The sidebar payload may omit the archived pre-compression parent but still + include the latest continuation with lineage metadata pointing back to the + parent. Boot restore should use that visible tip instead of loading the old + parent transcript and making the continuation look lost. + """ + js = SESSIONS_JS_PATH.read_text(encoding="utf-8") + source = f""" +const src = {js!r}; +function extractFunc(name) {{ + const re = new RegExp('function\\\\s+' + name + '\\\\s*\\\\('); + const start = src.search(re); + if (start < 0) throw new Error(name + ' not found'); + let i = src.indexOf('{{', start); + let depth = 1; i++; + while (depth > 0 && i < src.length) {{ + if (src[i] === '{{') depth++; + else if (src[i] === '}}') depth--; + i++; + }} + return src.slice(start, i); +}} +var _allSessions = [ + {{session_id:'child', title:'Duplicate Assistant Text Blocks', parent_session_id:'parent', message_count:86, updated_at:200, last_message_at:200, _lineage_root_id:'parent', _compression_segment_count:2}}, + {{session_id:'other', title:'Other', message_count:4, updated_at:100, last_message_at:100}}, +]; +eval(extractFunc('_sessionTimestampMs')); +eval(extractFunc('_isChildSession')); +eval(extractFunc('_sessionLineageKey')); +eval(extractFunc('_sessionLineageContainsSession')); +eval(extractFunc('_sidebarLineageKeyForRow')); +eval(extractFunc('_collapseSessionLineageForSidebar')); +eval(extractFunc('_resolveSessionIdFromSidebarLineage')); +console.log(JSON.stringify({{parent:_resolveSessionIdFromSidebarLineage('parent'), child:_resolveSessionIdFromSidebarLineage('child'), other:_resolveSessionIdFromSidebarLineage('other')}})); +""" + result = json.loads(_run_node(source)) + assert result == {"parent": "child", "child": "child", "other": "other"} + + def test_sidebar_attaches_child_sessions_to_collapsed_hidden_parent_lineage(): js = SESSIONS_JS_PATH.read_text(encoding="utf-8") source = f"""