fix(core): serialize Trace appends; tolerate malformed middle lines on read (#234)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei ZhengandClaude Fable 5 authored and GitHub committed 2026-08-07 17:49:40 +08:00
1 parent 9745773cb9
commit 4a899a265f
9 files changed
+344 -33

No files matched your search

@@ -2,9 +2,10 @@
* Trace service.
*
* History messages: all of the Session's index files concatenated in order
* (readTraceTolerant, tolerating a truncated last line), containing only the
* complete messages and events that were actually written to Trace (naturally
* excluding partial_*); in-flight increments are continued by SSE.
* (readTraceTolerant, tolerating a truncated last line and skipping malformed
* middle lines), containing only the complete messages and events that were
* actually written to Trace (naturally excluding partial_*); in-flight
* increments are continued by SSE.
* Performance analysis is derived from a single Trace file: nearest-neighbor
* pairing of request_begin/end, tool call duration pairing, reconnect / compaction
* counts, and Token trend.
@@ -1206,7 +1207,9 @@ export class TraceService {
const invalid = (message: string) => new HttpError(400, "invalid_trace", message);
let records: OmniMessage[];
try {
records = parseTraceLines(content);
// Strict parse: import is the gate for user-supplied files, so a malformed middle
// line is reported as 400 rather than silently dropped (read paths skip it instead).
records = parseTraceLines(content, { onMalformed: "throw" });
} catch {
throw invalid("The file is not valid Trace JSONL.");
}
@@ -189,7 +189,10 @@ describe("trace-import-export", () => {
it("import: malformed middle line → 400 invalid_trace", async () => {
const lines = toContent(sampleTrace(SID)).split("\n");
lines[1] = "{not json"; // corrupt a middle line (non-final: parseTraceLines throws loudly)
// Corrupt a middle (non-final) line. Read paths skip malformed middle lines (#215
// recovery), but import validates strictly (onMalformed: "throw") — damage in an
// uploaded file is reported, not silently dropped.
lines[1] = "{not json";
const res = await owner.post(`${base()}/import`, { dataBase64: b64(lines.join("\n")) });
expect(res.status).toBe(400);
expect(await errorCode(res)).toBe("invalid_trace");