Skip to content

SILENT_REPLY_TOKEN path does not set delivered:true — NO_REPLY hooks pollute main agent session #21343

Description

@JonathanWorks

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:

  1. A system event in the main agent session (Hook <name>: ok)
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    dedupe:parentPrimary canonical item in dedupe cluster

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions