From fb6136376372956ca5adde381db672479bb2ef98 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 30 May 2026 07:58:12 -0400 Subject: [PATCH] fix(auto-reply): guard date stamp formatting --- src/agents/date-time.test.ts | 16 ++++++++++++++-- src/agents/date-time.ts | 19 +++++++++++++++++++ .../reply/post-compaction-context.ts | 18 +----------------- src/auto-reply/reply/startup-context.ts | 18 +----------------- 4 files changed, 35 insertions(+), 36 deletions(-) diff --git a/src/agents/date-time.test.ts b/src/agents/date-time.test.ts index 29b5e9d17f3..1c1f6c0c461 100644 --- a/src/agents/date-time.test.ts +++ b/src/agents/date-time.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from "vitest"; -import { normalizeTimestamp } from "./date-time.js"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { formatDateStamp, normalizeTimestamp } from "./date-time.js"; describe("normalizeTimestamp", () => { it("normalizes numeric second and millisecond timestamps", () => { @@ -18,3 +18,15 @@ describe("normalizeTimestamp", () => { expect(normalizeTimestamp("999999999999999999999999")).toBeUndefined(); }); }); + +describe("formatDateStamp", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("falls back when nowMs is outside Date range", () => { + vi.spyOn(Date, "now").mockReturnValue(Date.UTC(2026, 4, 30, 12, 0, 0)); + + expect(formatDateStamp(8_640_000_000_000_001, "UTC")).toBe("2026-05-30"); + }); +}); diff --git a/src/agents/date-time.ts b/src/agents/date-time.ts index 8bf504631d9..2e2846210e4 100644 --- a/src/agents/date-time.ts +++ b/src/agents/date-time.ts @@ -1,4 +1,5 @@ import { execFileSync } from "node:child_process"; +import { asDateTimestampMs } from "../shared/number-coercion.js"; export type TimeFormatPreference = "auto" | "12" | "24"; export type ResolvedTimeFormat = "12" | "24"; @@ -40,6 +41,24 @@ export function resolveUserTimeFormat(preference?: TimeFormatPreference): Resolv return cachedTimeFormat; } +export function formatDateStamp(nowMs: number, timeZone: string): string { + const timestampMs = asDateTimestampMs(nowMs) ?? Date.now(); + const date = new Date(timestampMs); + const parts = new Intl.DateTimeFormat("en-US", { + timeZone, + year: "numeric", + month: "2-digit", + day: "2-digit", + }).formatToParts(date); + const year = parts.find((part) => part.type === "year")?.value; + const month = parts.find((part) => part.type === "month")?.value; + const day = parts.find((part) => part.type === "day")?.value; + if (year && month && day) { + return `${year}-${month}-${day}`; + } + return date.toISOString().slice(0, 10); +} + export function normalizeTimestamp( raw: unknown, ): { timestampMs: number; timestampUtc: string } | undefined { diff --git a/src/auto-reply/reply/post-compaction-context.ts b/src/auto-reply/reply/post-compaction-context.ts index 636c51baa1f..2ea2a0be72d 100644 --- a/src/auto-reply/reply/post-compaction-context.ts +++ b/src/auto-reply/reply/post-compaction-context.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import path from "node:path"; import { resolveAgentContextLimits } from "../../agents/agent-scope.js"; import { resolveCronStyleNow } from "../../agents/current-time.js"; -import { resolveUserTimezone } from "../../agents/date-time.js"; +import { formatDateStamp, resolveUserTimezone } from "../../agents/date-time.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { openRootFile } from "../../infra/boundary-file-read.js"; import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; @@ -40,22 +40,6 @@ function matchesSectionSet(sectionNames: string[], expectedSections: string[]): return counts.size === 0; } -function formatDateStamp(nowMs: number, timezone: string): string { - const parts = new Intl.DateTimeFormat("en-US", { - timeZone: timezone, - year: "numeric", - month: "2-digit", - day: "2-digit", - }).formatToParts(new Date(nowMs)); - const year = parts.find((p) => p.type === "year")?.value; - const month = parts.find((p) => p.type === "month")?.value; - const day = parts.find((p) => p.type === "day")?.value; - if (year && month && day) { - return `${year}-${month}-${day}`; - } - return new Date(nowMs).toISOString().slice(0, 10); -} - /** * Read critical sections from workspace AGENTS.md for post-compaction injection. * Returns formatted system event text, or null if no AGENTS.md or no relevant sections. diff --git a/src/auto-reply/reply/startup-context.ts b/src/auto-reply/reply/startup-context.ts index 21e774f50d5..9d0cb673300 100644 --- a/src/auto-reply/reply/startup-context.ts +++ b/src/auto-reply/reply/startup-context.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; import path from "node:path"; -import { resolveUserTimezone } from "../../agents/date-time.js"; +import { formatDateStamp, resolveUserTimezone } from "../../agents/date-time.js"; import type { OpenClawConfig } from "../../config/config.js"; import { openRootFile } from "../../infra/boundary-file-read.js"; import { uniqueStrings } from "../../shared/string-normalization.js"; @@ -64,22 +64,6 @@ function resolveStartupContextLimits(cfg?: OpenClawConfig) { }; } -function formatDateStamp(nowMs: number, timezone: string): string { - const parts = new Intl.DateTimeFormat("en-US", { - timeZone: timezone, - year: "numeric", - month: "2-digit", - day: "2-digit", - }).formatToParts(new Date(nowMs)); - const year = parts.find((part) => part.type === "year")?.value; - const month = parts.find((part) => part.type === "month")?.value; - const day = parts.find((part) => part.type === "day")?.value; - if (year && month && day) { - return `${year}-${month}-${day}`; - } - return new Date(nowMs).toISOString().slice(0, 10); -} - function shiftDateStampByCalendarDays(stamp: string, offsetDays: number): string { const [yearRaw, monthRaw, dayRaw] = stamp.split("-").map((part) => Number.parseInt(part, 10)); if (!yearRaw || !monthRaw || !dayRaw) {