Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -22178,6 +22178,12 @@ def start_session_turn(
PR #2279 next-turn drain delivers the wakeup when the active turn ends.
"""
msg = str(message or "").strip()
if _is_silent_control_message(msg):
return {
"status": "suppressed",
"reason": "silent_control_message",
"_status": 200,
}
if not msg:
return {"error": "message is required", "_status": 400}
stale_response = _agent_runtime_barrier_response(runner_local_owned=True)
Expand Down Expand Up @@ -22674,13 +22680,31 @@ def _legacy_goal_update(session_id: str, _action: str, text: str) -> dict:
return j(handler, payload)


def _is_silent_control_message(message) -> bool:
"""Return True only for the scheduler's exact suppression sentinel.

``[SILENT]`` is control-plane output, never conversation content. If a wake
relay POSTs it and 8701 restarts while the turn is pending, recovery
materializes it as a visible ``_recovered`` user message. Suppress it before
session lookup or pending-state mutation. Matching stays exact and
case-sensitive so ordinary user text is unaffected.
"""
return str(message or "").strip() == "[SILENT]"


def _handle_chat_start(handler, body, diag=None):
try:
diag.stage("validate_session_id") if diag else None
try:
require(body, "session_id")
except ValueError as e:
return bad(handler, str(e))
if _is_silent_control_message(body.get("message")):
return j(
handler,
{"status": "suppressed", "reason": "silent_control_message"},
status=200,
)
# Reject a stale local Agent runtime before materialising, claiming, or
# mutating any session state. Gateway-backed turns run in the gateway's
# process and do not depend on this WebUI process's imported checkout.
Expand Down
95 changes: 95 additions & 0 deletions tests/test_silent_control_suppression.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""Reserved ``[SILENT]`` must never become a WebUI conversation turn.

Cron agents use the exact final response ``[SILENT]`` as a delivery-suppression
sentinel. If an external wake relay accidentally POSTs that sentinel to
``/api/chat/start`` and 8701 restarts while the turn is pending, session repair
materializes it as ``{"role": "user", "_recovered": True}``. That creates a
visible user turn and can repeat on every restart.

Both server-side entry points therefore treat the exact normalized sentinel as
a successful no-op *before* session lookup or pending-state mutation.
"""
from __future__ import annotations

import io
import json

from api import routes


class _JSONHandler:
headers = {}

def __init__(self):
self.status = None
self.wfile = io.BytesIO()
self.headers_sent = {}

def send_response(self, status):
self.status = status

def send_header(self, key, value):
self.headers_sent[key] = value

def end_headers(self):
pass


def _payload(handler):
raw = handler.wfile.getvalue().decode("utf-8")
return json.loads(raw) if raw else {}


def test_http_chat_start_suppresses_silent_before_session_lookup(monkeypatch):
looked_up = []

def _unexpected_lookup(*args, **kwargs):
looked_up.append((args, kwargs))
raise AssertionError("[SILENT] must be suppressed before session lookup")

monkeypatch.setattr(routes, "_get_or_materialize_session", _unexpected_lookup)
handler = _JSONHandler()

routes._handle_chat_start(
handler,
{"session_id": "does-not-need-to-exist", "message": " [SILENT]\n"},
)

assert handler.status == 200
assert _payload(handler) == {
"status": "suppressed",
"reason": "silent_control_message",
}
assert looked_up == []


def test_server_side_start_suppresses_silent_before_session_lookup(monkeypatch):
looked_up = []

def _unexpected_lookup(*args, **kwargs):
looked_up.append((args, kwargs))
raise AssertionError("[SILENT] must be suppressed before session lookup")

monkeypatch.setattr(routes, "get_session", _unexpected_lookup)

result = routes.start_session_turn(
"does-not-need-to-exist",
"\t[SILENT] ",
source="process_wakeup",
)

assert result == {
"status": "suppressed",
"reason": "silent_control_message",
"_status": 200,
}
assert looked_up == []


def test_silent_suppression_is_exact_and_case_sensitive():
assert routes._is_silent_control_message("[SILENT]") is True
assert routes._is_silent_control_message(" [SILENT]\n") is True
assert routes._is_silent_control_message("[silent]") is False
assert routes._is_silent_control_message("prefix [SILENT]") is False
assert routes._is_silent_control_message("") is False
assert routes._is_silent_control_message(None) is False
Loading