fix(cron): refuse LINE session-key recipient fallback (#81628)

LINE chat ids are case-sensitive (push requires capital C/U/R) but the
session key holds the peer id lowercased for canonical routing. When
cron-tool runs without currentDeliveryContext (delivery-recovery, queue
replay after reply-token expiry), inferDeliveryFromSessionKey was
lifting the lowercased fragment straight into delivery.to, producing a
value LINE rejects with HTTP 400 — the job retried five times silently
and the dashboard reported "delivered" while the LINE group received
nothing.

Refuse the session-key fallback for channel === "line" so the missing
target surfaces explicitly instead of scheduling an undeliverable job.
This commit is contained in:
許元豪
2026-05-14 13:57:47 +08:00
committed by Peter Steinberger
parent d5b87672f8
commit ac2e72a8e6
2 changed files with 58 additions and 0 deletions

View File

@@ -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({

View File

@@ -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) {