Stage 303: PR #1717

This commit is contained in:
Nathan Esquenazi
2026-05-05 21:58:21 +00:00
4 changed files with 173 additions and 5 deletions
+7 -2
View File
@@ -1229,9 +1229,13 @@ def import_cli_session(
profile=None,
created_at=None,
updated_at=None,
parent_session_id=None,
):
"""Create a new WebUI session populated with CLI messages.
Returns the Session object.
"""Create a new WebUI session populated with CLI/agent messages.
Preserve parent_session_id from state.db so imported continuation segments
keep their lineage in the WebUI store and sidebar instead of reappearing as
detached orphan chats.
"""
s = Session(
session_id=session_id,
@@ -1242,6 +1246,7 @@ def import_cli_session(
profile=profile,
created_at=created_at,
updated_at=updated_at,
parent_session_id=parent_session_id,
)
s.save(touch_updated_at=False)
return s
+20 -3
View File
@@ -1270,9 +1270,15 @@ def _merge_cli_sidebar_metadata(ui_session: dict, cli_meta: dict) -> dict:
if cli_meta.get("last_message_at") is not None:
merged["last_message_at"] = cli_meta["last_message_at"]
if cli_meta.get("message_count") is not None:
merged["message_count"] = cli_meta["message_count"]
merged["message_count"] = max(
_numeric_count(merged.get("message_count")),
_numeric_count(cli_meta.get("message_count")),
)
elif cli_meta.get("actual_message_count") is not None:
merged["message_count"] = cli_meta["actual_message_count"]
merged["message_count"] = max(
_numeric_count(merged.get("message_count")),
_numeric_count(cli_meta.get("actual_message_count")),
)
if cli_meta.get("title"):
current_title = merged.get("title")
@@ -2622,7 +2628,13 @@ def handle_get(handler, parsed) -> bool:
_t3 = _time.monotonic()
if load_messages:
if is_messaging_session and cli_messages:
_all_msgs = cli_messages
sidecar_messages = getattr(s, "messages", []) or []
# Recovery/aggregate sidecars can intentionally contain a
# longer visible conversation than the single state.db
# segment for this messaging session id. Prefer the longer
# sidecar so repaired WebUI history is not hidden behind the
# canonical per-segment transcript.
_all_msgs = sidecar_messages if len(sidecar_messages) > len(cli_messages) else cli_messages
else:
_all_msgs = s.messages
else:
@@ -7661,6 +7673,7 @@ def _handle_session_import_cli(handler, body):
"raw_source": existing.raw_source or cli_meta.get("raw_source") or cli_meta.get("source_tag"),
"session_source": existing.session_source or cli_meta.get("session_source"),
"source_label": existing.source_label or cli_meta.get("source_label"),
"parent_session_id": existing.parent_session_id or cli_meta.get("parent_session_id"),
}
for attr, value in updates.items():
if getattr(existing, attr, None) != value:
@@ -7702,6 +7715,7 @@ def _handle_session_import_cli(handler, body):
cli_thread_id = None
cli_session_key = None
cli_platform = None
cli_parent_session_id = None
cli_read_only = False
for cs in get_cli_sessions():
if cs["session_id"] == sid:
@@ -7720,6 +7734,7 @@ def _handle_session_import_cli(handler, body):
cli_thread_id = cs.get("thread_id")
cli_session_key = cs.get("session_key")
cli_platform = cs.get("platform")
cli_parent_session_id = cs.get("parent_session_id")
cli_read_only = bool(cs.get("read_only"))
break
@@ -7750,6 +7765,7 @@ def _handle_session_import_cli(handler, body):
"raw_source": cli_raw_source or cli_source_tag,
"session_source": cli_session_source,
"source_label": cli_source_label,
"parent_session_id": cli_parent_session_id,
"read_only": True,
"messages": msgs,
"tool_calls": [],
@@ -7764,6 +7780,7 @@ def _handle_session_import_cli(handler, body):
profile=profile,
created_at=created_at,
updated_at=updated_at,
parent_session_id=cli_parent_session_id,
)
if cron_project_id:
s.project_id = cron_project_id
+34
View File
@@ -0,0 +1,34 @@
import json
def test_import_cli_session_preserves_parent_session_id():
from api.models import import_cli_session, SESSION_DIR, Session
parent_id = 'parent_lineage_001'
child_id = 'child_lineage_001'
# Ensure clean fixture state for direct model-level import.
for sid in (parent_id, child_id):
try:
(SESSION_DIR / f'{sid}.json').unlink(missing_ok=True)
except Exception:
pass
session = import_cli_session(
child_id,
'Child Session',
[{'role': 'user', 'content': 'hello', 'timestamp': 1.0}],
model='test-model',
parent_session_id=parent_id,
created_at=1.0,
updated_at=2.0,
)
assert session.parent_session_id == parent_id
payload = json.loads((SESSION_DIR / f'{child_id}.json').read_text(encoding='utf-8'))
assert payload['parent_session_id'] == parent_id
loaded = Session.load(child_id)
assert loaded.parent_session_id == parent_id
assert loaded.compact()['parent_session_id'] == parent_id
@@ -90,6 +90,7 @@ def test_session_import_cli_refresh_matches_messages_despite_timestamp_type_diff
self.raw_source = "weixin"
self.session_source = "messaging"
self.source_label = "WeChat"
self.parent_session_id = None
def compact(self):
return {"session_id": session_id, "title": "Imported"}
@@ -141,6 +142,7 @@ def test_session_import_cli_refresh_rejects_prefix_if_non_timing_content_diverge
self.session_source = "messaging"
self.source_label = "Telegram"
self.is_cli_session = True
self.parent_session_id = None
def compact(self):
return {"session_id": session_id, "title": "Imported"}
@@ -169,3 +171,113 @@ def test_session_import_cli_refresh_rejects_prefix_if_non_timing_content_diverge
assert response["session"]["messages"] == existing.messages
assert existing.messages[0]["content"] == "old-prefix"
assert save_calls == []
def test_session_import_cli_preserves_parent_metadata_on_existing_import(monkeypatch):
"""Refreshing an already-imported CLI session must persist lineage metadata."""
import api.routes as routes
session_id = "existing_parent_lineage_001"
parent_id = "root_parent_lineage_001"
class FakeSession:
def __init__(self):
self.messages = [{"role": "user", "content": "hello", "timestamp": 1.0}]
self.source_tag = "telegram"
self.raw_source = "telegram"
self.session_source = "messaging"
self.source_label = "Telegram"
self.parent_session_id = None
self.is_cli_session = True
def compact(self):
return {"session_id": session_id, "title": "Imported", "parent_session_id": self.parent_session_id}
def save(self, touch_updated_at=False):
save_calls.append(touch_updated_at)
save_calls = []
existing = FakeSession()
monkeypatch.setattr(routes.Session, "load", classmethod(lambda _cls, sid: existing if sid == session_id else None))
monkeypatch.setattr(routes, "require", lambda body, *keys: None)
monkeypatch.setattr(routes, "j", lambda _handler, payload, status=200, extra_headers=None: payload)
monkeypatch.setattr(routes, "get_cli_session_messages", lambda sid: existing.messages if sid == session_id else [])
monkeypatch.setattr(
routes,
"get_cli_sessions",
lambda: [{
"session_id": session_id,
"source_tag": "telegram",
"raw_source": "telegram",
"session_source": "messaging",
"source_label": "Telegram",
"parent_session_id": parent_id,
}],
)
response = routes._handle_session_import_cli(object(), {"session_id": session_id})
assert response["imported"] is False
assert existing.parent_session_id == parent_id
assert response["session"]["parent_session_id"] == parent_id
assert save_calls == [False]
def test_read_only_import_payload_includes_parent_session_id(monkeypatch):
"""Read-only CLI/session imports should also expose lineage in the payload."""
import api.routes as routes
session_id = "readonly_parent_lineage_001"
parent_id = "readonly_root_lineage_001"
messages = [{"role": "user", "content": "hello", "timestamp": 1.0}]
monkeypatch.setattr(routes.Session, "load", classmethod(lambda _cls, sid: None))
monkeypatch.setattr(routes, "require", lambda body, *keys: None)
monkeypatch.setattr(routes, "bad", lambda _handler, msg, status=400: {"ok": False, "error": msg, "status": status})
monkeypatch.setattr(routes, "j", lambda _handler, payload, status=200, extra_headers=None: payload)
monkeypatch.setattr(routes, "get_cli_session_messages", lambda sid: messages if sid == session_id else [])
monkeypatch.setattr(
routes,
"get_cli_sessions",
lambda: [{
"session_id": session_id,
"title": "Read-only child",
"model": "test-model",
"created_at": 1.0,
"updated_at": 2.0,
"source_tag": "discord",
"raw_source": "discord",
"session_source": "messaging",
"source_label": "Discord",
"parent_session_id": parent_id,
"read_only": True,
}],
)
response = routes._handle_session_import_cli(object(), {"session_id": session_id})
assert response["imported"] is False
assert response["session"]["parent_session_id"] == parent_id
assert response["session"]["messages"] == messages
def test_merge_cli_sidebar_metadata_keeps_larger_sidecar_message_count():
"""Sidebar metadata merge should not shrink repaired aggregate sidecar counts."""
import api.routes as routes
merged = routes._merge_cli_sidebar_metadata(
{"session_id": "sid", "message_count": 535, "title": "Recovered"},
{"session_id": "sid", "message_count": 407, "source_tag": "discord"},
)
assert merged["message_count"] == 535
def test_messaging_session_loader_prefers_longer_sidecar_transcript():
"""Pin the /api/session invariant that repaired sidecars can be longer than state.db segments."""
handler = _extract_handler("handle_get")
old = "if is_messaging_session and cli_messages:\n _all_msgs = cli_messages"
assert old not in handler
assert "sidecar_messages = getattr(s, \"messages\", []) or []" in handler
assert "len(sidecar_messages) > len(cli_messages)" in handler