fix: localize system event timestamps

This commit is contained in:
Peter Steinberger
2026-01-22 04:15:39 +00:00
parent 30a8478e1a
commit 5424b4173c
4 changed files with 74 additions and 12 deletions

View File

@@ -14,7 +14,7 @@ Docs: https://docs.clawd.bot
- MSTeams: add file uploads, adaptive cards, and attachment handling improvements. (#1410) Thanks @Evizero. - MSTeams: add file uploads, adaptive cards, and attachment handling improvements. (#1410) Thanks @Evizero.
### Breaking ### Breaking
- **BREAKING:** Envelope timestamps now default to host-local time (was UTC) so agents dont have to constantly convert. - **BREAKING:** Envelope and system event timestamps now default to host-local time (was UTC) so agents dont have to constantly convert.
### Fixes ### Fixes
- Config: avoid stack traces for invalid configs and log the config path. - Config: avoid stack traces for invalid configs and log the config path.

View File

@@ -74,12 +74,13 @@ Time format: 12-hour
If only the timezone is known, we still include the section and instruct the model If only the timezone is known, we still include the section and instruct the model
to assume UTC for unknown time references. to assume UTC for unknown time references.
## System event lines (UTC) ## System event lines (local by default)
Queued system events inserted into agent context are prefixed with a UTC timestamp: Queued system events inserted into agent context are prefixed with a timestamp using the
same timezone selection as message envelopes (default: host-local).
``` ```
System: [2026-01-12T20:19:17Z] Model switched. System: [2026-01-12 12:19:17 PST] Model switched.
``` ```
### Configure user timezone + format ### Configure user timezone + format

View File

@@ -5,8 +5,10 @@ import { enqueueSystemEvent, resetSystemEventsForTest } from "../../infra/system
import { prependSystemEvents } from "./session-updates.js"; import { prependSystemEvents } from "./session-updates.js";
describe("prependSystemEvents", () => { describe("prependSystemEvents", () => {
it("adds a UTC timestamp to queued system events", async () => { it("adds a local timestamp to queued system events by default", async () => {
vi.useFakeTimers(); vi.useFakeTimers();
const originalTz = process.env.TZ;
process.env.TZ = "America/Los_Angeles";
const timestamp = new Date("2026-01-12T20:19:17Z"); const timestamp = new Date("2026-01-12T20:19:17Z");
vi.setSystemTime(timestamp); vi.setSystemTime(timestamp);
@@ -20,11 +22,10 @@ describe("prependSystemEvents", () => {
prefixedBodyBase: "User: hi", prefixedBodyBase: "User: hi",
}); });
const expectedTimestamp = "2026-01-12T20:19:17Z"; expect(result).toMatch(/System: \[2026-01-12 12:19:17 [^\]]+\] Model switched\./);
expect(result).toContain(`System: [${expectedTimestamp}] Model switched.`);
resetSystemEventsForTest(); resetSystemEventsForTest();
process.env.TZ = originalTz;
vi.useRealTimers(); vi.useRealTimers();
}); });
}); });

View File

@@ -1,5 +1,6 @@
import crypto from "node:crypto"; import crypto from "node:crypto";
import { resolveUserTimezone } from "../../agents/date-time.js";
import { buildWorkspaceSkillSnapshot } from "../../agents/skills.js"; import { buildWorkspaceSkillSnapshot } from "../../agents/skills.js";
import { ensureSkillsWatcher, getSkillsSnapshotVersion } from "../../agents/skills/refresh.js"; import { ensureSkillsWatcher, getSkillsSnapshotVersion } from "../../agents/skills/refresh.js";
import type { ClawdbotConfig } from "../../config/config.js"; import type { ClawdbotConfig } from "../../config/config.js";
@@ -27,9 +28,32 @@ export async function prependSystemEvents(params: {
return trimmed; return trimmed;
}; };
const formatSystemEventTimestamp = (ts: number) => { const resolveExplicitTimezone = (value: string): string | undefined => {
const date = new Date(ts); try {
if (Number.isNaN(date.getTime())) return "unknown-time"; new Intl.DateTimeFormat("en-US", { timeZone: value }).format(new Date());
return value;
} catch {
return undefined;
}
};
const resolveSystemEventTimezone = (cfg: ClawdbotConfig) => {
const raw = cfg.agents?.defaults?.envelopeTimezone?.trim();
if (!raw) return { mode: "local" as const };
const lowered = raw.toLowerCase();
if (lowered === "utc" || lowered === "gmt") return { mode: "utc" as const };
if (lowered === "local" || lowered === "host") return { mode: "local" as const };
if (lowered === "user") {
return {
mode: "iana" as const,
timeZone: resolveUserTimezone(cfg.agents?.defaults?.userTimezone),
};
}
const explicit = resolveExplicitTimezone(raw);
return explicit ? { mode: "iana" as const, timeZone: explicit } : { mode: "local" as const };
};
const formatUtcTimestamp = (date: Date): string => {
const yyyy = String(date.getUTCFullYear()).padStart(4, "0"); const yyyy = String(date.getUTCFullYear()).padStart(4, "0");
const mm = String(date.getUTCMonth() + 1).padStart(2, "0"); const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
const dd = String(date.getUTCDate()).padStart(2, "0"); const dd = String(date.getUTCDate()).padStart(2, "0");
@@ -39,6 +63,42 @@ export async function prependSystemEvents(params: {
return `${yyyy}-${mm}-${dd}T${hh}:${min}:${sec}Z`; return `${yyyy}-${mm}-${dd}T${hh}:${min}:${sec}Z`;
}; };
const formatZonedTimestamp = (date: Date, timeZone?: string): string | undefined => {
const parts = new Intl.DateTimeFormat("en-US", {
timeZone,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hourCycle: "h23",
timeZoneName: "short",
}).formatToParts(date);
const pick = (type: string) => parts.find((part) => part.type === type)?.value;
const yyyy = pick("year");
const mm = pick("month");
const dd = pick("day");
const hh = pick("hour");
const min = pick("minute");
const sec = pick("second");
const tz = [...parts]
.reverse()
.find((part) => part.type === "timeZoneName")
?.value?.trim();
if (!yyyy || !mm || !dd || !hh || !min || !sec) return undefined;
return `${yyyy}-${mm}-${dd} ${hh}:${min}:${sec}${tz ? ` ${tz}` : ""}`;
};
const formatSystemEventTimestamp = (ts: number, cfg: ClawdbotConfig) => {
const date = new Date(ts);
if (Number.isNaN(date.getTime())) return "unknown-time";
const zone = resolveSystemEventTimezone(cfg);
if (zone.mode === "utc") return formatUtcTimestamp(date);
if (zone.mode === "local") return formatZonedTimestamp(date) ?? "unknown-time";
return formatZonedTimestamp(date, zone.timeZone) ?? "unknown-time";
};
const systemLines: string[] = []; const systemLines: string[] = [];
const queued = drainSystemEventEntries(params.sessionKey); const queued = drainSystemEventEntries(params.sessionKey);
systemLines.push( systemLines.push(
@@ -46,7 +106,7 @@ export async function prependSystemEvents(params: {
.map((event) => { .map((event) => {
const compacted = compactSystemEvent(event.text); const compacted = compactSystemEvent(event.text);
if (!compacted) return null; if (!compacted) return null;
return `[${formatSystemEventTimestamp(event.ts)}] ${compacted}`; return `[${formatSystemEventTimestamp(event.ts, params.cfg)}] ${compacted}`;
}) })
.filter((v): v is string => Boolean(v)), .filter((v): v is string => Boolean(v)),
); );