mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-03 04:49:26 +00:00
`_read_jsonl` parsed every line with a bare `json.loads` inside a list comprehension, so a single malformed line raised `JSONDecodeError` and took the whole `load()` down. An append interrupted mid-write (process crash, full disk) leaves exactly that: one truncated trailing line — and from then on every surface that opens the session errors on load, with no way back short of hand-editing the file. The session is effectively bricked, including its recoverable history. Skip unparseable lines and keep the good messages. This matches how the rest of this module already treats JSON (the inline-blob and roots/grants loaders all swallow `JSONDecodeError` and fall back) — `_read_jsonl` was the one strict outlier on the hot session-load path.
63 lines
2.1 KiB
Python
63 lines
2.1 KiB
Python
"""ConversationStore: a corrupt line in a .jsonl must not brick session load.
|
|
|
|
An append interrupted mid-write (crash, full disk) leaves one malformed trailing line.
|
|
load() must skip it and return the recoverable history, not raise on every open.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from coworker.conversations import ConversationStore
|
|
from coworker.sessions import SessionRecord
|
|
|
|
|
|
def _seed(store: ConversationStore, sid: str, n: int) -> None:
|
|
store.save(
|
|
SessionRecord(
|
|
session_id=sid,
|
|
workspace="/tmp",
|
|
model="m",
|
|
mode="interactive",
|
|
messages=[{"role": "user", "content": f"m{i}"} for i in range(n)],
|
|
)
|
|
)
|
|
|
|
|
|
def test_load_skips_a_corrupt_trailing_line(tmp_path):
|
|
store = ConversationStore(tmp_path / "state")
|
|
sid = "abc123def456"
|
|
_seed(store, sid, 2)
|
|
|
|
# Simulate a torn write: append a truncated JSON line to the session's log.
|
|
jsonl = tmp_path / "state" / "conversations" / f"{sid}.jsonl"
|
|
with open(jsonl, "a", encoding="utf-8") as f:
|
|
f.write('{"role": "user", "content": "unterm\n') # no closing brace/quote
|
|
|
|
loaded = store.load(sid) # must not raise
|
|
assert loaded is not None
|
|
# The two good messages survive; the corrupt line is dropped.
|
|
assert [m["content"] for m in loaded.messages] == ["m0", "m1"]
|
|
|
|
|
|
def test_load_skips_a_corrupt_middle_line(tmp_path):
|
|
store = ConversationStore(tmp_path / "state")
|
|
sid = "def456abc123"
|
|
jsonl = tmp_path / "state" / "conversations" / f"{sid}.jsonl"
|
|
jsonl.parent.mkdir(parents=True, exist_ok=True)
|
|
jsonl.write_text(
|
|
'{"role": "user", "content": "first"}\n'
|
|
"not json at all\n"
|
|
'{"role": "assistant", "content": "third"}\n',
|
|
encoding="utf-8",
|
|
)
|
|
# Register the session in the index so load() reaches the .jsonl.
|
|
store._conn.execute(
|
|
"INSERT INTO sessions (session_id, workspace, model, mode, title, n_msgs) "
|
|
"VALUES (?, '/tmp', 'm', 'interactive', 't', 2)",
|
|
(sid,),
|
|
)
|
|
store._conn.commit()
|
|
|
|
loaded = store.load(sid)
|
|
assert loaded is not None
|
|
assert [m["content"] for m in loaded.messages] == ["first", "third"]
|