Summary
When a hook agent turn returns NO_REPLY (matching SILENT_REPLY_TOKEN), the early-exit path in src/cron/isolated-agent/run.ts returns delivered: undefined. This causes the calling code in src/gateway/server/hooks.ts to unconditionally fire enqueueSystemEvent() and requestHeartbeatNow(), injecting noise into the main agent session context window.
This is the root cause of hook-driven context pollution for any hook that classifies inbound events and discards non-actionable ones (e.g., Gmail classifiers that return NO_REPLY for newsletters).
Root cause
In src/cron/isolated-agent/run.ts around line 725:
if (synthesizedText.toUpperCase() === SILENT_REPLY_TOKEN.toUpperCase()) {
return withRunSession({ status: "ok", summary, outputText, ...telemetry });
// ^ no delivered: true
}
The announce flow path (a few lines below) correctly sets delivered: true when it successfully sends output to a channel. But the SILENT_REPLY_TOKEN early-exit skips the announce flow entirely and never sets delivered.
Impact
Every NO_REPLY hook response generates:
- A system event in the main agent session (
Hook <name>: ok)
- A heartbeat wake (if
wakeMode: "now")
For high-frequency hooks like Gmail watchers, this fills the main agent's context window with dozens of irrelevant Hook Gmail: ok entries per day.
Relationship to existing PRs
PRs #20242 and #20199 both correctly guard enqueueSystemEvent() behind !result.delivered in hooks.ts — but without the run.ts fix, SILENT_REPLY_TOKEN responses still have delivered: undefined (falsy), so the guard does not trigger and the pollution continues.
PR #20678 addresses both sides:
hooks.ts: Guard enqueueSystemEvent + requestHeartbeatNow with !result.delivered
run.ts: Return delivered: true from the SILENT_REPLY_TOKEN early-exit path
Fix
The fix is a one-line change in run.ts:
if (synthesizedText.toUpperCase() === SILENT_REPLY_TOKEN.toUpperCase()) {
- return withRunSession({ status: "ok", summary, outputText, ...telemetry });
+ return withRunSession({ status: "ok", summary, outputText, delivered: true, ...telemetry });
}
Combined with the hooks.ts guard from #20242/#20199, this ensures silent hook responses are treated as "handled" and do not leak into the main session.
References
Summary
When a hook agent turn returns
NO_REPLY(matchingSILENT_REPLY_TOKEN), the early-exit path insrc/cron/isolated-agent/run.tsreturnsdelivered: undefined. This causes the calling code insrc/gateway/server/hooks.tsto unconditionally fireenqueueSystemEvent()andrequestHeartbeatNow(), injecting noise into the main agent session context window.This is the root cause of hook-driven context pollution for any hook that classifies inbound events and discards non-actionable ones (e.g., Gmail classifiers that return
NO_REPLYfor newsletters).Root cause
In
src/cron/isolated-agent/run.tsaround line 725:The announce flow path (a few lines below) correctly sets
delivered: truewhen it successfully sends output to a channel. But theSILENT_REPLY_TOKENearly-exit skips the announce flow entirely and never setsdelivered.Impact
Every
NO_REPLYhook response generates:Hook <name>: ok)wakeMode: "now")For high-frequency hooks like Gmail watchers, this fills the main agent's context window with dozens of irrelevant
Hook Gmail: okentries per day.Relationship to existing PRs
PRs #20242 and #20199 both correctly guard
enqueueSystemEvent()behind!result.deliveredinhooks.ts— but without therun.tsfix,SILENT_REPLY_TOKENresponses still havedelivered: undefined(falsy), so the guard does not trigger and the pollution continues.PR #20678 addresses both sides:
hooks.ts: GuardenqueueSystemEvent+requestHeartbeatNowwith!result.deliveredrun.ts: Returndelivered: truefrom theSILENT_REPLY_TOKENearly-exit pathFix
The fix is a one-line change in
run.ts:if (synthesizedText.toUpperCase() === SILENT_REPLY_TOKEN.toUpperCase()) { - return withRunSession({ status: "ok", summary, outputText, ...telemetry }); + return withRunSession({ status: "ok", summary, outputText, delivered: true, ...telemetry }); }Combined with the
hooks.tsguard from #20242/#20199, this ensures silent hook responses are treated as "handled" and do not leak into the main session.References