Merge pull request #1103 from mkbehr/feat/cron-context-messages

feat(cron): Add parameter to control context messages
This commit is contained in:
Peter Steinberger
2026-01-22 03:52:34 +00:00
committed by GitHub
3 changed files with 81 additions and 5 deletions

View File

@@ -17,6 +17,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

View File

@@ -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,64 @@ 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("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 });
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:");
});
});

View File

@@ -16,7 +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 = 3;
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";
@@ -34,6 +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({ minimum: 0, maximum: REMINDER_CONTEXT_MESSAGES_MAX }),
),
});
type CronToolOptions = {
@@ -86,7 +89,13 @@ function extractMessageText(message: ChatMessage): { role: string; text: string
async function buildReminderContextLines(params: {
agentSessionKey?: string;
gatewayOpts: GatewayCallOptions;
contextMessages: number;
}) {
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();
@@ -95,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(-REMINDER_CONTEXT_MESSAGES);
const recent = parsed.slice(-maxMessages);
if (recent.length === 0) return [];
const lines: string[] = [];
let total = 0;
@@ -124,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.",
"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<string, unknown>;
@@ -149,6 +158,10 @@ export function createCronTool(opts?: CronToolOptions): AnyAgentTool {
throw new Error("job required");
}
const job = normalizeCronJobCreate(params.job) ?? params.job;
const contextMessages =
typeof params.contextMessages === "number" && Number.isFinite(params.contextMessages)
? params.contextMessages
: 0;
if (
job &&
typeof job === "object" &&
@@ -160,6 +173,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);