Files
tai/taicpp/docs/shell.md
T
retoor 4eff597be7 sessions: vault discovery tools, shared stdin, ctrl-key fixes
New sessions/session_export tools plus a sessions_block vault map in the system prompt (tai.py, ported to detz); recall/search carry a session footer instead of letting the model grep sealed bytes. write_file refuses new markdown at working/repo roots, and the blanket *.md gitignore is gone so docs stay visible. One LineInput owns fd 0 for every prompt (ask, approvals, shell_ask, secrets), ending phantom-empty answers; Ctrl-C re-prompts (double exits), Ctrl-D exits, Ctrl-Z suspends; mid-turn Ctrl-C announces itself and kills foreground shell children via RunInterrupted. Covered by SessionsTests, repl_keys.py pty checks, agent_diff sessions_block cases.
2026-10-28 13:11:45 +01:00

6.5 KiB

shell: processes, boxes, live output, speech

Scope: src/shell/ (namespace detz::shell). Source of truth is ../tai.py; this file states what the C++ port does today, how it is verified, and the gaps. Wiring for other modules: wiring/shell.md.

Command Output Rule (repository AGENTS.md)

Every command an agent runs for the user streams live and is returned to the agent. The window is a fixed-height message (LiveStream, or the chat TUI's live block through the StreamSink protocol). A command that stops on a prompt (sudo, ssh, y/n) runs on a pty and asks through ask(text, secret); the answer is typed into the command, a decline (nullopt) sends end-of-file, and time spent waiting for the user never counts against the timeout. Anything that runs a process goes through RunLive with ShellIo::Stream and ShellIo::Ask. No bare subprocess-style calls for agent commands.

Caps (constants, all from tai.py)

constant value where
kShellTimeout (SHELL_TIMEOUT) 120 s runlive.h
kRunOutputCap (RUN_OUTPUT_CAP) 4,000,000 chars, pty path only runlive.h
kRunPromptIdle (RUN_PROMPT_IDLE) 1.0 s of silence before a prompt is asked runlive.h
kRunMaxAsks (RUN_MAX_ASKS) 20 questions per command runlive.h
kRunAnswerCap (RUN_ANSWER_CAP) 1000 chars typed per answer runlive.h
kRunRows x kRunCols 24 x 120 pty window pty.h
kStreamHeight (STREAM_HEIGHT) 4 lines livestream.h
kShellSnapshotMaxFiles 25 files targets.h
ShellSession caps SID_MAX 64, CHAIN_CAP 64, ROWS_CAP 1000, TREE_DEPTH 8, TREE_KIDS 10, TREE_LINES 40 session.h

Parity table (tai.py symbol -> C++)

tai.py C++ notes
run_live, run_live_pty RunLive::Run, RunLive::RunPty pipe path with reader thread, pty path with prompt detection; RunInterrupted on Ctrl-C when allow_interrupt (foreground only), child group killed
KeyboardInterrupt out of run_live RunInterrupted (runlive.h) converted to TurnInterrupted by AgentReplHost::run_turn, so the REPL prints interrupted and checkpoints
RUN_CTRL_RE, RUN_PROMPT_RE RunLive::StripControl, RunLive::LooksLikePrompt hand-written scanners, same alternation
PtyProc PtyProc posix_openpt, TIOCSCTTY, TIOCSWINSZ, poll reader, echo probe
subprocess.Popen/run, shutil.which Popen, Subprocess::Run, Subprocess::Which (proc.h) errno reporting through a CLOEXEC pipe, Python OSError/TimeoutExpired texts
LiveStream, stream_status, human_size LiveStream, StreamStatus, HumanSize
Spinner Spinner stderr tty only, 80 ms frames, shimmer + clock when color is on
LaneBoard LaneBoard list_agents injected as AgentLister
Glow (every static method) Glow (glow.h) describe_call over JsonValue, agent_row/lanes over AgentRow
Sandbox, container_engine, box_state, update_box_caps, ensure_box, box_exec, box_python, box_transcribe, box_speak, box_opencode_binary, box_opencode_run Sandbox (sandbox.h) BoxOpencodeExec is the exec half; the caller runs opencode_parse_events
BOX_CONTAINERFILE, BOX_STT, BOX_TTS, BOX_OPENCODE_INSTALL_SCRIPT BoxData (boxdata.cpp, generated from tai.py) byte-identical, checked by the box parity case
curl_hint, CURL_HINT CurlHint, kCurlHint
ShellSession (current, register, tree_lines, report, ...) ShellSession (session.h) SessionDb interface replaces store.db; pid/boot/chain logic identical
shell_target_paths, segment_targets ShellTargets::Paths, ShellTargets::Segment shlex, glob and os.walk ported (Shlex, PyGlob)
Tools.shell_stream, Tools.shell_ask ShellIo::Stream, ShellIo::Ask over ShellApp
edge_token, edge_timestamp, edge_ssml, edge_chunks, ws_send, ws_recv_exact, ws_recv, edge_skew, edge_connect, edge_synthesize EdgeSpeech (edge.h) TLS bytes via libcurl connect-only, EdgeSocket seam for tests
play_audio, listen_audio, RECORDERS, TRANSCRIBERS Audio (audio.h)

Old invented API (Sandbox::run(allow-list), ShellSession::exec, PtyProc::spawn/read_all, Spinner::tick/done, LiveStream::push/drain/joined, LaneBoard::set/render, Glow::highlight, speak_text) is gone; tai.py has none of it. Replacements are listed in wiring/shell.md.

How it is verified

  • tests/shell_parity.py builds tests/shell_parity_driver.cpp and runs every case through the driver and through the same function of ../tai.py, comparing bytes. Real processes, a real pty (including the real su password prompt), real files, a scripted fake podman, a fake /proc, a fake websocket server. Fuzzed inputs for markdown, wrap, truncate, diff, json. python3 tests/shell_parity.py [--fuzz N] [--filter NAME] [--live] [-v].
  • tests/shell_test.cpp: unit tests, build line in its header.
  • Both build with -Wall -Wextra and must be warning-free; the parity run fails on any compiler warning.

Gaps and deviations (stated, not hidden)

  • Unicode classes: \w, \d, str.isalnum, str.lower use hand-written ranges (exact for ASCII, Latin-1, Greek, Cyrillic basics; approximate beyond). Affects markdown italics boundaries, number tokens in highlight_python, complete_files case folding for exotic scripts.
  • The pipe path of run_live keeps all output in memory (no cap), exactly like tai.py; only the pty path is capped.
  • After a timeout only the shell is killed (Popen.kill), as in tai.py. A grandchild that keeps the pipe open makes run_live wait up to 10 s for the reader and leaves the grandchild running.
  • Spinner and LaneBoard are time based; the parity run compares the first frames, the final clear and the frame count within 2, not every frame.
  • Edge speech: wire handling is verified offline against tai.py with a scripted server. The live service answered 403 Forbidden to tai.py and to the port alike when last tried (2026-10-25, this machine), so end-to-end audio was not verified.
  • Sandbox::EnsureBox image build and the opencode install were exercised only against a scripted fake engine, not a real multi-minute podman build.
  • ShellIo::Ask REPL fallback: Ctrl-C/EOF during the question maps to a decline through the shared fd-0 reader, like tai.py maps KeyboardInterrupt/EOFError (decline sends end-of-file to the command).
  • BackendError here is detz::shell::BackendError; the agent core converts it at the call site.