mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 17:51:22 +00:00
fix(agents): heartbeat always targets main session — prevent routing to active subagent sessions (#61803)
Merged via squash.
Prepared head SHA: 5d79db3940
Co-authored-by: 100yenadmin <239388517+100yenadmin@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
@@ -82,7 +82,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Providers/Mistral: send `reasoning_effort` for `mistral/mistral-small-latest` (Mistral Small 4) with thinking-level mapping, and mark the catalog entry as reasoning-capable so adjustable reasoning matches Mistral’s Chat Completions API. (#62162) Thanks @neeravmakwana.
|
||||
- OpenAI TTS/Groq: send `wav` to Groq-compatible speech endpoints, honor explicit `responseFormat` overrides on OpenAI-compatible paths, and only mark voice-note output as voice-compatible when the actual format is `opus`. (#62233) Thanks @neeravmakwana.
|
||||
- BlueBubbles/network: respect explicit private-network opt-out for loopback and private `serverUrl` values across account resolution, status probes, monitor startup, and attachment downloads, while keeping public-host attachment hostname pinning intact. (#59373) Thanks @jpreagan.
|
||||
|
||||
- Agents/heartbeat: keep heartbeat runs pinned to the main session so active subagent transcripts are not overwritten by heartbeat status messages. (#61803) thanks @100yenadmin.
|
||||
## 2026.4.5
|
||||
|
||||
### Breaking
|
||||
|
||||
@@ -898,6 +898,98 @@ describe("runHeartbeatOnce", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "subagent key via forcedSessionKey (opts.sessionKey)",
|
||||
injectVia: "opts" as const,
|
||||
},
|
||||
{
|
||||
name: "subagent key via heartbeat.session config",
|
||||
injectVia: "config" as const,
|
||||
},
|
||||
])("falls back to main session when subagent key enters via $name", async ({ injectVia }) => {
|
||||
const replySpy = vi.fn();
|
||||
try {
|
||||
const tmpDir = await createCaseDir("hb-subagent-guard");
|
||||
const storePath = path.join(tmpDir, "sessions.json");
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: tmpDir,
|
||||
heartbeat: {
|
||||
every: "5m",
|
||||
target: "last",
|
||||
},
|
||||
},
|
||||
},
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: storePath },
|
||||
};
|
||||
const mainSessionKey = resolveMainSessionKey(cfg);
|
||||
const agentId = resolveAgentIdFromSessionKey(mainSessionKey);
|
||||
const subagentKey = `agent:${agentId}:subagent:task-abc`;
|
||||
|
||||
if (injectVia === "config" && cfg.agents?.defaults?.heartbeat) {
|
||||
cfg.agents.defaults.heartbeat.session = subagentKey;
|
||||
}
|
||||
|
||||
await fs.writeFile(
|
||||
storePath,
|
||||
JSON.stringify({
|
||||
[mainSessionKey]: {
|
||||
sessionId: "sid-main",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "whatsapp",
|
||||
lastTo: "120363401234567890@g.us",
|
||||
},
|
||||
[subagentKey]: {
|
||||
sessionId: "sid-subagent",
|
||||
updatedAt: Date.now() + 10_000,
|
||||
lastChannel: "whatsapp",
|
||||
lastTo: "99999@g.us",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
replySpy.mockClear();
|
||||
replySpy.mockResolvedValue([{ text: "Main session heartbeat" }]);
|
||||
const sendWhatsApp = vi
|
||||
.fn<
|
||||
(
|
||||
to: string,
|
||||
text: string,
|
||||
opts?: unknown,
|
||||
) => Promise<{ messageId: string; toJid: string }>
|
||||
>()
|
||||
.mockResolvedValue({ messageId: "m1", toJid: "jid" });
|
||||
|
||||
await runHeartbeatOnce({
|
||||
cfg,
|
||||
...(injectVia === "opts" ? { sessionKey: subagentKey } : {}),
|
||||
deps: createHeartbeatDeps(sendWhatsApp, { getReplyFromConfig: replySpy }),
|
||||
});
|
||||
|
||||
// The heartbeat must use the main session, not the subagent session.
|
||||
expect(replySpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
SessionKey: mainSessionKey,
|
||||
}),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
);
|
||||
// Must NOT use the subagent session key.
|
||||
expect(replySpy).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
SessionKey: subagentKey,
|
||||
}),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
);
|
||||
} finally {
|
||||
replySpy.mockReset();
|
||||
}
|
||||
});
|
||||
|
||||
it("suppresses duplicate heartbeat payloads within 24h", async () => {
|
||||
const tmpDir = await createCaseDir("hb-dup-suppress");
|
||||
const storePath = path.join(tmpDir, "sessions.json");
|
||||
|
||||
Reference in New Issue
Block a user