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
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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | โ | Unique rule identifier. Appears in logs and in /channel rules |
server | string | โ | Exact match against the MCP server name. Empty = any server. "*" = any (explicit) |
channel | string | โ | Match. Supports literal ("alerts"), wildcard ("*") and prefix-glob ("alerts/*") |
contentRegex | string | โ | Go regexp against the message Content. Empty = any content |
mode | string | โ | "notify" (default), "confirm", "auto" |
prompt | string | required for confirm/auto | Template sent to the agent when the rule fires. Variables: {{content}}, {{channel}}, {{server}}, {{seq}}, {{timestamp}} |
tools | string[] | required for auto | Whitelist of tools the agent can invoke. In auto mode, validation rejects rules without tools to prevent unfettered autonomous access |
rateLimit | string (Go duration) | โ | Per-rule cap: after a fire, ignore matches within this window. Examples: "5m", "30s", "1h" |
dedupWindow | string (Go duration) | โ | Per (rule, content prefix) dedup: same rule + same prefix within the window is a no-op |
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:| Symptom | Cause | Fix |
|---|---|---|
mode "auto" requires a non-empty tools whitelist | mode: "auto" without tools field | Add "tools": [...] or switch to mode: "confirm" |
invalid contentRegex: error parsing regexp | Invalid regex | Test in regex101.com with โGolangโ flavor; remember metachars need JSON double-escape (\\b, \\d) |
invalid mode "Notify" | Case-sensitive | Use lowercase: notify, confirm, auto |
duplicate rule name "x" | Two rules with the same name | Names must be unique within the file |
invalid rateLimit "5min" | Go time.Duration format | Use 5m, 30s, 1h (not 5min, 30sec) |
/channel rules reload.
/channel commands
All subcommands support autocomplete (Tab after /channel ).
| Subcommand | Argument | Description |
|---|---|---|
/channel or /channel list | โ | Lists up to 20 most recent ring messages with seq, timestamp, server, channel and content preview |
/channel <name> | channel name | Filters the listing by the given channel |
/channel ack | โ | Marks all messages as read and clears the pending notify banner |
/channel inject | โ | Splices the last 10 messages into the history as a system message for the next turn โ useful when you want to provide explicit context to the LLM without waiting for auto-injection of the 5 most recent |
/channel pause | โ | Pauses the trigger engine. Messages keep entering the ring/persistence, but no actions are emitted (no banner, no auto, no confirm) |
/channel resume | โ | Reactivates the trigger engine |
/channel rules | โ | Lists active rules with their modes, filters and prompts |
/channel rules reload | โ | Re-reads ~/.chatcli/mcp/triggers.json without restarting ChatCLI. On validation error, keeps the previous rules active |
/channel confirm <id> | id required | Accepts a pending confirm action; defaults to yes |
/channel confirm <id> no | id required | Refuses a pending confirm action without running anything |
/channel run <seq> | seq required | Manually triggers the agent on a specific ring message (use the seq shown by /channel list) โ useful to investigate something that came in as notify |
Examples
Listing
Filtering
Manually running a message
Inspecting rules
Auto-injection vs /channel inject โ when to use which
| You want | Command | Token cost |
|---|---|---|
| The agent to always know about the last 5 messages | nothing โ automatic every turn | ~5 lines in the system prompt |
| Force explicit (and deeper) context for the next turn | /channel inject | ~10 lines as a permanent system message in history |
| Investigate a specific past message | /channel run <seq> | full agent run |
/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
| Scenario | Behavior |
|---|---|
| SSE server drops mid-session | Supervisor reconnects with full-jitter exponential backoff (500ms โ 30s cap). Pending RPCs wait, no extra timeout |
| Streamable HTTP server drops mid-session | Same backoff strategy, but the listener (open GET) is the channel that reconnects โ POSTs keep working when the server comes back |
| Stdio server crashes | onClose fires, manager marks as disconnected in /mcp status. Notifications stop because the process is dead |
| HTTP server returns 405 / 404 / 501 on GET | Server does not support push listener. Clean stop, no retry storms โ log info โserver does not support pushโ |
| Persistence file corrupted | Invalid lines are skipped individually. File remains usable |
| Disk full on append | Warning once, persistence disabled for that session, in-memory ring keeps running |
| Notification arrives during shutdown | Push becomes a no-op once Close() ran โ no race with the file close |
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
| Item | Value | Why |
|---|---|---|
| In-memory ring | 200 messages | Covers normal usage (several hours of CI/alerts) without bloating memory |
| Auto-injection | 5 messages | Balance between useful context and token cost per turn |
/channel inject | 10 messages | More depth for explicit investigation |
| Persistence file | 10 MiB before rotation | Enough for ~50k (short) messages |
| Load on boot | 200 messages | Keeps the ring โwarmโ without reading a huge file |
| Reconnect backoff | 500ms โ 30s, full jitter | Recovers fast from blip, avoids thundering herd |
| Confirm expiration | 30 min | Bounded memory; user who vanished for hours doesnโt accumulate confirms |
| Engine action buffer | 64 | Defense against โburst stormโ โ drop with warn rather than back-pressure |
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.The agent is triggered in auto mode and opens a tool I didn't authorize
The agent is triggered in auto mode and opens a tool I didn't authorize
Next steps
MCP Integration
MCP Config
mcp_servers.json (including the channels field).Hooks System
Command Reference
/channel.