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