fix(heartbeat): telegram accountId + cron jobId compat (#516, thanks @YuriNachos)

This commit is contained in:
Peter Steinberger
2026-01-08 20:46:58 +01:00
parent 9d42972b8a
commit 871c9e5286
3 changed files with 60 additions and 9 deletions

View File

@@ -2,6 +2,7 @@
## Unreleased ## Unreleased
- Heartbeat: resolve Telegram account IDs from config-only tokens; cron tool accepts canonical `jobId` and legacy `id` for job actions. (#516) — thanks @YuriNachos
- Discord: stop provider when gateway reconnects are exhausted and surface errors. (#514) — thanks @joshp123 - Discord: stop provider when gateway reconnects are exhausted and surface errors. (#514) — thanks @joshp123
- Auto-reply: preserve block reply ordering with timeout fallback for streaming. (#503) — thanks @joshp123 - Auto-reply: preserve block reply ordering with timeout fallback for streaming. (#503) — thanks @joshp123
- Auto-reply: avoid splitting outbound chunks inside parentheses. (#499) — thanks @philipp-spiess - Auto-reply: avoid splitting outbound chunks inside parentheses. (#499) — thanks @philipp-spiess

View File

@@ -19,9 +19,17 @@ describe("cron tool", () => {
{ action: "update", jobId: "job-1", patch: { foo: "bar" } }, { action: "update", jobId: "job-1", patch: { foo: "bar" } },
{ id: "job-1", patch: { foo: "bar" } }, { id: "job-1", patch: { foo: "bar" } },
], ],
[
"update",
{ action: "update", id: "job-2", patch: { foo: "bar" } },
{ id: "job-2", patch: { foo: "bar" } },
],
["remove", { action: "remove", jobId: "job-1" }, { id: "job-1" }], ["remove", { action: "remove", jobId: "job-1" }, { id: "job-1" }],
["remove", { action: "remove", id: "job-2" }, { id: "job-2" }],
["run", { action: "run", jobId: "job-1" }, { id: "job-1" }], ["run", { action: "run", jobId: "job-1" }, { id: "job-1" }],
["run", { action: "run", id: "job-2" }, { id: "job-2" }],
["runs", { action: "runs", jobId: "job-1" }, { id: "job-1" }], ["runs", { action: "runs", jobId: "job-1" }, { id: "job-1" }],
["runs", { action: "runs", id: "job-2" }, { id: "job-2" }],
])("%s sends id to gateway", async (action, args, expectedParams) => { ])("%s sends id to gateway", async (action, args, expectedParams) => {
const tool = createCronTool(); const tool = createCronTool();
await tool.execute("call1", args); await tool.execute("call1", args);
@@ -35,6 +43,20 @@ describe("cron tool", () => {
expect(call.params).toEqual(expectedParams); expect(call.params).toEqual(expectedParams);
}); });
it("prefers jobId over id when both are provided", async () => {
const tool = createCronTool();
await tool.execute("call1", {
action: "run",
jobId: "job-primary",
id: "job-legacy",
});
const call = callGatewayMock.mock.calls[0]?.[0] as {
params?: unknown;
};
expect(call?.params).toEqual({ id: "job-primary" });
});
it("normalizes cron.add job payloads", async () => { it("normalizes cron.add job payloads", async () => {
const tool = createCronTool(); const tool = createCronTool();
await tool.execute("call2", { await tool.execute("call2", {

View File

@@ -47,7 +47,8 @@ const CronToolSchema = Type.Union([
gatewayUrl: Type.Optional(Type.String()), gatewayUrl: Type.Optional(Type.String()),
gatewayToken: Type.Optional(Type.String()), gatewayToken: Type.Optional(Type.String()),
timeoutMs: Type.Optional(Type.Number()), timeoutMs: Type.Optional(Type.Number()),
jobId: Type.String(), jobId: Type.Optional(Type.String()),
id: Type.Optional(Type.String()),
patch: Type.Object({}, { additionalProperties: true }), patch: Type.Object({}, { additionalProperties: true }),
}), }),
Type.Object({ Type.Object({
@@ -55,21 +56,24 @@ const CronToolSchema = Type.Union([
gatewayUrl: Type.Optional(Type.String()), gatewayUrl: Type.Optional(Type.String()),
gatewayToken: Type.Optional(Type.String()), gatewayToken: Type.Optional(Type.String()),
timeoutMs: Type.Optional(Type.Number()), timeoutMs: Type.Optional(Type.Number()),
jobId: Type.String(), jobId: Type.Optional(Type.String()),
id: Type.Optional(Type.String()),
}), }),
Type.Object({ Type.Object({
action: Type.Literal("run"), action: Type.Literal("run"),
gatewayUrl: Type.Optional(Type.String()), gatewayUrl: Type.Optional(Type.String()),
gatewayToken: Type.Optional(Type.String()), gatewayToken: Type.Optional(Type.String()),
timeoutMs: Type.Optional(Type.Number()), timeoutMs: Type.Optional(Type.Number()),
jobId: Type.String(), jobId: Type.Optional(Type.String()),
id: Type.Optional(Type.String()),
}), }),
Type.Object({ Type.Object({
action: Type.Literal("runs"), action: Type.Literal("runs"),
gatewayUrl: Type.Optional(Type.String()), gatewayUrl: Type.Optional(Type.String()),
gatewayToken: Type.Optional(Type.String()), gatewayToken: Type.Optional(Type.String()),
timeoutMs: Type.Optional(Type.Number()), timeoutMs: Type.Optional(Type.Number()),
jobId: Type.String(), jobId: Type.Optional(Type.String()),
id: Type.Optional(Type.String()),
}), }),
Type.Object({ Type.Object({
action: Type.Literal("wake"), action: Type.Literal("wake"),
@@ -88,7 +92,7 @@ export function createCronTool(): AnyAgentTool {
label: "Cron", label: "Cron",
name: "cron", name: "cron",
description: description:
"Manage Gateway cron jobs (status/list/add/update/remove/run/runs) and send wake events.", "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.",
parameters: CronToolSchema, parameters: CronToolSchema,
execute: async (_toolCallId, args) => { execute: async (_toolCallId, args) => {
const params = args as Record<string, unknown>; const params = args as Record<string, unknown>;
@@ -121,7 +125,13 @@ export function createCronTool(): AnyAgentTool {
); );
} }
case "update": { case "update": {
const id = readStringParam(params, "jobId", { required: true }); const id =
readStringParam(params, "jobId") ?? readStringParam(params, "id");
if (!id) {
throw new Error(
"jobId required (id accepted for backward compatibility)",
);
}
if (!params.patch || typeof params.patch !== "object") { if (!params.patch || typeof params.patch !== "object") {
throw new Error("patch required"); throw new Error("patch required");
} }
@@ -134,19 +144,37 @@ export function createCronTool(): AnyAgentTool {
); );
} }
case "remove": { case "remove": {
const id = readStringParam(params, "jobId", { required: true }); const id =
readStringParam(params, "jobId") ?? readStringParam(params, "id");
if (!id) {
throw new Error(
"jobId required (id accepted for backward compatibility)",
);
}
return jsonResult( return jsonResult(
await callGatewayTool("cron.remove", gatewayOpts, { id }), await callGatewayTool("cron.remove", gatewayOpts, { id }),
); );
} }
case "run": { case "run": {
const id = readStringParam(params, "jobId", { required: true }); const id =
readStringParam(params, "jobId") ?? readStringParam(params, "id");
if (!id) {
throw new Error(
"jobId required (id accepted for backward compatibility)",
);
}
return jsonResult( return jsonResult(
await callGatewayTool("cron.run", gatewayOpts, { id }), await callGatewayTool("cron.run", gatewayOpts, { id }),
); );
} }
case "runs": { case "runs": {
const id = readStringParam(params, "jobId", { required: true }); const id =
readStringParam(params, "jobId") ?? readStringParam(params, "id");
if (!id) {
throw new Error(
"jobId required (id accepted for backward compatibility)",
);
}
return jsonResult( return jsonResult(
await callGatewayTool("cron.runs", gatewayOpts, { id }), await callGatewayTool("cron.runs", gatewayOpts, { id }),
); );