fix cron scheduling and reminder delivery regressions (#9733)

* fix(cron): prevent timer from allowing process exit (fixes #9694)

The cron timer was using .unref(), which caused the Node.js event
loop to exit or sleep if no other handles were active. This prevented
cron jobs from firing in some environments.

* fix(cron): infer delivery target for isolated jobs (fixes #9683)

When creating isolated agentTurn jobs (e.g. reminders) without explicit
delivery options, the job would default to 'announce' but fail to
resolve the target conversation. Now, we infer the channel and
recipient from the agent's current session key.

* fix(cron): enhance delivery inference for threaded sessions and null inputs (#9733)

Improves the delivery inference logic in the cron tool to correctly handle threaded session keys and cases where delivery is explicitly set to null. This ensures that the appropriate delivery mode and target are inferred based on the agent's session key, enhancing the reliability of job execution.

* fix: preserve telegram topic delivery inference (#9733) (thanks @tyler6204)

* fix: simplify cron delivery merge spread (#9733) (thanks @tyler6204)
This commit is contained in:
Tyler Yust
2026-02-05 13:08:41 -08:00
committed by GitHub
parent f32eeae3bc
commit 821520a057
4 changed files with 191 additions and 1 deletions

View File

@@ -233,4 +233,97 @@ describe("cron tool", () => {
expect(call.method).toBe("cron.add");
expect(call.params?.agentId).toBeNull();
});
it("infers delivery from threaded session keys", async () => {
callGatewayMock.mockResolvedValueOnce({ ok: true });
const tool = createCronTool({
agentSessionKey: "agent:main:slack:channel:general:thread:1699999999.0001",
});
await tool.execute("call-thread", {
action: "add",
job: {
name: "reminder",
schedule: { at: new Date(123).toISOString() },
payload: { kind: "agentTurn", message: "hello" },
},
});
const call = callGatewayMock.mock.calls[0]?.[0] as {
params?: { delivery?: { mode?: string; channel?: string; to?: string } };
};
expect(call?.params?.delivery).toEqual({
mode: "announce",
channel: "slack",
to: "general",
});
});
it("preserves telegram forum topics when inferring delivery", async () => {
callGatewayMock.mockResolvedValueOnce({ ok: true });
const tool = createCronTool({
agentSessionKey: "agent:main:telegram:group:-1001234567890:topic:99",
});
await tool.execute("call-telegram-topic", {
action: "add",
job: {
name: "reminder",
schedule: { at: new Date(123).toISOString() },
payload: { kind: "agentTurn", message: "hello" },
},
});
const call = callGatewayMock.mock.calls[0]?.[0] as {
params?: { delivery?: { mode?: string; channel?: string; to?: string } };
};
expect(call?.params?.delivery).toEqual({
mode: "announce",
channel: "telegram",
to: "-1001234567890:topic:99",
});
});
it("infers delivery when delivery is null", async () => {
callGatewayMock.mockResolvedValueOnce({ ok: true });
const tool = createCronTool({ agentSessionKey: "agent:main:dm:alice" });
await tool.execute("call-null-delivery", {
action: "add",
job: {
name: "reminder",
schedule: { at: new Date(123).toISOString() },
payload: { kind: "agentTurn", message: "hello" },
delivery: null,
},
});
const call = callGatewayMock.mock.calls[0]?.[0] as {
params?: { delivery?: { mode?: string; channel?: string; to?: string } };
};
expect(call?.params?.delivery).toEqual({
mode: "announce",
to: "alice",
});
});
it("does not infer delivery when mode is none", async () => {
callGatewayMock.mockResolvedValueOnce({ ok: true });
const tool = createCronTool({ agentSessionKey: "agent:main:discord:dm:buddy" });
await tool.execute("call-none", {
action: "add",
job: {
name: "reminder",
schedule: { at: new Date(123).toISOString() },
payload: { kind: "agentTurn", message: "hello" },
delivery: { mode: "none" },
},
});
const call = callGatewayMock.mock.calls[0]?.[0] as {
params?: { delivery?: { mode?: string; channel?: string; to?: string } };
};
expect(call?.params?.delivery).toEqual({ mode: "none" });
});
});