refactor(infra): canonicalize session usage timestamps (#100687)

Co-authored-by: sheyanmin <44184140+sheyanmin@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-07-06 08:39:46 +01:00
committed by GitHub
parent c1cbc15e60
commit f6f3423ce9
3 changed files with 43 additions and 9 deletions
+1
View File
@@ -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.
+39
View File
@@ -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");
+3 -9
View File
@@ -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;