Skip to content

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.

Terminal window
snippbot dispatcher tail # Live-tail the decision log
snippbot dispatcher mismatches # Shadow-mode mismatches over last N hours
snippbot dispatcher stats # Per-source / per-tier counts over recent N decisions
snippbot dispatcher soak # PASS/FAIL verdict vs per-source soak budgets
snippbot dispatcher inject --tier N # Fire a synthetic envelope at a chosen tier
snippbot dispatcher health # Live snapshot from daemon /api/dispatcher/health

Live-tail recent dispatcher decisions. Prints one line per decision with a mismatch marker, timestamp, source, kind, tier reached, action chosen, and reason.

Terminal window
snippbot dispatcher tail # Last 20 decisions
snippbot dispatcher tail -n 100 # Last 100
snippbot dispatcher tail -f # Follow mode (Ctrl-C to stop)
snippbot dispatcher tail --source channel # Filter by IntentSource
snippbot dispatcher tail --mismatch-only # Only shadow-mode divergences
snippbot dispatcher tail -f --source autonomy # Follow autonomy producer only
OptionDescription
-n, --limitHow many recent entries to show first (default: 20)
-f, --followTail mode — keep reading new entries until Ctrl-C
--sourceFilter by IntentSource: user_message, channel, hook, scheduler, autonomy, device, proactive, api, internal, loop
--mismatch-onlyOnly 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_hours

A leading flags a shadow-mode mismatch (the dispatcher would have routed differently than the legacy code path).


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.

Terminal window
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
OptionDescription
--hoursWindow 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 (prefix)Meaning
action_kind_divergedDispatcher picked a different DispatchAction.kind than the legacy path delivered
target_room_id_divergedSame kind, but target room_id differs
target_channel_id_divergedSame kind, but target channel_id differs
target_device_id_divergedSame kind, but target device_id differs
agent_divergedSame kind/room, but the resolved agent_id differs
dispatcher_suppressed_existing_actedDispatcher produced no action (e.g. Tier 2/4 suppress); legacy delivered anyway (common when quiet hours are active — expected during shadow)

Each reason string carries the values inline, e.g. agent_diverged:dispatcher=A,existing=B.


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.

Terminal window
snippbot dispatcher stats # Last 200 decisions
snippbot dispatcher stats --limit 1000 # Last 1000
OptionDescription
--limitSample 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%)

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).

Terminal window
snippbot dispatcher soak # All sources, last 48h
snippbot dispatcher soak --hours 24 # Last 24h
snippbot dispatcher soak --source channel # One source only
snippbot dispatcher soak --json # Machine-readable, for CI
OptionDescription
--hoursSoak window in hours (default: 48, per migration plan)
--sourceRestrict to a single source: scheduler, channel, hook, user_message, autonomy
--jsonEmit JSON instead of a table (one record per source)
CodeMeaning
0Every source passed (or returned NO_BUDGET / INSUFFICIENT — informational)
1At least one source FAILED a budget

Derived from the soak plans in PLAN_DISPATCHER_PRODUCER_MIGRATION.md. Tuples are (min_envelopes, max_mismatch_rate, max_p50_latency_ms):

SourceMin envelopesMax mismatch rateMax p50 latency
scheduler105%
channel505%
hook2010%
user_message10002%2.0 ms
autonomy105%

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.
StatusMeaning
PASSBudget met — shadow data looks clean. Real enforce is not DISPATCHER_ROUTE_*=enforce — it’s the per-method flag for that producer (SNIPPBOT_SCHEDULER_PUSH_ENFORCE, SNIPPBOT_SCHEDULER_ANNOUNCE_ENFORCE, SNIPPBOT_AUTONOMY_ENFORCE). Only those three are enforce-capable; see the architecture enforce note.
FAILOne or more budgets exceeded — investigate before enforcing
INSUFFICIENTBelow min_envelopes — soak longer before judging
NO_BUDGETSource has no defined budget — informational only

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.

Terminal window
snippbot dispatcher inject --tier 1 # One T1 decision
snippbot dispatcher inject --tier 2 # One T2 decision
snippbot dispatcher inject --tier 3 --count 5 # 5 T3 decisions
snippbot dispatcher inject --tier 4 --count 10 # 10 T4 decisions
OptionDescription
--tier(required) Which resolver tier to exercise: 1, 2, 3, or 4
--countHow 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:

Terminal window
jq 'select(.envelope.payload.inject_test != true)' ~/.snippbot/dispatch/dispatch-*.jsonl

Sample 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).


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.

Terminal window
snippbot dispatcher health

No options.

Sample output:

Dispatcher Health (scope: own)
Sample size: 200
Suppression rate: 14%
Classifier configured: True
Classifier budget: 50 calls/user/day
Classifier avg latency: 234ms

If the daemon is unreachable, the command transparently falls back to local stats (same output as snippbot dispatcher stats --limit 200).


Reasons and fixes:

  1. No producer is in shadow/enforce — route modes default to off, so no envelopes are emitted. Set a DISPATCHER_ROUTE_* env var (or the route mode under /settings?section=dispatcher) to shadow for the producer you want to observe, then generate traffic (e.g. trigger an autonomous followup). Loops route through the dispatcher regardless (they’re always enforced via LoopExecutor), so a running loop always produces source=loop rows.
  2. Daemon not runningtail and stats read JSONL files directly, but no new entries appear until the daemon is up.
  3. Log directory missing — confirm ~/.snippbot/dispatch/ exists. If not, the daemon hasn’t initialized the dispatcher yet; restart with snippbot start.

health and inject require the daemon. Verify:

Terminal window
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.

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.

If snippbot dispatcher soak returns FAIL for a source:

  1. Run snippbot dispatcher mismatches --hours 48 --source <src> to see the divergences
  2. Group by Reason column — most fall into a small number of buckets (e.g., all target_room_diverged mismatches typically share a root cause)
  3. Check the producer-migration plan for known divergences — quiet-hours suppression on autonomy followups is an expected mismatch and should not block enforce flipping
  4. Soak playbooks per source: SOAK_DISPATCHER_AUTONOMY

Default retention is 30 days; daily rotation. To shrink:

  • Change log.retention_days in dispatcher settings (range 7–365)
  • Or set SNIPPBOT_DECISION_LOG_SAMPLE_RATE=0.1 to log only 10% of decisions (off by default — full logging is the recommended posture)

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? }
}

  • 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 health and inject to reach the daemon)
  • secrets — managing third-party API keys (Tier 3 uses your default internal model)
  • Implementation plans: unified dispatcher, producer migration