diff --git a/CHANGELOG.md b/CHANGELOG.md index e71f99a66f83..494096e61377 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai - **Small-context compaction:** cap the effective reserve against the known model context window so small local models do not enter compaction from the first token. (#100621) Thanks @vincentkoc. - **Plugin install diagnostics:** suppress the misleading hook-pack fallback after plugin install failures only when the hook manifest is absent, while preserving actionable malformed hook-pack errors. (#100554) Thanks @vincentkoc. - **Config validation diagnostics:** emit each unchanged sanitized validation-warning payload once per config path, reset deduplication after a clean validation, and preserve the warning fingerprint across transient invalid reads and failed refreshes. (#100569, #25574) Thanks @vincentkoc. +- **Session usage logs:** normalize malformed transcript timestamps before sorting and Gateway serialization so invalid dates cannot surface as null usage-log times. (#99418) Thanks @sheyanmin. - **Config size-drop guard:** compare writes against canonical bytes for parseable object configs instead of raw BOM and indentation overhead, while preserving raw audit telemetry and the conservative malformed-input fallback. (#100591, #71865) Thanks @vincentkoc. - **Control UI coalesced updates:** show a clear queued-restart completion banner when an update joins an already-running Gateway restart. (#93082) Thanks @goutamadwant. - **Control UI connection errors:** preserve structured pairing and authentication failures for pending RPC callers while keeping generic disconnect behavior unchanged. (#54758) Thanks @ruanrrn. diff --git a/src/infra/session-cost-usage.test.ts b/src/infra/session-cost-usage.test.ts index 2c76ce22b41e..bd44f3c3d776 100644 --- a/src/infra/session-cost-usage.test.ts +++ b/src/infra/session-cost-usage.test.ts @@ -2919,6 +2919,45 @@ example expect(logs?.[0]?.content).toBe("hello there"); }); + it("normalizes malformed log timestamps with the transcript timestamp rules", async () => { + const root = await makeSessionCostRoot("logs-malformed-timestamp"); + const sessionsDir = path.join(root, "agents", "main", "sessions"); + await fs.mkdir(sessionsDir, { recursive: true }); + const sessionFile = path.join(sessionsDir, "sess-malformed.jsonl"); + + await fs.writeFile( + sessionFile, + [ + JSON.stringify({ + type: "message", + timestamp: "not-a-valid-date-string", + message: { role: "user", content: "bad timestamp entry" }, + }), + JSON.stringify({ + type: "message", + timestamp: "still-not-a-valid-date-string", + message: { + role: "assistant", + content: "nested timestamp entry", + timestamp: Date.parse("2026-02-21T17:46:00.000Z"), + }, + }), + JSON.stringify({ + type: "message", + timestamp: "2026-02-21T17:47:00.000Z", + message: { role: "assistant", content: "valid timestamp entry" }, + }), + ].join("\n"), + "utf-8", + ); + + const logs = await loadSessionLogs({ sessionFile }); + expect(logs).toHaveLength(3); + expect(logs?.[0]?.timestamp).toBe(0); + expect(logs?.[1]?.timestamp).toBe(Date.parse("2026-02-21T17:46:00.000Z")); + expect(logs?.[2]?.timestamp).toBe(Date.parse("2026-02-21T17:47:00.000Z")); + }); + it("buckets hourly message counts into UTC quarter-hour slots", async () => { const root = await makeSessionCostRoot("cost-quarter"); const sessionFile = path.join(root, "session.jsonl"); diff --git a/src/infra/session-cost-usage.ts b/src/infra/session-cost-usage.ts index e2f4d41d26a7..517c6a32ce30 100644 --- a/src/infra/session-cost-usage.ts +++ b/src/infra/session-cost-usage.ts @@ -2740,15 +2740,9 @@ export async function loadSessionLogs(params: { } // Get timestamp - let timestamp = 0; - if (typeof parsed.timestamp === "string") { - timestamp = new Date(parsed.timestamp).getTime(); - if (Number.isNaN(timestamp)) { - timestamp = 0; - } - } else if (typeof message.timestamp === "number") { - timestamp = message.timestamp; - } + // Keep detail logs on the usage-summary timestamp path, including nested + // fallback; direct Date parsing can leak NaN as null through Gateway JSON. + const timestamp = parseTimestamp(parsed)?.getTime() ?? 0; // Get usage for assistant messages let tokens: number | undefined;