Bug description
When a hook is triggered via /hooks/agent with deliver: true, the response is delivered twice to the target channel — once through the announce flow and once through the main agent heartbeat.
Root cause
dispatchAgentHook in src/gateway/server/hooks.ts calls runCronIsolatedAgentTurn() and then unconditionally posts a summary to the main agent session via enqueueSystemEvent() and wakes the heartbeat via requestHeartbeatNow(). When the isolated run already delivered its output (via the subagent announce flow or direct outbound), this causes the main agent to wake up and generate a second response that is also delivered to the target channel.
This is the same pattern that was fixed for the cron service path in ea95e88 (#15692), but the hooks handler was not updated.
Steps to reproduce
- Configure two agents (
main and a secondary agent, e.g. familyoffice) with Telegram bindings
- Send a POST to
/hooks/agent with:
{
"message": "Hello",
"agentId": "familyoffice",
"deliver": true,
"channel": "telegram",
"to": "<group-chat-id>",
"wakeMode": "now"
}
- Observe that:
- The announce flow delivers the response to the target group chat (correct)
enqueueSystemEvent posts a summary to the main agent session
requestHeartbeatNow wakes all agents including main
- The main agent processes the system event and delivers to its last active chat (duplicate)
Expected behavior
When result.delivered is true, skip both enqueueSystemEvent and requestHeartbeatNow — the output was already delivered to the target channel.
Existing fix for reference
Commit ea95e88 applied the same fix to src/cron/service/timer.ts (executeJobCore), checking res.delivered before posting the summary. The RunCronAgentTurnResult.delivered flag and its JSDoc already document this contract:
delivered: true — Callers should skip posting a summary to the main session to avoid duplicate messages.
The hooks handler simply needs to honor this flag.
Proposed fix
--- a/src/gateway/server/hooks.ts
+++ b/src/gateway/server/hooks.ts
@@ -86,11 +86,13 @@
const summary = result.summary?.trim() || result.error?.trim() || result.status;
const prefix =
result.status === "ok" ? `Hook ${value.name}` : `Hook ${value.name} (${result.status})`;
- enqueueSystemEvent(`${prefix}: ${summary}`.trim(), {
- sessionKey: mainSessionKey,
- });
- if (value.wakeMode === "now") {
- requestHeartbeatNow({ reason: `hook:${jobId}` });
+ if (!result.delivered) {
+ enqueueSystemEvent(`${prefix}: ${summary}`.trim(), {
+ sessionKey: mainSessionKey,
+ });
+ if (value.wakeMode === "now") {
+ requestHeartbeatNow({ reason: `hook:${jobId}` });
+ }
}
Environment
- OpenClaw version: 2026.2.18 (commit d833dcd)
- Channel: Telegram (multi-agent setup with bindings)
- Trigger: external scheduler via
/hooks/agent
Bug description
When a hook is triggered via
/hooks/agentwithdeliver: true, the response is delivered twice to the target channel — once through the announce flow and once through the main agent heartbeat.Root cause
dispatchAgentHookinsrc/gateway/server/hooks.tscallsrunCronIsolatedAgentTurn()and then unconditionally posts a summary to the main agent session viaenqueueSystemEvent()and wakes the heartbeat viarequestHeartbeatNow(). When the isolated run already delivered its output (via the subagent announce flow or direct outbound), this causes the main agent to wake up and generate a second response that is also delivered to the target channel.This is the same pattern that was fixed for the cron service path in ea95e88 (#15692), but the hooks handler was not updated.
Steps to reproduce
mainand a secondary agent, e.g.familyoffice) with Telegram bindings/hooks/agentwith:{ "message": "Hello", "agentId": "familyoffice", "deliver": true, "channel": "telegram", "to": "<group-chat-id>", "wakeMode": "now" }enqueueSystemEventposts a summary to the main agent sessionrequestHeartbeatNowwakes all agents includingmainExpected behavior
When
result.deliveredistrue, skip bothenqueueSystemEventandrequestHeartbeatNow— the output was already delivered to the target channel.Existing fix for reference
Commit ea95e88 applied the same fix to
src/cron/service/timer.ts(executeJobCore), checkingres.deliveredbefore posting the summary. TheRunCronAgentTurnResult.deliveredflag and its JSDoc already document this contract:The hooks handler simply needs to honor this flag.
Proposed fix
Environment
/hooks/agent