dispatcher
Inspect the Unified Intent Dispatcher’s runtime decisions. These commands read the local decision-log files directly at ~/.snippbot/dispatch/dispatch-YYYY-MM-DD.jsonl, so they work even when the daemon is not running. health and inject additionally call the daemon’s REST endpoints when reachable.
Overview
Section titled “Overview”snippbot dispatcher tail # Live-tail the decision logsnippbot dispatcher mismatches # Shadow-mode mismatches over last N hourssnippbot dispatcher stats # Per-source / per-tier counts over recent N decisionssnippbot dispatcher soak # PASS/FAIL verdict vs per-source soak budgetssnippbot dispatcher inject --tier N # Fire a synthetic envelope at a chosen tiersnippbot dispatcher health # Live snapshot from daemon /api/dispatcher/healthsnippbot dispatcher tail
Section titled “snippbot dispatcher tail”Live-tail recent dispatcher decisions. Prints one line per decision with a mismatch marker, timestamp, source, kind, tier reached, action chosen, and reason.
snippbot dispatcher tail # Last 20 decisionssnippbot dispatcher tail -n 100 # Last 100snippbot dispatcher tail -f # Follow mode (Ctrl-C to stop)snippbot dispatcher tail --source channel # Filter by IntentSourcesnippbot dispatcher tail --mismatch-only # Only shadow-mode divergencessnippbot dispatcher tail -f --source autonomy # Follow autonomy producer only| Option | Description |
|---|---|
-n, --limit | How many recent entries to show first (default: 20) |
-f, --follow | Tail mode — keep reading new entries until Ctrl-C |
--source | Filter by IntentSource: user_message, channel, hook, scheduler, autonomy, device, proactive, api, internal |
--mismatch-only | Only show entries where extra.shadow_mismatch == true |
Sample output:
⚠ 2026-05-19T14:21:03 autonomy followup tier_2 suppress tier2:quiet_hours 2026-05-19T14:20:55 channel message tier_1 deliver_to_channel tier1:channel_binding 2026-05-19T14:20:48 user_msg message tier_1 deliver_to_chat tier1:room_id+msg 2026-05-19T14:20:30 proactive insight tier_2 schedule_for tier2:deferred:quiet_hoursA leading ⚠ flags a shadow-mode mismatch (the dispatcher would have routed differently than the legacy code path).
snippbot dispatcher mismatches
Section titled “snippbot dispatcher mismatches”List shadow-mode mismatches over the last N hours. A mismatch means the dispatcher’s decision diverged from the existing (legacy) code path that’s still doing the actual delivery — useful during producer migration soak.
snippbot dispatcher mismatches # Last 24h (default)snippbot dispatcher mismatches --hours 48 # Last 48h (the per-plan soak window)snippbot dispatcher mismatches --hours 168 # Last week| Option | Description |
|---|---|
--hours | Window to scan in hours (default: 24) |
Sample output:
Shadow Mismatches (3 in last 48h)┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┓┃ Decided At ┃ Source ┃ Envelope ID ┃ Reason ┃ Dispatcher Action ┃┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━┩│ 2026-05-19T14:21:03 │ autonomy │ env_a8f2c19b3d4e │ action_kind_diverged │ suppress ││ 2026-05-19T11:02:55 │ scheduler │ env_3c1d09b5a8f1 │ target_room_diverged │ deliver_to_chat ││ 2026-05-18T22:14:33 │ proactive │ env_91b22f4d7e08 │ action_kind_diverged │ schedule_for │└─────────────────────┴────────────┴────────────────────┴────────────────────────┴──────────────────────┘Interpreting reasons (set by the shadow controller):
| Reason | Meaning |
|---|---|
action_kind_diverged | Dispatcher picked a different DispatchAction.kind than the legacy path delivered |
target_room_diverged | Same kind, but target room/channel differs |
target_agent_diverged | Same kind/room, but agent_id differs |
dispatcher_suppressed_existing_delivered | Policy gate suppressed; legacy delivered anyway (common when quiet hours are active — expected during shadow) |
dispatcher_deferred_existing_delivered | Tier 2 scheduled for later; legacy delivered now |
snippbot dispatcher stats
Section titled “snippbot dispatcher stats”Per-source and per-tier counts over the most recent N decisions. Useful for spot-checking that Tier 1 is hitting its ≥80% target and that classifier calls are within budget.
snippbot dispatcher stats # Last 200 decisionssnippbot dispatcher stats --limit 1000 # Last 1000| Option | Description |
|---|---|
--limit | Sample size for aggregation (default: 200) |
Sample output:
Dispatcher Stats (last 200 decisions)
┌────────────────┬───────┬───────┐│ Source │ Count │ Share │├────────────────┼───────┼───────┤│ user_message │ 112 │ 56% ││ channel │ 36 │ 18% ││ autonomy │ 24 │ 12% ││ proactive │ 16 │ 8% ││ scheduler │ 8 │ 4% ││ hook │ 4 │ 2% │└────────────────┴───────┴───────┘
┌────────┬───────┬───────┐│ Tier │ Count │ Share │├────────┼───────┼───────┤│ tier_1 │ 164 │ 82% ││ tier_2 │ 22 │ 11% ││ tier_3 │ 8 │ 4% ││ tier_4 │ 6 │ 3% │└────────┴───────┴───────┘
Suppressed: 28 (14%)Classifier calls: 8 (4%)Shadow mismatches: 2 (1%)snippbot dispatcher soak
Section titled “snippbot dispatcher soak”Evaluate per-source soak budgets and emit a PASS / FAIL verdict. Reads the decision log over the last N hours (default 48 — matching the per-plan soak window) and checks each migration against its budgets (envelope count, mismatch rate, p50 dispatch latency).
snippbot dispatcher soak # All sources, last 48hsnippbot dispatcher soak --hours 24 # Last 24hsnippbot dispatcher soak --source channel # One source onlysnippbot dispatcher soak --json # Machine-readable, for CI| Option | Description |
|---|---|
--hours | Soak window in hours (default: 48, per migration plan) |
--source | Restrict to a single source: scheduler, channel, hook, user_message, autonomy |
--json | Emit JSON instead of a table (one record per source) |
Exit codes
Section titled “Exit codes”| Code | Meaning |
|---|---|
0 | Every source passed (or returned NO_BUDGET / INSUFFICIENT — informational) |
1 | At least one source FAILED a budget |
Per-source budgets
Section titled “Per-source budgets”Derived from the soak plans in PLAN_DISPATCHER_PRODUCER_MIGRATION.md. Tuples are (min_envelopes, max_mismatch_rate, max_p50_latency_ms):
| Source | Min envelopes | Max mismatch rate | Max p50 latency |
|---|---|---|---|
scheduler | 10 | 5% | — |
channel | 50 | 5% | — |
hook | 20 | 10% | — |
user_message | 1000 | 2% | 2.0 ms |
autonomy | 10 | 5% | — |
Sample output:
Dispatcher Soak Report (window: last 48h, sources: 5)
┌──────────────┬──────────────┬──────┬──────────┬────────┬────────┬────────────────────┐│ Source │ Status │ N │ Mismatch │ p50 ms │ p95 ms │ Reasons │├──────────────┼──────────────┼──────┼──────────┼────────┼────────┼────────────────────┤│ autonomy │ PASS │ 42 │ 2.4% │ 1.1 │ 3.8 │ all soak budgets… ││ channel │ INSUFFICIENT │ 18 │ 5.6% │ — │ — │ only 18 envelopes… ││ hook │ NO_BUDGET │ 0 │ — │ — │ — │ No soak budget… ││ scheduler │ PASS │ 24 │ 0.0% │ — │ — │ all soak budgets… ││ user_message │ FAIL │ 1240 │ 3.1% │ 1.3 │ 4.1 │ mismatch rate 3.1% │└──────────────┴──────────────┴──────┴──────────┴────────┴────────┴────────────────────┘
✗ At least one source FAILED its soak budget — see above.Status meanings
Section titled “Status meanings”| Status | Meaning |
|---|---|
PASS | Budget met — safe to flip DISPATCHER_ROUTE_<source>=enforce |
FAIL | One or more budgets exceeded — investigate before flipping |
INSUFFICIENT | Below min_envelopes — soak longer before judging |
NO_BUDGET | Source has no defined budget — informational only |
snippbot dispatcher inject
Section titled “snippbot dispatcher inject”Fire synthetic envelopes engineered to hit a specific resolver tier. Producer migrations always hit Tier 1 (every real envelope carries deterministic hints), so to verify Tier 2 / Tier 3 / Tier 4 admin UI rows render correctly an operator needs synthetic traffic.
snippbot dispatcher inject --tier 1 # One T1 decisionsnippbot dispatcher inject --tier 2 # One T2 decisionsnippbot dispatcher inject --tier 3 --count 5 # 5 T3 decisionssnippbot dispatcher inject --tier 4 --count 10 # 10 T4 decisions| Option | Description |
|---|---|
--tier | (required) Which resolver tier to exercise: 1, 2, 3, or 4 |
--count | How many synthetic envelopes to inject (default: 1, max: 100) |
Decisions appear in the admin UI immediately and have payload field inject_test=true so they can be filtered out of soak statistics:
jq 'select(.envelope.payload.inject_test != true)' ~/.snippbot/dispatch/dispatch-*.jsonlSample output:
✓ Injected 1 envelope(s) for tier_2
┌───────────────────┬───────────┬──────────────────┬────────────┬────────────┬──────────────────┐│ Envelope ID │ Tier Used │ Action │ Suppressed │ Latency ms │ Reason │├───────────────────┼───────────┼──────────────────┼────────────┼────────────┼──────────────────┤│ env_inj_a1b2c3d4 │ tier_2 │ schedule_for │ — │ 0.84 │ tier2:deferred… │└───────────────────┴───────────┴──────────────────┴────────────┴────────────┴──────────────────┘
These decisions appear in /admin/dispatcher and `snippbot dispatcher tail`.Requires the daemon to be running (calls POST /api/dispatcher/inject).
snippbot dispatcher health
Section titled “snippbot dispatcher health”Print a live health snapshot from the daemon’s /api/dispatcher/health endpoint. Falls back to local decision-log aggregation when the daemon is unreachable.
snippbot dispatcher healthNo options.
Sample output:
Dispatcher Health (scope: own)
Sample size: 200Suppression rate: 14%Classifier configured: TrueClassifier budget: 50 calls/user/dayClassifier avg latency: 234msIf the daemon is unreachable, the command transparently falls back to local stats (same output as snippbot dispatcher stats --limit 200).
Troubleshooting
Section titled “Troubleshooting””No decisions logged yet.”
Section titled “”No decisions logged yet.””Reasons and fixes:
- No producer is migrated yet — check the
DISPATCHER_ROUTE_*env vars; until at least one isshadoworenforce, no envelopes are emitted. Today the autonomy producer at_deliver_chat_roomis shadow-integrated by default — trigger an autonomous followup (long, reflective chat message) to generate one. - Daemon not running —
tailandstatsread JSONL files directly, but no new entries appear until the daemon is up. - Log directory missing — confirm
~/.snippbot/dispatch/exists. If not, the daemon hasn’t initialized the dispatcher yet; restart withsnippbot start --dev.
”Daemon health endpoint unreachable”
Section titled “”Daemon health endpoint unreachable””health and inject require the daemon. Verify:
snippbot status # Daemon up?curl http://localhost:18781/api/dispatcher/health # Endpoint reachable?If the daemon is up but the endpoint 401s, your ~/.snippbot/config.toml api_key may be stale — rotate with snippbot auth regenerate-key <id> and update the config.
”Tier 3 budget exhausted”
Section titled “”Tier 3 budget exhausted””stats shows Classifier calls at the daily cap (default 50/user/day). Tier 3 silently falls back to Tier 4 once exhausted. To raise: edit dispatcher settings under /settings?section=dispatcher (Classifier daily budget slider) or set SNIPPBOT_TIER3_CLASSIFIER=off if you’d rather force Tier 4 always.
High mismatch rate during shadow soak
Section titled “High mismatch rate during shadow soak”If snippbot dispatcher soak returns FAIL for a source:
- Run
snippbot dispatcher mismatches --hours 48 --source <src>to see the divergences - Group by
Reasoncolumn — most fall into a small number of buckets (e.g., alltarget_room_divergedmismatches typically share a root cause) - Check the producer-migration plan for known divergences — quiet-hours suppression on autonomy followups is an expected mismatch and should not block enforce flipping
- Soak playbooks per source: SOAK_DISPATCHER_AUTONOMY
Decision-log files are huge
Section titled “Decision-log files are huge”Default retention is 30 days; daily rotation. To shrink:
- Change
log.retention_daysin dispatcher settings (range 7–365) - Or set
SNIPPBOT_DECISION_LOG_SAMPLE_RATE=0.1to log only 10% of decisions (off by default — full logging is the recommended posture)
Decision-log schema
Section titled “Decision-log schema”Full schema reference: Unified Intent Dispatcher → Decision log.
Each line in ~/.snippbot/dispatch/dispatch-YYYY-MM-DD.jsonl is one JSON object with three top-level keys:
{ "envelope": { source, kind, user_id, room_id?, urgency, ... }, "result": { tier_used, actions[], suppressed, classifier_called, ... }, "extra": { shadow_mismatch?, reason? }}Related
Section titled “Related”- Unified Intent Dispatcher architecture — what the dispatcher does, envelope schema, tier semantics
- Proactivity concept — user-facing proactivity controls (the dispatcher’s Tier 2 gate)
- auth — managing API keys (required for
healthandinjectto reach the daemon) - secrets — managing third-party API keys (Tier 3 uses your default internal model)
- Implementation plans: unified dispatcher, producer migration