Skip to content

Commit 28eb4cf

Browse files
committed
fix(codex): ignore invalid history timestamps
1 parent a9cbec9 commit 28eb4cf

5 files changed

Lines changed: 50 additions & 2 deletions

File tree

extensions/codex/src/node-cli-sessions.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,35 @@ describe("codex cli node sessions", () => {
7575
]);
7676
});
7777

78+
it("ignores Date-invalid Codex history timestamps", async () => {
79+
const sessionId = "019e2007-1f7e-7eb1-a42b-8c01f4b9b5cf";
80+
await fs.writeFile(
81+
path.join(tempDir, "history.jsonl"),
82+
JSON.stringify({ session_id: sessionId, ts: 8_700_000_000_000, text: "bad timestamp" }),
83+
);
84+
85+
const command = createCodexCliSessionNodeHostCommands().find(
86+
(entry) => entry.command === CODEX_CLI_SESSIONS_LIST_COMMAND,
87+
);
88+
const raw = await command?.handle(JSON.stringify({ filter: "bad timestamp", limit: 5 }));
89+
const parsed = JSON.parse(raw ?? "{}") as {
90+
sessions?: Array<{
91+
sessionId?: string;
92+
updatedAt?: string;
93+
lastMessage?: string;
94+
messageCount?: number;
95+
}>;
96+
};
97+
98+
expect(parsed.sessions).toEqual([
99+
{
100+
sessionId,
101+
lastMessage: "bad timestamp",
102+
messageCount: 1,
103+
},
104+
]);
105+
});
106+
78107
it("lists sessions from Codex session files when history is absent", async () => {
79108
const sessionId = "019e23d1-f33d-78e3-959e-0f56f30a5249";
80109
const sessionDir = path.join(tempDir, "sessions", "2026", "05", "14");

extensions/codex/src/node-cli-sessions.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
33
import os from "node:os";
44
import path from "node:path";
55
import process from "node:process";
6+
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
67
import type {
78
OpenClawPluginNodeHostCommand,
89
OpenClawPluginNodeInvokePolicy,
@@ -371,8 +372,8 @@ async function readHistorySessions(
371372
if (typeof parsed.text === "string" && parsed.text.trim()) {
372373
entry.lastMessage = truncateText(parsed.text.trim(), 140);
373374
}
374-
if (typeof parsed.ts === "number" && Number.isFinite(parsed.ts)) {
375-
entry.updatedAt = new Date(parsed.ts * 1000).toISOString();
375+
if (typeof parsed.ts === "number") {
376+
entry.updatedAt = timestampMsToIsoString(parsed.ts * 1000) ?? entry.updatedAt;
376377
}
377378
summaries.set(sessionId, entry);
378379
}

src/plugin-sdk/number-runtime.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export {
2222
parseStrictPositiveInteger,
2323
positiveSecondsToSafeMilliseconds,
2424
nonNegativeSecondsToSafeMilliseconds,
25+
timestampMsToIsoString,
2526
resolveExpiresAtMsFromDurationSeconds,
2627
resolveExpiresAtMsFromDurationOrEpoch,
2728
resolveExpiresAtMsFromEpochSeconds,

src/shared/number-coercion.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
parseStrictNonNegativeInteger,
2525
parseStrictPositiveInteger,
2626
resolveTimerTimeoutMs,
27+
timestampMsToIsoString,
2728
} from "./number-coercion.js";
2829

2930
describe("number-coercion", () => {
@@ -117,6 +118,14 @@ describe("number-coercion", () => {
117118
expect(nonNegativeSecondsToSafeMilliseconds("-1")).toBeUndefined();
118119
});
119120

121+
test("timestamp ISO helper rejects Date-invalid timestamps", () => {
122+
expect(timestampMsToIsoString(0)).toBe("1970-01-01T00:00:00.000Z");
123+
expect(timestampMsToIsoString(8_640_000_000_000_000)).toBe("+275760-09-13T00:00:00.000Z");
124+
expect(timestampMsToIsoString(8_640_000_000_000_001)).toBeUndefined();
125+
expect(timestampMsToIsoString(Number.POSITIVE_INFINITY)).toBeUndefined();
126+
expect(timestampMsToIsoString("0")).toBeUndefined();
127+
});
128+
120129
test("expiry helpers resolve safe absolute timestamps", () => {
121130
expect(
122131
resolveExpiresAtMsFromDurationSeconds("3600", {

src/shared/number-coercion.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,14 @@ export const MAX_TIMER_TIMEOUT_MS = 2_147_000_000;
9797
export const MAX_TIMER_TIMEOUT_SECONDS = Math.floor(MAX_TIMER_TIMEOUT_MS / 1000);
9898
export const MAX_DATE_TIMESTAMP_MS = 8_640_000_000_000_000;
9999

100+
export function timestampMsToIsoString(value: unknown): string | undefined {
101+
const timestampMs = asFiniteNumberInRange(value, {
102+
min: -MAX_DATE_TIMESTAMP_MS,
103+
max: MAX_DATE_TIMESTAMP_MS,
104+
});
105+
return timestampMs === undefined ? undefined : new Date(timestampMs).toISOString();
106+
}
107+
100108
export function clampTimerTimeoutMs(valueMs: unknown, minMs = 1): number | undefined {
101109
const value = asFiniteNumber(valueMs);
102110
if (value === undefined) {

0 commit comments

Comments
 (0)