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

82 lines
6.5 KiB
Markdown

# 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](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.