diff --git a/src/agents/tools/cron-tool.test.ts b/src/agents/tools/cron-tool.test.ts index d54faed1339..77e29a5646f 100644 --- a/src/agents/tools/cron-tool.test.ts +++ b/src/agents/tools/cron-tool.test.ts @@ -12,6 +12,7 @@ vi.mock("../agent-scope.js", async () => { }; }); +import { buildAgentPeerSessionKey } from "../../routing/session-key.js"; import { createCronTool } from "./cron-tool.js"; describe("cron tool", () => { @@ -823,6 +824,53 @@ describe("cron tool", () => { }); }); + it("does not surface lowercased LINE recipients when current delivery context is unavailable (#81628)", async () => { + // Reproduces openclaw/openclaw#81628. LINE chat IDs are case-sensitive — push + // requires capital C/U/R; lowercased recipients return HTTP 400. The runtime + // already lowercases LINE peer IDs when canonicalizing the session key, and + // when the delivery-recovery / post-reply-token-expiry push path is missing + // currentDeliveryContext, inferDeliveryFromSessionKey lifts the lowercased + // fragment straight into delivery.to. + const sessionKey = buildAgentPeerSessionKey({ + agentId: "main", + channel: "line", + peerKind: "group", + peerId: "Cabcdef0123456789abcdef0123456789", + }); + expect(sessionKey).toBe("agent:main:line:group:cabcdef0123456789abcdef0123456789"); + + const delivery = await executeAddAndReadDelivery({ + callId: "call-line-group-no-context-81628", + agentSessionKey: sessionKey, + // Intentionally no currentDeliveryContext — emulates the delivery-recovery + // boundary that reloads queued entries from disk after the reply token has + // expired. + }); + + expect(delivery?.to).toBeUndefined(); + }); + + it("does not surface lowercased LINE DM recipients with per-account-channel-peer scope (#81628)", async () => { + const sessionKey = buildAgentPeerSessionKey({ + agentId: "main", + channel: "line", + peerKind: "direct", + accountId: "primary", + dmScope: "per-account-channel-peer", + peerId: "Uabcdef0123456789abcdef0123456789", + }); + expect(sessionKey).toBe( + "agent:main:line:primary:direct:uabcdef0123456789abcdef0123456789", + ); + + const delivery = await executeAddAndReadDelivery({ + callId: "call-line-direct-no-context-81628", + agentSessionKey: sessionKey, + }); + + expect(delivery?.to).toBeUndefined(); + }); + it("does not let current delivery context override explicit delivery targets", async () => { expect( await executeAddAndReadDelivery({ diff --git a/src/agents/tools/cron-tool.ts b/src/agents/tools/cron-tool.ts index 726b83f1574..f7e186941a2 100644 --- a/src/agents/tools/cron-tool.ts +++ b/src/agents/tools/cron-tool.ts @@ -581,6 +581,16 @@ function inferDeliveryFromSessionKey(agentSessionKey?: string): CronDelivery | n channel = normalizeOptionalLowercaseString(parts[0]) as CronMessageChannel | undefined; } + // LINE chat ids are case-sensitive (push requires capital C/U/R) but the + // session key holds the peer id lowercased for canonical routing. Rebuilding + // `to` from the session-key fragment would yield a value LINE rejects with + // HTTP 400, so refuse the fallback for LINE and let the caller surface the + // missing target instead of silently scheduling an undeliverable job. + // openclaw/openclaw#81628 + if (channel === "line") { + return null; + } + const marker = parts[markerIndex]; const delivery: CronDelivery = { mode: "announce", to: peerId }; if (channel) {