fix: harden routing/session isolation for followups and heartbeat

This commit is contained in:
Peter Steinberger
2026-02-24 23:13:51 +00:00
parent 7655c0cb3a
commit ccbeb332e0
15 changed files with 209 additions and 15 deletions

View File

@@ -591,6 +591,8 @@ describe("runHeartbeatOnce", () => {
SessionKey: sessionKey,
From: "+1555",
To: "+1555",
OriginatingChannel: "whatsapp",
OriginatingTo: "+1555",
Provider: "heartbeat",
}),
expect.objectContaining({ isHeartbeat: true, suppressToolErrorWarnings: false }),

View File

@@ -663,6 +663,10 @@ export async function runHeartbeatOnce(opts: {
Body: appendCronStyleCurrentTimeLine(prompt, cfg, startedAt),
From: sender,
To: sender,
OriginatingChannel: delivery.channel !== "none" ? delivery.channel : undefined,
OriginatingTo: delivery.to,
AccountId: delivery.accountId,
MessageThreadId: delivery.threadId,
Provider: hasExecCompletion ? "exec-event" : hasCronEvents ? "cron-event" : "heartbeat",
SessionKey: sessionKey,
};

View File

@@ -1,6 +1,10 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import { resolveOutboundTarget, resolveSessionDeliveryTarget } from "./targets.js";
import {
resolveHeartbeatDeliveryTarget,
resolveOutboundTarget,
resolveSessionDeliveryTarget,
} from "./targets.js";
import {
installResolveOutboundTargetPluginRegistryHooks,
runResolveOutboundTargetCoreTests,
@@ -175,6 +179,22 @@ describe("resolveSessionDeliveryTarget", () => {
expect(resolved.threadId).toBe(999);
});
it("does not inherit lastThreadId in heartbeat mode", () => {
const resolved = resolveSessionDeliveryTarget({
entry: {
sessionId: "sess-heartbeat-thread",
updatedAt: 1,
lastChannel: "slack",
lastTo: "user:U123",
lastThreadId: "1739142736.000100",
},
requestedChannel: "last",
mode: "heartbeat",
});
expect(resolved.threadId).toBeUndefined();
});
it("falls back to a provided channel when requested is unsupported", () => {
const resolved = resolveSessionDeliveryTarget({
entry: {
@@ -280,4 +300,25 @@ describe("resolveSessionDeliveryTarget", () => {
expect(resolved.threadId).toBe(42);
expect(resolved.to).toBe("63448508");
});
it("does not return inherited threadId from resolveHeartbeatDeliveryTarget", () => {
const cfg: OpenClawConfig = {};
const resolved = resolveHeartbeatDeliveryTarget({
cfg,
entry: {
sessionId: "sess-heartbeat-outbound",
updatedAt: 1,
lastChannel: "slack",
lastTo: "user:U123",
lastThreadId: "1739142736.000100",
},
heartbeat: {
target: "last",
},
});
expect(resolved.channel).toBe("slack");
expect(resolved.to).toBe("user:U123");
expect(resolved.threadId).toBeUndefined();
});
});

View File

@@ -115,9 +115,10 @@ export function resolveSessionDeliveryTarget(params: {
}
}
const accountId = channel && channel === lastChannel ? lastAccountId : undefined;
const threadId = channel && channel === lastChannel ? lastThreadId : undefined;
const mode = params.mode ?? (explicitTo ? "explicit" : "implicit");
const accountId = channel && channel === lastChannel ? lastAccountId : undefined;
const threadId =
mode !== "heartbeat" && channel && channel === lastChannel ? lastThreadId : undefined;
const resolvedThreadId = explicitThreadId ?? threadId;
return {