Skip to content

Typed slash-command registry + ClientCommand routing (fixes get_transcript leak)#330

Merged
edwin-zvs merged 1 commit into
mainfrom
proto-slash-registry
Jun 2, 2026
Merged

Typed slash-command registry + ClientCommand routing (fixes get_transcript leak)#330
edwin-zvs merged 1 commit into
mainfrom
proto-slash-registry

Conversation

@edwin-zvs

Copy link
Copy Markdown
Contributor

Why

Slash commands were described in three hand-synchronized, string-keyed places:
the zarvis adapter's SLASH_COMMANDS popup array, its after-submit
strip_prefix/== ladder, and the TUI's run_slash_command table. Each
consumer re-derived a command's behavior from its name string — fragile ("keep
in lockstep" comments), and the reason UI-control commands leaked into the
model-facing transcript
: a /zoom was encoded as a fake tui ToolUse that
agentd_get_transcript returned verbatim to any reading model.

This replaces the strings with typed dimensions. A command is parsed into a
CommandId once, at the input edge; every decision after that — routing,
persistence, model-visibility, rendering, the popup list — is a match on the id
or a read of a descriptor field.

What

protocol — new slash module (single source of truth):

  • CommandId (dispatch key) + dimension enums: Routing, ModelVisibility,
    TranscriptPolicy, Render, Args.
  • A SlashCommand descriptor table (COMMANDS, one row per command) with
    resolve() / by_id() / popup_names() and an is_model_hidden() predicate.
  • New SessionEvent::ClientCommand { id, args } carrying the typed CommandId,
    replacing the tui ToolUse convention for client-routed commands.

zarvis adapter:

  • The after-submit dispatch resolves the verb once, then branches on
    cmd.routing / cmd.id — no more string matching. model/reset/compact
    mutate state here; /loop synthesizes its tool call; client commands emit
    ClientCommand; unknown verbs fall back to the legacy tui tool.
  • The / popup is driven by slash::popup_names() (SLASH_COMMANDS deleted).

client (TUI): dispatches ClientCommand by reconstructing the canonical verb
from the registry and reusing run_slash_command.

mcp: agentd_get_transcript filters slash::is_model_hidden events, so
control commands stay in the durable transcript (forensics) but never reach a
reading model.

Verified

  • Unit tests (5 in slash): table invariants, alias/case-insensitive resolve,
    every client command is model-invisible, and the is_model_hidden filter.

  • agentd-adapter-zarvis: all 33 interactive tests pass (incl. the updated
    popup test).

  • End-to-end against a live daemon + real agentd-mcp: injected a message
    and a /zoom ClientCommand, then read the session two ways —

    [on disk]  'DEMO kept message' : 1   message kept
    [on disk]  'client_command'    : 1   forensics kept
    [MCP tool] model sees message        : 1
    [MCP tool] model sees client_command : 0   filtered — leak closed
    

Notes for review

  • Behavior-preserving refactor, two intentional differences:
    1. The / popup now shows canonical names only — /exit is an alias of
      /quit (still resolves when typed) so it's no longer a separate ghost
      entry; popup order is now sorted for determinism.
    2. Unknown /foo still uses the legacy tui fallback (kept so stray slashes
      don't get sent to the model); both the tui fallback and ClientCommand
      are now covered by the transcript filter.
  • The descriptor is a flat struct, so a few nonsensical combinations are
    representable (e.g. a Client command marked Effect); guarded with a test
    rather than encoded in the type — reasonable for ~15 commands.
  • No recording: internal refactor with no intended user-visible change beyond
    the popup note above.

…mand

Replaces the three hand-synchronized, string-keyed slash-command lists
(the zarvis adapter's SLASH_COMMANDS popup array, its after-submit
string-prefix ladder, and the TUI's run_slash_command table) with one
typed source of truth, and fixes the agentd_get_transcript leak by making
model-visibility a property of each command rather than an ad-hoc
"string-sniff the `tui` tool" check.

protocol (new `slash` module):
- CommandId (dispatch key) + dimension enums: Routing, ModelVisibility,
  TranscriptPolicy, Render, Args.
- A SlashCommand descriptor table (COMMANDS) — one row per command — plus
  resolve()/by_id()/popup_names() helpers and an is_model_hidden() predicate.
- New SessionEvent::ClientCommand { id, args } carrying the typed CommandId,
  replacing the `tui` ToolUse convention for client-routed commands.

zarvis adapter:
- The after-submit dispatch resolves the verb once, then branches on
  cmd.routing / cmd.id (no more strip_prefix/== string matching). Adapter
  commands (model/reset/compact) mutate state here; /loop synthesizes the
  tool call; client commands emit ClientCommand. Unknown verbs fall back to
  the legacy `tui` tool.
- The `/` popup is now driven by slash::popup_names() (SLASH_COMMANDS deleted).

client (TUI):
- Dispatches ClientCommand by reconstructing the canonical verb from the
  registry and reusing run_slash_command.

mcp:
- agentd_get_transcript filters slash::is_model_hidden events, so control
  commands (/zoom, and the legacy `tui` fallback) stay in the durable
  transcript for forensics but never reach a reading model.

Verified end-to-end against a live daemon + agentd-mcp: a /zoom ClientCommand
stays on disk but is stripped from the model-facing transcript tool.
@edwin-zvs
edwin-zvs merged commit d10d7f8 into main Jun 2, 2026
1 check passed
@edwin-zvs
edwin-zvs deleted the proto-slash-registry branch June 2, 2026 05:46
@edwin-zvs edwin-zvs mentioned this pull request Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant