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
This commit is contained in:
eviaaaaa 2026-02-20 15:05:49 +08:00 committed by Kaspre
parent 218f8d74b6
commit d57cbc5244
2 changed files with 12 additions and 4 deletions

View File

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

View File

@ -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 });
}
};