From 654f9e5053f4dff8fc4eeaf7ac85584d3ce1d700 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 22 Jan 2026 03:52:03 +0000 Subject: [PATCH] fix: cap cron context messages (#1103) (thanks @mkbehr) --- CHANGELOG.md | 1 + src/agents/tools/cron-tool.test.ts | 36 ++++++++++++++++++++++++++++++ src/agents/tools/cron-tool.ts | 21 ++++++++++++----- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c24e9e37..a95a07d5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Docs: https://docs.clawd.bot - Config: avoid stack traces for invalid configs and log the config path. - Doctor: warn when gateway.mode is unset with configure/config guidance. - macOS: include Textual syntax highlighting resources in packaged app to prevent chat crashes. (#1362) +- Cron: cap reminder context history to 10 messages and honor `contextMessages`. (#1103) Thanks @mkbehr. - UI: refresh debug panel on route-driven tab changes. (#1373) Thanks @yazinsai. ## 2026.1.21 diff --git a/src/agents/tools/cron-tool.test.ts b/src/agents/tools/cron-tool.test.ts index 04acb881e..4b7cd6615 100644 --- a/src/agents/tools/cron-tool.test.ts +++ b/src/agents/tools/cron-tool.test.ts @@ -129,6 +129,42 @@ describe("cron tool", () => { expect(text).toContain("User: Remind me about the thing at 2pm"); }); + it("caps contextMessages at 10", async () => { + const messages = Array.from({ length: 12 }, (_, idx) => ({ + role: "user", + content: [{ type: "text", text: `Message ${idx + 1}` }], + })); + callGatewayMock.mockResolvedValueOnce({ messages }).mockResolvedValueOnce({ ok: true }); + + const tool = createCronTool({ agentSessionKey: "main" }); + await tool.execute("call5", { + action: "add", + contextMessages: 20, + job: { + name: "reminder", + schedule: { atMs: 123 }, + payload: { kind: "systemEvent", text: "Reminder: the thing." }, + }, + }); + + expect(callGatewayMock).toHaveBeenCalledTimes(2); + const historyCall = callGatewayMock.mock.calls[0]?.[0] as { + method?: string; + params?: { limit?: number }; + }; + expect(historyCall.method).toBe("chat.history"); + expect(historyCall.params?.limit).toBe(10); + + const cronCall = callGatewayMock.mock.calls[1]?.[0] as { + params?: { payload?: { text?: string } }; + }; + const text = cronCall.params?.payload?.text ?? ""; + expect(text).not.toMatch(/Message 1\\b/); + expect(text).not.toMatch(/Message 2\\b/); + expect(text).toContain("Message 3"); + expect(text).toContain("Message 12"); + }); + it("does not add context when contextMessages is 0 (default)", async () => { callGatewayMock.mockResolvedValueOnce({ ok: true }); diff --git a/src/agents/tools/cron-tool.ts b/src/agents/tools/cron-tool.ts index 4ef5e4924..e8995a0b9 100644 --- a/src/agents/tools/cron-tool.ts +++ b/src/agents/tools/cron-tool.ts @@ -16,6 +16,7 @@ const CRON_ACTIONS = ["status", "list", "add", "update", "remove", "run", "runs" const CRON_WAKE_MODES = ["now", "next-heartbeat"] as const; +const REMINDER_CONTEXT_MESSAGES_MAX = 10; const REMINDER_CONTEXT_PER_MESSAGE_MAX = 220; const REMINDER_CONTEXT_TOTAL_MAX = 700; const REMINDER_CONTEXT_MARKER = "\n\nRecent context:\n"; @@ -33,7 +34,9 @@ const CronToolSchema = Type.Object({ patch: Type.Optional(Type.Object({}, { additionalProperties: true })), text: Type.Optional(Type.String()), mode: optionalStringEnum(CRON_WAKE_MODES), - contextMessages: Type.Optional(Type.Number()), + contextMessages: Type.Optional( + Type.Number({ minimum: 0, maximum: REMINDER_CONTEXT_MESSAGES_MAX }), + ), }); type CronToolOptions = { @@ -88,7 +91,11 @@ async function buildReminderContextLines(params: { gatewayOpts: GatewayCallOptions; contextMessages: number; }) { - if (params.contextMessages <= 0) return []; + const maxMessages = Math.min( + REMINDER_CONTEXT_MESSAGES_MAX, + Math.max(0, Math.floor(params.contextMessages)), + ); + if (maxMessages <= 0) return []; const sessionKey = params.agentSessionKey?.trim(); if (!sessionKey) return []; const cfg = loadConfig(); @@ -97,13 +104,13 @@ async function buildReminderContextLines(params: { try { const res = (await callGatewayTool("chat.history", params.gatewayOpts, { sessionKey: resolvedKey, - limit: 12, + limit: maxMessages, })) as { messages?: unknown[] }; const messages = Array.isArray(res?.messages) ? res.messages : []; const parsed = messages .map((msg) => extractMessageText(msg as ChatMessage)) .filter((msg): msg is { role: string; text: string } => Boolean(msg)); - const recent = parsed.slice(-params.contextMessages); + const recent = parsed.slice(-maxMessages); if (recent.length === 0) return []; const lines: string[] = []; let total = 0; @@ -126,7 +133,7 @@ export function createCronTool(opts?: CronToolOptions): AnyAgentTool { label: "Cron", name: "cron", description: - "Manage Gateway cron jobs (status/list/add/update/remove/run/runs) and send wake events. Use `jobId` as the canonical identifier; `id` is accepted for compatibility. Use `contextMessages` to add previous messages as context to the job text.", + "Manage Gateway cron jobs (status/list/add/update/remove/run/runs) and send wake events. Use `jobId` as the canonical identifier; `id` is accepted for compatibility. Use `contextMessages` (0-10) to add previous messages as context to the job text.", parameters: CronToolSchema, execute: async (_toolCallId, args) => { const params = args as Record; @@ -152,7 +159,9 @@ export function createCronTool(opts?: CronToolOptions): AnyAgentTool { } const job = normalizeCronJobCreate(params.job) ?? params.job; const contextMessages = - typeof params.contextMessages === "number" ? params.contextMessages : 0; + typeof params.contextMessages === "number" && Number.isFinite(params.contextMessages) + ? params.contextMessages + : 0; if ( job && typeof job === "object" &&