From 00d2fbfda4d8fd783bfce491f6958dfeb0ca31fb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 26 Apr 2026 02:20:39 +0100 Subject: [PATCH] test(cron): cover delivery context edge cases --- docs/automation/cron-jobs.md | 10 +++- docs/channels/matrix.md | 5 ++ docs/cli/cron.md | 4 ++ src/agents/openclaw-tools.tts-config.test.ts | 24 ++++++++ src/agents/tools/cron-tool.test.ts | 59 +++++++++++++++++++- src/agents/tools/cron-tool.ts | 2 +- 6 files changed, 100 insertions(+), 4 deletions(-) diff --git a/docs/automation/cron-jobs.md b/docs/automation/cron-jobs.md index 92559675856..90f4e1e4716 100644 --- a/docs/automation/cron-jobs.md +++ b/docs/automation/cron-jobs.md @@ -140,7 +140,7 @@ forever. | `webhook` | POST finished event payload to a URL | | `none` | No runner fallback delivery | -Use `--announce --channel telegram --to "-1001234567890"` for channel delivery. For Telegram forum topics, use `-1001234567890:topic:123`. Slack/Discord/Mattermost targets should use explicit prefixes (`channel:`, `user:`). +Use `--announce --channel telegram --to "-1001234567890"` for channel delivery. For Telegram forum topics, use `-1001234567890:topic:123`. Slack/Discord/Mattermost targets should use explicit prefixes (`channel:`, `user:`). Matrix room IDs are case-sensitive; use the exact room ID or `room:!room:server` form from Matrix. For isolated jobs, chat delivery is shared. If a chat route is available, the agent can use the `message` tool even when the job uses `--no-deliver`. If the @@ -148,6 +148,11 @@ agent sends to the configured/current target, OpenClaw skips the fallback announce. Otherwise `announce`, `webhook`, and `none` only control what the runner does with the final reply after the agent turn. +When an agent creates an isolated reminder from an active chat, OpenClaw stores +the preserved live delivery target for the fallback announce route. Internal +session keys may be lowercase; provider delivery targets are not reconstructed +from those keys when current chat context is available. + Failure notifications follow a separate destination path: - `cron.failureDestination` sets a global default for failure notifications. @@ -418,6 +423,9 @@ openclaw doctor - Delivery mode `none` means no runner fallback send is expected. The agent can still send directly with the `message` tool when a chat route is available. - Delivery target missing/invalid (`channel`/`to`) means outbound was skipped. +- For Matrix, copied or legacy jobs with lowercased `delivery.to` room IDs can + fail because Matrix room IDs are case-sensitive. Edit the job to the exact + `!room:server` or `room:!room:server` value from Matrix. - Channel auth errors (`unauthorized`, `Forbidden`) mean delivery was blocked by credentials. - If the isolated run returns only the silent token (`NO_REPLY` / `no_reply`), OpenClaw suppresses direct outbound delivery and also suppresses the fallback diff --git a/docs/channels/matrix.md b/docs/channels/matrix.md index e4257fcbf3f..8dcb4b9cc4f 100644 --- a/docs/channels/matrix.md +++ b/docs/channels/matrix.md @@ -883,6 +883,11 @@ Matrix accepts these target forms anywhere OpenClaw asks you for a room or user - Rooms: `!room:server`, `room:!room:server`, or `matrix:room:!room:server` - Aliases: `#alias:server`, `channel:#alias:server`, or `matrix:channel:#alias:server` +Matrix room IDs are case-sensitive. Use the exact room ID casing from Matrix +when configuring explicit delivery targets, cron jobs, bindings, or allowlists. +OpenClaw keeps internal session keys canonical for storage, so those lowercase +keys are not a reliable source for Matrix delivery IDs. + Live directory lookup uses the logged-in Matrix account: - User lookups query the Matrix user directory on that homeserver. diff --git a/docs/cli/cron.md b/docs/cli/cron.md index b2acd31321f..c34939f0a4f 100644 --- a/docs/cli/cron.md +++ b/docs/cli/cron.md @@ -138,6 +138,10 @@ Delivery ownership note: - `announce` fallback-delivers the final reply only when the agent did not send directly to the resolved target. `webhook` posts the finished payload to a URL. `none` disables runner fallback delivery. +- Reminders created from an active chat preserve the live chat delivery target + for fallback announce delivery. Internal session keys may be lowercase; do not + use them as a source of truth for case-sensitive provider IDs such as Matrix + room IDs. ## Common admin commands diff --git a/src/agents/openclaw-tools.tts-config.test.ts b/src/agents/openclaw-tools.tts-config.test.ts index 3ee8ccff9c0..6ab362dd949 100644 --- a/src/agents/openclaw-tools.tts-config.test.ts +++ b/src/agents/openclaw-tools.tts-config.test.ts @@ -199,4 +199,28 @@ describe("createOpenClawTools cron context wiring", () => { }, }); }); + + it("uses agent route context when auto-threading context is unavailable", async () => { + const { createOpenClawTools } = await import("./openclaw-tools.js"); + + createOpenClawTools({ + agentSessionKey: "agent:main:matrix:channel:!abcdef1234567890:example.org", + agentChannel: "matrix", + agentAccountId: "bot-a", + agentTo: "room:!FallbackRoom:Example.Org", + agentThreadId: "$FallbackThread:Example.Org", + disableMessageTool: true, + disablePluginTools: true, + }); + + expect(mocks.createCronToolOptions).toHaveBeenCalledWith({ + agentSessionKey: "agent:main:matrix:channel:!abcdef1234567890:example.org", + currentDeliveryContext: { + channel: "matrix", + to: "room:!FallbackRoom:Example.Org", + accountId: "bot-a", + threadId: "$FallbackThread:Example.Org", + }, + }); + }); }); diff --git a/src/agents/tools/cron-tool.test.ts b/src/agents/tools/cron-tool.test.ts index 57dd09da763..95d5e1bf97a 100644 --- a/src/agents/tools/cron-tool.test.ts +++ b/src/agents/tools/cron-tool.test.ts @@ -15,6 +15,14 @@ vi.mock("../agent-scope.js", async () => { import { createCronTool } from "./cron-tool.js"; describe("cron tool", () => { + type TestDelivery = { + mode?: string; + channel?: string; + to?: string; + accountId?: string; + threadId?: string | number; + }; + function createTestCronTool( opts?: Parameters[0], ): ReturnType { @@ -64,7 +72,7 @@ describe("cron tool", () => { currentDeliveryContext?: NonNullable< Parameters[0] >["currentDeliveryContext"]; - delivery?: { mode?: string; channel?: string; to?: string } | null; + delivery?: TestDelivery | null; }) { const tool = createTestCronTool({ agentSessionKey: params.agentSessionKey, @@ -79,7 +87,7 @@ describe("cron tool", () => { }); const call = callGatewayMock.mock.calls[0]?.[0] as { - params?: { delivery?: { mode?: string; channel?: string; to?: string } }; + params?: { delivery?: TestDelivery }; }; return call?.params?.delivery; } @@ -464,6 +472,53 @@ describe("cron tool", () => { }); }); + it("keeps explicit delivery account and thread while filling target from context", async () => { + expect( + await executeAddAndReadDelivery({ + callId: "call-explicit-delivery-fields-win", + agentSessionKey: "agent:main:matrix:channel:!abcdef1234567890:example.org", + currentDeliveryContext: { + channel: "matrix", + to: "!AbCdEf1234567890:example.org", + accountId: "context-bot", + threadId: "$ContextThread:Example.Org", + }, + delivery: { + mode: "announce", + accountId: "explicit-bot", + threadId: "$ExplicitThread:Example.Org", + }, + }), + ).toEqual({ + mode: "announce", + channel: "matrix", + to: "!AbCdEf1234567890:example.org", + accountId: "explicit-bot", + threadId: "$ExplicitThread:Example.Org", + }); + }); + + it("trims current context fields without changing provider target casing", async () => { + expect( + await executeAddAndReadDelivery({ + callId: "call-trim-current-context", + agentSessionKey: "agent:main:matrix:channel:!abcdef1234567890:example.org", + currentDeliveryContext: { + channel: " Matrix ", + to: " !AbCdEf1234567890:Example.Org ", + accountId: " Bot-A ", + threadId: " $RootEvent:Example.Org ", + }, + }), + ).toEqual({ + mode: "announce", + channel: "matrix", + to: "!AbCdEf1234567890:Example.Org", + accountId: "bot-a", + threadId: "$RootEvent:Example.Org", + }); + }); + it("infers delivery from current context even when no session key is available", async () => { expect( await executeAddAndReadDelivery({ diff --git a/src/agents/tools/cron-tool.ts b/src/agents/tools/cron-tool.ts index 1bf4af5bdb7..29629f96acb 100644 --- a/src/agents/tools/cron-tool.ts +++ b/src/agents/tools/cron-tool.ts @@ -644,8 +644,8 @@ Use jobId as the canonical identifier; id is accepted for compatibility. Use con inferDeliveryFromSessionKey(opts.agentSessionKey); if (inferred) { (job as { delivery?: unknown }).delivery = { - ...delivery, ...inferred, + ...delivery, } satisfies CronDelivery; } }