Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions api/agent_sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,10 @@ def read_session_lineage_metadata(db_path: Path, session_ids: list[str] | set[st
entry['relationship_type'] = 'child_session'
entry['parent_title'] = parent_row.get('title')
entry['parent_source'] = parent_row.get('source')
parent_source = str(parent_row.get('source') or '').strip().lower()
child_source = str(row.get('source') or '').strip().lower()
if parent_source and child_source and parent_source != child_source:
entry['_cross_surface_child_session'] = True
parent_root = _continuation_root_id(rows, parent_id)
if parent_root:
entry['_parent_lineage_root_id'] = parent_root
Expand Down
4 changes: 4 additions & 0 deletions static/sessions.js
Original file line number Diff line number Diff line change
Expand Up @@ -1823,6 +1823,10 @@ function _attachChildSessionsToSidebarRows(collapsedRows, rawSessions){
const orphans=[];
for(const child of rawSessions||[]){
if(!_isChildSession(child)) continue;
if(child._cross_surface_child_session){
orphans.push({...child,_orphan_child_session:true});
continue;
}
const parentSid=child.parent_session_id;
let parentRow=visibleBySid.get(parentSid);
let parentSegment=null;
Expand Down
46 changes: 46 additions & 0 deletions tests/test_session_lineage_collapse.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ def test_collapsed_lineage_contains_active_hidden_segment():
assert '"containsRoot":true' in result




def test_sidebar_attaches_child_sessions_to_collapsed_hidden_parent_lineage():
js = SESSIONS_JS_PATH.read_text(encoding="utf-8")
source = f"""
Expand Down Expand Up @@ -162,3 +164,47 @@ def test_sidebar_attaches_child_sessions_to_collapsed_hidden_parent_lineage():
assert [row["session_id"] for row in rows] == ["tip"]
assert rows[0]["_child_session_count"] == 1
assert rows[0]["_child_sessions"][0]["session_id"] == "child"


def test_cross_surface_webui_child_session_remains_top_level_when_parent_is_messaging():
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('_isChildSession'));
eval(extractFunc('_sidebarLineageKeyForRow'));
eval(extractFunc('_attachChildSessionsToSidebarRows'));
const collapsed = [{{session_id:'telegram_parent', title:'Telegram parent', source_label:'Telegram'}}];
const raw = [
collapsed[0],
{{
session_id:'webui_tip',
title:'Current WebUI continuation',
parent_session_id:'telegram_parent',
relationship_type:'child_session',
parent_source:'telegram',
source_label:'Telegram',
session_source:'messaging',
raw_source:'telegram',
_cross_surface_child_session:true,
}},
];
const rows = _attachChildSessionsToSidebarRows(collapsed, raw);
console.log(JSON.stringify(rows));
"""
rows = json.loads(_run_node(source))
assert [row["session_id"] for row in rows] == ["telegram_parent", "webui_tip"]
assert rows[1].get("_orphan_child_session") is True
assert "_child_sessions" not in rows[0]
38 changes: 35 additions & 3 deletions tests/test_session_lineage_metadata_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ def _ensure_state_db(path):
return conn


def _insert_state_row(conn, sid, *, parent=None, ended_at=None, end_reason=None, started_at=None):
def _insert_state_row(conn, sid, *, parent=None, ended_at=None, end_reason=None, started_at=None, source='webui'):
conn.execute(
"""
INSERT INTO sessions
(id, source, title, model, started_at, message_count, parent_session_id, ended_at, end_reason)
VALUES (?, 'webui', ?, 'openai/gpt-5', ?, 2, ?, ?, ?)
VALUES (?, ?, ?, 'openai/gpt-5', ?, 2, ?, ?, ?)
""",
(sid, sid, started_at or time.time(), parent, ended_at, end_reason),
(sid, source, sid, started_at or time.time(), parent, ended_at, end_reason),
)
conn.commit()

Expand Down Expand Up @@ -202,3 +202,35 @@ def test_cli_close_parent_preserves_cross_surface_continuation_lineage(_isolate)
assert rows["lineage_api_webui_child"].get("_lineage_root_id") == "lineage_api_cli_parent"
finally:
conn.close()


def test_cross_surface_child_session_metadata_marks_orphan_top_level_candidate(_isolate):
conn = _ensure_state_db(_isolate)
t0 = time.time() - 100
try:
_save_webui_session("lineage_api_telegram_parent", title="Telegram parent", updated_at=t0)
_save_webui_session("lineage_api_webui_tip", title="WebUI tip", updated_at=t0 + 10)
_insert_state_row(
conn,
"lineage_api_telegram_parent",
source="telegram",
started_at=t0,
ended_at=t0 + 5,
end_reason="compression",
)
_insert_state_row(
conn,
"lineage_api_webui_tip",
source="webui",
parent="lineage_api_telegram_parent",
started_at=t0 + 6,
)

rows = {row["session_id"]: row for row in all_sessions()}
tip = rows["lineage_api_webui_tip"]

assert tip.get("relationship_type") == "child_session"
assert tip.get("parent_source") == "telegram"
assert tip.get("_cross_surface_child_session") is True
finally:
conn.close()
Loading