Story-3191 · D17 lane 2 · merge gated on Story-3189

The front door shrinks to seven exact tokens

On 2026-07-31 counsel asked "is the Sugar Pipes hearing tomorrow?" and got a menu whose line 2 read M-013 — Rahul Rathode vs Sugar Pipes Industries delhi. The deterministic front door rejected the sentence containing its own answer: three English messages, three identical menus, zero agent runs. D17's rule is now the code: code decides what may be SEEN, the model decides what was MEANT.

TODAY:      unmatched input  ->  menu
SHOULD BE:  unmatched input  ->  Vani
Merge gate. This router sends matter-less runs (matter_id=None) and MiraConverseInput.matter_id is a required str until Story-3189 (pramaan-agents) lands. Merging this first routes counsel into a 500. Review freely; merge second.

The deterministic surface, after

Seven tokens, each matched against the whole message (fullmatch), bare word only, no synonyms, no phrases. A leading slash is optional on 1–5 (WhatsApp's / autocomplete).

#TokenWhere it livesAction
1stopcommand tabletwo-way: honest can't-halt reply if a run is in flight, else UNBIND (U13)
2logoutcommand tableunbind — aliases disconnect, sign out (aliases are free)
3helpcommand tablestatic capability list
4matterscommand tablethe RLS-bounded matter menu
5currentcommand tablestatic scope statement from sticky
6a matter code, alonedisambiguation treepin that matter (CC-31 fail-closed)
7a bare numberdisambiguation treeanswers a menu — only when a menu was just shown

status is not a token (founder ruling, 2026-07-31): account questions are sentences for Vani and the brain-side account_status tool. The bare word routes to her like any other text.

Two defects retired, not preserved

The U8/U9 hinge: menu state gets its own column

Token 7 turns on state: a bare number answers a menu only when a menu was just shown. Before this change the marker overloaded last_matter_id = NULL on the sticky row — so showing a menu destroyed the sticky scope, and a firm menu had no marker at all (any number from a multi-firm counsel picked a firm, menu or not). Migration 0235 adds business.channel_sticky_context.pending_menu_kind ('firm'/'matter'): menu state is now orthogonal to the sticky scope. A launched turn clears the markers — which is exactly the UAT pair that differs only in what preceded it:

Preceding turn2 meansCase
a normal answer (run launched)a sentence for VaniU8 — must NOT switch
a menu (matters)menu entry 2U9 — must switch

How a matter-less turn reaches Vani

  1. decide_channel_turn resolves firm_scope — firm pinned, matter_id=None (one live binding, no sticky, not single-matter).
  2. The webhook envelope carries resolved_scope.matter_id = None.
  3. The ingress consumer skips the chat-session resolver and launches a converse AgentCommand with no matter and no session (the AgentCommand matrix admits both-None converse launches; agents.agent_run already admits them: NULL scope + run_origin='user', migration 0216).
  4. The clerk stamps a well-formed matter-less MiraConverseInput (NULL matter, empty frame) — the payload 3189's brain accepts.

A multi-firm counsel with no sticky still gets the firm menu: the one cardinality question code still owns, because a run cannot launch without a firm. Menus stay RLS-scoped; an inaccessible matter is never offered (CC-11, BDD-proven).

What was verified

Open question flagged for the PR

Whether Meta enforces STOP as a platform-level opt-out on WhatsApp is UNCONFIRMED — the two-way form (halt in flight, unbind when idle) is safe under either answer, which is why it was chosen. The channel-side cancel path (a real halt) does not exist yet and is its own Story (pramaan-spec#3200); until then the in-flight reply says it cannot stop mid-answer and never claims a halt that did not halt.

The review round — four findings, one theme

Review found four places where state claimed something happened when it had not — D17's own theme. All four are fixed in this PR; a fifth (removing the disconnect alias) was rejected: an alias is another exact token, not a compatibility path, and the founder ruled aliases are free.