Files
tai/taicpp/docs/shell.md
T
retoorandClaude Sonnet 5.5 ed2bfade80 taicpp: port shell, sandbox, pty, glow, markdown, diff and speech helpers from tai.py
Differentially verified against tai.py (4890 cases incl. real pty runs).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 05:02:18 +01:00

6.2 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
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 during the question is not mapped to a decline (tai.py maps KeyboardInterrupt); the REPL owner decides signal handling.
  • BackendError here is detz::shell::BackendError; the agent core converts it at the call site.