Skip to content

Commit b6be422

Browse files
committed
fix(cron): accept threaded delivery in gateway schema
1 parent 599b1b8 commit b6be422

6 files changed

Lines changed: 115 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
1818

1919
- Providers/Cloudflare AI Gateway: strip assistant prefill turns from Anthropic Messages payloads when thinking is enabled, so Claude requests through Cloudflare AI Gateway no longer fail Anthropic conversation-ending validation. Fixes #72905; carries forward #73005. Thanks @AaronFaby and @sahilsatralkar.
2020
- Channels/sessions: prevent guarded inbound session recording from creating route-only phantom sessions while still allowing last-route updates for sessions that already exist. Carries forward #73009. Thanks @jzakirov.
21+
- Cron: accept `delivery.threadId` in Gateway cron add/update schemas so scheduled announce delivery can target Telegram forum topics and other threaded channel destinations through the documented delivery path. Fixes #73017. Thanks @coachsootz.
2122
- Plugins/runtime deps: stage bundled plugin dependencies imported by mirrored root dist chunks, so packaged memory and status commands do not miss `chokidar` or similar root-chunk dependencies after update. Fixes #72882 and #72970; carries forward #72992. Thanks @shrimpy8, @colin-chang, and @Schnup03.
2223
- Agents/runtime context: deliver hidden runtime context through prompt-local system context while keeping the transcript-only custom entry out of provider user turns, and strip stale copied runtime-context prefaces from user-facing replies. Fixes #72386; carries forward #72969. Thanks @jhsmith409.
2324
- Channels/Telegram: skip the optional webhook-info API call during polling-mode status checks and startup bot-label probes so long-polling setups avoid an unnecessary Telegram round trip. Carries forward #72990. Thanks @danielgruneberg.

docs/automation/cron-jobs.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ If an isolated run hits a live model-switch handoff, cron retries with the switc
150150
| `webhook` | POST finished event payload to a URL |
151151
| `none` | No runner fallback delivery |
152152

153-
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:<id>`, `user:<id>`). Matrix room IDs are case-sensitive; use the exact room ID or `room:!room:server` form from Matrix.
153+
Use `--announce --channel telegram --to "-1001234567890"` for channel delivery. For Telegram forum topics, use `-1001234567890:topic:123`; direct RPC/config callers may also pass `delivery.threadId` as a string or number. Slack/Discord/Mattermost targets should use explicit prefixes (`channel:<id>`, `user:<id>`). Matrix room IDs are case-sensitive; use the exact room ID or `room:!room:server` form from Matrix.
154154

155155
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 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.
156156

src/agents/tools/cron-tool.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ vi.mock("../agent-scope.js", async () => {
1515
import { createCronTool } from "./cron-tool.js";
1616

1717
describe("cron tool", () => {
18+
type SchemaLike = {
19+
anyOf?: Array<{ type?: string }>;
20+
description?: string;
21+
properties?: Record<string, SchemaLike>;
22+
};
23+
1824
type TestDelivery = {
1925
mode?: string;
2026
channel?: string;
@@ -145,6 +151,18 @@ describe("cron tool", () => {
145151
);
146152
});
147153

154+
it("advertises delivery threadId in the tool schema", () => {
155+
const tool = createTestCronTool();
156+
const parameters = tool.parameters as SchemaLike;
157+
const jobThreadId = parameters.properties?.job?.properties?.delivery?.properties?.threadId;
158+
const patchThreadId = parameters.properties?.patch?.properties?.delivery?.properties?.threadId;
159+
160+
for (const threadId of [jobThreadId, patchThreadId]) {
161+
expect(threadId?.description).toContain("Thread/topic id");
162+
expect(threadId?.anyOf?.map((entry) => entry.type)).toEqual(["string", "number"]);
163+
}
164+
});
165+
148166
it.each([
149167
[
150168
"update",

src/agents/tools/cron-tool.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,11 @@ const CronDeliverySchema = Type.Optional(
183183
mode: optionalStringEnum(CRON_DELIVERY_MODES, { description: "Delivery mode" }),
184184
channel: Type.Optional(Type.String({ description: "Delivery channel" })),
185185
to: Type.Optional(Type.String({ description: "Delivery target" })),
186+
threadId: Type.Optional(
187+
Type.Union([Type.String(), Type.Number()], {
188+
description: "Thread/topic id for channels that support threaded delivery",
189+
}),
190+
),
186191
bestEffort: Type.Optional(Type.Boolean()),
187192
accountId: Type.Optional(Type.String({ description: "Account target for delivery" })),
188193
failureDestination: Type.Optional(
@@ -576,9 +581,10 @@ PAYLOAD TYPES (payload.kind):
576581
{ "kind": "agentTurn", "message": "<prompt>", "model": "<optional>", "thinking": "<optional>", "timeoutSeconds": <optional, 0 means no timeout> }
577582
578583
DELIVERY (top-level):
579-
{ "mode": "none|announce|webhook", "channel": "<optional>", "to": "<optional>", "bestEffort": <optional-bool> }
584+
{ "mode": "none|announce|webhook", "channel": "<optional>", "to": "<optional>", "threadId": "<optional>", "bestEffort": <optional-bool> }
580585
- Default for isolated agentTurn jobs (when delivery omitted): "announce"
581586
- announce: send to chat channel (optional channel/to target)
587+
- threadId: chat thread/topic id for channels that support threaded delivery
582588
- webhook: send finished-run event as HTTP POST to delivery.to (URL required)
583589
- If the task needs to send to a specific chat/recipient, set announce delivery.channel/to; do not call messaging tools inside the run.
584590

src/gateway/protocol/schema/cron.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ export const CronFailureDestinationSchema = Type.Object(
183183

184184
const CronDeliverySharedProperties = {
185185
channel: Type.Optional(Type.Union([Type.Literal("last"), NonEmptyString])),
186+
threadId: Type.Optional(Type.Union([Type.String(), Type.Number()])),
186187
accountId: Type.Optional(NonEmptyString),
187188
bestEffort: Type.Optional(Type.Boolean()),
188189
failureDestination: Type.Optional(CronFailureDestinationSchema),

src/gateway/server-methods/cron.validation.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,93 @@ describe("cron method validation", () => {
8282
getRuntimeConfig.mockReset().mockReturnValue({} as OpenClawConfig);
8383
});
8484

85+
it("accepts threadId on announce delivery add params", async () => {
86+
getRuntimeConfig.mockReturnValue({
87+
channels: {
88+
telegram: {
89+
botToken: "telegram-token",
90+
},
91+
},
92+
plugins: {
93+
entries: {
94+
telegram: { enabled: true },
95+
},
96+
},
97+
} as OpenClawConfig);
98+
99+
const { context, respond } = await invokeCronAdd({
100+
name: "topic announce add",
101+
enabled: true,
102+
schedule: { kind: "every", everyMs: 60_000 },
103+
sessionTarget: "isolated",
104+
wakeMode: "next-heartbeat",
105+
payload: { kind: "agentTurn", message: "hello" },
106+
delivery: {
107+
mode: "announce",
108+
channel: "telegram",
109+
to: "-1001234567890",
110+
threadId: 123,
111+
},
112+
});
113+
114+
expect(context.cron.add).toHaveBeenCalledWith(
115+
expect.objectContaining({
116+
delivery: expect.objectContaining({
117+
mode: "announce",
118+
channel: "telegram",
119+
to: "-1001234567890",
120+
threadId: 123,
121+
}),
122+
}),
123+
);
124+
expect(respond).toHaveBeenCalledWith(true, { id: "cron-1" }, undefined);
125+
});
126+
127+
it("accepts threadId on announce delivery update params", async () => {
128+
getRuntimeConfig.mockReturnValue({
129+
channels: {
130+
telegram: {
131+
botToken: "telegram-token",
132+
},
133+
},
134+
plugins: {
135+
entries: {
136+
telegram: { enabled: true },
137+
},
138+
},
139+
} as OpenClawConfig);
140+
141+
const { context, respond } = await invokeCronUpdate(
142+
{
143+
id: "cron-1",
144+
patch: {
145+
delivery: {
146+
mode: "announce",
147+
channel: "telegram",
148+
to: "-1001234567890",
149+
threadId: "456",
150+
},
151+
},
152+
},
153+
createCronJob({
154+
delivery: { mode: "announce", channel: "telegram", to: "-1001234567890" },
155+
}),
156+
);
157+
158+
expect(context.cron.update).toHaveBeenCalledWith(
159+
"cron-1",
160+
expect.objectContaining({
161+
delivery: expect.objectContaining({
162+
mode: "announce",
163+
channel: "telegram",
164+
to: "-1001234567890",
165+
threadId: "456",
166+
}),
167+
}),
168+
);
169+
expect(respond).toHaveBeenCalledWith(true, { id: "cron-1" }, undefined);
170+
});
171+
85172
it("rejects ambiguous announce delivery on add when multiple channels are configured", async () => {
86173
getRuntimeConfig.mockReturnValue({
87174
session: {

0 commit comments

Comments
 (0)