Differentially verified against tai.py (4890 cases incl. real pty runs). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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.pybuildstests/shell_parity_driver.cppand runs every case through the driver and through the same function of../tai.py, comparing bytes. Real processes, a real pty (including the realsupassword prompt), real files, a scripted fakepodman, 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 -Wextraand 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.loweruse hand-written ranges (exact for ASCII, Latin-1, Greek, Cyrillic basics; approximate beyond). Affects markdown italics boundaries, number tokens inhighlight_python,complete_filescase folding for exotic scripts. - The pipe path of
run_livekeeps 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 makesrun_livewait up to 10 s for the reader and leaves the grandchild running. SpinnerandLaneBoardare 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 Forbiddento tai.py and to the port alike when last tried (2026-10-25, this machine), so end-to-end audio was not verified. Sandbox::EnsureBoximage build and the opencode install were exercised only against a scripted fake engine, not a real multi-minute podman build.ShellIo::AskREPL fallback: Ctrl-C during the question is not mapped to a decline (tai.py mapsKeyboardInterrupt); the REPL owner decides signal handling.BackendErrorhere isdetz::shell::BackendError; the agent core converts it at the call site.