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.
10 KiB
foundation: store, config, session, repl, plexer
Port of the vault, settings, debug log, conversation persistence, chat REPL and
plexer controllers of tai.py (spec files: settings.md, logging.md,
uiflow.md). Behaviour is the contract: every operation below is replayed
through both implementations by tests/foundation_parity.py and diffed.
Layout
| Directory | Classes (one header + source each) |
|---|---|
src/store |
Store (store.cpp, store_vault.cpp, store_files.cpp, store_settings.cpp, store_mcp.cpp), SqlDb, Schema, DebugLog, Spill, Json, Fts, TagRules, Patterns, VaultFiles, Clock, Utf8, Sha256, SealPort/NullSeal, errors |
src/config |
Config, SettingsCatalog, SettingsRules, BootSettings, Prompts, constants |
src/session |
ShellContext (tai.py ShellSession), Conversation (persistence half of Agent), DirDefaults, Term, Md5 |
src/repl |
Repl, ReplHost, ReplStyle, LineInput, Interrupt, Boot |
src/plexer |
PlexerController, NativePlexerController, TmuxPlexerController, PlexerControllerFactory, FlowValidator, PlexerFlow, PlexerError, Plexer (constants), ProcRunner |
Where history lives and how it is keyed (the pane question)
tai.py does not read TMUX_PANE or TAI_PLEXER_PANE to key a conversation.
The key is the shell context id (ShellSession.current), derived from the
nearest shell ancestor of the process:
YYYYMMDD-HHMMSS-p<PID>-<BOOT8> start time of that shell, its pid, 8 hex of the kernel boot id
A terminal pane is exactly one long-lived shell, so quitting and restarting tai
in the same pane (tmux, the native plexer, any terminal tab) resolves to the
same id and resumes the same messages; another pane has another shell and a
fresh history. boot() exports the id as TAI_SESSION; a plain word in
TAI_SESSION pins one shared context for every shell.
Files, all under <TAI_HOME>/profiles:
| File | Content |
|---|---|
<profile>.session.<sid>.json |
user/assistant/tool messages of the main bot, system messages never stored |
<profile>.bot.<bot>.session.<sid>.json |
same for another bot |
<profile>.turn.<sid>.json |
{"open", "goal", "steps", "tools", "updated"} of the last turn (--continue) |
<profile>.sys.md, <profile>.bots.json, <profile>.bot.<bot>.sys.md |
system messages and the bot registry |
At most FILES_CAP (20) scoped files per profile and bot are kept; the oldest
are pruned on save. Load: Conversation::switch_profile (also run by the
constructor) calls Store::load_session(profile, "main", ctx_id). Save:
Conversation::close_turn/checkpoint and the REPL after every turn and on
exit. A session file that does not parse is renamed *.corrupt-<epoch>.
The shell_sessions table records every shell id with its parent chain
(ShellContext::register_in, /ctx).
Symbol map
| tai.py | C++ |
|---|---|
SCHEMA, SCHEMA_INDEXES, ensure_schema, migrate_profiles, rebuild_with_profile, table_pk_columns, ensure_column, setup_fts |
Schema |
Store.* (history, events, search, secrets, tags, graph, records, chunks, audit, schedules, files, bots, settings, mcp, dir defaults) |
Store same method names |
Store.list_sessions |
Store::list_sessions (bot, sid, path, mtime, message count, turn goal; newest first) |
fts_escape, fts_match*, fts_rank |
Fts |
normalize_tag(s), singular_noun, tag_variants, parse_node_id |
TagRules |
PROFILE_RE, BOT_RE, SECRET_RE, MCP_SERVER_RE |
Patterns |
atomic_write, sealed_write, quarantine |
VaultFiles |
now_iso, uuid4().hex |
Clock |
spill_output, spilled_view, spill_or_truncate, truncate, chunk_lines |
Spill |
DebugLog, LOG, --debug, --debug-format |
DebugLog, DebugLog::Global(), Boot::configure_debug |
SETTINGS_CATALOG, CONFIG_SETTING_FIELDS, SETTING_EXPORT_KEYS |
SettingsCatalog(), ConfigSettingFields(), SettingExportKeys() |
normalize_setting_value, parse_setting_env, parse_setting_stored, validate_setting_scope |
SettingsRules |
get_or_create_settings, get_all_settings, upsert_settings |
Store |
export_settings_to_env, boot_settings, Config.apply_store_settings |
BootSettings::ExportToEnv, BootSettings::Boot, Config::ApplyStoreSettings |
Config, parse_backends, split_models, known_model_ids |
Config::Build/FromEnv, ParseBackends, SplitModels, KnownModelIds |
DEFAULT_SYSTEM and the other prompt texts, orientation_block, capability_block, sessions_block |
Prompts (generated byte for byte from tai.py) |
ShellSession |
session::ShellContext |
Agent.switch_profile, switch_bot, apply_system, reset_history, update_system, checkpoint, close_turn, mark_turn_open |
session::Conversation |
apply_dir_defaults |
session::DirDefaults::apply |
boot (config, seal, store, settings, shell context, agent) |
repl::Boot::open |
banner, resume_turn, repl, one-shot tail of main, handle_command for /quit /exit /help /profiles /bots /bot /profile /ctx /clear |
repl::Repl |
Glow.statusbar, Glow.prompt_text, banner glow strings, strip_ansi, truncate_ansi |
repl::ReplStyle |
PlexerError, plexer_dir, PLEXER_* constants |
plexer::PlexerError, plexer::Plexer |
PlexerController, NativePlexerController, TmuxPlexerController, PlexerControllerFactory.detect |
same names in detz::plexer |
validate_flow_*, PlexerFlow |
plexer::FlowValidator, plexer::PlexerFlow |
The python Seal is not part of this area: Store takes a store::SealPort
(enabled, lock, unlock, safe_unlock, kPrefix = "tai1$"). NullSeal
is the python "seal disabled" path.
Deliberate differences
- Python dicts returned by
Storearestore::Jsonobjects (insertion ordered); settings rows areSettingRow. ShellSessionis namedShellContextbecausedetz::shell::ShellSessionis the command runner.- A bare
/at the prompt prints "unknown command, try /help" (tai.py raises IndexError). - Without readline there is no line editing, arrow-key history or tab completion;
LineInputis a plain line reader. - One
LineInputowns fd 0: the REPL prompt,ask_user, approvals, shell_ask and secret reads all go throughLineInput::shared(), because two buffered readers on one fd steal each other's bytes and poison each other's EOF state (the 0 ms phantom-empty answers). EOF is one-shot (a tty recovers, a closed pipe EOFs again) and the SIGINT flag counts arrivals so two rapid Ctrl-C are both observed. - Ctrl keys at the REPL prompt: Ctrl-C clears the line and re-prompts, a second Ctrl-C within 2 s exits 0; Ctrl-D exits 0; Ctrl-Z keeps the default SIGTSTP suspend (no handler installed).
PlexerController::asktakes the timeout asJsonso an int stays an int on the wire like in python; adoubleoverload exists.- Unbounded python bigints in JSON become doubles.
Gaps (stated, not hidden)
| Gap | Effect |
|---|---|
No Seal implementation here (SealPort only) |
Boot::open uses NullSeal when TAI_PASSPHRASE is unset (vault stored in plain text, which python seals on its next start); an explicit TAI_PASSPHRASE without a seal_factory is refused with a SealError; a vault sealed by python is refused ("memory is sealed"). rotate_seal and the default-key upgrade are not ported |
readline (history recall, complete_input) |
typed input is still recorded in input_history |
start_scheduler, running_agents, probe_backend |
host hooks (ReplHost::on_start, running_agents, backend_status) |
ensure_self_backup, snapshot_self, do_release |
script-specific, belong to release tooling |
DebugLog uncaught-exception hook |
std::set_terminate analog; logged line carries the C++ type name, python's carries the python one |
| Glow banner on a real tty | pieces are compared with python functions, the composed screen is not |
Caps (all static)
FILES_CAP 20 scoped files, ROWS_CAP 1000 shell_sessions, CHAIN_CAP 64 process levels,
INPUT_HISTORY_CAP 1000 rows of 4000 chars, LOG_DB_KEEP 20000 log rows, LOG_META_MAX 16384 bytes,
MEM_EVENTS_CAP 10000 events in the in-memory index, MCP_MAX_SERVERS 32, DIR_DEFAULTS_CAP 512,
SPILL_LIMIT 6000 chars, AUDIT_MAX_CHARS 20000, FILE_RECORD_MAX 50000, flow limits in Plexer.
Tests
Run from taicpp/: make -f tests/foundation.mk test (needs only g++, sqlite3, python3, tmux for the tmux cases).
| Test | What it proves |
|---|---|
tests/foundation_parity.py + foundation_probe |
one scenario replayed through tai.py and the C++ store: results, every table, the FTS index, the schema DDL and every profile file identical (timestamps and random ids masked). Pure functions (JSON, tags, settings rules, prompts, chunking, shell context against a fake /proc, flow validator and driver, Glow pieces), DebugLog sinks, REPL slash commands, native plexer against a scripted backend, tmux against a private server, and vault portability (each implementation reads the vault the other wrote) |
tests/repl_persist.py + repl_probe |
the history bug: the REPL started repeatedly under a long-lived shell per pane resumes the same session file, another pane stays separate (also in real tmux panes), Ctrl-C mid-turn, an uncaught exception reaching the debug log, a pinned TAI_SESSION is shared, an interrupted turn is resumed by --continue; every assertion reads the vault |
tests/repl_keys.py |
Ctrl keys over a real pty against build/detz, no model: single Ctrl-C re-prompts, Ctrl-C aborts /secret set and the next prompt still reads, double Ctrl-C exits 0, Ctrl-D exits 0, Ctrl-Z suspends and SIGCONT resumes (via a session-keeping middle process, else the kernel discards SIGTSTP), session file saved |
tests/tmux_ask.py |
tmux ask (menu, text popup, message, cancel, timeout) with a real client attached through a pty, both implementations, same replies |
tests/foundation_test.cpp |
hash vectors, UTF-8 helpers, seal-enabled store paths with a stand-in seal, boot settings, dir defaults, turn files, plexer detection, line input |
repl_probe runs the real Repl, Conversation and Store with a scripted
host in place of the model loop; no model is called.