- New ask_user core tool: mid-turn user question with option picking,
refusing cleanly in autonomous mode, for background workers, and
without a tty; system prompt points at it instead of prose questions.
- Structural loop control for prompt-JSON CLI backends (opencode,
grok, claude, codex, gemini): shared cli_json_turn repairs a
missing JSON envelope exactly once, then accepts prose; prompt
contract hardened against narrate-without-calling.
- research/agentic-tool-loop-design: 74 sources plus 2 conclusions
backing the loop-control decisions; INDEX updated to 6 subjects.
Leading @tags on any turn now route and pin: @provider pins the
backend, @model-id pins a model, @botname routes one turn (bare
@bot switches sticky), tags combine freely, @auto clears the pins,
and @path references a file by absolute path. Selections save into
the dir_defaults table keyed by directory and re-apply on next boot
there. Completion for all four kinds in the REPL (readline) and in
TUI textboxes (mentions= source on TuiTextBox/TuiPromptDialog).
Bump 1.22.0 -> 1.22.1.
Paste: enable CSI ? 2004 mode, parse ESC[200~...ESC[201~ into one
TuiPaste event routed to the focused panel, literal body, over-cap
pastes dropped whole. TextBox inserts with CR/CRLF normalized,
Terminal queues bytes to the pty, unmarked CRLF coalesces to one
Enter, idle flush no longer shreds slow pastes.
Complete: new TuiAutocomplete popup owned by TuiTextBox via
complete=, typing filters, arrows move, Enter accepts, Esc
dismisses. tui_complete_command feeds COMMANDS with HELP_TEXT
descriptions. Demo prompt dialog wired as the showcase.
The run_turn loop printed a bare 'tai' header before every backend
call, so each tool round got its own noise line while the REPL prompt
already identifies the program. The label carried no step, backend,
or bot info. Adds a regression test asserting no bare banner line.
tai.py (8643 lines) is now a thin facade over tai_pkg/ (33 feature modules, one class or feature per file). tai-orig.py preserves the pre-split single-file version byte for byte (sha256 verified against snapshot); import tai and ./tai.py behavior unchanged.
Equivalence proof, all run on this tree immediately pre-commit: differential battery tai-orig.py vs facade 3718 checks 0 failures (surface, pure fuzz, tool dispatch, store/audit, release ops, fake-HOME install roundtrip, CLI runs); test_tai.py 261 OK; test_seal.py 16 OK; --version/--help byte-identical; dir(tai) zero removals.
Fix included: opencode wire-protocol schema echo leaked into replies; opencode_parse now scans with JSONDecoder.raw_decode capped at 200 scans (13 OpencodeParseTests). Test count 261 = 237 HEAD + 8 pre-existing uncommitted ToolArgumentsTests (now committed with this change, disclosed) + 13 OpencodeParseTests + 3 EmbeddedStringsTests. Patch-target-only test migration (24 hunks), zero assertion changes. tai_pkg clean-regenerated after mid-verification tamper was found and reverted.
- Plexer: always-active terminal multiplexing. NativePlexerController/
TmuxPlexerController share one interface, chosen by which env var is
already set (TAI_PLEXER_SOCKET vs TMUX_PANE); NativePlexerBackend is
the pty.fork()-based wrapper (raw mode, SIGWINCH resize forwarding,
a Unix-socket control protocol, continuous session logging, a
Ctrl+A prefix key distinct from tmux's own Ctrl+B). Auto-wraps new
interactive shells via a .bashrc hook mirroring the existing
command-not-found install pattern, gated on $TMUX/$TAI_PLEXER_ACTIVE/
a $HOME/.tai-no-plexer bypass. New plexer_send/read/type/status
tools, env-gated on a detected controller.
- tests/container.py + docker-compose.yml + tests/docker/Dockerfile:
a disposable podman/docker image for the tests that must never touch
a real machine (installing the plexer hook into a real ~/.bashrc, a
live tmux session). Required suite (test_seal.py, test_tai.py,
tests/plexer.py, tests/bashrc.py) gates the result; tests/rag.py
runs as best-effort since its real-LLM dependency can't be satisfied
inside a disposable container with no nested engine.
- tests/bashrc.py: real end-to-end hook install + bash -i auto-wrap,
anti-recursion, tmux-active skip, non-interactive skip, a live
TmuxPlexerController test, and an install/uninstall round-trip.
- tests/plexer.py: NativePlexerBackend under a real pty, plus a
dedicated robustness pass (file permissions, >64KB payload framing,
a malformed control request, a genuine self-deadlock fix in the
write-to-child path).
- tai_test.py: the shared async process-driving framework (pipe/pty
transport, group-wide kill-and-reap, a StdinBridge for --interactive)
both test files build on.
- ChatClient last-resort backend: when there's no local opencode CLI
but the sandbox box is already running, install opencode into it
(official Node tarball, not apt -- apt's nodejs/npm pull in
webpack/babel/jest as transitive deps) and route through it instead
of giving up. Only reachable after the first retry round already
failed everything else; never creates the box itself. TmuxPlexerController.status()
fixed to use `list-panes` instead of `display-message -p`, which
silently fell back to the default pane instead of erroring against
a dead target.
- Fix 4 pre-existing test_tai.py bugs surfaced by running it in a
clean environment for the first time: two stale mock signatures
(subprocess.run's stdout= kwarg, Chat.complete's deadline= kwarg),
one $TMUX-dependent catalog assertion, one tty-dependent color
assertion.
- research/: a sourced research archive (Source Dossier/Citation
Card/Conclusion tiers) backing the above design decisions, with its
own audit.py consistency checker.
- .gitignore: fix a blanket *.md rule that was about to silently drop
the entire research/ archive from version control; add __pycache__/
and the one-off debug transcript this session started from.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
web_fetch is now a real HTTP client: methods, custom headers, string
bodies, configurable timeout, status plus content-type plus size on
every response, content-aware rendering (HTML to text, JSON raw),
and server error bodies surfaced on HTTP failures. Shell, schema
descriptions, and the system prompt steer all HTTP(S) to web_fetch;
curl/wget shell use earns a model-side hint instead of a refusal.
Shell output streams live in a rolling 4-line window for the active
agent only, with an exit line showing return code, elapsed time,
line count, and byte size. Workers, pipes, and capture stay silent.
Fixed step budget replaced by a progress budget: productive steps
(unseen actions, unseen results, user interaction) reset the stall
counter; identical repeats get a loop warning at 3 and a stop at 5;
wider stalls stop at the patience budget; 500-step total cap
backstops everything. 210 + 16 tests green.
Per-profile bots (system plus history, shared vault): create_bot with
nicknames, /bots and /bot with resume, @mention one-turn routing filed
in both histories.
Unified retrieval: one trigger-synced FTS5 index over records, events,
and audit with safe tokenized MATCH, porter stemming, BM25 rank, and
marked snippets. New search tool with kind filters and graph expansion;
recall, record_search, and audit rank through the same index.
Sealed search without weakening the seal: each boot decrypts events
into a :memory: FTS5 index (newest 10000, incrementally synced, never
on disk), so sealed stores rank like plaintext ones. LIKE fallback kept
for partial tokens. Documented in README under Sealed search with the
researched alternatives (SQLCipher, blind indexes, SSE) and rejections.
Also in this batch: sysinfo, create_skill, markdown renderer with
colors, scheduler with subagent rule, --yolo/--auto, denial-loop fix,
read-before-write guard, WAL plus timeouts, records/graph/spillover,
audit time travel, self-backup and releases, lazy skill blueprints,
smart tagging, profile isolation, lazy tool payload, install
subsystem, and multi-profile rotation fix. 176 + 16 tests green.