From 4642fae1937c072636d7e91c4ceeed397ae50600 Mon Sep 17 00:00:00 2001 From: Michael Behr Date: Sat, 17 Jan 2026 09:06:54 -0500 Subject: [PATCH 1/3] feat(cron): add contextMessages param to control reminder context --- src/agents/tools/cron-tool.test.ts | 27 ++++++++++++++++++++++++++- src/agents/tools/cron-tool.ts | 9 +++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/agents/tools/cron-tool.test.ts b/src/agents/tools/cron-tool.test.ts index 6fa452e08..04acb881e 100644 --- a/src/agents/tools/cron-tool.test.ts +++ b/src/agents/tools/cron-tool.test.ts @@ -85,7 +85,7 @@ describe("cron tool", () => { }); }); - it("adds recent context for systemEvent reminders when session key is available", async () => { + it("adds recent context for systemEvent reminders when contextMessages > 0", async () => { callGatewayMock .mockResolvedValueOnce({ messages: [ @@ -102,6 +102,7 @@ describe("cron tool", () => { const tool = createCronTool({ agentSessionKey: "main" }); await tool.execute("call3", { action: "add", + contextMessages: 3, job: { name: "reminder", schedule: { atMs: 123 }, @@ -127,4 +128,28 @@ describe("cron tool", () => { expect(text).toContain("Assistant: We agreed to review on Tuesday."); expect(text).toContain("User: Remind me about the thing at 2pm"); }); + + it("does not add context when contextMessages is 0 (default)", async () => { + callGatewayMock.mockResolvedValueOnce({ ok: true }); + + const tool = createCronTool({ agentSessionKey: "main" }); + await tool.execute("call4", { + action: "add", + job: { + name: "reminder", + schedule: { atMs: 123 }, + payload: { text: "Reminder: the thing." }, + }, + }); + + // Should only call cron.add, not chat.history + expect(callGatewayMock).toHaveBeenCalledTimes(1); + const cronCall = callGatewayMock.mock.calls[0]?.[0] as { + method?: string; + params?: { payload?: { text?: string } }; + }; + expect(cronCall.method).toBe("cron.add"); + const text = cronCall.params?.payload?.text ?? ""; + expect(text).not.toContain("Recent context:"); + }); }); diff --git a/src/agents/tools/cron-tool.ts b/src/agents/tools/cron-tool.ts index 75af0becd..a84a2b774 100644 --- a/src/agents/tools/cron-tool.ts +++ b/src/agents/tools/cron-tool.ts @@ -16,7 +16,6 @@ const CRON_ACTIONS = ["status", "list", "add", "update", "remove", "run", "runs" const CRON_WAKE_MODES = ["now", "next-heartbeat"] as const; -const REMINDER_CONTEXT_MESSAGES = 3; const REMINDER_CONTEXT_PER_MESSAGE_MAX = 220; const REMINDER_CONTEXT_TOTAL_MAX = 700; const REMINDER_CONTEXT_MARKER = "\n\nRecent context:\n"; @@ -34,6 +33,7 @@ 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()), }); type CronToolOptions = { @@ -86,7 +86,9 @@ function extractMessageText(message: ChatMessage): { role: string; text: string async function buildReminderContextLines(params: { agentSessionKey?: string; gatewayOpts: GatewayCallOptions; + contextMessages: number; }) { + if (params.contextMessages <= 0) return []; const sessionKey = params.agentSessionKey?.trim(); if (!sessionKey) return []; const cfg = loadConfig(); @@ -101,7 +103,7 @@ async function buildReminderContextLines(params: { const parsed = messages .map((msg) => extractMessageText(msg as ChatMessage)) .filter((msg): msg is { role: string; text: string } => Boolean(msg)); - const recent = parsed.slice(-REMINDER_CONTEXT_MESSAGES); + const recent = parsed.slice(-params.contextMessages); if (recent.length === 0) return []; const lines: string[] = []; let total = 0; @@ -149,6 +151,8 @@ export function createCronTool(opts?: CronToolOptions): AnyAgentTool { throw new Error("job required"); } const job = normalizeCronJobCreate(params.job) ?? params.job; + const contextMessages = + typeof params.contextMessages === "number" ? params.contextMessages : 0; if ( job && typeof job === "object" && @@ -160,6 +164,7 @@ export function createCronTool(opts?: CronToolOptions): AnyAgentTool { const contextLines = await buildReminderContextLines({ agentSessionKey: opts?.agentSessionKey, gatewayOpts, + contextMessages, }); if (contextLines.length > 0) { const baseText = stripExistingContext(payload.text); From ffbf75d74087a0a1d1b1db08aadc4e77065bd4fc Mon Sep 17 00:00:00 2001 From: Michael Behr Date: Sat, 17 Jan 2026 09:14:33 -0500 Subject: [PATCH 2/3] update description --- src/agents/tools/cron-tool.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agents/tools/cron-tool.ts b/src/agents/tools/cron-tool.ts index a84a2b774..4ef5e4924 100644 --- a/src/agents/tools/cron-tool.ts +++ b/src/agents/tools/cron-tool.ts @@ -126,7 +126,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.", + "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.", parameters: CronToolSchema, execute: async (_toolCallId, args) => { const params = args as Record; From 654f9e5053f4dff8fc4eeaf7ac85584d3ce1d700 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 22 Jan 2026 03:52:03 +0000 Subject: [PATCH 3/3] 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" &&