fix(hooks): suppress main session events for silent/delivered hook turns (#20678)

* fix(hooks): suppress main session events for silent/delivered hook turns

When a hook agent turn returns NO_REPLY (SILENT_REPLY_TOKEN), mark the
result as delivered so the hooks handler skips enqueueSystemEvent and
requestHeartbeatNow. Without this, every Gmail notification classified
as NO_REPLY still injects a system event into the main agent session,
causing context window growth proportional to email volume.

Two-part fix:
- cron/isolated-agent/run.ts: set delivered:true when synthesizedText
  matches SILENT_REPLY_TOKEN so callers know no notification is needed
- gateway/server/hooks.ts: guard enqueueSystemEvent + requestHeartbeatNow
  with !result.delivered (addresses duplicate delivery, refs #20196)

Refs: https://github.com/openclaw/openclaw/issues/20196

* Changelog: document hook silent-delivery suppression fix

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
Jonathan Works
2026-02-22 09:47:42 -08:00
committed by GitHub
parent 3c6a15ce98
commit 8c089bbe32
3 changed files with 9 additions and 6 deletions

View File

@@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Hooks/Cron: suppress duplicate main-session events for delivered hook turns and mark `SILENT_REPLY_TOKEN` (`NO_REPLY`) early exits as delivered to prevent hook context pollution. (#20678) Thanks @JonathanWorks.
- Providers/OpenRouter: inject `cache_control` on system prompts for OpenRouter Anthropic models to improve prompt-cache reuse. (#17473) Thanks @rrenamed.
- Providers/OpenRouter: allow pass-through OpenRouter and Opencode model IDs in live model filtering so custom routed model IDs are treated as modern refs. (#14312) Thanks @Joly0.
- Providers/OpenRouter: default reasoning to enabled when the selected model advertises `reasoning: true` and no session/directive override is set. (#22513) Thanks @zwffff.

View File

@@ -753,7 +753,7 @@ export async function runCronIsolatedAgentTurn(params: {
return withRunSession({ status: "ok", summary, outputText, ...telemetry });
}
if (synthesizedText.toUpperCase() === SILENT_REPLY_TOKEN.toUpperCase()) {
return withRunSession({ status: "ok", summary, outputText, ...telemetry });
return withRunSession({ status: "ok", summary, outputText, delivered: true, ...telemetry });
}
try {
const didAnnounce = await runSubagentAnnounceFlow({

View File

@@ -86,11 +86,13 @@ export function createGatewayHooksRequestHandler(params: {
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}` });
}
}
} catch (err) {
logHooks.warn(`hook agent failed: ${String(err)}`);