From d57cbc5244607e8ea2e2671fec40e7528cc4263d Mon Sep 17 00:00:00 2001 From: eviaaaaa <2278596667@qq.com> Date: Fri, 20 Feb 2026 15:05:49 +0800 Subject: [PATCH] fix(heartbeat): propagate sessionKey in exec/hooks to fix async context loss This commit fixes a critical issue where asynchronous execution results and webhook wakes were failing to reach users in non-main sessions (such as Discord threads, DMs, or Slack channels). The root cause was that `requestHeartbeatNow` was being called without a `sessionKey`. This caused the heartbeat system to: 1. Default to the Main Session. 2. Coalesce unrelated wake requests into a single generic 'default' wake, losing the specific session context needed to flush the event queue. Changes: - agents: `emitExecSystemEvent` now strictly propagates the `sessionKey` to `requestHeartbeatNow`. - gateway: `dispatchWakeHook` now includes the target `sessionKey` from the wake request. - gateway: Node events (`exec.started`, `exec.finished`, `exec.denied`) now carry the originating `sessionKey` to the heartbeat system. - test: Updated `server-node-events.test.ts` to assert that `sessionKey` is correctly passed when requesting heartbeats. # Conflicts: # src/agents/bash-tools.exec-runtime.ts # src/gateway/server-node-events.ts --- src/gateway/server-node-events.test.ts | 14 +++++++++++--- src/gateway/server/hooks.ts | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/gateway/server-node-events.test.ts b/src/gateway/server-node-events.test.ts index a5a7578ddbc..d58be257ec6 100644 --- a/src/gateway/server-node-events.test.ts +++ b/src/gateway/server-node-events.test.ts @@ -154,14 +154,18 @@ describe("node exec events", () => { exitCode: 0, timedOut: false, output: "done", + sessionKey: "agent:test:main", }), }); expect(enqueueSystemEventMock).toHaveBeenCalledWith( "Exec finished (node=node-2 id=run-2, code 0)\ndone", - { sessionKey: "node-node-2", contextKey: "exec:run-2" }, + { sessionKey: "agent:test:main", contextKey: "exec:run-2" }, ); - expect(requestHeartbeatNowMock).toHaveBeenCalledWith({ reason: "exec-event" }); + expect(requestHeartbeatNowMock).toHaveBeenCalledWith({ + reason: "exec-event", + sessionKey: "agent:test:main", + }); }); it("suppresses noisy exec.finished success events with empty output", async () => { @@ -189,6 +193,7 @@ describe("node exec events", () => { exitCode: 0, timedOut: false, output: "x".repeat(600), + sessionKey: "agent:test:main", }), }); @@ -197,7 +202,10 @@ describe("node exec events", () => { expect(text.startsWith("Exec finished (node=node-2 id=run-long, code 0)\n")).toBe(true); expect(text.endsWith("…")).toBe(true); expect(text.length).toBeLessThan(280); - expect(requestHeartbeatNowMock).toHaveBeenCalledWith({ reason: "exec-event" }); + expect(requestHeartbeatNowMock).toHaveBeenCalledWith({ + reason: "exec-event", + sessionKey: "agent:test:main", + }); }); it("enqueues exec.denied events with reason", async () => { diff --git a/src/gateway/server/hooks.ts b/src/gateway/server/hooks.ts index 0ba718adcc3..844b5f3a7d3 100644 --- a/src/gateway/server/hooks.ts +++ b/src/gateway/server/hooks.ts @@ -37,7 +37,7 @@ export function createGatewayHooksRequestHandler(params: { const sessionKey = resolveMainSessionKeyFromConfig(); enqueueSystemEvent(value.text, { sessionKey }); if (value.mode === "now") { - requestHeartbeatNow({ reason: "hook:wake" }); + requestHeartbeatNow({ reason: "hook:wake", sessionKey }); } };