chat / agent / coder turns, and โ optionally โ triggers the agent automatically via a rules engine with three operating modes.
sse(classic HTTP+SSE) โ persistent listener on the GET/ssestream.http(Streamable HTTP, MCP spec 2025-03-26) โ opt-in listener via GET on the configured endpoint.stdioโ any JSON-RPC message withoutidemitted by the child process is treated as a notification.
confirm or auto rules in ~/.chatcli/mcp/triggers.json).Architecture at a glance
cli/mcp/channels.go, cli/mcp/triggers/triggers.go, cli/channel_triggers.go, cli/agent_system_prompt.go). The split is deliberate: ChannelManager is process-local and independent of the CLI; the trigger engine is a separate package without a reverse dependency; the CLI plugs in via OnMessage.
How a message reaches the agent
Server emits notification
id (a notification in the spec). Can be over the SSE stream, the GET of Streamable HTTP, or a stdout line in stdio.Transport extracts and routes
ChannelManager.ProcessSSENotification. The method becomes the channel: notifications/ci-pipeline โ channel ci-pipeline; channel/message with params.channel โ channel params.channel.Subscription filter
channels: [...] and the channel is not listed, the message is dropped (debug log). Empty list means โaccept everythingโ.Persist + ring + unread
seq, lands in the in-memory ring (FIFO, default 200), is append-only written to ~/.chatcli/mcp/channels.jsonl, and the unread counter increments.Fan-out to subscribers
OnMessage callback receives a copy. The CLI registers two: the trigger engine (evaluates rules) and the banner renderer (prepares the UI).System prompt โ next turn
Reactive trigger (optional)
Action that lands in one of three queues: notify (discreet banner), confirm (yes/no prompt), auto (runs the agent at the next prompt tick).Persistence
Guarantees
- Durable across sessions: messages received while ChatCLI was closed are visible on the next boot (up to the load limit).
- Append-only: we never rewrite the file. A process crash mid-write leaves at worst one truncated line โ the loader skips line-by-line, so a single corrupt line never poisons the rest.
- Automatic rotation: when the file reaches 10 MiB, it is renamed to
.1and a fresh file is opened. We keep one historical file โ channels are telemetry, not forensic audit log. - Best-effort writes: if the write fails (disk full, permissions), ChatCLI logs a warning once, marks persistence as disabled only for that session, and keeps serving the in-memory ring.
Boot โ replay
On startup, ChatCLI reads the last 200 lines combined fromchannels.jsonl.1 + channels.jsonl in chronological order. Replayed messages:
- enter the ring;
- do not count as unread (they were already seen);
- preserve their original
seq(the internal counter is advanced past the highest observed).
How to configure
Persistence is on by default whenHOME is resolvable. In containerized environments without HOME (rare), ChannelManager falls back to in-memory-only and logs an info on startup. There is no flag to โdisable persistenceโ โ to wipe the state, delete the file manually (rm ~/.chatcli/mcp/channels.jsonl*).
Per-server filter โ channels
The optional channels field on the MCP server config is an allow-list that decides which channels from that server are accepted into the ring.
- Empty/omitted โ accepts every channel the server emits.
- Explicit
"*"โ equivalent to empty. - List โ only literally listed channels pass; others are dropped (debug log).
- Whitespace in entries is trimmed, so
" alerts "matches"alerts".
"channels": ["alerts"] does not match "alerts/critical". Use the exact channel name the server emits. Globs (alerts/*) only exist in trigger rules, which run after this filter.Auto-injection into the system prompt
On every turn (chat, agent, coder), the 5 most recent ring messages are added to the system prompt as an extra block:
Important guarantees
- No cache hint: this block is volatile and intentionally lives outside the Anthropic cached prefix. Putting something that changes every turn inside the cache would trash the entire KV cache โ worse than not caching at all.
- Same content in chat / agent / coder: the system-prompt builders for all three modes were unified to include the block.
- Empty ring โ block omitted: zero overhead when you have never received anything.
Why only 5?
Calibrated for balance: enough to give recent context (CI running + hottest alert) without using too many tokens when the user has multiple chatty servers. If you need more history for a specific turn, use/channel inject (which injects the most recent 10).
Reactive Triggers (rules engine)
This is the opt-in part. When you want ChatCLI to react to events (e.g., โif CI failed, ask the agent to investigateโ; โif thereโs a critical alert, run the agentโ), define rules in~/.chatcli/mcp/triggers.json.
Rule schema
Matching โ practical examples
alerts/critical channel on any server.
alerts/critical, alerts/warning, alerts/info โ any sub-channel under alerts/. Does not match errors/critical (different prefix).
prom-alerts, channel alerts/*, and content matching the regex. All must be satisfied (AND).
Modes
mode: notify (default โ zero surprise)
When the rule matches:
- Immediately: a toast prints on stderr with the rule name and a content preview.
- The next time you type (next prompt tick), a banner shows above the prompt with the item in the inbox.
- Nothing runs. You decide whether to act.
mode: confirm (user-in-the-loop)
When the rule matches:
- Stderr toast + prompt in the banner: โrun
/channel confirm <id> yesor/channel confirm <id> noโ. - Nothing runs until the user answers.
/channel confirm <id> yes(or just/channel confirm <id>, defaulting to yes) fires the agent with the rule template./channel confirm <id> nodiscards the action.
mode: auto (autonomous โ strong opt-in)
Requires tools whitelist (validation rejects auto rules without tools). When the rule matches:
- Stderr toast informing the trigger is queued.
- At the next prompt tick (after draining park resumes and before processing user input), the agent runs automatically with the rule template.
- Execution is rendered inside an
AUTO-AGENT envelope boxโ visually distinct from a normal turn response. Escaborts as in any agent execution.
Guard-rails for auto
toolswhitelist required โ startup validation rejects anautorule withouttools.rateLimitanddedupWindowrecommended โ without them, a noisy server can spawn dozens of turns per minute.Escalways aborts, regardless of whether the agent was triggered by user or rule./channel pauseshuts everything off globally โ use before a sensitive operation to avoid interference.
Rules configuration
CI watcher (notify only โ safe default)
Prod alerts โ confirm
Canary deploys โ auto (with tight whitelist)
Validation
Schema failures reject the entire file (atomic apply โ either every rule lands, or nothing changes). Common errors:/channel rules reload.
/channel commands
All subcommands support autocomplete (Tab after /channel ), and completion is multi-level: clear offers --all, rules offers reload, confirm completes the live pending IDs then the yes/no decision, and run completes recent message seqs. The command palette (bare /channel) shares the same source.
Examples
Listing
Filtering
Manually running a message
Inspecting rules
@channels โ the agent reads the inbox
Everything above is user-driven. In agent/coder mode the model gets its own read-mostly window into the same inbox through the @channels builtin, so pushed events can steer a task mid-flight without waiting for the user to paste them.
Auto-injection vs /channel inject โ when to use which
/channel inject is an explicit action that adds a system message to history (sticks until the next compaction). /channel run is the โinvestigate nowโ path without a registered rule.
Reconnection and fault tolerance
Mcp-Session-Id on initialize, the transport echoes the header on every request (including the listener GET). Reconnection preserves the session automatically.Use cases
CI/CD
notify rule on ci-pipeline. You see failures in the banner; /channel run <seq> launches the agent to investigate with tools.Prod alerts (Prometheus/Datadog)
confirm rule on alerts/critical with a prompt template. Each alert becomes an โinvestigate?โ question you answer on demand.Canary deploys
auto rule with a tight whitelist (kubectl_*, http_request). Every canary kicks off smoke checks automatically.External webhooks (GitHub/Jira/Slack)
Limits and trade-offs
Troubleshooting
I see notifications in /channel list but the agent doesn't react automatically
I see notifications in /channel list but the agent doesn't react automatically
notify rules or no rules at all. notify is passive by design. For action:- Add a
confirmorautorule to~/.chatcli/mcp/triggers.jsonand run/channel rules reload. - Or run
/channel run <seq>manually against the message.
HTTP server sends push, but nothing shows in /channel list
HTTP server sends push, but nothing shows in /channel list
/mcp logs <name>โ if you seeserver returned 405 on GET <url>,404, or501, the server does not implement push and the listener stopped cleanly.- Otherwise, check that the server is emitting notifications in JSON-RPC format without
id. Non-JSONdata:content is also captured (lands in therawchannel), but doesnโt trigger rules tied to specific channels.
The message shows in /channel list but no rule fires
The message shows in /channel list but no rule fires
/channel rulesโ confirm the rule exists and is active.- Was
/channel pauserun?/channel resumereactivates. - Matching: does the
server/channel/contentRegexactually match? Remember thatchannel: "alerts"does NOT match"alerts/critical"โ use"alerts/*". rateLimitordedupWindowโ a recent fire may be suppressing.
The JSONL grew huge. Can I delete it?
The JSONL grew huge. Can I delete it?
rm ~/.chatcli/mcp/channels.jsonl*. On next boot, the ring starts empty.At runtime, rotation automatically cuts at 10 MiB โ you can reach ~20 MiB total (active + .1) before the next cut.Rules went silent after editing triggers.json
Rules went silent after editing triggers.json
/channel rules reload. If reload fails (invalid schema), the error appears in the response and ChatCLI keeps the previous rules active โ no โno rulesโ state.If the error is a regex, paste it into https://regex101.com flavor โGolangโ to isolate.Auto rule fires in an infinite loop
Auto rule fires in an infinite loop
- Significantly bump
rateLimit(e.g.,"15m"). - Refine the
contentRegexto only catch the pathological case. /channel pausewhile you investigate the config offline.
I want to see EVERYTHING that arrived (not just last 5) on the next turn
I want to see EVERYTHING that arrived (not just last 5) on the next turn
/channel inject โ puts the last 10 as a system message in history. Persists in history until the next compaction.To view without injecting into the LLM: /channel list shows up to 20.Next steps
MCP Integration
MCP Config
mcp_servers.json (including the channels field).Hooks System
Command Reference
/channel.