Store desktop session metadata only and replay MR or L4 history

This commit is contained in:
Liang Jiaqing committed 2026-10-17 13:20:04 +08:00
1 parent 2ba5529060
commit a2eb5414c2
7 files changed
+433 -68

No files matched your search

+12 -2
View File
@@ -832,6 +832,11 @@ def extract_ui_messages(path):
try:
with open(path, encoding='utf-8', errors='replace') as f: content = f.read()
except Exception: return []
return extract_ui_messages_text(content)
def extract_ui_messages_text(content):
"""Replay log text, including archive members, without extracting to disk."""
pairs = _pairs(content)
if not pairs: return []
# tool_results live in the *next* Prompt's content; index look-ahead.
@@ -839,16 +844,21 @@ def extract_ui_messages(path):
for i in range(len(pairs) - 1):
next_tr[i] = _tool_results_from_prompt(pairs[i + 1][0])
stamps = re.findall(r'^=== Prompt ===\s+(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d)', content, re.M)
out, assistant, round_turn = [], None, 0
for i, (prompt, response) in enumerate(pairs):
try:
ts = time.mktime(time.strptime(stamps[i], '%Y-%m-%d %H:%M:%S'))
except (IndexError, ValueError, OverflowError):
ts = None
user = _user_text(prompt)
seg = _format_response_segment(response, next_tr[i])
if user:
if assistant is not None: out.append(assistant)
out.append({'role': 'user', 'content': user})
out.append({'role': 'user', 'content': user, 'ts': ts})
# Turn 1 marker too — agent_loop yields one per LLM call, including the
# first, so fold_turns treats every non-last call uniformly as a fold.
assistant = {'role': 'assistant',
assistant = {'role': 'assistant', 'ts': ts,
'content': f"\n\n**LLM Running (Turn 1) ...**\n\n{seg}"}
round_turn = 1
else:
+10 -7
View File
@@ -430,15 +430,19 @@ def canonical_session_record(
plan_path = item.get("plan_path", "")
if not isinstance(title, str) or not isinstance(cwd, str) or not isinstance(plan_path, str):
raise BackupFormatError("session text field is invalid")
messages = _record_list(item.get("messages"), [], "messages")
llm_history_value = item.get("llm_history")
llm_history = None if llm_history_value is None else _record_list(
llm_history_value, [], "llm history"
)
try:
from .desktop_history import metadata
except ImportError:
from desktop_history import metadata
llm_no_value = item.get("llm_no")
llm_no = None if llm_no_value is None else _non_negative_integer(
llm_no_value, 0, "model index"
)
if "messages" in item:
_record_list(item["messages"], [], "messages")
history = item.get("llm_history")
if history is not None and (not isinstance(history, list) or any(not isinstance(m, dict) for m in history)):
raise BackupFormatError("session history is invalid")
pinned = item.get("pinned", False)
untitled = item.get("untitled", True)
if not isinstance(pinned, bool) or not isinstance(untitled, bool):
@@ -449,7 +453,7 @@ def canonical_session_record(
"cwd": cwd,
"created_at": _finite_timestamp(item.get("created_at"), current_time),
"updated_at": _finite_timestamp(item.get("updated_at"), current_time),
"messages": messages,
**metadata(item),
"msg_seq": _non_negative_integer(item.get("msg_seq"), 0, "message sequence"),
"pinned": pinned,
"untitled": untitled,
@@ -458,7 +462,6 @@ def canonical_session_record(
),
"plan_path": plan_path,
"llm_no": llm_no,
"llm_history": llm_history,
}
+52 -55
View File
@@ -250,7 +250,11 @@ class Session:
untitled: bool = True
plan_scan_baseline: int = 0
plan_path: str = ""
llm_history: Optional[List[dict]] = None
llm_history: Optional[List[dict]] = None # memory only; reconstructed from MR/L4
first_message: str = ""
first_message_at: float = 0
log_refs: List[dict] = field(default_factory=list)
has_content: bool = False
# 该会话绑定的模型下标(mykey.py 配置块顺序,== agent.llmclients 下标)。
# None = 未绑定,发消息时回退到全局默认 ui.llmNo,保持旧会话平滑迁移。
llm_no: Optional[int] = None
@@ -334,20 +338,16 @@ class AgentManager:
return str(Path(self.ga_root) / "mykey.py")
def _session_dict(self, s: "Session") -> dict:
llm_hist = None
if s.agent and hasattr(s.agent, 'llmclient'):
try: llm_hist = s.agent.llmclient.backend.history
except Exception: pass
if llm_hist is None:
llm_hist = s.llm_history
from desktop_history import metadata
return {"id": s.id, "title": s.title, "cwd": s.cwd,
"created_at": s.created_at, "updated_at": s.updated_at,
"messages": s.messages, "msg_seq": s.msg_seq,
"pinned": s.pinned, "untitled": s.untitled,
"msg_seq": s.msg_seq, "pinned": s.pinned, "untitled": s.untitled,
"plan_scan_baseline": s.plan_scan_baseline,
"plan_path": s.plan_path or "",
"llm_no": s.llm_no,
"llm_history": llm_hist}
"plan_path": s.plan_path or "", "llm_no": s.llm_no,
**metadata({"first_message": s.first_message,
"first_message_at": s.first_message_at,
"log_refs": s.log_refs, "has_content": s.has_content,
"messages": s.messages})}
def _session_file(self, sid: str) -> Path:
return self._sessions_dir / f"{sid}.json"
@@ -425,9 +425,6 @@ class AgentManager:
self._sessions_dir.mkdir(parents=True, exist_ok=True)
with self.lock:
data = self._session_dict(s)
data["messages"] = copy.deepcopy(data["messages"])
if data.get("llm_history"):
data["llm_history"] = copy.deepcopy(data["llm_history"])
tmp.write_text(json.dumps(data, ensure_ascii=False, default=str), encoding="utf-8")
os.replace(tmp, self._session_file(s.id))
except Exception as e:
@@ -452,22 +449,23 @@ class AgentManager:
for s in sessions:
self._persist_session(s, strict=strict)
def _session_from_item(self, item: dict) -> "Session":
def _session_from_item(self, item: dict) -> Optional["Session"]:
from desktop_history import legacy_anchors, load
anchors = legacy_anchors(item)
item = canonical_session_record(item, default_cwd=self.ga_root)
msgs = item["messages"]
return Session(id=item["id"], title=item["title"],
cwd=item["cwd"],
created_at=item["created_at"],
updated_at=item["updated_at"],
messages=msgs,
msg_seq=item["msg_seq"],
pinned=item["pinned"],
untitled=item["untitled"],
plan_scan_baseline=_load_plan_baseline(item, msgs),
restored = load(self.ga_root, item, anchors)
if restored is None:
return None
msgs, history, meta = restored
return Session(id=item["id"], title=item["title"], cwd=item["cwd"],
created_at=item["created_at"], updated_at=item["updated_at"],
messages=msgs, msg_seq=max(item["msg_seq"], len(msgs)),
pinned=item["pinned"], untitled=item["untitled"],
plan_scan_baseline=min(item["plan_scan_baseline"], len(msgs)),
plan_path=_sanitize_desktop_plan_path(item["id"], item["plan_path"]),
status="idle", agent=None,
llm_history=item["llm_history"],
llm_no=item["llm_no"])
status="idle", agent=None, llm_history=history, llm_no=item["llm_no"],
first_message=meta["first_message"], first_message_at=meta["first_message_at"],
log_refs=meta["log_refs"], has_content=meta["has_content"])
def _load_sessions(self):
# New format: one file per session under temp/desktop_sessions/.
@@ -479,7 +477,11 @@ class AgentManager:
if not _is_desktop_session_id(item.get("id")):
continue
sess = self._session_from_item(item)
if sess is None:
f.write_text(json.dumps(canonical_session_record(item), ensure_ascii=False), encoding="utf-8")
continue
self.sessions[sess.id] = sess
self._persist_session(sess)
except Exception as e:
bridge_print(f"[bridge] load session {f.name} failed: {e}")
except Exception as e:
@@ -496,14 +498,15 @@ class AgentManager:
continue
try:
sess = self._session_from_item(item)
if sess is None:
continue
self.sessions[sess.id] = sess
self._persist_session(sess)
except Exception as e:
bridge_print(f"[bridge] migrate session failed: {e}")
# Retire the legacy file so we do not migrate again next launch.
with contextlib.suppress(Exception):
self._sessions_file.rename(
self._sessions_file.parent / (self._sessions_file.name + ".migrated"))
self._sessions_file.unlink()
except Exception as e:
bridge_print(f"[bridge] migrate sessions failed: {e}")
@@ -559,6 +562,9 @@ class AgentManager:
except ValueError:
skipped += 1
continue
if sess is None:
skipped += 1
continue
self.sessions[sid] = sess
new_sessions.append(sess)
added += 1
@@ -752,6 +758,9 @@ class AgentManager:
no = sess.llm_no if sess.llm_no is not None else _global_default_llm_no()
if no is not None:
agent.next_llm(int(no))
if sess.llm_history:
from continue_cmd import _derive_hist_info, _load_history_into
_load_history_into(agent, copy.deepcopy(sess.llm_history), _derive_hist_info(sess.llm_history))
agent.inc_out = True
agent.verbose = True
threading.Thread(target=agent.run, daemon=True, name=f"GA-{sess.id}").start()
@@ -924,6 +933,12 @@ class AgentManager:
msg = {"id": sess.msg_seq, "role": role, "content": content, "ts": time.time()}
msg.update(extra)
sess.messages.append(msg)
if role == "user":
from desktop_history import preview
if not sess.first_message:
sess.first_message = preview(content)
sess.first_message_at = msg["ts"]
sess.has_content = True
sess.updated_at = time.time()
if role == "user" and content.strip() and sess.title == "New chat":
sess.title = content.strip().replace("\n", " ")[:40]
@@ -1085,13 +1100,14 @@ class AgentManager:
return
if sess.agent is None:
sess.agent = self.make_agent(sess)
if sess.llm_history:
try:
sess.agent.llmclient.backend.history = sess.llm_history
except Exception:
pass
agent = sess.agent
from desktop_history import preview
log_path = getattr(agent, "log_path", None)
with self.lock:
if log_path and not any(r["name"] == Path(log_path).name for r in sess.log_refs):
sess.log_refs.append({"name": Path(log_path).name,
"started_at": time.time(), "first_message": preview(prompt)})
self._persist_session(sess)
sess.running_llm_no = agent.llm_no
try:
running_backend = agent.llmclient.backend
@@ -1290,25 +1306,6 @@ class AgentManager:
if no is not None and hasattr(agent, "next_llm"):
with contextlib.suppress(Exception):
agent.next_llm(int(no))
if sess.llm_history:
try:
agent.llmclient.backend.history = sess.llm_history
except Exception as e:
bridge_print(f"[bridge] restore llm_history failed: {e}")
else:
history = []
for m in sess.messages:
role = m.get("role")
content = m.get("content", "")
if role == "user":
history.append({"role": "user", "content": [{"type": "text", "text": content}]})
elif role == "assistant":
history.append({"role": "assistant", "content": [{"type": "text", "text": content}]})
if history:
try:
agent.llmclient.backend.history = history
except Exception as e:
bridge_print(f"[bridge] inject history failed: {e}")
with self.lock:
sess.agent = agent
sess.status = "idle"
+185
View File
@@ -0,0 +1,185 @@
"""Desktop metadata and MR/L4 replay. Never writes conversation bodies."""
from __future__ import annotations
import datetime as dt
import math
from pathlib import Path
import re
import zipfile
try:
from .continue_cmd import _pairs, _parse_native_history, _user_text, extract_ui_messages_text
except ImportError:
from continue_cmd import _pairs, _parse_native_history, _user_text, extract_ui_messages_text
_HEADER = re.compile(r'^=== Prompt ===\s+(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d)', re.M)
_LOG_NAME = re.compile(r'model_responses_[\w-]+\.txt\Z')
def _stamp(value, default=0):
try:
value = float(value)
return value if math.isfinite(value) and 0 <= value <= 253402214400 else default
except (TypeError, ValueError):
return default
def preview(value):
return value.strip()[:256] if isinstance(value, str) else ''
def metadata(item):
"""Bounded text anchors only; legacy message bodies are used but never returned."""
users = [m for m in item.get('messages', []) if isinstance(m, dict) and m.get('role') == 'user']
first = users[0] if users else {}
refs = []
for ref in item.get('log_refs', []) or []:
if not isinstance(ref, dict):
continue
name = ref.get('name', '')
refs.append({'name': name if isinstance(name, str) and _LOG_NAME.fullmatch(name) else '',
'started_at': _stamp(ref.get('started_at')),
'first_message': preview(ref.get('first_message'))})
return {'storage_version': 2,
'first_message': preview(item.get('first_message') or first.get('content')),
'first_message_at': _stamp(item.get('first_message_at'), _stamp(first.get('ts'), _stamp(item.get('created_at')))),
'log_refs': refs,
'has_content': bool(item.get('has_content') or users or item.get('llm_history') or refs)}
def legacy_anchors(item):
"""Old Desktop versions could create multiple MR files after restarts."""
return [{'name': '', 'started_at': _stamp(m.get('ts'), _stamp(item.get('created_at'))),
'first_message': preview(m.get('content'))}
for m in item.get('messages', [])
if isinstance(m, dict) and m.get('role') == 'user' and preview(m.get('content'))]
def _first_time(text):
m = _HEADER.search(text)
if m:
try:
return dt.datetime.strptime(m[1], '%Y-%m-%d %H:%M:%S').timestamp()
except (ValueError, OverflowError, OSError):
pass
return 0
def _matches(text, ref):
started = _first_time(text)
expected = ref.get('started_at', 0)
if not started or not expected or abs(started - expected) > 300:
return False
anchor = preview(ref.get('first_message'))
if not anchor:
return False
pairs = _pairs(text)
if not pairs:
return False
# Native prompts may prepend attachment paths; raw archives contain USER markers.
prompt = _user_text(pairs[0][0]) or pairs[0][0]
return anchor in prompt
def resolve(root, ref):
"""MR first, then time/first-message matching in L4. ZIP reads stay in memory."""
root = Path(root)
mr = root / 'temp' / 'model_responses'
name = ref.get('name', '')
direct = mr / name if name and _LOG_NAME.fullmatch(name) else None
candidates = ([direct] if direct and direct.is_file() else [])
candidates += [p for p in mr.glob('model_responses_*.txt') if p != direct]
for path in candidates:
try:
with path.open(encoding='utf-8', errors='replace') as f:
head = f.read(65536)
started = _first_time(head)
if not started or abs(started - ref.get('started_at', 0)) > 300:
continue
text = head + f.read()
if _matches(text, ref):
return text, {**ref, 'name': path.name, 'started_at': started}
except OSError:
continue
stamp = ref.get('started_at', 0)
if not stamp:
return None
l4 = root / 'memory' / 'L4_raw_sessions'
# Month/minute names avoid scanning/decompressing the full archive corpus.
moment = dt.datetime.fromtimestamp(stamp)
moments = [moment + dt.timedelta(minutes=i) for i in range(-5, 6)]
prefixes = {t.strftime('%m%d_%H%M') for t in moments}
months = {t.strftime('%Y-%m') for t in moments}
for month in sorted(months):
archive = l4 / (month + '.zip')
try:
with zipfile.ZipFile(archive) as z:
for member in z.infolist():
if Path(member.filename).name[:9] not in prefixes or not member.filename.endswith('.txt'):
continue
text = z.read(member).decode('utf-8', errors='replace')
if _matches(text, ref):
return text, {**ref, 'started_at': _first_time(text)}
except (OSError, zipfile.BadZipFile, RuntimeError):
continue
for path in l4.glob('*.txt'):
if path.name[:9] in prefixes:
try:
text = path.read_text(encoding='utf-8', errors='replace')
if _matches(text, ref):
return text, {**ref, 'started_at': _first_time(text)}
except OSError:
pass
return None
def replay(text):
pairs = _pairs(text)
native = _parse_native_history(pairs) if pairs else None
if native is not None:
return extract_ui_messages_text(text), native
# Raw logs / stripped L4: replay preserved prose; continuation is semantic.
messages = []
for prompt, response in pairs:
match = re.search(r'^=== USER ===\s*\n(.*?)(?=^=== ASSISTANT ===|\Z)', prompt, re.M | re.S)
user = match[1].strip() if match else ''
if user:
messages.append({'role': 'user', 'content': user})
if response.strip():
messages.append({'role': 'assistant', 'content': response.strip()})
return messages, None
def load(root, item, anchors=None):
"""Return replay and small updated index, or None when every source is gone."""
meta = metadata(item)
refs = meta['log_refs'] or anchors or [
{'name': '', 'started_at': meta['first_message_at'], 'first_message': meta['first_message']}]
messages, history, found, seen = [], [], [], set()
semantic = False
for ref in refs:
result = resolve(root, ref)
if result is None:
continue
text, located = result
identity = (located.get('name'), _first_time(text))
if identity in seen:
continue
seen.add(identity)
ui, native = replay(text)
if not ui:
continue
found.append(located)
messages.extend({**m, 'ts': located['started_at']} for m in ui)
if native is None:
semantic = True
else:
history.extend(native)
if not found and meta['has_content']:
return None
if semantic:
history = [{'role': m['role'], 'content': [{'type': 'text', 'text': m['content']}]} for m in messages]
for i, message in enumerate(messages, 1):
message['id'] = i
meta['log_refs'] = found or meta['log_refs']
return messages, history or None, meta
+14 -3
View File
@@ -147,7 +147,8 @@ class TestPersistSessionConcurrentMutation:
f = manager._session_file(sess.id)
data = json.loads(f.read_text(encoding="utf-8"))
assert data["id"] == sess.id
assert isinstance(data["messages"], list)
assert "messages" not in data
assert data["storage_version"] == 2
def test_concurrent_mutation_llm_history(self, manager: AgentManager):
"""Mutating llm_history from another thread during persist must not corrupt."""
@@ -176,7 +177,7 @@ class TestPersistSessionConcurrentMutation:
f = manager._session_file(sess.id)
data = json.loads(f.read_text(encoding="utf-8"))
assert isinstance(data["llm_history"], list)
assert "llm_history" not in data
class TestTransactionalDataImport:
@@ -563,6 +564,12 @@ class TestSessionContinuityAfterRestart:
def test_persist_and_reload_preserves_llm_no(self, manager: AgentManager):
"""Full cycle: persist session with llm_no, reload, verify llm_no survives."""
sess = _make_session(sid="sess-roundtrip")
from test_desktop_history import seed_mr, record
seed_mr(Path(manager.ga_root), "model_responses_roundtrip.txt")
meta = record("model_responses_roundtrip.txt")
sess.log_refs = meta["log_refs"]
sess.first_message = meta["first_message"]
sess.first_message_at = meta["first_message_at"]
sess.llm_no = 5
sess.llm_history = [{"role": "user", "content": [{"type": "text", "text": "hi"}]}]
manager.sessions[sess.id] = sess
@@ -574,7 +581,8 @@ class TestSessionContinuityAfterRestart:
reloaded = manager.sessions.get("sess-roundtrip")
assert reloaded is not None
assert reloaded.llm_no == 5
assert reloaded.llm_history == [{"role": "user", "content": [{"type": "text", "text": "hi"}]}]
assert reloaded.llm_history[0]["content"][0]["text"] == "hello"
assert len(reloaded.llm_history) == 2
assert reloaded.agent is None
@@ -1311,6 +1319,9 @@ class TestCanonicalSessionRestart:
"unknown": "ignored",
},
}
from test_desktop_history import seed_mr, record as log_record
seed_mr(Path(manager.ga_root), "model_responses_canonical.txt")
records["valid.json"].update(log_record("model_responses_canonical.txt"))
for name, record in records.items():
(manager._sessions_dir / name).write_text(json.dumps(record), encoding="utf-8")
+3 -1
View File
@@ -464,7 +464,9 @@ class TestDataBackupImport:
)
assert document["id"] == "sess-valid"
assert document["updated_at"] == 11.5
assert document["messages"][0]["role"] == "user"
assert "messages" not in document
assert "llm_history" not in document
assert document["storage_version"] == 2
assert "unknown" not in document
def test_mocked_windows_reparse_source_is_rejected_before_read(
+157
View File
@@ -0,0 +1,157 @@
"""Metadata-only persistence and MR -> L4 recovery contract."""
import datetime as dt
import json
from pathlib import Path
import zipfile
from frontends import desktop_history as h
from frontends.data_backup import canonical_session_record
STAMP = dt.datetime(2026, 10, 17, 10, 0).timestamp()
def log_text(user='hello', answer='world', stamp=STAMP):
moment = dt.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d %H:%M:%S')
prompt = json.dumps({'role': 'user', 'content': [{'type': 'text', 'text': user}]})
response = repr([{'type': 'text', 'text': answer}])
return f'=== Prompt === {moment}\n{prompt}\n=== Response === {moment}\n{response}\n'
def seed_mr(root, name='model_responses_test.txt', **kwargs):
path = root / 'temp' / 'model_responses' / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(log_text(**kwargs), encoding='utf-8')
return path
def record(name='model_responses_test.txt', user='hello', stamp=STAMP):
return {'first_message': user, 'first_message_at': stamp, 'has_content': True,
'log_refs': [{'name': name, 'started_at': stamp, 'first_message': user}]}
def seed_zip(root, text, stamp=STAMP):
moment = dt.datetime.fromtimestamp(stamp)
path = root / 'memory' / 'L4_raw_sessions' / (moment.strftime('%Y-%m') + '.zip')
path.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(path, 'w', zipfile.ZIP_DEFLATED) as z:
z.writestr(moment.strftime('%m%d_%H%M') + '-1017_1001.txt', text)
return path
def test_canonical_never_persists_bodies():
data = canonical_session_record({'id': 'sess-test', 'messages': [
{'role': 'user', 'content': 'x' * 10000, 'ts': STAMP},
{'role': 'assistant', 'content': 'PRIVATE_BODY', 'turn_segs': ['PRIVATE_BODY']}],
'llm_history': [{'role': 'user', 'content': 'PRIVATE_HISTORY'}]})
assert len(data['first_message']) == 256
assert data['first_message_at'] == STAMP
assert 'messages' not in data and 'llm_history' not in data
assert 'PRIVATE' not in json.dumps(data)
assert len(json.dumps(data)) < 1200
def test_mr_wins_over_archive(tmp_path):
seed_mr(tmp_path, answer='MR answer')
seed_zip(tmp_path, log_text(answer='L4 answer'))
ui, history, meta = h.load(tmp_path, record())
assert 'MR answer' in ui[-1]['content']
assert history[-1]['content'][0]['text'] == 'MR answer'
assert meta['log_refs'][0]['name'] == 'model_responses_test.txt'
def test_l4_zip_replay_never_extracts(tmp_path):
seed_zip(tmp_path, log_text())
before = {p for p in tmp_path.rglob('*') if p.is_file()}
ui, history, _ = h.load(tmp_path, record())
assert ui[0]['content'] == 'hello'
assert 'world' in ui[-1]['content']
assert len(history) == 2
assert before == {p for p in tmp_path.rglob('*') if p.is_file()}
def test_raw_l4_semantic_recovery(tmp_path):
text = ('=== Prompt === 2026-10-17 10:00:00\n=== USER ===\nhello\n'
'=== Response === 2026-10-17 10:00:01\nraw answer\n')
seed_zip(tmp_path, text)
ui, history, _ = h.load(tmp_path, record())
assert ui[-1]['content'] == 'raw answer'
assert history[0]['content'][0]['text'] == 'hello'
def test_missing_sources_silently_skip_nonempty(tmp_path):
assert h.load(tmp_path, record()) is None
assert h.load(tmp_path, {'id': 'empty'})[0] == []
def test_wrong_time_or_first_message_is_not_another_session(tmp_path):
seed_mr(tmp_path)
assert h.load(tmp_path, record(user='other')) is None
assert h.load(tmp_path, record(stamp=STAMP + 3600)) is None
def test_legacy_metadata_discovers_mr(tmp_path):
seed_mr(tmp_path)
item = {'messages': [{'role': 'user', 'content': 'hello', 'ts': STAMP}]}
result = h.load(tmp_path, item, h.legacy_anchors(item))
assert result[2]['log_refs'][0]['name'] == 'model_responses_test.txt'
def test_multiple_segments_partial_missing_and_dedup(tmp_path):
seed_mr(tmp_path)
seed_mr(tmp_path, 'model_responses_next.txt', user='next', stamp=STAMP + 3600)
item = record()
item['log_refs'] += record('model_responses_next.txt', 'next', STAMP + 3600)['log_refs']
item['log_refs'] += record('model_responses_missing.txt', 'gone', STAMP + 7200)['log_refs']
item['log_refs'].append(item['log_refs'][0])
ui, history, meta = h.load(tmp_path, item)
assert [m['content'] for m in ui if m['role'] == 'user'] == ['hello', 'next']
assert len(history) == 4
assert len(meta['log_refs']) == 2
def test_corrupt_zip_is_silent(tmp_path):
path = seed_zip(tmp_path, log_text())
path.write_bytes(b'not a zip')
assert h.load(tmp_path, record()) is None
def test_path_traversal_is_not_a_log_reference(tmp_path):
data = h.metadata(record('../../outside.txt'))
assert data['log_refs'][0]['name'] == ''
assert h.load(tmp_path, data) is None
def test_month_boundary_matches_archive(tmp_path):
stamp = dt.datetime(2026, 10, 1, 0, 0).timestamp()
seed_zip(tmp_path, log_text(stamp=stamp), stamp)
assert h.load(tmp_path, record(stamp=stamp - 60)) is not None
def test_missing_file_can_be_recovered_by_metadata(tmp_path):
seed_mr(tmp_path, 'model_responses_renamed.txt')
result = h.load(tmp_path, record())
assert result[2]['log_refs'][0]['name'] == 'model_responses_renamed.txt'
def test_large_history_does_not_grow_persisted_metadata():
a = {'id': 'sess-test', **record(), 'messages': [{'role': 'user', 'content': 'hello'}]}
small = canonical_session_record(a, now=STAMP)
a['messages'].extend({'role': 'assistant', 'content': 'x' * 5000} for _ in range(200))
a['llm_history'] = [{'role': 'assistant', 'content': 'y' * 100000}]
assert canonical_session_record(a, now=STAMP) == small
assert len(json.dumps(small)) < 1200
def test_archive_then_hot_continuation_chain(tmp_path):
seed_zip(tmp_path, log_text())
seed_mr(tmp_path, 'model_responses_next.txt', user='next', stamp=STAMP + 3600)
item = record()
item['log_refs'] += record('model_responses_next.txt', 'next', STAMP + 3600)['log_refs']
ui, history, _ = h.load(tmp_path, item)
assert [m['content'] for m in ui if m['role'] == 'user'] == ['hello', 'next']
assert len(history) == 4
def test_replay_preserves_native_message_time():
messages, _ = h.replay(log_text())
assert [m['ts'] for m in messages] == [STAMP, STAMP]