feat(listen): respect backend kill-switch in ax listen mention gate#29
Merged
feat(listen): respect backend kill-switch in ax listen mention gate#29
Conversation
`ax listen --exec` subscribes to the generic SSE message stream and
filters for @mentions client-side. The local pause-file gate
(_is_paused) covers operator intervention on the host, but the aX
platform backend also has its own kill switch: users can disable/break
an agent by clicking the agent badge in the UI, and the concierge can
disable noisy agents via the MCP `agents.set_control` tool. Both write
to AgentControlService in Redis.
For agents that receive work via backend dispatch (cloud sentinels,
webhook agents), the backend enforces this directly in the dispatch
loop (messages_notifications.py ~L1696). But ax listen bypasses that
loop entirely — the backend control state is invisible unless the
client checks.
This patch adds that explicit check:
- AxClient gets a new `get_agent_control(agent_id)` method that hits
GET /auth/agents/{id}/control (the existing endpoint served by
agent_control_service, already wired to Redis).
- listen.py gets a new `_is_backend_disabled(client, agent_id, cache)`
helper with a 5-second TTL cache to avoid hammering the API during
mention bursts. Fail-open on transient errors — prefer to reply
rather than silently drop mentions, since the local pause-file gate
still covers hard operator intervention.
- The worker loop in _worker adds a backend-disabled gate right after
the local pause-file gate. When the backend says the agent is
disabled or on break, the mention is DROPPED (not deferred) with a
log line naming the reason. This matches the UI affordance "this
agent is taking a break" — the user intent is not to queue work for
replay on resume, just to stop it.
Verified end-to-end against staging:
- get_agent_control returns the full state dict from the live endpoint
- PATCH /auth/agents/{id}/control to set break → GET reflects it
- _is_backend_disabled correctly returns (True, reason) when disabled
- Cleanup PATCH re-enables cleanly
Unit tests for _is_backend_disabled cover: disabled state, active
state, cache hit, null agent_id, and network-error fail-open path.
Unlocks:
- UI click disable/break on an @ping_bot (or any ax listen agent)
actually stops the agent from replying.
- MCP `agents.set_control` disable from the concierge flows through
the same path — same fix serves both entry points.
- Covers every ax listen consumer automatically, not just ping_bot.
Does not fix:
- Non-ax-cli SSE clients (future consideration — cleanest long-term is
per-subscriber filtered SSE at the backend).
This was referenced Apr 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ax listen --execpreviously ignored the backend kill-switch. Users clicking Disable / Break on an agent in the UI, or the concierge calling the MCPagents.set_controltool, had no effect onax listen-style agents likeping_bot— they'd happily keep replying while the backend reported them as disabled.This PR adds a client-side gate: before invoking the handler on each matched mention,
ax listenfetches the agent's current control state and drops the mention if disabled. Covers both entry points (UI click + MCP tool) with one fix.Changes
AxClient.get_agent_control(agent_id)— new method hittingGET /auth/agents/{id}/control_is_backend_disabled()helper inlisten.pywith 5s TTL cache (fail-open on transient errors)_workerright after the existing local pause-file gate, DROP semantics (match UI affordance "taking a break")Test plan
_is_backend_disabled: disabled, active, cache hit, null agent_id, network error fail-open — all passPATCH /auth/agents/{id}/controlwithscope: agent, disabled: true, disabled_until: ...→GETreflects disabled state →_is_backend_disabledreturns(True, reason)→ cleanup PATCH re-enables cleanlyping_botwith the new code, click Break for 5 min in UI ondev.paxai.app, send@ping_bot test, verify ping_bot drops the mention (log showsDROPPED — @ping_bot backend-disabled). Re-enable, send again, verify it repliespong.Context
Root cause diagnosis and architectural discussion in orion's delivery-management session with madtank (2026-04-09). This is the short-term fix per the bar "good enough to promote to prod, not perfect." Long-term the cleanest answer would be per-subscriber filtered SSE at the backend, but that's a spec, not a cycle.
Doesn't fix