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"""