diff --git a/packages/core/src/__tests__/foreign-session-timeclip.test.ts b/packages/core/src/__tests__/foreign-session-timeclip.test.ts new file mode 100644 index 0000000000..921bd46a1c --- /dev/null +++ b/packages/core/src/__tests__/foreign-session-timeclip.test.ts @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { + createDigestAccumulator, + finishDigest, + normalizeCodexThreadRow, + renderForeignSessionDigestForPrompt, +} from '../foreign-session.js'; + +describe('foreign session timestamp bounds', () => { + it('rejects timestamps outside the ECMAScript TimeClip range', () => { + assert.equal( + normalizeCodexThreadRow({ id: 'future', rollout_path: '/p', updated_at_ms: 1e16 }) + ?.updatedAtMs, + 0, + ); + assert.equal( + normalizeCodexThreadRow({ id: 'past', rollout_path: '/p', updated_at_ms: -1e16 }) + ?.updatedAtMs, + 0, + ); + }); + + it('renders out-of-range finite timestamps as unknown instead of throwing', () => { + const digest = finishDigest(createDigestAccumulator(), { + source: 'codex', + id: 'safeid', + title: 'Future session', + cwd: '/repo', + updatedAtMs: 1e16, + }); + + assert.match(renderForeignSessionDigestForPrompt(digest), /^updated_at=unknown$/m); + }); +}); diff --git a/packages/core/src/foreign-session.ts b/packages/core/src/foreign-session.ts index 8975847ae7..06d226ff05 100644 --- a/packages/core/src/foreign-session.ts +++ b/packages/core/src/foreign-session.ts @@ -400,6 +400,12 @@ export const CODEX_SUPPORTED_THREAD_SOURCES = ['cli', 'vscode', 'atlas', 'chatgp * `updated_at_ms` in ms on newer ones; this lets one path normalize both. */ export const FOREIGN_SESSION_MIN_EPOCH_MS = 1_577_836_800_000; +/** Largest absolute timestamp accepted by ECMAScript Date's TimeClip operation. */ +const FOREIGN_SESSION_MAX_EPOCH_MS = 8_640_000_000_000_000; + +function isValidForeignEpochMs(value: number): boolean { + return Number.isFinite(value) && Math.abs(value) <= FOREIGN_SESSION_MAX_EPOCH_MS; +} function normalizeEpochMs(value: unknown): number | undefined { const n = @@ -409,7 +415,8 @@ function normalizeEpochMs(value: unknown): number | undefined { ? Number(value) : undefined; if (n === undefined) return undefined; - return n > 0 && n < FOREIGN_SESSION_MIN_EPOCH_MS ? n * 1000 : n; + const normalized = n > 0 && n < FOREIGN_SESSION_MIN_EPOCH_MS ? n * 1000 : n; + return isValidForeignEpochMs(normalized) ? normalized : undefined; } /** @@ -630,9 +637,9 @@ export function renderForeignSessionDigestForPrompt(digest: ForeignSessionDigest `title=${safe(digest.title)}`, `cwd=${safe(digest.cwd)}`, ...(digest.gitBranch ? [`git_branch=${safe(digest.gitBranch)}`] : []), - // A non-finite timestamp (corrupt store row) would make new Date().toISOString() - // throw RangeError, so guard it rather than let the render crash. - `updated_at=${Number.isFinite(digest.updatedAtMs) ? new Date(digest.updatedAtMs).toISOString() : 'unknown'}`, + // Invalid timestamps (corrupt store rows) make toISOString() throw RangeError, + // including finite values outside Date's TimeClip range. + `updated_at=${isValidForeignEpochMs(digest.updatedAtMs) ? new Date(digest.updatedAtMs).toISOString() : 'unknown'}`, '', '## User messages (chronological)', ...digest.userMessages.map((m, i) => `${i + 1}. ${safe(m)}`),