Skip to content

Commit 0b7ff66

Browse files
committed
fix(trajectory): report malformed export rows
1 parent ba8a649 commit 0b7ff66

4 files changed

Lines changed: 265 additions & 54 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
1818
- Media/files: sniff `input_file` bytes before trusting declared MIME headers, rejecting spoofed image or zip payloads before they become agent-visible text.
1919
- Config persistence: ignore malformed array/scalar auth profile, cron job state, and session store entries instead of hydrating them into numeric profile ids, crashed cron rows, or invalid session records.
2020
- Providers: reject malformed successful Runway, BytePlus, and Ollama embedding responses with provider-owned errors instead of raw parser/type failures, silent bad vectors, or long bogus polling.
21+
- Trajectory export: skip and report malformed session/runtime JSONL rows in `manifest.json` instead of letting wrong-shaped session rows crash support bundle export.
2122
- Hooks: raise bounded gateway lifecycle hook wait budgets to 5 seconds for shutdown and 10 seconds for pre-restart, giving short restart notification handlers time to finish before shutdown continues. (#82273) Thanks @bryanbaer.
2223
- Plugin releases: require external package compatibility metadata in the npm plugin publish plan, matching the ClawHub package contract before packages ship.
2324
- Agents/OpenAI-compatible: honor per-model `max_completion_tokens`/`max_tokens` params in embedded OpenAI-completions runs so high-token Kimi-style routes keep their configured completion cap. Fixes #82230. Thanks @albert-zen.

src/trajectory/export.test.ts

Lines changed: 119 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -317,17 +317,35 @@ describe("exportTrajectoryBundle", () => {
317317
writeSimpleSessionFile(sessionFile);
318318
fs.writeFileSync(
319319
runtimeFile,
320-
`${JSON.stringify({})}\n${JSON.stringify({
321-
traceSchema: "openclaw-trajectory",
322-
schemaVersion: 1,
323-
traceId: "session-1",
324-
source: "runtime",
325-
type: "session.started",
326-
ts: "2026-04-22T08:00:00.000Z",
327-
seq: 1,
328-
sourceSeq: 1,
329-
sessionId: "session-1",
330-
})}\n`,
320+
[
321+
"",
322+
JSON.stringify({}),
323+
"",
324+
JSON.stringify({
325+
traceSchema: "openclaw-trajectory",
326+
schemaVersion: 1,
327+
traceId: "session-1",
328+
source: "runtime",
329+
type: "bad-data",
330+
ts: "2026-04-22T08:00:00.000Z",
331+
seq: 1,
332+
sourceSeq: 1,
333+
sessionId: "session-1",
334+
data: [],
335+
}),
336+
'{"traceSchema":',
337+
JSON.stringify({
338+
traceSchema: "openclaw-trajectory",
339+
schemaVersion: 1,
340+
traceId: "session-1",
341+
source: "runtime",
342+
type: "session.started",
343+
ts: "2026-04-22T08:00:00.000Z",
344+
seq: 1,
345+
sourceSeq: 1,
346+
sessionId: "session-1",
347+
}),
348+
].join("\n") + "\n",
331349
"utf8",
332350
);
333351

@@ -340,6 +358,95 @@ describe("exportTrajectoryBundle", () => {
340358

341359
expect(bundle.manifest.runtimeEventCount).toBe(1);
342360
expect(eventTypes(bundle.events)).toContain("session.started");
361+
expect(bundle.manifest.warnings).toEqual([
362+
{
363+
source: "runtime",
364+
code: "invalid-runtime-event",
365+
count: 2,
366+
rows: [2, 4],
367+
message: "Skipped a runtime trajectory JSONL row that does not match the session schema.",
368+
},
369+
{
370+
source: "runtime",
371+
code: "invalid-runtime-json",
372+
count: 1,
373+
rows: [5],
374+
message: "Skipped a runtime trajectory JSONL row that is not valid JSON.",
375+
},
376+
]);
377+
});
378+
379+
it("skips and reports malformed session jsonl rows without poisoning transcript export", async () => {
380+
const tmpDir = makeTempDir();
381+
const sessionFile = path.join(tmpDir, "session.jsonl");
382+
const outputDir = path.join(tmpDir, "bundle");
383+
const header = {
384+
type: "session",
385+
version: 3,
386+
id: "session-1",
387+
timestamp: "2026-04-01T05:46:39.000Z",
388+
cwd: tmpDir,
389+
};
390+
const userEntry = {
391+
type: "message",
392+
id: "entry-user",
393+
parentId: null,
394+
timestamp: "2026-04-01T05:46:40.000Z",
395+
message: userMessage("hello"),
396+
};
397+
const assistantEntry = {
398+
type: "message",
399+
id: "entry-assistant",
400+
parentId: "entry-user",
401+
timestamp: "2026-04-01T05:46:41.000Z",
402+
message: assistantMessage([{ type: "text", text: "done" }]),
403+
};
404+
fs.writeFileSync(
405+
sessionFile,
406+
[
407+
JSON.stringify(header),
408+
"null",
409+
'{"type":',
410+
JSON.stringify({
411+
type: "message",
412+
id: "entry-corrupt",
413+
parentId: null,
414+
timestamp: "2026-04-01T05:46:39.500Z",
415+
}),
416+
JSON.stringify(userEntry),
417+
JSON.stringify(assistantEntry),
418+
].join("\n") + "\n",
419+
"utf8",
420+
);
421+
422+
const bundle = await exportTrajectoryBundle({
423+
outputDir,
424+
sessionFile,
425+
sessionId: "session-1",
426+
workspaceDir: tmpDir,
427+
});
428+
429+
expect(bundle.manifest.transcriptEventCount).toBe(2);
430+
expect(eventTypes(bundle.events)).toEqual(["user.message", "assistant.message"]);
431+
expect(bundle.manifest.warnings).toEqual([
432+
{
433+
source: "session",
434+
code: "invalid-session-row",
435+
count: 2,
436+
rows: [2, 4],
437+
message: "Skipped a session JSONL row that is not a session entry object.",
438+
},
439+
{
440+
source: "session",
441+
code: "invalid-session-json",
442+
count: 1,
443+
rows: [3],
444+
message: "Skipped a session JSONL row that is not valid JSON.",
445+
},
446+
]);
447+
expect(
448+
JSON.parse(fs.readFileSync(path.join(outputDir, "manifest.json"), "utf8")).warnings,
449+
).toEqual(bundle.manifest.warnings);
343450
});
344451

345452
it("uses the recorded runtime pointer before current environment overrides", async () => {
@@ -550,6 +657,7 @@ describe("exportTrajectoryBundle", () => {
550657

551658
expect(bundle.manifest.runtimeEventCount).toBe(0);
552659
expect(eventTypes(bundle.events)).not.toContain("other-runtime");
660+
expect(bundle.manifest.warnings).toBeUndefined();
553661
});
554662

555663
it("redacts non-workspace paths in strings that also contain workspace paths", async () => {

0 commit comments

Comments
 (0)