diff --git a/CHANGELOG.md b/CHANGELOG.md index 17d22ff82..a8deecc88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Memory: allow custom OpenAI-compatible embedding endpoints for memory search (remote baseUrl/apiKey/headers). (#819 — thanks @mukhtharcm) ### Fixes +- System events: include local timestamps when events are injected into prompts. (#245 — thanks @thewilloftheshadow) - Cron: accept `jobId` aliases for cron update/run/remove params in gateway validation. (#252 — thanks @thewilloftheshadow) - Models/Google: normalize Gemini 3 model ids to preview variants before runtime selection. (#795 — thanks @thewilloftheshadow) - TUI: keep the last streamed response instead of replacing it with “(no output)”. (#747 — thanks @thewilloftheshadow) diff --git a/src/auto-reply/reply/session-updates.test.ts b/src/auto-reply/reply/session-updates.test.ts new file mode 100644 index 000000000..96c8c6681 --- /dev/null +++ b/src/auto-reply/reply/session-updates.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it, vi } from "vitest"; + +import type { ClawdbotConfig } from "../../config/config.js"; +import { + enqueueSystemEvent, + resetSystemEventsForTest, +} from "../../infra/system-events.js"; +import { prependSystemEvents } from "./session-updates.js"; + +describe("prependSystemEvents", () => { + it("adds a local timestamp to queued system events", async () => { + vi.useFakeTimers(); + const timestamp = new Date("2026-01-12T20:19:17"); + vi.setSystemTime(timestamp); + + enqueueSystemEvent("Model switched.", { sessionKey: "agent:main:main" }); + + const result = await prependSystemEvents({ + cfg: {} as ClawdbotConfig, + sessionKey: "agent:main:main", + isMainSession: false, + isNewSession: false, + prefixedBodyBase: "User: hi", + }); + + const expectedTimestamp = timestamp.toLocaleString("en-US", { + hour12: false, + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); + + expect(result).toContain( + `System: [${expectedTimestamp}] Model switched.`, + ); + + resetSystemEventsForTest(); + vi.useRealTimers(); + }); +}); diff --git a/src/auto-reply/reply/session-updates.ts b/src/auto-reply/reply/session-updates.ts index b437fb132..61bb8c124 100644 --- a/src/auto-reply/reply/session-updates.ts +++ b/src/auto-reply/reply/session-updates.ts @@ -4,7 +4,7 @@ import { buildWorkspaceSkillSnapshot } from "../../agents/skills.js"; import type { ClawdbotConfig } from "../../config/config.js"; import { type SessionEntry, saveSessionStore } from "../../config/sessions.js"; import { buildProviderSummary } from "../../infra/provider-summary.js"; -import { drainSystemEvents } from "../../infra/system-events.js"; +import { drainSystemEventEntries } from "../../infra/system-events.js"; export async function prependSystemEvents(params: { cfg: ClawdbotConfig; @@ -25,10 +25,27 @@ export async function prependSystemEvents(params: { return trimmed; }; + const formatSystemEventTimestamp = (ts: number) => + new Date(ts).toLocaleString("en-US", { + hour12: false, + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); + const systemLines: string[] = []; - const queued = drainSystemEvents(params.sessionKey); + const queued = drainSystemEventEntries(params.sessionKey); systemLines.push( - ...queued.map(compactSystemEvent).filter((v): v is string => Boolean(v)), + ...queued + .map((event) => { + const compacted = compactSystemEvent(event.text); + if (!compacted) return null; + return `[${formatSystemEventTimestamp(event.ts)}] ${compacted}`; + }) + .filter((v): v is string => Boolean(v)), ); if (params.isMainSession && params.isNewSession) { const summary = await buildProviderSummary(params.cfg); diff --git a/src/infra/system-events.ts b/src/infra/system-events.ts index 54e9e6a98..b2842142d 100644 --- a/src/infra/system-events.ts +++ b/src/infra/system-events.ts @@ -2,7 +2,7 @@ // prefixed to the next prompt. We intentionally avoid persistence to keep // events ephemeral. Events are session-scoped and require an explicit key. -type SystemEvent = { text: string; ts: number }; +export type SystemEvent = { text: string; ts: number }; const MAX_EVENTS = 20; @@ -66,11 +66,11 @@ export function enqueueSystemEvent(text: string, options: SystemEventOptions) { if (entry.queue.length > MAX_EVENTS) entry.queue.shift(); } -export function drainSystemEvents(sessionKey: string): string[] { +export function drainSystemEventEntries(sessionKey: string): SystemEvent[] { const key = requireSessionKey(sessionKey); const entry = queues.get(key); if (!entry || entry.queue.length === 0) return []; - const out = entry.queue.map((e) => e.text); + const out = entry.queue.slice(); entry.queue.length = 0; entry.lastText = null; entry.lastContextKey = null; @@ -78,6 +78,10 @@ export function drainSystemEvents(sessionKey: string): string[] { return out; } +export function drainSystemEvents(sessionKey: string): string[] { + return drainSystemEventEntries(sessionKey).map((event) => event.text); +} + export function peekSystemEvents(sessionKey: string): string[] { const key = requireSessionKey(sessionKey); return queues.get(key)?.queue.map((e) => e.text) ?? [];