System events: add local timestamps in prompt injection

Closes #245
This commit is contained in:
Shadow
2026-01-12 21:38:49 -06:00
parent 78a3d965e0
commit fcc814accd
4 changed files with 71 additions and 6 deletions

View File

@@ -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)

View File

@@ -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();
});
});

View File

@@ -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);

View File

@@ -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) ?? [];