fix: localize system event timestamps
This commit is contained in:
@@ -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 don’t have to constantly convert.
|
- **BREAKING:** Envelope and system event timestamps now default to host-local time (was UTC) so agents don’t 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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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)),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user