Skip to main content

Module side_question

Module side_question 

Source
Expand description

Backend half of the /btw slash command — a faithful recreation of Claude Code CLI’s own native /btw: a fast, one-shot, tool-less answer that is aware of the current conversation (via a text prefix, not --resume), WITHOUT touching the asking pane’s persisted transcript and WITHOUT blocking/queuing behind that pane’s own live turn.

§Why a throwaway block

SubprocessController::spawn_turn (blockcontroller/subprocess/host_spawn.rs) is strictly one-turn-at-a- time PER BLOCK — a second spawn_turn on a block that’s already running queues behind it (inner.pending_messages), plus there’s a cross- process session-ownership lease on top. Calling it again on the SAME block id that owns the pane’s live turn would therefore queue behind that turn and defeat the entire point of /btw.

So this spawns the one-shot turn against a FRESH, throwaway block id — a different block id means a different SubprocessController instance (blockcontroller’s registry is keyed per block), so there is no queue/lease contention with the asking pane’s real live turn, while still going through the exact same run_agent_turn/AgentTurnDeps pipeline every real agent turn uses — identity injection, the OAuth spawn gate that “fails closed”, muxbus/bashwrap env, PATH, git identity — instead of hand-rolling a second, parallel spawn path that bypasses all of it. See input.rs’s run_agent_turn doc comment for why that machinery must not be reimplemented.

The throwaway block deliberately carries no agentId / agentName / agentMode / agent:* meta:

  • No agentId ⇒ shell::resolve_global_output_zone (file_ops.rs) resolves to None for this block, so spawn_turn’s own stdout mirroring never writes into any real agent’s persisted agent:<defId>:current transcript zone.
  • It is never linked into any tab/layout tree and is deleted (with its controller and MPS-persisted history) the moment the turn ends — see cleanup_throwaway_block.
  • Known scope limitation: with no db_agent_instances row bound to this block, inject_identity_env_async (identity/resolver/inject.rs) takes its documented “block has no agent instance row — nothing to inject” early return, the same behavior an ad hoc quick-launch pane already gets. A /btw asked from a pane whose agent is bound to a specific non-ambient identity does not inherit that binding; it runs on ambient credentials. Reproducing the source pane’s exact bound-identity resolution would mean minting (and tearing down) a real db_agent_instances row for a block that is never a real agent — left as a follow-up.
  • Similarly, no agentMode meta ⇒ always the plain host subprocess branch, never the container docker exec branch, regardless of what the source pane is. A /btw from a container agent’s pane therefore answers from the HOST’s view of the provider CLI.

§Providers

/btw is an AgentMux feature, not a pass-through to a CLI’s own, so it runs on Claude, Codex and Gemini panes alike (BtwProvider, from the source pane’s agentProvider). Each gets its own tool-less one-shot argv — build_side_question_argv, build_codex_side_question_argv, build_gemini_side_question_argv — and its own answer translator. The turn being tool-less on every provider is what keeps a side question outside the agent identity system: it can’t call the agentmux tools, so it never acts as the pane’s agent (#3579). Other providers are refused with an error chunk.

§Output contract

The turn’s own stdout is written to the THROWAWAY block’s own FileStore-backed output file by spawn_turn exactly like any other turn (needed so this module can read it back) — but that file’s block scope (block:<throwaway_id>) is never surfaced to any pane; nothing is ever subscribed to it, and the block + its persisted history are deleted as soon as the turn ends.

Instead, this module polls that file for newly-appended JSON lines (there is no in-process “call me back on new bytes” primitive in this codebase — reactive::progress_watcher polls the same way for the same reason), translates each line through the provider’s translator (agents/translator/ — for Claude, the SAME ClaudeTranslator the drone Agent block runner uses), and republishes one mps::EVENT_BTW_ANSWER_CHUNK per translated AgentEvent — scoped to the SOURCE pane’s block id (block_id in the request) plus the minted request_id, never the throwaway block’s own scope. A final chunk carries "done": true.

Enums§

BtwProvider 🔒
The CLI a /btw runs on — the source pane’s. See the module doc’s “Providers” section.

Constants§

COPIED_META_KEYS 🔒
Meta keys deliberately copied from the source block onto the throwaway block — its whole one-shot CLI configuration, nothing agent-identity- shaped. See this module’s doc comment for what’s excluded and why.
MAX_READ_BYTES 🔒
Cap on bytes read per poll — defensive only; a tool-less, single-turn response is never remotely this large.
MAX_WAIT 🔒
Max time to wait for a translated terminal event (AgentEvent::Done / Error) before giving up and synthesizing one. A --max-turns 1 tool-less turn should finish in a few seconds; this is a generous backstop against a wedged/never-exiting subprocess, not the expected path.
POLL_INTERVAL 🔒
How often to re-check the throwaway block’s output file for new bytes. Tight relative to reactive::progress_watcher’s 3s sweep — that loop amortizes a fixed per-tick cost across every live agent block; this one drives a single, short-lived, tool-less turn where a snappy overlay matters more than sweep cost.

Functions§

btw_scope 🔒
The MPS scope a /btw answer streams on — the SOURCE pane’s block id, never the throwaway block that actually ran the turn.
build_prompt 🔒
The prompt sent to the CLI: context_snapshot prepended to question. No --resume on this path — live session context is deliberately carried as a text prefix instead (concurrent --resume on a live session is unsafe per this module’s own doc comment).
build_throwaway_meta 🔒
Build the throwaway block’s meta from the source block’s — its one-shot CLI config with provider’s tool-less /btw argv, minus every agent-identity-shaped key. gemini_policy is the deny-all policy path (read only for Gemini). Pure and unit-testable; see the module doc comment for the exclusion rationale.
cleanup_throwaway_block 🔒
Kill the throwaway controller, delete its block row, and purge any MPS- persisted history under its scope — mirrors COMMAND_DELETE_SUB_BLOCK’s kill-then-delete ordering (server/websocket.rs). Best-effort: this runs after the turn is already over, so a failure here is a leftover row/controller to log, not a failed /btw answer.
drain_and_publish 🔒
Poll the throwaway block’s own output FileStore subject for newly appended stream-json lines, translate each into AgentEvents, and publish one EVENT_BTW_ANSWER_CHUNK per event on scope. Returns once a terminal event (Done/Error) has been published, or after synthesizing one on timeout / unexpected exit. See the module doc comment for why this polls FileStore directly instead of hooking into spawn_turn’s own output path.
ensure_gemini_policy 🔒
Write GEMINI_DENY_ALL_TOOLS_POLICY where --policy can read it, and return its path. Under the AgentMux data dir rather than a shared temp dir, where another local user could swap in a permissive policy. Written to a temp file and renamed, so a concurrent /btw never reads a half-written policy.
ensure_gemini_policy_in 🔒
publish_chunk 🔒
Publish one EVENT_BTW_ANSWER_CHUNK. See mps::EVENT_BTW_ANSWER_CHUNK’s own doc comment for the payload shape and scope-string contract.
register
run_side_question 🔒
Drive one /btw request end to end: spawn the throwaway turn, drain + translate + republish its output, then clean up.
spawn_throwaway_turn 🔒
Create the throwaway block and drive it through run_agent_turn. Returns the throwaway block id on success — including when run_agent_turn itself fails after the block was created (the block + controller still exist and get cleaned up normally; drain_and_publish’s own not-running-with-nothing-published fallback surfaces that failure, and in the common failure case — the identity/OAuth spawn gate — the block’s own output file already carries a translatable error result frame, so the real message reaches the frontend either way). Only returns Err when nothing was ever created (source block missing, insert failed) — there is then genuinely nothing to poll or clean up.