refactor(gateway): promote autonomous silence matcher to shared response_filters helper

Follow-up to the salvaged #71756: instead of webhook importing cron's
private _is_cron_silence_response, the loose autonomous-lane matcher now
lives in gateway/response_filters.py as is_autonomous_silence_response,
sharing LIVE_GATEWAY_SILENT_MARKERS with the interactive exact-marker
rule so the marker sets can never drift. Cron and webhook both delegate
to it. Interactive gateway behavior unchanged.
This commit is contained in:
teknium1 2026-07-26 16:22:42 -07:00 committed by Teknium
parent 55d3272286
commit 136f8dab67
4 changed files with 84 additions and 44 deletions

View file

@ -292,7 +292,9 @@ SILENT_MARKER = "[SILENT]"
# a marker is the entire response OR appears as its own first/last line — but
# NOT when a token merely appears mid-sentence in a genuine report (e.g.
# "I considered staying [SILENT] but here is the summary…" must deliver).
_CRON_SILENCE_TOKENS = frozenset({"[SILENT]", "SILENT", "NO_REPLY", "NO REPLY"})
# The actual matcher is shared with the webhook lane —
# gateway.response_filters.is_autonomous_silence_response — so the two
# autonomous lanes cannot drift apart.
def _is_cron_silence_response(text: str) -> bool:
@ -303,31 +305,13 @@ def _is_cron_silence_response(text: str) -> bool:
variants the model emits when it drops the brackets (#51438, #46917).
Whitespace-trimmed and case-insensitive. A token buried mid-sentence is
treated as real content and delivered.
Delegates to the shared autonomous-lane matcher in
:mod:`gateway.response_filters` (also used by the webhook adapter).
"""
if not isinstance(text, str):
return False
stripped = text.strip()
if not stripped:
return False
from gateway.response_filters import is_autonomous_silence_response
def _is_token(line: str) -> bool:
return " ".join(line.strip().upper().split()) in _CRON_SILENCE_TOKENS
# Whole response is exactly a token.
if _is_token(stripped):
return True
# Marker on its own first or last line (trailing/leading note on a
# separate line — e.g. "2 deals filtered\n\n[SILENT]").
lines = [ln for ln in stripped.splitlines() if ln.strip()]
if lines and (_is_token(lines[0]) or _is_token(lines[-1])):
return True
# Bracketed sentinel used as a same-line prefix — the documented cron
# pattern "[SILENT] No changes detected". Restricted to the bracketed
# form so a bare word like "Silent retry succeeded" is NOT swallowed.
upper = stripped.upper()
if upper.startswith("[SILENT]"):
return True
return False
return is_autonomous_silence_response(text)
# ---------------------------------------------------------------------------
# Persistent thread pool for parallel cron jobs.

View file

@ -63,6 +63,7 @@ from gateway.platforms.webhook_filters import (
DEFAULT_SCRIPT_TIMEOUT_SECONDS,
WebhookRouteProcessor,
)
from gateway.response_filters import is_autonomous_silence_response
logger = logging.getLogger(__name__)
@ -76,29 +77,24 @@ def _is_webhook_silence_response(content: Any) -> bool:
already replied, a routine close). Nobody is waiting on the other end, so
there is no reader for whom a "nothing happened" message is useful.
The reason this is cron's looser rule rather than the live gateway's is what
the two lanes optimise for. In an interactive chat, swallowing a real answer
because it happens to open with a marker is much worse than showing a stray
marker, so ``is_intentional_silence_response`` demands the response be
EXACTLY a marker. A webhook run has the opposite payoff: the cost of a
leaked non-story is a pointless notification on every tick, and models
reliably add a sentence explaining why they stayed quiet which under the
strict rule flips the whole thing back to "deliver". That is not a
hypothetical: it is why a Helper support lane kept messaging its owner to
The reason this is the loose autonomous rule rather than the live gateway's
is what the two lanes optimise for. In an interactive chat, swallowing a
real answer because it happens to open with a marker is much worse than
showing a stray marker, so ``is_intentional_silence_response`` demands the
response be EXACTLY a marker. A webhook run has the opposite payoff: the
cost of a leaked non-story is a pointless notification on every tick, and
models reliably add a sentence explaining why they stayed quiet which
under the strict rule flips the whole thing back to "deliver". That is not
a hypothetical: it is why a Helper support lane kept messaging its owner to
report that it had nothing to report.
So reuse cron's matcher, which already treats a marker on its own first or
last line as silence while still delivering prose that merely mentions one
mid-sentence. Sharing the function keeps the two autonomous lanes from
drifting apart, and keeps the interactive path untouched.
So use the shared autonomous-lane matcher (also used by cron), which treats
a marker on its own first or last line as silence while still delivering
prose that merely mentions one mid-sentence. Sharing the function keeps
the two autonomous lanes from drifting apart, and keeps the interactive
path untouched.
"""
if not isinstance(content, str):
return False
try:
from cron.scheduler import _is_cron_silence_response
except Exception: # pragma: no cover - cron package always ships with the gateway
return False
return _is_cron_silence_response(content)
return is_autonomous_silence_response(content)
# Sentinel returned by _resolve_request_profile when a /p/<profile>/ prefix
# names a profile this gateway does not serve (→ 404). Distinct from None

View file

@ -70,6 +70,47 @@ def is_intentional_silence_response(response: Any) -> bool:
return any(candidate in LIVE_GATEWAY_SILENT_MARKERS for candidate in _canonical_silence_candidates(stripped))
def is_autonomous_silence_response(response: Any) -> bool:
"""Loose silence matcher for autonomous lanes (cron, webhook).
Autonomous lanes instruct the agent to emit ``[SILENT]`` when a tick
produced nothing worth a human's attention, and models reliably bracket
the marker with a short note explaining why they stayed quiet. Unlike
:func:`is_intentional_silence_response` (the interactive-chat rule, which
demands the response be EXACTLY a marker), this suppresses when a marker
is the whole response, sits on its own first or last line, or the
bracketed sentinel opens the response (the documented
``[SILENT] No changes detected`` pattern). A token buried mid-sentence
in a genuine report is still delivered.
Shares :data:`LIVE_GATEWAY_SILENT_MARKERS` so the interactive and
autonomous marker sets can never drift apart.
"""
if not isinstance(response, str):
return False
stripped = response.strip()
if not stripped:
return False
def _is_token(line: str) -> bool:
return _canonical_silence_candidate(line) in LIVE_GATEWAY_SILENT_MARKERS
# Whole response is exactly a token.
if _is_token(stripped):
return True
# Marker on its own first or last line (leading/trailing note on a
# separate line — e.g. "2 deals filtered\n\n[SILENT]").
lines = [ln for ln in stripped.splitlines() if ln.strip()]
if lines and (_is_token(lines[0]) or _is_token(lines[-1])):
return True
# Bracketed sentinel used as a same-line prefix — the documented pattern
# "[SILENT] No changes detected". Restricted to the bracketed form so a
# bare word like "Silent retry succeeded" is NOT swallowed.
if stripped.upper().startswith("[SILENT]"):
return True
return False
def is_intentional_silence_agent_result(agent_result: dict | None, response: Any) -> bool:
"""Silence markers suppress delivery only for successful agent turns."""
if not isinstance(agent_result, dict):

View file

@ -1,4 +1,5 @@
from gateway.response_filters import (
is_autonomous_silence_response,
is_intentional_silence_agent_result,
is_intentional_silence_response,
)
@ -25,3 +26,21 @@ def test_blank_and_prose_mentions_are_not_silence():
def test_failed_agent_result_never_counts_as_intentional_silence():
assert is_intentional_silence_agent_result({"failed": False}, "NO_REPLY")
assert not is_intentional_silence_agent_result({"failed": True}, "NO_REPLY")
def test_autonomous_silence_accepts_marker_with_own_line_note():
"""The loose rule for cron/webhook lanes: marker + explanation suppresses."""
assert is_autonomous_silence_response("[SILENT]")
assert is_autonomous_silence_response("[SILENT]\n\nNothing new this tick.")
assert is_autonomous_silence_response("2 deals filtered\n\n[SILENT]")
assert is_autonomous_silence_response("no_reply\nduplicate inbound, already handled")
assert is_autonomous_silence_response("[SILENT] No changes detected")
def test_autonomous_silence_still_delivers_mid_sentence_mentions():
assert not is_autonomous_silence_response(
"I considered staying [SILENT] but this one moved money, so: refunded $240."
)
assert not is_autonomous_silence_response("Silent retry succeeded; all good.")
assert not is_autonomous_silence_response("")
assert not is_autonomous_silence_response(None)