fix(cron): accept threaded delivery in gateway schema

This commit is contained in:
Peter Steinberger
2026-04-27 21:37:12 +01:00
parent 599b1b8462
commit b6be422306
6 changed files with 115 additions and 2 deletions

View File

@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.

View File

@@ -150,7 +150,7 @@ If an isolated run hits a live model-switch handoff, cron retries with the switc
| `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:<id>`, `user:<id>`). Matrix room IDs are case-sensitive; use the exact room ID or `room:!room:server` form from Matrix.
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.
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.

View File

@@ -15,6 +15,12 @@ vi.mock("../agent-scope.js", async () => {
import { createCronTool } from "./cron-tool.js";
describe("cron tool", () => {
type SchemaLike = {
anyOf?: Array<{ type?: string }>;
description?: string;
properties?: Record<string, SchemaLike>;
};
type TestDelivery = {
mode?: string;
channel?: string;
@@ -145,6 +151,18 @@ describe("cron tool", () => {
);
});
it("advertises delivery threadId in the tool schema", () => {
const tool = createTestCronTool();
const parameters = tool.parameters as SchemaLike;
const jobThreadId = parameters.properties?.job?.properties?.delivery?.properties?.threadId;
const patchThreadId = parameters.properties?.patch?.properties?.delivery?.properties?.threadId;
for (const threadId of [jobThreadId, patchThreadId]) {
expect(threadId?.description).toContain("Thread/topic id");
expect(threadId?.anyOf?.map((entry) => entry.type)).toEqual(["string", "number"]);
}
});
it.each([
[
"update",

View File

@@ -183,6 +183,11 @@ const CronDeliverySchema = Type.Optional(
mode: optionalStringEnum(CRON_DELIVERY_MODES, { description: "Delivery mode" }),
channel: Type.Optional(Type.String({ description: "Delivery channel" })),
to: Type.Optional(Type.String({ description: "Delivery target" })),
threadId: Type.Optional(
Type.Union([Type.String(), Type.Number()], {
description: "Thread/topic id for channels that support threaded delivery",
}),
),
bestEffort: Type.Optional(Type.Boolean()),
accountId: Type.Optional(Type.String({ description: "Account target for delivery" })),
failureDestination: Type.Optional(
@@ -576,9 +581,10 @@ PAYLOAD TYPES (payload.kind):
{ "kind": "agentTurn", "message": "<prompt>", "model": "<optional>", "thinking": "<optional>", "timeoutSeconds": <optional, 0 means no timeout> }
DELIVERY (top-level):
{ "mode": "none|announce|webhook", "channel": "<optional>", "to": "<optional>", "bestEffort": <optional-bool> }
{ "mode": "none|announce|webhook", "channel": "<optional>", "to": "<optional>", "threadId": "<optional>", "bestEffort": <optional-bool> }
- Default for isolated agentTurn jobs (when delivery omitted): "announce"
- announce: send to chat channel (optional channel/to target)
- threadId: chat thread/topic id for channels that support threaded delivery
- webhook: send finished-run event as HTTP POST to delivery.to (URL required)
- If the task needs to send to a specific chat/recipient, set announce delivery.channel/to; do not call messaging tools inside the run.

View File

@@ -183,6 +183,7 @@ export const CronFailureDestinationSchema = Type.Object(
const CronDeliverySharedProperties = {
channel: Type.Optional(Type.Union([Type.Literal("last"), NonEmptyString])),
threadId: Type.Optional(Type.Union([Type.String(), Type.Number()])),
accountId: Type.Optional(NonEmptyString),
bestEffort: Type.Optional(Type.Boolean()),
failureDestination: Type.Optional(CronFailureDestinationSchema),

View File

@@ -82,6 +82,93 @@ describe("cron method validation", () => {
getRuntimeConfig.mockReset().mockReturnValue({} as OpenClawConfig);
});
it("accepts threadId on announce delivery add params", async () => {
getRuntimeConfig.mockReturnValue({
channels: {
telegram: {
botToken: "telegram-token",
},
},
plugins: {
entries: {
telegram: { enabled: true },
},
},
} as OpenClawConfig);
const { context, respond } = await invokeCronAdd({
name: "topic announce add",
enabled: true,
schedule: { kind: "every", everyMs: 60_000 },
sessionTarget: "isolated",
wakeMode: "next-heartbeat",
payload: { kind: "agentTurn", message: "hello" },
delivery: {
mode: "announce",
channel: "telegram",
to: "-1001234567890",
threadId: 123,
},
});
expect(context.cron.add).toHaveBeenCalledWith(
expect.objectContaining({
delivery: expect.objectContaining({
mode: "announce",
channel: "telegram",
to: "-1001234567890",
threadId: 123,
}),
}),
);
expect(respond).toHaveBeenCalledWith(true, { id: "cron-1" }, undefined);
});
it("accepts threadId on announce delivery update params", async () => {
getRuntimeConfig.mockReturnValue({
channels: {
telegram: {
botToken: "telegram-token",
},
},
plugins: {
entries: {
telegram: { enabled: true },
},
},
} as OpenClawConfig);
const { context, respond } = await invokeCronUpdate(
{
id: "cron-1",
patch: {
delivery: {
mode: "announce",
channel: "telegram",
to: "-1001234567890",
threadId: "456",
},
},
},
createCronJob({
delivery: { mode: "announce", channel: "telegram", to: "-1001234567890" },
}),
);
expect(context.cron.update).toHaveBeenCalledWith(
"cron-1",
expect.objectContaining({
delivery: expect.objectContaining({
mode: "announce",
channel: "telegram",
to: "-1001234567890",
threadId: "456",
}),
}),
);
expect(respond).toHaveBeenCalledWith(true, { id: "cron-1" }, undefined);
});
it("rejects ambiguous announce delivery on add when multiple channels are configured", async () => {
getRuntimeConfig.mockReturnValue({
session: {