Skip to main content

LiteTUI

Agent-local CLI control

A registered seat from this build can advertise an authenticated loopback JSONL endpoint using the existing GUI management dispatcher. Interactive and --rpc seats share these operations; existing stdio clients are unchanged. This code half adds the server and capability reader only. The corresponding LiteHarness CLI adapter and live attachment proof are separate work; do not assume the installed CLI exposes these verbs. No spawn, resume, replacement, or reparenting operation is added to this endpoint.

The per-seat token is kept under the seat's data root in an owner-only file (0600 on POSIX, protected user-only DACL on Windows). Presence advertises only its path and runtime identity, never the token. If protection or ownership cannot be established, attachment is unavailable rather than unauthenticated. This is OS-user-local control, not protection from another process running as the same user. No remote listener, automatic replacement or authority reparenting is provided. A timeout after dispatch has an ambiguous outcome: inspect state and do not automatically retry a mutating command.

litetui --capabilities --backend B --model M reads only LiteTUI's existing pinned Claude catalogue or Codex model cache (no app, network, refresh or model startup). Unknown offline capability fails loudly; Haiku does not support high. Connected capability requests use the backend's catalogue and refuse unknown models rather than guessing. There is no second CLI-owned model list. Model load/context and engine start use their existing backend owners and VRAM guards; local attachment does not bypass them. Unsupported remote load/context or non-NInfer engine operations are errors, not successful no-ops. Positive model/engine operations require separate runtime authorization and validation.

A terminal chat client and agent harness for local LLMs. Textual TUI, streaming, tool use, vision, per-agent memory, and compaction.

Two engines, one seam (src/litetui/llm_backend.py): LM Studio's desktop server, or LiteTUI's own llama-server from llama.cpp. Switch between them mid-conversation with /backend — the history survives.

Originally built against qwen3.8-27b on an RTX 5090. LM Studio and llama.cpp run locally; the optional Codex OAuth backend sends inference requests to the hosted Codex service using subscription credentials.

uv sync
run.bat            # or: uv run --locked litetui

Either engine will do. When both are detected, a one-time picker runs at first boot; /backend changes the answer later.

engine where notes
lmstudio http://localhost:1234 Needs LM Studio's server up. Control (load / unload / context length) goes through the official lmstudio SDK, not a lms shell-out.
llamacpp http://localhost:7470 LiteTUI's own llama-server in router mode: one process, every chat GGUF on the box behind it. Attaches to LiteSuite's server on :8088 when that is healthy, otherwise spawns its own — detached, console-safe, with a log file, and killed with the app.

Neither engine loads anything at boot, and the router runs --models-max 2 so a model switch cannot quietly fill 32 GB of VRAM. The router's model list is generated: LiteSuite's install dir, LM Studio's dirs, the HuggingFace cache and any custom roots, deduplicated, with voice and embedding GGUFs filtered out by the file's own general.architecture header rather than by guessing from names.

Seat-scoped MCP servers

Use litetui --mcp-servers none for a scout/doc seat with no project MCP servers, or --mcp-servers SOTS_MCP_CORE,VibeUE for an exact named set. The default is all. This is invocation-only: it never rewrites .mcp.json or shared settings. Unknown selected names produce a startup warning rather than silently looking like an empty tool set. Native Codex receives process-local config overrides for project registrations; unrelated global Codex servers are outside this scope.

Host-managed stdio servers start lazily by default. /mcp shows not started (lazy) until tool_search loads one of their tools or a tool is called. After the first real connection, names and schemas are cached in .mcp-tool-cache/ under the data root, keyed by launch config and executable/ argument-file timestamps. A cold or invalidated cache has no tool names yet: an unmatched keyword tool_search discovers enabled cold servers on demand, then answers from their live tools. This can start multiple cold servers once, since their tool keywords are unknowable until discovery. An exact server-name query or select:mcp__server__tool limits discovery to that server. Empty tool lists remain cold. Parallel first calls wait behind startup/maintenance rather than failing busy. HTTP servers retain their existing startup. --mcp-start eager restores eager host startup.

Native Codex owns its direct MCP processes and fixed thread inventory; host lazy-start/cache does not defer those native registrations. Seat selection works there, but an included native server may still start at thread open. There is no automatic idle stop. Disconnect/reconnect are explicit lifecycle operations; no live project-server or RAM benchmark is implied by this feature.

llama.cpp speculative decoding (opt-in)

In /modelcfg → Load → Speculative decoding, choose:

  • Unset: preserve existing draft-model settings and server defaults.
  • Off (spec_type: "none"): suppress all draft inputs, including a previously saved external draft path. Clearing the selector is not the same as Off.
  • External draft (draft-simple): use an existing draft-model file.
  • Embedded MTP (draft-mtp): use the target GGUF's prediction heads, ignoring any saved external draft path. Offered only when the installed binary supports the mode/flags and discovery finds a positive integer <architecture>.nextn_predict_layers in the model metadata—not its filename.

MTP uses max/min/probability defaults 3 / 0 / 0 when tuning is blank; explicit values are preserved. Stored settings live in llama_load_settings[model]. Eligibility is rechecked at apply/load time using file-identity-cached metadata. INI regeneration validates the complete proposed preset before evicting resident models; an already-served target load validates only that target's profile. Applying load settings still restarts an owned router and reloads the selected model; it does not restore other residents. An adopted router's preset belongs to its owner and cannot be rewritten here. If another model's stale profile prevents the restart, the resident stays serving; the UI may already have saved the valid target profile.

Speculation is never enabled automatically. Performance depends on the model, quantization and workload; enabling MTP is not a guaranteed speedup. No binary upgrade or model benchmark is performed by this setting.

Claude Agent (experimental native backend)

Install the optional, pinned runtime while LiteTUI is closed:

uv sync --locked --extra claude
claude auth login
uv run --locked --extra claude litetui --backend claude

Or select /backend claude in an installed session. Authentication is delegated entirely to the official SDK/CLI, matching LiteSuite's ClaudeAdapter: LiteTUI does not read/copy login tokens or implement OAuth. The tested pair is Python Agent SDK 0.2.159 and bundled CLI 2.1.281. An explicit claude_executable setting must report that tested CLI version; reconnect after changing it. Other backends need no Claude extra. Anthropic's permission requirements for offering subscription login in third-party products remain a separate distribution gate; a local smoke test is not distribution approval.

Claude owns its agent loop, tools and native context; LiteTUI owns compaction. LiteTUI renders text/thinking and native activity, persists session references and input delivery, and supplies guarded host services through in-process MCP. Native calls pass mandatory PreToolUse policy checks; host calls pass the existing LiteTUI dispatcher. Questions and approvals work in TUI and JSONL RPC.

  • /claude status: native identity and pending delivery states (not prompt content).
  • /claude new: fresh independent segment; old native history and held inputs stay saved.
  • /claude continue: explicitly continue unsent prepared inputs after restart.
  • /claude resolve: explicitly resolve uncertain deliveries without replaying them. Inspect their effects/history first; this is not rollback.
  • Returning to Claude resumes the selected Claude segment. Other-provider history is not imported. Busy prompts are durably queued for their original segment.

LiteTUI reads Claude's effective session window before sending, rather than treating all model IDs as 1M or using another model's cumulative usage. It pauses native tool turns at the configured auto-compaction threshold, drains the interrupted turn, summarises, and admits one continuation in the fresh session. Existing local-backend tool-loop pauses also resume once. User stop, newer input, and changed owners win; failed or uncertain turns are never automatically replayed. This interrupted-turn recovery does not enable the optional wake_after_compact preference: ordinary manual compaction still follows that preference.

A failed window measurement holds the input unsent and keeps the session alive; /claude continue retries once the metadata service recovers. A preflight-held, non-RPC input remains prepared if summarising fails. On success, its original content, profile and source become a fresh prepared input, recoverable through /claude continue even if the automatic continuation is cancelled. Already delivered work receives a resume instruction, not a replay of the original prompt or tools. An RPC input with an operation_id is not transferred between sessions: when already above threshold, inspect /claude status, run /claude new, then resend as a new RPC operation ID. The old held input remains saved without replay; /claude continue cannot shrink that full session. A session already beyond its hard limit may also refuse the summary request; no success or automatic recovery is claimed in that case.

Native agents/background launches, image attachments, host goal-loop followups, legacy subagent/summary calls, raw native slash passthrough and local loading/context/sampling controls are not enabled. Unsupported calls are refused rather than sent through an OpenAI fallback. Unknown context/usage remains unknown; cumulative cost is not described as per-turn cost.

Initial configuration is controlled: user/project/local settings, external MCP, skills and Chrome integration are not automatically loaded. Native prompt preset plus a small LiteTUI append replaces the local-backend system prompt. Changes to tool inventory or session-creation prompt/configuration require a new session. The SDK retains its own session files; LiteTUI's companion claude_ledger.json tracks selected segments and prepared/submitted/terminal/uncertain deliveries. String-query delivery does not currently supply a stable native user-message UUID; LiteTUI does not fabricate provider acknowledgment. In-flight deliveries become uncertain after restart and are never automatically replayed. Native activity cards are saved at turn settlement; a mid-turn crash can lose those display cards, not the SDK's authoritative history. There is no old-segment picker or rollback.

Live probes are opt-in, not default tests: e2e/claude_contract_probe.py (isolated offline controls; --with-query uses no credentials and an unreachable endpoint), e2e/claude_live_probe.py, and LITETUI_CLAUDE_LIVE=1 for e2e/claude_tui_smoke.py, e2e/claude_rpc_smoke.py, e2e/claude_negative_probe.py, e2e/claude_timeout_probe.py, and e2e/claude_restart_probe.py. The restart probe uses two real OS processes and checks native identity, context continuity, foreign-history isolation, and owned child-process cleanup. It does not run inference on a second provider. Evidence and remaining release gates are in artifacts/claude-implementation-progress.md.

What it does

Chat + agent loop. Streams replies, renders the thinking trace in a collapsible block, and runs a tool loop until the model gives a plain answer.

Four tools, pi-style, capped at 2000 lines / 50KB per result: bash · read · write · web_fetch. Toggle with Ctrl+T.

Vision. Paste an image with Ctrl+O, or give it a path — quoted or bare, with or without a question after it:

C:\shots\screen.png what is the error in this dialog?
"C:\My Folder\screen.png"

A model can only see images if LM Studio reports it as type: vlm. A GGUF shipped without an mmproj projector loads as type: llm and returns HTTP 400 on image input — the fix is to place a matching mmproj-*.gguf beside the weights, not to change anything here.

Conversations

Every launch opens an agent home, and every conversation lives inside one:

.agents/<Name>/
    settings.json  the agent's backend, model and thinking level
    memory.md      an INDEX the agent maintains
    soul.md        who it is here: preferences, standing corrections
    handoff.md     in flight / owed / absent-by-decision / caveats
    memories/      the memories themselves, one file per idea
    conversations/<uuid>/
        convo.jsonl    append-only transcript
        settings.json  this conversation's contextual settings

A plain litetui launch opens the agent named LiteTUI by default (your seat name, when you have set one); if that home is busy, the launch gets its own, LiteTUI-<8 hex>. /resume picks an agent first. Memory, soul and handoff belong to the agent and are shared by all of its conversations.

Upgrading from 0.25.0: conversations saved under .convos/<uuid>/ stay on disk as a read-only archive. They are not migrated and cannot be continued; litetui --convo <old id> explains how to export one (--export-conversation).

The agent is told its own uuid and absolute path in the system prompt, and memory.md, soul.md and handoff.md are injected once, into the system message at the start of the conversation — a snapshot, not a live view. They are not re-sent each turn: three files on every request is affordable at 1M context and is not on a local 27B, where it crowds out the conversation. To see current contents the agent reads them with the read tool, and the prompt says so. Index lines are capped at ~50 tokens: pointers, never the memory itself.

The transcript is append-only, always

convo.jsonl is never rewritten and never backed up and replaced. A torn append costs one line, which the reader tolerates; a failed rewrite costs the conversation. Record types applied in file order:

type effect
meta id / created / model
msg one message, verbatim
edit replace ONE message in place
truncate drop a head range, splice new messages in front
snapshot whole list (read for older files; no longer written)

/compact records a truncate rather than a fresh copy of everything. Measured on a 30-message conversation: 303 bytes instead of 5,445 — and the raw history stays on disk behind the marker.

Commands

/new /clear start a new conversation (new folder on disk)
/system <text> set the system prompt
/model [n|name] show or switch the agent's model; startup defaults stay unchanged
/backend [--default] [name] switch the agent's backend; only --default also changes the startup default
/load /unload put a model into memory, or free it
/modelcfg per-model Info / Load / Inference screen (see below)
/think [level] off · minimal · low · medium · high · xhigh · unset
/compact [hint] summarise older messages, keep the last 4
/convos list saved conversations with sizes
/resume <n|id> load one (id = uuid prefix)
/reconnect /quit

Esc stops the current turn (asks first; Esc again forces). Ctrl+O paste image · Ctrl+X clear image · Ctrl+L new conversation · Ctrl+T tools.

/modelcfg — a control an engine cannot drive is greyed, never hidden

Three tabs per model: Info, Load, Inference. Load covers context length, GPU offload, threads, batch sizes, parallel slots, flash attention, KV cache quantization, mlock/mmap, RoPE, seed, a draft model for speculative decoding, a vision mmproj, and the chat template. Inference layers per-model sampling over your global /settings, plus structured output (a JSON schema) and named presets.

The two engines do not expose the same knobs. A control the active engine cannot drive renders greyed with the reason attached — it is never silently absent and never a switch that does nothing. That distinction is the point: a missing control is a question, and a fake one is a bug you find much later.

/think — unset is not off

unset sends no reasoning_effort at all, and LM Studio's own default for an absent value is xhigh. So "unset" means maximum thinking, not none. /think off is a different thing and sends reasoning_effort: "none".

The six accepted values come from the server's own 400 body, not its docs, which omit the parameter entirely:

Invalid 'reasoning_effort' value: 'x'.
Supported values: none, minimal, low, medium, high, xhigh.

/compact writes to memory before it summarises

Compaction is the moment context is about to be destroyed, so the compaction prompt runs in two steps: persist first (durable lesson → a new file in memories/ plus one pointer line; a correction → soul.md; anything in flight → handoff.md), then produce the summary. Tools are passed to that call so the instruction can actually reach disk — without them it would be theatre.

It reports which of the three happened, including persisted: nothing — TOOLS ARE OFF, so it could not write.

The summarisation call forces reasoning_effort: "none" regardless of /think: at the server default a 12k budget was spent entirely on the thinking trace and returned an empty answer, and a compaction that returns nothing is worse than not compacting.

Skills

skills/<name>/SKILL.md, same shape Claude Code uses — YAML frontmatter with name and description, then the body.

Only the index (name + one line each) goes into the system prompt. The body loads on demand through the skill tool. Inlining every body would spend the context window on instructions the model doesn't need this turn; an index with no way to open it would be worse.

A directory with no SKILL.md is skipped. One that can't be read becomes a skill whose description says so — a skill that vanishes on a decode error looks exactly like one that was never written.

MCP

A standard mcp.json in the repo root; .mcp.json (the Claude Code project convention) is read too when present, with mcp.json winning a name collision:

{ "mcpServers": {
    "files": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "."] },
    "litesuite-tools": { "type": "http", "url": "http://localhost:7423/mcp" }
} }

An entry with a url and no command is reached over plain-JSON HTTP POST — one request per call, the response body IS the JSON-RPC reply (the stateless variant of MCP streamable-HTTP; what LiteSuite's /mcp endpoint implements). Everything else is spawned as a stdio child.

Each server's tools are registered as mcp__<server>__<tool> and dispatched like any other tool, so the agent loop has no MCP-specific branch. disabled: true skips one.

A server that fails to start is recorded, not swallowed — the others still load. A tool that silently never appears looks like one the model chose not to use.

Server stderr goes to mcp.log, never to the terminal. An MCP server is a long-running child process, and a child that inherits this console paints straight over a TUI that owns every cell.

Two instances at once

Running two LiteTUI windows against one model server is supported, and they run in parallel. llama.cpp and LM Studio both serve concurrent requests; two instances on the same loaded model cost one set of weights and do not wait on each other.

The one thing you will be asked about: a different model

Loading a different model in a second instance puts a second set of weights in VRAM, and on a card that was already full that is an OOM. So whenever another LiteTUI is running, a load that would add a model you do not already have resident stops and asks first:

Another LiteTUI instance is running (another session). Loading a different model puts a second model in VRAM and can OOM depending on your setup. Load qwen/qwen3-8b anyway? [Load] [Cancel]

  • It asks every time, for as long as the instance is alive. There is no "don't ask again" — the cost is real every time, not just the first.
  • It asks on every route into a load: /model, the picker, a /modelcfg context change (a reload is a load), the pre-turn load, and the rpc set_model a host drives. The check lives inside the backend, so a route added later is covered without anyone remembering to add it.
  • The same model raises nothing. That is the parallel case, and it is the point.
  • A headless child (LiteSuite's rpc LiteTUI) cannot show a modal, so it refuses instead of deciding for you, and the host renders the refusal.

"Another instance" means a LiteHarness registry row with cli = litetui and a live session — which includes LiteSuite's headless children, because they load models too.

The agent remembers its own setup

An agent's backend, model and thinking level are saved in its home, .agents/<Name>/settings.json. Switching model or engine publishes there first, so the agent keeps its choice across launches; another agent you have open does not move. A conversation's own settings.json keeps the contextual knobs (the codex reasoning effort, which is a separate vocabulary, and the llama.cpp or LM Studio load settings for the model in use), and the global settings.json keeps being the defaults a new agent is born from, plus the app-wide knobs (theme, seat name, dialog style).

Resuming a conversation does not restore the backend, model or thinking level it was saved with: the agent's home decides, and a saved load setting for a different model is reported as not applied. A new agent home with no default model asks you to choose one with /model before the first send. A launch flag (--backend, --model, a thinking level) that differs from the agent's saved setting is refused with an explanation, and LITETUI_MODEL, LITETUI_THINKING and LITETUI_BACKEND only seed a new agent home; change a saved agent with /model, /backend and /think. Reconnecting does not request a local model load. If the saved model/provider is unavailable or connection fails, sending is blocked with an explanation; choose an available model/provider or retry /reconnect rather than silently falling back to another model.

The shared settings.json

Two processes from one checkout share one data root and therefore one settings.json — 70 keys, one file. A save reads it, writes back only the keys this instance changed, and replaces it atomically, so the think level you set in one window survives a theme change in the other. Env-sourced fields (LITETUI_MODEL, LITETUI_THINKING, LITETUI_BACKEND, …) are written on every save: the file records what you chose, so unsetting a variable must not silently revert the knob.

⚠️ background-tasks.json no longer lives in the data root. Each conversation keeps its own background-tasks.json in its conversation folder, so two windows only meet in a store when they share a conversation (which the session lease forbids). The old shared file is neither read nor written, and its rows are not copied into a conversation; task output logs still live in output/tasks/.

⚠️ The model ceiling is shared too. The llama.cpp router holds at most --models-max models and that limit belongs to whichever instance started it, so a load can push out a model the other window is mid-turn on. The instance doing the loading names what is resident before it displaces anything.

Or keep them completely separate

Give one its own root before it starts:

LITETUI_DATA_ROOT=~/.litetui-b litetui

That instance gets its own settings.json, .agents/, tasks and jobs — and, being a separate data root, no shared anything. LITETUI_SEAT_NAME names the default agent; LITETUI_MODEL, LITETUI_THINKING and LITETUI_BACKEND seed a new agent home and do not change a saved one.

Launching backends from a terminal or another agent

Run litetui --help (or python -m litetui.cli --help) for the complete argument contract. Unknown flags are errors. Options apply to this invocation and do not overwrite saved defaults; on an existing agent, a backend, model or thinking level that differs from the agent's saved one is refused.

Normal launches open the TUI in the current terminal. Add --rpc for JSONL over stdio. Agents must wait for the ready event with launch_status: "ready" and check its backend, model, base_url, thinking_level and context_length before sending a {"type":"prompt","message":"..."} command. Failed startup or an unavailable explicit model blocks the launch prompt. --prompt can supply an initial prompt for a headed launch; avoid putting sensitive text on process argv.

Backend Connect/start Model and context
custom --base-url URL; optional --start-server --server-command JSON Server-native arguments load the model. --model selects an advertised ID; --context-length declares the client budget, not a server resize.
llamacpp --server-mode connect or --start-server; optional --server-executable PATH and --base-url URL --load-model --model ID --context-length N; optional --model-path GGUF adds its directory to discovery.
lmstudio --server-mode connect or --start-server using installed lms server start --load-model --model ID --context-length N uses the LM Studio SDK.
ninfer Connect to discovery or --base-url URL; --start-server uses an allocated port Startup accepts --model-path ARTIFACT --context-length N --server-executable PATH. --model must match the served ID. Requires RTX 5090.
strata Connect to the install's port (8080) or --base-url URL; --start-server runs the install's own server Strata runs Qwen3.8-Flash-Next on one GPU plus system RAM. --model-path strata-MODEL.json picks a prepared model; --context-length N with --start-server starts it at that size. Install it and download models from LiteSuite's Model Hub, or with Strata's own START-HERE.bat.
codex Subscription login; connects using the configured Codex adapter (native app-server is managed by that adapter when enabled) --model ID --reasoning-effort LEVEL. Endpoint, context and output limits belong to Codex; local-server overrides are rejected.

For Codex, --codex-engine native selects the official app-server agent loop; --codex-engine http selects LiteTUI's own loop over the subscription HTTP API. Precedence is CLI flag > LITETUI_CODEX_ENGINE > saved conversation/global codex_native_engine setting > http default. The environment variable is resolved at CLI startup and accepts the same lowercase native/http values; an unset or empty variable leaves the saved setting alone. An invalid selected value is a startup error (an explicit valid flag supersedes the environment). Both inputs are invocation-only, including on resume: unrelated settings saves and new conversations do not remember the override. The existing Settings → Engine toggle remains the way to save a preference. Neither input selects the backend: use --backend codex or an already Codex-configured agent.

litetui --backend codex --codex-engine native
litetui --backend codex --codex-engine http
$env:LITETUI_CODEX_ENGINE = 'native'
litetui --backend codex
Remove-Item Env:LITETUI_CODEX_ENGINE

--server-mode auto preserves each backend's existing behavior. connect never starts an inference server. --start-server explicitly authorizes starting one; it does not restart or evict a server already running. An explicit llama.cpp URL never falls back to another router. --load-model explicitly authorizes loading or reloading on llama.cpp/LM Studio; context changes require this flag.

--thinking-level and --reasoning-effort are mutually exclusive aliases. none is accepted as an alias for UI off. Available levels depend on the backend/model: NInfer uses off/low/medium/xhigh; Codex uses its model metadata; custom servers receive the selected value and their template defines support. --max-tokens sets the output limit for local/custom servers. Use --server-timeout SECONDS for slow model loads (default 600).

Owner mode: --owner NAME --memory DIR

A seat whose long memory is a folder it does not own, so the same memory carries across conversations, compactions and restarts:

litetui --owner Alpha --memory C:\work\widgets-memory

DIR must be an absolute path to an existing folder, not a link or junction at any level, holding SOUL.md (the charter), NOW.md (where the seat is right now) and DOMAIN.md (what it knows; newest rows first). It should sit in a git repository: the handoff below is a commit. NAME opens (or creates) the agent home of that name for this launch. Both flags are per launch only; nothing is saved in the agent's settings.json, and a busy NAME, or a spawned seat named differently, is refused with the flag to change rather than opening another home.

A resume must use the saved conversation's store: supply the same --owner NAME and --memory DIR alongside --convo ID. Switching an ordinary conversation to owner mode, dropping owner flags, or changing its owner folder is refused before the conversation is adopted. The refusal gives the matching launch command. Ambiguous saved addresses require a new conversation without --convo; stored snapshots and recorded Claude segment prompts are never rewritten to migrate stores. New transcript snapshots/system edits and native segment records carry optional store provenance (version, mode, folder) in the same record as the prompt, not in settings. Older readers ignore this extra field. For older records without it, compatibility requires the complete generated address at its original slot immediately after an exactly recognized host base/plan prefix (and the same supplied native launch prefix, if any). File bodies are not searched. Changed templates or custom prefixes that cannot identify that slot refuse with legacy prompt has no verifiable host store slot and a command to start a new conversation without --convo; old files are not backfilled. The shipped two-message CLI shape is also recognized: message 0 must be the same supplied launch text (--system-prompt, --system-prompt-file, or the combined --cognitive-file text), optionally followed immediately by the generator's first index/store opening; message 1 must contain the complete host prefix and address. A fleet extension also qualifies when it begins with the host's exact composition: the supplied launch text with trailing whitespace stripped, two newlines, and the shared fleet-registration opening. The same joining function writes and recognizes that prefix; extra intervening text does not qualify. Only that beginning is verified. Everything after the first suffix opening is opaque, never parsed for identity, later section order, or a cut. Missing/changed launch text, an intervening prefix, or an incomplete host message refuses; markers inside the supplied launch text do not disqualify it. No later messages are searched. Native preset-only legacy sessions with no recorded prompt remain unverifiable and refuse with the same actionable new-conversation notice.

New prompts also carry an injection receipt beside their contents: quoted store headers in an index or memory file cannot suppress the real snapshot. A same-store legacy resume without that receipt preserves its prompt without guessing whether another block is needed. One notice on screen and in the agent's context explains that the store may not have loaded, names the exact files to read, and gives the explicit rebuild action: run /new, then send your next message. This does not block the first legacy send. Native Claude receives the notice with its next input, not by rewriting its fixed prompt; delivery is recorded only after the query returns. A crash between accepted query and receipt persistence can repeat the notice once on recovery rather than risk omitting it. Modern conversations get no legacy notice.

  • Injected in place of the home's own files. SOUL.md, NOW.md and DOMAIN.md, in that order, replace memory.md, soul.md and handoff.md in the snapshot every backend starts a conversation (or a Claude segment) with, capped at 8,000, 6,000 and the owner_domain_cap setting (default 30,000) characters. A file cut at its cap says so and names the path to read for the rest. The system prompt names DIR, the three files and the NOW.md template (headings Updated, Doing now, Next 3 steps, Waiting on, Crew alive, Do NOT redo; at most 60 lines).
  • One seat per folder. DIR/.owner.lock is created exclusively and records the holder's pid and agent id. A second launch is refused naming the holder; a lock whose process is gone is taken over with a one-line notice; the lock is removed on exit and is never committed by the handoff. A kernel lease on the persistent DIR/.owner.guard serializes metadata transactions, including stale takeover and release. Never remove the guard file: its inode anchors the lease. Add .owner.lock and .owner.guard to the memory repository's ignore file so a git add -A cannot commit them. A recycled pid can make a dead holder look alive (the refusal names the metadata file to delete).
  • Writes. NOW.md and DOMAIN.md are the seat's own store: writing them is never asked about, on any tool profile. SOUL.md gets no such exemption: it is judged like any other write outside the workspace, so it asks only on the strict profile. Matching follows the filesystem: on Windows now.md or NOW.md. name the same file and match; a link, a hardlink, a sibling or a subfolder never does.
  • Compaction rewrites NOW.md, then commits it, then truncates. STEP 1 of the compaction prompt has the seat rewrite NOW.md in the template and append anything new to DOMAIN.md. Before anything is cut, LiteTUI checks that NOW.md changed and runs liteharness owner-handoff --memory DIR (a subprocess; LiteTUI checks at launch that it exists) to validate the template and commit both files. If that fails the seat gets one retry turn naming the reason (the retry has its own tool-round budget on the local backends); if it fails again compaction goes ahead and a message in the transcript says NOW.md was NOT committed: <reason>, carrying the second reason. It is never skipped silently. If the retry turn itself errors, Claude goes ahead with the first summary and the same message; a local backend aborts the compaction and leaves the conversation unchanged. Pressing Stop during the rewrite runs no check and no retry. The next conversation starts from the rewritten files.
  • Codex. Codex compacts natively and has no STEP 1, so a manual /compact first sends one ordinary turn asking the seat for STEP 1, runs the same check and commit, and only then asks Codex to compact. Limit: Codex's own automatic compaction cannot be intercepted and can still fire without the handoff. To make the manual path the likely one, owner mode shows context 70%: run /compact to write NOW.md first once each time the context rises through 70% of the window, on every backend. The reminder says "70%" whatever the first reading was, and a momentary unknown reading re-arms it. If the driven Codex turn is not answered (the engine defers it or refuses it), the check fails, the retry fails, and compaction goes ahead with the NOW.md was NOT committed message; a turn that is not admitted at all compacts nothing.

Not covered: the GUI memory pane and sibling-seat launches from the UI do not carry the owner folder.

Codex HTTP failure handling

The OAuth/Responses transport retries HTTP 500/502/503/504 and selected connection failures up to two times, with 0.5s then 1s backoff, only before that completion request emits any text, reasoning, tool delta or usage chunk. The same retry budget covers connection loss while waiting for the first chunk. A retry resends that request, not the agent turn; previously completed host tools are not rerun. The server may still have processed a failed request before the connection broke, so this is not an exactly-once generation guarantee.

After any output, failure is surfaced without automatic replay. SSE error/response.failed/response.incomplete events report their supplied code/message/reason (including max_output_tokens or content_filter when present), rather than guessing that context is the cause. These events are not automatically retried. Bounded, secret-redacted diagnostics also go to .logs/runtime-errors.log; raw response bodies and authorization headers do not. HTTP server failures name the status and retry count rather than blaming login. Existing 401 login refresh, 403 access refusal and 429 usage-limit behavior is unchanged. Native Codex app-server retry behavior is separate and unchanged.

Codex tool-argument diagnostics

The OAuth/Responses transport preserves terminal-only function arguments from argument-done, item-done and completed events, including expanded parallel batches. It appends only the missing suffix, not duplicate JSON. Conflicting snapshots fail before pending calls are released. This is a demonstrated protocol fix, not a conclusive attribution of historical missing-argument incidents: saved conversations retain assembled calls, not the original stream. There is no local multi_tool_use.parallel unpacker.

LITETUI_CAPTURE_TOOL_EVENTS=1 enables metadata-only diagnostics for a future explicitly authorized session (OFF by default; no restart/probe is implied). Only a currently leased agent can append to its own conversation's tool-call-stream.jsonl. Ownership and non-linked paths are checked on every append; linked/reparse-point paths, hard-linked files and unowned fallbacks are refused. No arbitrary output path or raw-argument mode exists.

Records contain event type, per-request sequence, bounded call ID/tool name, and argument/delta character count only. No argument content, headers, auth, reasoning, output text or tool results are saved: arguments can contain sensitive credentials, paths and private text. This instrument shows event order and where nonempty fields arrived, but cannot reconstruct a wrapper or validate its JSON.

Retention is one fixed file per conversation, capped at 1 MiB. At the cap, recording stops without rotation, deletion or growth. Missing folders are not created. Records append across requests (sequence numbers restart); disabling the flag does not remove evidence. Explicit owner cleanup is separate. This uses the store's normal lease/path protections, not a sandbox against hostile filesystem races. Storage refusal only stops diagnostics, not the model reply.

parallel_tool_calls remains True. Setting it to False would ask the provider for at most one function per response, serializing independent tools into extra model round trips (latency/token cost). It would neither repair received batches nor guarantee provider compliance; no new parallel-disable switch was added.

Examples (PowerShell):

# Existing Prism or another OpenAI-compatible server, no environment override needed
litetui --backend custom --base-url http://127.0.0.1:1235/v1 --model ternary-bonsai-2-27b --thinking-level off --context-length 32768

# Start our llama.cpp router, then load a model before accepting the first prompt
litetui --backend llamacpp --start-server --load-model --model qwen3.8-27b-q4_k_m --context-length 32768 --thinking-level medium

# Start LM Studio's API server (LM Studio/lms must already be installed)
litetui --backend lmstudio --start-server --load-model --model qwen/qwen3.8-27b --context-length 32768 --thinking-level medium

# Start NInfer from an explicitly chosen artifact; verify its advertised model ID
litetui --backend ninfer --start-server --model-path D:/models/model.ninfer --model YOUR_SERVED_MODEL_ID --context-length 32768 --thinking-level xhigh

litetui --backend codex --model gpt-5.6-sol --reasoning-effort high

# Custom executable arguments are a JSON array, never a shell command.
# A PowerShell script file avoids nested Start-Process -Command quote loss.
$serverArgs = @('D:/Prism/llama-server.exe', '-m', '{model_path}', '--alias', '{model}', '-c', '{context_length}', '--host', '{host}', '--port', '{port}', '--jinja') | ConvertTo-Json -Compress
litetui --backend custom --base-url http://127.0.0.1:1235/v1 --start-server --server-command $serverArgs --model-path D:/models/bonsai.gguf --model bonsai --context-length 32768 --thinking-level off

Custom command placeholders are {model}, {model_path}, {context_length}, {host} and {port}. Add native flags (GPU layers, projector, cache settings) as separate array elements. Only loopback endpoints can be started locally. Output goes to custom-server.log in the data root. LiteTUI cleans up custom processes it starts; attaching to an existing server never claims ownership. For authentication use --api-key-env VARIABLE_NAME; the secret stays in the environment. Custom URL, key-variable name and declared context budget are also available under /settings, and /backend includes Custom.

The internal LaunchSpec accepts the same controls in its optional launch object (base_url, server_mode, load_model, context_length, max_tokens, server_executable, model_path, server_command, api_key_env, timeout). Its supervised headless path supports all five backends and verifies readiness before forwarding the prompt. For headed launches use the public CLI in a visible terminal; the internal headed transport remains unavailable.

Harness seat

LiteTUI registers as a LiteHarness agent and monitors its own inbox, so other agents can reach it and mail wakes it — the message is delivered as a turn.

It does not run liteharness.hooks watch or check_inbox. Both are consumers that move files out of inbox/new/ for whichever agent id they resolve, and a second consumer on a shared mailbox is the defect the ls-liteharness fix retracted — mail vanished for three hours. watch_inbox also writes to stdout, which a Textual app cannot survive.

Instead it reads the maildir directly under one rule:

Only ever touch a file whose to is this agent.

Anything addressed elsewhere is left in new/ exactly as found — unread, unmoved, unclaimed. Expired messages (past ttl_minutes) are cleared without delivery; the agent's own echo is skipped.

Approval replies and explicit authority changes

A pending RPC approval widget also accepts an addressed inbox line APPROVE appr-<id> or DENY appr-<id> from the spawner recorded when that request started. It resolves once, never remembers permission; a wrong sender, addressee, unknown/expired id or duplicate cannot grant approval. The RPC host's own answer path is unchanged. Ordinary mail is still delivered as a turn.

Spawner authority is captured from the launch identity envelope at process startup, not read back from registry metadata. Editing spawned_by in a presence row or re-registering a running seat does not grant another leader approval authority. After a leader restart, changing that authority is an explicit security action: use the supported liteharness spawn --resume <id> --kill-old --spawned-by <newleader> flow to start the resumed seat with the new launcher identity. There is no automatic parent-lineage redirect, registry takeover or fallback to a different approver.

Tests

Focused local merge check

Before integrating a candidate, the reviewer names its affected production domain, existing neighboring tests, and intentional omissions in a JSON evidence file:

{
  "base": "<full target commit ID before integration>",
  "candidate": "<full reviewed worker commit ID>",
  "domain": "sidecar jobs and owned app startup",
  "review": "<reviewer and durable review reference>",
  "neighbors": ["tests/test_sidecar_jobs_t1082.py", "tests/test_sidecar_reader.py"],
  "exceptions": []
}

On a clean, committed integration-preview tree containing both commits, run:

python tools/merge_gate.py --base <target-before> --candidate <worker-tip> --review-evidence <review.json> tests/test_new_regression.py

Use the project's existing development interpreter; the final positional test files are optional additions. Keep evidence outside the checkout or in an ignored scratch directory. The command requires a nonempty domain, review reference and neighbor selection; neighbors must already exist at the base commit. Evidence must name the exact resolved base/candidate IDs. exceptions must be present, with reasons for intentional omissions (or []), and cannot bypass required tests.

Every run includes test_autoscroll.py and test_sidecar_jobs_t1082.py: these two files were found red on main by neighboring checks on 2026-10-07. This is a small incident-derived floor, not automatic dependency discovery. Select additional existing neighbors by tracing the changed behavior, not just the new test file. The command reuses run_all's inventory and per-file collection accounting, runs only the selected files with their proper runner, checks git diff --check, and fails on errors, zero-item selected files, timeouts, or a changed/dirty tree. Inherited PYTEST_ADDOPTS and configured pytest addopts are deliberately cleared so they cannot silently expand the selection. Other pytest configuration remains. Its PASS names the exact HEAD tested; rerun if the integrated tree changes.

Enforced: evidence shape and commit identity, existing-neighbor selection, commit ancestry, clean stable tree, and selected-check exit status. Reviewed by people: whether the domain and neighbors cover the actual change, exceptions are justified, and the review reference is genuine. This command does not authenticate the reviewer, install hooks, merge, or enforce GitHub branch protection. The merge owner must require it on the actual integration tree before accepting that tree; a worker-only green is not integration evidence. Required review, current-intent and human-look gates remain separate.

Full validation (when explicitly requested)

uv run --locked python tests/run_all.py

Believe the exit code rather than a pass count. The runner maintains a reviewed inventory of pytest files and legacy standalone scripts; bare pytest is not a substitute for that inventory. CI's full validation is unchanged. Neither engine nor network is required by the selected unit tests; no live-service checks are included in the focused merge command.

Bundled

chrome-bridge/ — drive Chrome from Python (navigate, read page, click, screenshot) via an MV3 extension. pccontrol/ — Win32 mouse/keyboard/window automation and OCR helpers. Both are separate tools that happen to live here; chrome-bridge also has a canonical copy elsewhere, so treat this one as a snapshot rather than the source of truth.

Metadata

Release files for litetui 0.26.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for litetui 0.26.0
File Size Uploaded
litetui-0.26.0.tar.gz 2.7 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for litetui 0.26.0
File Interpreter ABI Platform
litetui-0.26.0-py3-none-any.whl Python 3 none any Details

Total release size: 4.2 MB

Release files / litetui-0.26.0.tar.gz

Download URL litetui-0.26.0.tar.gz
Size 2.7 MB
Tags Source
SHA-256 checksum
How to use checksums
8b33df4e17635ea45fa5623df15ff3af77c3a56028a4ab3cb6db9aa0bfd794ce
BLAKE2b-256 checksum
How to use checksums
c375c4569ecd91ae97d84891b0b4f8ae93d456da02f7f6a912f9d06ac4f3cb91
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.9

Release files / litetui-0.26.0-py3-none-any.whl

Download URL litetui-0.26.0-py3-none-any.whl
Size 1.5 MB
Tags Python 3
SHA-256 checksum
How to use checksums
81344686726422aa54a8204c7b0a3ed0cd7d2958a43d0adcf7b87f80904a7a0e
BLAKE2b-256 checksum
How to use checksums
c33f0476d13c9055bd23afb0648898c6962d9bf18ae0f16c9e9c82d7a00116d9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.9

Release history Release notifications | RSS feed

This release

0.26.0 This release

2 release files

0.25.1

2 release files

0.24.0

2 release files

0.23.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page