Architecture
System architecture
| Crate | Responsibility |
|---|---|
latch-protocol |
Events, task/memory/evidence records, provider and extension types, shared display formatting |
latch-kernel |
Store, context-engine port, continuity, prompts, policy, tools, providers, extensions, validation/evidence, failure and progress supervision, permissions, child-agent graph/workers, capability vocabulary, token estimation, loop, session resume |
latch-ui |
Interface-neutral input/output contracts, provider configuration display/edit types, shared slash catalog, semantic transcript/diff/sidebar/agent/group reducers; no terminal or HTTP dependencies |
latch-tui |
Typed transcript, slash palette, input editor, prompt history, semantic rendering |
latch-cli |
Configuration, resume orchestration, provider setup, slash-command coordination, Linux/Windows loopback Web adapter |
latch-cli/src/cli/interactive.rs owns the shared interactive session controller,
including steering, cancellation, human approvals, setup persistence and live
profile changes. The terminal adapter supplies channels to that controller;
interface contracts live in latch-ui and are re-exported by latch-tui for
compatibility. Durable protocol stays provider-neutral; provider serialization
stays in the kernel.
The Linux/Windows Web adapter in latch-cli/src/web/ embeds web/app/ assets and supplies
HTTP input/output channels to the same controller. Its actor owns one active
session in the canonical launch workspace. The host opens one reusable
EventStore for session listing and selection; HTTP reads never reopen the
store or trigger its write-capable startup migration/projection rebuild. The
active agent retains its kernel-owned store. Its browser projection uses the
shared latch-ui transcript, diff, sidebar, agent and group reducers. SSE
reconnects reconstruct current state without owning history or cancelling runs.
Authentication, same-origin checks, command receipts and media routes remain
transport concerns. See the Web guide for contracts and module
ownership. Windows Web execution uses the same embedded native boundary as
the CLI and TUI. web/app/markdown.js owns browser Markdown formatting,
including escaped semantic tables and code-aware cell splitting; view.js
uses it for live text, durable assistant cells and agent reports. Wide tables
scroll in their own focusable region without changing transcript/page width.
latch-ui/src/activity.rs reduces durable run, request, tool and permission
lifecycle events together with transient stream activity into sidebar phases.
Providers emit connected/receiving/reasoning/tool-call signals through
latch-protocol::StreamActivity; these carry no reasoning text and do not change
provider request serialization or durable history. The shared controller relays
these signals and cancellation/run-failure observations to both adapters.
TUI refresh ticks and Web connection heartbeats update presentation only;
neither is evidence of provider progress or task completion. Replayed unfinished
runs are interrupted rather than displayed as a live operation.
The runtime platform model — kernel invariants extensions can never bypass,
replaceable ports, the capability vocabulary, transport independence, and the
implemented-versus-deferred port map — is
RUNTIME_CAPABILITY_MODEL.md.
Architecture diagram source (Mermaid):
flowchart LR
U[User / TUI] --> K[Agent loop]
K --> CE[Context engine port]
CE --> C[ContinuityEngine default]
C --> S[(SQLite events + memory)]
K --> P[Prompt compiler]
K --> V[validate: kernel-run validation]
V --> E[Evidence ledger]
E --> CP[Completion derivation]
K --> M[Provider adapter]
M -->|deltas| U
M -->|tool calls| K
K --> G[Policy + scheduler]
G --> W[Workspace / processes]
G --> L[Durable change ledger]
K --> F[Failure supervision]
K --> PS[Progress supervision]
K --> PB[Permission broker]
PB --> U
K --> AS[Root AgentSupervisor]
AS --> A1[Child session worker]
AS --> A2[Child session worker]
A1 --> S
A2 --> S
X[stdio extensions] <--> K
Session history management¶
EventStore::delete_sessions records explicit root-session history tombstones in
deleted_sessions using one immediate SQLite transaction. It validates every
UUID, root identity and optional workspace scope before committing a batch of
1–100 selections. list_sessions, latest_session and resolve_session exclude
tombstones; the CLI rechecks selection before building a resumed controller.
History deletion does not alter raw events, child topology, memories, FTS,
operations, group projections or artifacts. Workspace mutation/lifecycle queries
continue to include deleted history so old validation cannot become valid again.
Already-open controllers can append without resurrecting history visibility.
The TUI picker owns selection and confirmation only; a CLI callback invokes the kernel operation. The Web actor serializes deletion with activation, protects the current session, limits deletion to the launch workspace and emits a change notification after commit. HTTP/SSE and browser projections own no durable truth.
Providers and inference profiles¶
ChatGPT subscription authentication¶
latch-cli/src/cli/login.rs coordinates explicit human login/logout and provider
configuration; interactive.rs shares that flow between TUI and Web without
blocking cancellation or accepting model prompts during authentication. /login
selects the subscription for the live session only after token/config commit;
existing explicit new-session defaults stay intact. Shared LoginChallenge and
LoginFinished outputs are transient UI state, not durable protocol events.
kernel/src/oauth.rs owns OpenAI Codex device authorization, token validation,
restricted chatgpt-auth.toml storage under the resolved state directory, and
refresh/login/logout serialization with an OS file lock. Tokens are reloaded
at each subscription request, so independent child providers and other Latch
processes share rotated credentials rather than stale snapshots. Failed refresh
never deletes credentials or falls back to paid API billing. doctor and setup
check credential presence offline; they never refresh tokens.
The openai-codex provider kind is distinct from API-key openai. It requires
oauth:openai-codex, the fixed HTTPS Codex endpoint and Responses transport;
configuration and provider construction both refuse credential/endpoint mixing.
provider.rs owns account/session headers and all subscription wire mapping,
reusing stateless Responses history, encrypted reasoning and tool-result replay.
Subscription streams must report terminal completion; a truncated stream is not
success. The subscription catalog is refreshed through the authenticated Codex
/models endpoint after login, with a current offline fallback; its context
windows and efforts are separate from the public API catalog and API prices.
Catalog refresh resolves the official stable Codex version through GitHub's
public release redirect (three-second timeout, no credentials and no redirects
followed), then uses it only as the catalog client_version. If resolution fails,
it uses the last version that produced a valid catalog, or the embedded 0.166.0
fallback. The model's minimal_client_version is not a sufficient discovery gate.
Startup remains offline; requests still identify the application as Latch.
Entitlement is server-owned.
Tokens and lock files must be ordinary, unaliased files in protected state;
model tools, extensions and sandboxed commands do not receive them. Local logout
removes saved tokens; it does not revoke already in-flight requests or OpenAI's
remote authorization. See ChatGPT subscriptions.
ResolvedPaths (kernel/src/paths.rs) owns configuration and storage path
discovery. New installations use a private ~/.latch/ root for config,
secrets, SQLite sessions, artifacts, and regenerable cache data. When the new
config is absent, an exact live XDG config is read in place. The explicit
latch migrate command copies the SQLite database with SQLite's backup API
and copies artifacts and secrets before publishing the new config. It writes a
migration marker and renames legacy config and secrets to backup names so
deleting the new config cannot reactivate the old installation.
latch uninstall is explicit host maintenance in latch-cli/src/cli/uninstall.rs,
not a model tool or an execution-backend fallback. Its numbered English menu
keeps all storage by default; EOF/cancellation removes nothing. Full removal
lists new/legacy/configured storage paths and requires DELETE; custom roots
with unrelated entries or shared home/workspace scope fail before deletion.
Symlinks/junctions are not traversed. The CLI reads only the storage selector
from config, so provider validation and authentication are never prerequisites.
It removes the running executable and identical standard-install copies, without
editing PATH/package-manager records. Windows uses a credential-free, fixed
Windows PowerShell helper to remove program files after parent exit; protected
boundary recovery journals outside app storage are never purged. Native CI tests
use unique disposable binaries, including paths with spaces/quotes, so the
installed binary and real user storage cannot become test deletion targets.
latch doctor reads the same ResolvedPaths result and reports every path,
its source, and migration provenance. It prints credential references only.
Default interactive launches require a configuration file at the resolved
location; an ambient OpenAI credential does not turn legacy fallback defaults
into a configured installation. Missing files or credentials trigger setup;
explicit profile overrides retain their launch behavior. SetupPaths reports
the resolved location and whether the file exists. The Web adapter hides
unpersisted fallback provider rows during onboarding and uses the shared setup
catalog, persistence and validation for all saves. Save results remain visible
in the dialog, and provider/model discovery updates refresh the current page.
Adding a provider through setup or login first materializes any real legacy
single-provider installation using the same conversion as registry fallback,
including unknown-model transport, metadata, credential reference and effective
new-session default. The new explicit provider map must not hide the old account.
Missing-config startup defaults are never materialized as configured accounts.
TUI Add provider allocates an unused instance id (deepseek, deepseek-2, …),
matching Web setup; editing an existing provider remains an explicit operation.
All configured families stay available together in /model after save and reload.
Updating the active provider’s credential rebuilds its live adapter and refreshes
the profile header when the new credential resolves; this also unlocks the
first-run stub without requiring a second model selection.
Setup saves validate the proposed provider profile before writing. Config and
secrets are staged in their destination directories and synced; a new secret
is committed before config publishes its file:<id> reference. A failed
config commit can leave an unused secret, while a failed secret commit leaves
the previous config in place.
On Windows the staged file is flushed before rename; std::fs cannot open a
directory for the post-rename directory sync used on Unix.
Validation checks provider ids, HTTP(S) URLs, symbolic credential syntax,
provider defaults, model selections, and positive context windows. Credential
parse errors omit the supplied value so a pasted key cannot enter diagnostics.
Provider handling is a layered subsystem, not a set of endpoint conditionals:
Provider configuration [providers.<id>] + legacy [provider]
-> Provider capabilities
-> Model catalog / metadata
-> InferenceProfile provider + model + reasoning effort
-> Agent runtime
-> Provider adapter OpenAI-compatible / Anthropic wire format
ProviderRegistry (kernel/src/providers.rs) owns provider instances, the
built-in catalog, user metadata merging, effort capability resolution, alias
resolution, and provider construction. Precedence for model metadata is
explicit user configuration > built-in catalog > conservative default; an
optional enabled_models list filters the offered catalog without becoming
metadata, while per-model config tables store sparse user overrides. An
unknown model gets no invented context window, pricing, cache shape, or
reasoning parameters. ProviderConfig entries carry a symbolic credential
reference (env:NAME, file:NAME, keyring:NAME), and
providers.<id>.default_model selects a model when switching provider;
[inference] is the new-session default, seeded only when absent or invalid.
The live session profile is durable in the event log and never writes either
default back to config. kernel/src/credentials.rs resolves credential
references at process start from the environment
or a 0600 local secrets file. Secret values never enter the config file, the
durable event log, the model context, the transcript, or ordinary logs, and
provider error bodies are redacted.
The configuration center has an explicit Set as new-session default action;
it updates [inference] without changing the live session profile or either
provider's default_model.
InferenceProfile is provider-neutral and credential-free. The agent holds the
effective profile; Agent::set_inference_profile swaps the provider adapter,
model, token estimator, context window, pricing metadata, and reasoning-replay
policy together, resets architecture prefix accounting, and appends a durable
InferenceProfileChanged event. Continuity observes that event and starts a
fresh cache epoch with the reason inference profile changed, so provider
cache locality is never claimed across incompatible wire semantics. Resume
restores the last durable profile but resolves credentials freshly. A child's
effective profile is pinned durably in its own session at spawn
(InferenceProfileChanged, reason inherited from root at spawn); worker
recreation and process resume rebuild the child from that record, while a new
child inherits the root's current profile. Reasoning effort is emitted on the
wire only when the resolved model capability advertises the selected value
(the neutral set is none/minimal/low/medium/high/xhigh/max, but
each model exposes only its documented subset); otherwise the adapter sends no
effort field.
Transport is a per-model capability: OpenAI reasoning models use the
Responses API (stateless store: false replay with
reasoning.encrypted_content, captured from the response stream into the
durable assistant event and echoed back exactly once), Anthropic models use
Messages with adaptive thinking and exact thinking/redacted-block replay,
DeepSeek uses Chat Completions with explicit reasoning_effort and the
thinking toggle, and OpenCode Go resolves transport and reasoning efforts per
model from its documented list.
OpenCode Zen uses the existing adapters for its documented Responses,
Messages, and Chat Completions models and a Gemini generateContent adapter
for its Gemini 3 rows at https://opencode.ai/zen/v1; Go's stable session
header stays Go-only. The Zen transport list follows
https://opencode.ai/docs/en/zen/.
The Gemini adapter streams models/{model}:streamGenerateContent?alt=sse,
serializes contents with text, inlineData images, functionCall, and
functionResponse parts (multimodal results nest as functionResponse.parts),
preserves provider-native function-call ids with a deterministic fallback only
when the API omits one, and correlates tool results by call id (the function
name is recovered from history because the wire requires it, never used as the
correlation key). Thinking controls nest under
generationConfig.thinkingConfig: includeThoughts requests summaries and
the model's declared capability decides whether thinkingLevel or
thinkingBudget is emitted. Gemini thought and function-call signatures are
positional replay state: the provider-neutral ReasoningArtifact sequence
records every thought part, signed text part, and tool-call position in order,
survives the durable event log and SQLite, and is replayed exactly on resume;
non-Gemini providers ignore those variants. Run boundaries are durable
(RunStarted/RunCompleted), so per-run accounting is explicit while session
totals remain cumulative.
effort_map is user configuration only for Custom/Advanced models: the
resolved per-level map travels on the ModelDescriptor and each transport
adapter serializes the form it documents (reasoning_effort,
reasoning.effort, output_config.effort, thinking.budget_tokens,
thinkingConfig.thinkingLevel/thinkingBudget, or the documented off
switch). Validation enforces coverage of every exposed level, a default that
is exposed, and transport expressibility; the kernel loop and durable memory
never see a wire mapping. Built-in models keep using the adapter's catalog
mapping and need no copied map. Gemini is the exception that proves the rule:
its forms are model-declared, so each Gemini ModelDescriptor carries a
gemini_thinking capability (levels with the documented level set and
optional off level, or budget with zero_allowed); built-in rows get theirs
from the catalog and Custom/Advanced declares it explicitly. Validation
rejects a value map on a budget model, a budget_tokens map on a level
model, and a disabled map when the model documents no off switch.
Model discovery supports OpenCode Go/Zen, DeepSeek and authenticated ChatGPT
subscriptions. Setup/login refresh the configured service; /model refresh or
setup's Refresh action updates it later. Startup and doctor remain offline.
The CLI fetches bounded lists and allowlists ids, labels, declared transport,
context, reasoning and image metadata; it ignores remote URLs, instructions,
prices and permission claims. Id-only unknown models on multi-protocol gateways
remain unresolved; DeepSeek/Codex use their fixed family protocols. The TUI
and Web receive provider-neutral catalogs and never perform provider requests.
Metadata precedence is explicit user overrides > discovered facts > offline
builtins (existing Go wire routes remain pinned). Generic effort labels do not
invent Gemini level/budget or Anthropic adaptive controls; unknown native models
use the service default until those facts are declared in Advanced. Sanitized
facts live in providers.<id>.catalog, separate from user
models overrides. Each successful refresh replaces picker availability but
retains past descriptors for resume; deleting the availability cache cannot
break a saved model's transport. Failed/empty refreshes do not replace the saved
catalog. OpenCode endpoints and session headers are unchanged; discovered model
API URLs are not adopted. Service listings are not a guarantee of key entitlement,
quota or plan availability, and grant no tool permissions.
New setup enables the full catalog with one credential; a deliberate subset
remains a local allowlist. /model all clears those allowlists, including old
single-model setup filters. Retired experimental DeepSeek vision names and the
old initial Codex-only 5.4/5.3 list are not advertised by offline pickers; their
wire descriptors remain available to old sessions. Fresh service lists determine
current picker availability.
The TUI consumes a provider-neutral catalog from the CLI for /model and
/setup; it never inspects base URLs, model families, or wire parameters.
configuration_center.rs owns the provider-list navigation state and emits
field-level actions for Add, Credential, Models, Add custom model, Default
model, Advanced (base URL, provider name, display name, transport, context
window, replay policy, adaptive thinking, input modalities, aliases, effort
levels, default effort, the effort wire map, and the Gemini thinking
capability), new-session default, and Remove. It holds only symbolic credential references and display statuses;
the CLI/kernel owns resolution and persistence. The CLI sends
SetupProviders rows with credential, model, and override readiness, the
resolved storage destination shown on Save review, and refreshes them after
setup changes. The TUI renders the rows through the existing windowed choice
surface.
Known providers use KnownProviderFlow under the configuration center: it
collects a credential, enabled model selection, and a provider default before
Save. Custom Provider uses CustomProviderFlow, which additionally asks for
protocol and base URL and one custom model. Both plans carry selections only;
catalog metadata stays in the kernel, and each Advanced edit writes exactly
one override field. Custom model entries require an explicit transport
override before activation; a user-declared OpenAI-compatible provider
resolves its models through the protocol chosen at provider setup.
An unknown id can retain display metadata in setup while the registry excludes
it from /model; legacy single-provider custom models receive their former
default transport during normalization to preserve existing installations.
Configuration may come from the interactive flows, config.toml, or CLI
overrides, with precedence CLI/session override > durable session profile >
user config > built-in defaults.
Prompt architecture¶
PromptCompiler assembles the model-facing prompt from prioritized fragments
and splits it into a session-independent system prefix (the cacheable
behavioral core and kernel semantics) and a session context (workspace,
repository instructions, mode). The agent passes the prefix as the provider
system field and renders the session context as the first provider-visible
message, so the system + tools prefix is byte-identical across sessions and
mode switches and stays in the provider's prompt cache. The architecture
follows publicly documented Codex CLI prompt
structure (small behavioral core, explicit autonomy-and-persistence and
validation sections) and Anthropic's official proportional-effort guidance
(skip planning for straightforward work, verify with the narrowest meaningful
check, delegate only parallelizable work).
The stable prefix is a small behavioral core, in order: core.identity,
core.general, core.effort, core.scope, core.planning, core.tool_use,
core.editing, core.validation, and core.communication. Latch kernel
semantics follow: latch.kernel_truth, latch.context_and_staleness,
latch.permissions, and latch.subagents. Mode text (mode.ask,
mode.plan, mode.work) and per-session context (environment.workspace,
environment.instructions.*) come last.
The prompt asks the model to call complete after validation and include its
final summary in that same assistant message, because a verified claim can end
the run immediately. This instruction improves model behavior but the kernel
still determines completion from durable evidence.
Effort is proportional. Simple, local work inspects only what it touches, makes
the smallest coherent change, runs the narrowest meaningful check, and stops
once the requested behavior is directly demonstrated. Standard work completes
the requested scope with targeted validation and broadens only on evidence.
Complex or long-horizon work may plan, inspect dependencies, validate broadly,
and delegate independent work. The stopping rule is explicit: stop at direct,
relevant evidence instead of searching for extra confidence, unrelated defects,
or cleanup. The continue-until-done rule lives in exactly one fragment
(core.effort) so persistence never competes with the stop rule.
Only the stable fragments are cacheable; dynamic context is never mixed into them. The compiled prompt is intentionally bounded and covered by tests that pin the fragment order, forbid obsolete blanket-persistence text, keep Latch-specific tool/runtime guidance and kernel semantics, and cap its size (the cacheable static prefix is pinned by a test cap of 1,329 estimated tokens; the session and mode fragments are ordered after it and are not counted).
Live steering¶
Each session has one agent loop and one conversation. While a run is in flight the
interactive layer keeps a SteeringQueue handle: submitted text is pushed
there in FIFO order and the TUI shows a single steering queued notice. The
queue is an atomic run-closing handshake. A run opens it at start; when the
model answers with plain text the loop atomically closes the queue and takes
everything accepted before that instant. If anything was accepted, the run
stays open and must consume it with another turn before it may exit; if
nothing was pending the run closes for good. A submission that loses the race
is rejected with a deterministic Closed outcome, is never enqueued, and the
interactive layer surfaces it as a new request instead of leaving it for a
later run. Aborted runs (Ctrl+C or a provider error) drop accepted-but-
unconsumed steers rather than leaking them; Ctrl+C semantics are unchanged.
The loop drains accepted messages only at safe model boundaries — after every
prior tool transaction has a terminal result and before the next
ModelRequest is constructed — and records each message as a normal durable
UserMessage with the same constraint-memory and extension-observation
provenance as an ordinary prompt. Injected turns therefore appear in the
volatile/append-only portion of the current context epoch, resume and replay
identically, and can never be placed between an assistant tool call and its
results. Newly drained steers become the retrieval query for the next
materialization (an ordered combination when several arrive together), so a
direction change can pull older session material into the volatile kernel
context tail; ordinary continuation turns keep no query and do not trigger
surprise recall.
A steer can also arrive while the model is executing an assistant turn that
proposed several sequential side-effecting calls. The in-flight call finishes
normally, but any not-yet-started side-effecting call is not blindly executed:
it receives a synthetic terminal result (superseded by newer user steering)
and the model re-plans under the newer instruction. Classification reuses the
safety capability set — only calls that the kernel already classifies as
pure workspace reads are allowed to proceed — and read-only calls that were
already started concurrently may finish. Every tool call still has exactly one
terminal result, and cancellation stays separate: Ctrl+C cancels the run,
while steering never touches the in-flight request, tool, or managed process.
Durable child-agent graph¶
The root AgentSupervisor owns an agent graph and independently spawned async
workers; the root Agent never stores child Agent values. A child id is its
durable session id. AgentSpawned is the first event in that session and is
committed in the same SQLite transaction as session creation, carrying the
root/parent relationship, name, optional type, depth, and compact delegation
brief. Status, queued/delivered messages, reports, interrupt, and close are
append-only child-session events. Graph replay reduces those events in stable
name/id order. A persisted Starting or Running state with no surviving
runtime is reconciled once to Interrupted; it is never rerun implicitly.
First starts reserve the worker registry before publishing the child in the runtime graph. First starts and reconstruction share the registry lock through worker creation and registration, so concurrent messages cannot create a second worker or replace the worker carrying the initial delegation brief.
The model-facing schema is fixed: spawn_agent, send_agent_message,
continue_agent, wait_agents, list_agents, interrupt_agent, and
close_agent exist regardless of graph contents. Spawn is asynchronous;
queue-only messages do not wake an idle child, while continue starts a turn or
uses the child's steering queue at its next safe boundary. wait_agents
returns statuses only — report bodies travel on exactly one channel, the
durable kernel notification delivered with the request that reads the wait
result. Interrupt cancels a turn but retains the worker/mailbox. Close
cancels, waits for resource release, and records the terminal lifecycle. Root
shutdown closes all live workers. A child whose worker restarts still owes its
opening turn: an undelivered delegation brief is replayed from AgentSpawned
ahead of any follow-up. Depth is limited to one, so a child receives the
stable tools but every agent control call fails with a normal terminal tool
result.
Child context begins fresh: its first user turn is the delegation brief, and
its session context independently loads workspace repository instructions. Task
state, evidence, failure and progress supervision, continuity, and cache epochs
are reconstructed solely from that child session. AgentReport exposes only
semantic evidence references; it never imports evidence into the parent ledger.
All executors share the root workspace mutation/ownership coordinator and the
live root policy locks as a capability ceiling, but processes, grants, and
permission brokers remain session-local. OpenAI-compatible transports that use
session metadata are cloned with the child id, preserving OpenCode Go's stable
x-opencode-session behavior.
Workers never append provider-visible events to the parent asynchronously.
They queue compact reports in the supervisor; the root loop drains them only
before constructing a model request or after a plain assistant answer, when all
prior tool calls already have terminal results. It then appends
AgentNotificationDelivered, the sole graph event visible to parent context
and the durable resume dedupe marker; the TUI renders it as one quiet
transcript line. Internal graph/mailbox events are also excluded from FTS
recall.
Agent groups: durable coordination overlay¶
Three layers stay separate. AgentGraph owns topology and lifecycle,
AgentSupervisor owns execution and control, and AgentGroup is an optional,
root-scoped coordination overlay: a shared task DAG, atomic claims, a durable
peer mailbox, and replayable progress state. The group never owns workers and
never replaces the supervisor; children remain independent durable sessions
created by the existing spawn path.
The group is created lazily on first use (create task, join, message, or a
spawn delegated with task_id), so an ordinary session never grows group
state. One root session has at most one group in this version. A group id is
durable and stable across resume.
Group events are ordinary durable events in the root session:
AgentGroupCreated, AgentGroupMemberJoined, GroupTaskCreated,
GroupTaskClaimed, GroupTaskStatusChanged, GroupTaskReleased,
GroupMessageQueued, and GroupMessageDelivered. A SQLite projection
(agent_groups, group_members, group_tasks, group_messages,
group_message_deliveries) exists for atomic claims and efficient lookup, and
is rebuilt from the event log at every store open; no group truth lives only in
the projection. GroupState::replay reduces the same events deterministically
for status, readiness, blockers, and conflict warnings.
A task is a small coordination record: title, description, status, dependency
ids, optional assignee, required (default true), and summary/findings/touched
paths on completion. A task is ready only when it is Pending and every
dependency is Completed. The graph is validated as a real DAG at creation:
unknown ids, self-dependency, duplicates, and cycles are hard local errors, and
a cyclic graph is never silently repaired. Blocked and Cancelled
dependencies never satisfy a dependent task; group status and group_status
surface those dependency failures explicitly.
Claiming is a true atomic compare-and-set. EventStore::claim_group_task
opens BEGIN IMMEDIATE, re-reads the projection, verifies Pending with
complete dependencies, appends GroupTaskClaimed, and commits — so any number
of concurrent claimers produce exactly one owner. Release, start, complete,
block, cancel, and reassignment use the same transactional pattern with the
ownership check inside the transaction. Only the assignee may transition a
task; only the root may create, cancel, or reassign one, and only to a group
member. Completing a group task is coordination truth, never root evidence:
the child evidence ledger and root certification remain untouched.
Membership is explicit and durable (AgentGroupMemberJoined); children of the
root join when they are delegated a task or first participate. Messages are
compact text and information-only: no transcript copying and no automatic
context merging. GroupMessageQueued is durable in the root log; delivery
happens only at a recipient's safe model boundary, where
GroupMessageDelivered is appended to that recipient's own session in the same
transaction as its delivery marker. FIFO per recipient, exactly-once across
restarts, and sending never wakes an idle or completed child — an explicit
continue_agent is still required for new work.
The kernel gates terminal root complete: while the active group has required
tasks that are not Completed or Cancelled, the completion tool returns a
concise denial and canonical completion does not become terminal. Optional and
cancelled tasks never block. If a child is interrupted, its claimed task stays
assigned; only an explicit release, completion, reassignment, or cancellation
changes ownership. Groups remain coordinated concurrency, not a swarm: there is
no autonomous scheduler, no automatic claiming, no recursive spawning, and
depth stays one. The workspace stays shared; overlapping
expected_paths/touched_files on concurrently active tasks produces an
advisory conflict warning only, never a hard edit block.
The model-facing surface is exactly three fixed tools: group_task (create,
list, claim, start, complete, block, release, cancel), group_message (send,
list), and group_status (one compact snapshot). spawn_agent gained an
optional task_id, so the root can delegate a ready task in one atomic step
without a new spawn primitive.
The validation shift: models express intent, the kernel owns truth¶
The model names what must hold — validate {"requirement": "existing unittest
passes", "command": "python3 -B -m unittest test_calc -v"} — and the kernel
does everything else: it executes the command under shell policy, appends a
ValidationResult event, creates or supersedes evidence for the requirement
with the real source event recorded internally, registers the requirement, and
derives completion. The model never supplies or sees an internal event id,
call id, or ledger id. record_evidence remains for non-command claims but
accepts only pending and unavailable; passed/failed statuses are
kernel-owned, so a model cannot self-certify.
One proving command can cover several named requirements: requirement keeps
its existing primary name, and optional requirements lists additional exact
names that the command proves. The kernel records one ValidationResult and an
EvidenceCreated for each name at the same workspace generation. This refreshes
earlier stale requirements together without deleting obligations or weakening
write invalidation. A failing command fails every named requirement; a pass
overlapping a writer certifies none. Existing single-requirement calls remain
valid. Requirement names are nonempty and deduplicated case-insensitively.
Validation uses the configured permissions.shell_timeout_seconds default,
just like shell execution; an explicit timeout_seconds overrides it.
Linux validation enables Bash pipefail, so filtering test output cannot
silently turn an upstream failure into Passed. Windows validation refuses
cmd.exe pipelines, which cannot preserve upstream exit status; run checks
separately or redirect output to a file. Ordinary shell semantics are unchanged.
Evidence is a current-state ledger: the newest entry per claim is the current
evidence; earlier entries stay in durable history. A validation that failed and
now passes supersedes the failure. Completion is derived, never declared:
InProgress until the model claims implementation, then Verified when every
required validation has current passing evidence, Blocked when a required
validation is unavailable, and ImplementedNotVerified otherwise.
Each kernel validation observation stores the workspace generation it checked.
The generation is the global insertion order of the latest durable mutation or
write-uncertainty event from any session sharing that workspace, so it rebuilds
from history on resume. Guarded edits and write-capable shell commands record
possible mutation before executing; observed changes, undo, detected external
changes, and write-capable managed process lifecycle events also advance the
generation. A Passed observation from another generation stays in the ledger
for audit but is stale for completion and canonical context. A running
write-capable managed process prevents a pass from certifying completion
because it can write after validation. Read-only operations do not advance the
generation. Shell and managed commands classified as read-only run with actual
read-only filesystem grants, including in WORK mode: configured Git helpers
cannot silently write behind the classifier. Explicit metadata or external
write grants retain write tracking. Validation and synchronous shell commands share the workspace
mutation lock with guarded edits; a pass overlapping an active managed writer
or another session's mutation remains stale.
Revalidation after the process exits on the current generation restores
Verified eligibility. Natural managed exits are reaped and committed by a
weak-owner watcher in tools/process.rs, independently of model polling;
exec_poll still returns buffered output. Failed exit commits remain retryable.
Validation reconciles local process exits before taking its generation snapshot.
A command that passes while certification is blocked returns BLOCKED, the
process ids and owning sessions, and the recovery action. It remains historical
Passed evidence, never current certification.
Completion state and loop termination are deliberately separate. A complete
claim sets the terminal flag only so the loop can exit in the same turn; the
loop resolves it against the derived completion, and only Verified ends the
run on the strength of its own claim. When the kernel has recorded a
workspace mutation during the run — read from durable FileChanged events and
never from model-authored touched_files — and completion is still
InProgress (missing the implementation claim) or ImplementedNotVerified,
the loop spends exactly one corrective turn instead
of exiting: the instruction travels the same durable KernelContext re-ground
channel as progress supervision, asking the model to validate the change or
record why verification is unavailable. A one-shot flag bounds it, so a model
that keeps claiming completion is honored on the next claim and the run still
ends ImplementedNotVerified. A run that mutated nothing exits immediately;
an empty required-validation set is never by itself a reason to keep going.
If a terminal complete response contains no summary text, the kernel commits
a CompletionReport event with the derived completion state, files changed by
Latch, shells or extensions during this run, current required-validation
statuses, and recorded blockers. TUI, Web, machine events and resume all retain
this kernel notice. Stale evidence is reported as unverified. This report adds
no provider turn or fabricated assistant event and does not change completion
truth. Concurrent process events and loop events reach live consumers once in
durable sequence order.
State, memory, and supervision¶
Model task_update constraints are TaskConstraint memory; only actual user
messages create UserConstraint provenance. Hypotheses are rejected or stay
hypotheses — a rejected hypothesis is never promoted to a decision. Decisions,
task constraints, and open questions have deterministic supersession/resolution
so canonical current state stays clean while raw events and memory records
retain history.
Failure supervision tracks a streak per validation lineage (requirement, or
shell command). Successful inspection tools never reset it; the lineage's own
validation passing resolves it, and a materially different failure signature
restarts the count. Streaks replay from durable events, so --resume does not
forget a stalled loop. At the configured budget the kernel requests re-ground.
A passing command whose certification is blocked does not clear that lineage.
Its blocker, rather than the command spelling or output, identifies a repeated
attempt. Once the retry budget is exhausted, validation against the same
active managed writers returns a suppression diagnostic without executing the
command or fabricating new evidence. Exited or changed blockers permit another
attempt; a current passing validation clears the streak. All exact requirements
in a covering command replay their own lineages.
Progress supervision is separate and deterministic: every read_file,
read_image, search, read_artifact, git_status, git_diff, and conservative
read-only shell observation is keyed by canonical subject (including range
arguments) plus result digest, scoped to a progress epoch. Workspace mutations (Latch, shell,
or detected external), new validation/evidence, meaningful task-state changes,
mode switches, and new user turns advance the epoch, so legitimate re-reads
after real change are never confused with redundancy. Consecutive turns that
only repeat unchanged observations cross the stagnation budget, at which point
the kernel injects a re-ground instruction listing exactly what is already
known; repeats after that are suppressed with a synthetic terminal result
instead of spending a tool cycle. Supervision state replays from durable
events, so live and --resume behavior are identical.
There is no fixed model-turn ceiling. failure.max_model_turns is an optional,
off-by-default circuit breaker for operators; long productive tasks are
governed by the stagnation and failure supervisors, not by count.
Important lifecycle transitions append to SQLite. Streaming token deltas are transient. Operations are marked running before execution and complete afterward; resume surfaces an unfinished record as uncertain.
Read-only batches execute concurrently. A mutation lock serializes edits,
writes, checkpoints, and undo. Shell and validation processes use bounded
timeout, cancellation, captured status, and artifact spill for large output.
Managed exec_* processes are owned by the kernel, buffered in memory, spilled
to artifacts past a cap, and durably closed with ProcessExited; a resumed
session reports honestly that children did not survive the restart. If an
artifact append fails, later polls retain the partial artifact reference, label
it incomplete, and report dropped output even when the artifact was previously
created successfully.
Context engine port¶
The agent loop talks to a narrow internal contract rather than to a concrete
engine: context.rs defines ContextEngine with ContextRequest,
ContextBudget, and ContextView, and ContinuityEngine implements it while
keeping its historical materialize/MaterializeBudget/MaterializedContext
names as aliases. The request carries session id, canonical TaskState,
retrieval query, evidence ledger, failure manager, the session-independent
compiled system prompt and the session context, budget, extension context, and
re-ground instruction; the view returns the system prompt, the session context,
the provider-visible recent events of the current durable cache epoch,
canonical/recalled renderings, episodes, and ContextStats. The
port exposes no EventStore or SQLite handle, so a replacement engine (for
example a remote context service) cannot mutate durable session truth outside
its structured view. Implementing the port is kernel-authority work and is
operator-installed, never model- or extension-supplied. AgentRuntime accepts
any engine (the default remains ContinuityEngine), and replacement is
runtime-wide: Agent::set_context_engine_factory installs the single
ContextEngineFactory policy that AgentSupervisor applies to every child
spawn, worker reconstruction, and process resume, priced for the child's
effective inference profile. ContinuityEngine roots default to the identical
continuity factory (ContinuityEngine::for_model(...)), and a non-default root
engine without a configured child policy fails child spawn instead of silently
falling back. The CI invariant tier pins port replaceability, provider-visible
parity, byte-identical default behavior, child propagation, resume
reconstruction, and fail-closed behavior. See
RUNTIME_CAPABILITY_MODEL.md for the
surrounding capability model and planned ports.
Context budgeting¶
Context is token-native. latch-kernel::tokens::TokenEstimator is a
conservative, provider/model-aware estimator (ASCII ~4 chars/token,
punctuation ~2, wide CJK/emoji characters priced explicitly); all pre-request
numbers are estimates, and provider-reported usage is authoritative after a
request. ContextStats records instructions, canonical state, recent
transcript, recall, tool schemas, and extension context, plus the model's
context window, reserve, and headroom. The context window defaults to 256k
tokens and is overridable per model ([models.<name>] context_window_tokens).
The continuity engine receives a MaterializeBudget that already subtracts
tool/extension costs, and the agent recomputes totals from the exact
components, so recalled material is counted once.
Recent estimation prices the provider-facing shape: tool-call arguments and
replayed reasoning_content are included, kernel bookkeeping events are not.
ContextStats.request_tokens is measured on the exact assembled request
(system, messages, tool schemas) after any extension transform, and
ContextStats.common_prefix_tokens is the byte prefix shared with the
previous request under Latch's canonical serialization and token estimator.
Their ratio is the estimated architecture cacheability: a Latch
architecture diagnostic, not a provider measurement. When provider usage is
known, provider prefix utilization = cache_read_tokens /
common_prefix_tokens shows how much of that estimated prefix the provider
actually reused, and the measured provider cache hit rate is
cache_read_tokens / (cache_read_tokens + cache_miss_tokens) from
provider-reported categories. Unknown categories stay unknown rather than
being fabricated. The sidebar labels all three distinctly and also shows the
current cache epoch generation, its conversation span, the last rotation
reason, and the tokens retained by that rotation.
Prompt cache layout¶
The provider system field is session-independent by construction: the
behavioral core and Latch kernel semantics only, byte-identical across
workspaces, repositories, and modes. Session-specific content (workspace
identity, repository instructions, and mode) travels as the first
provider-visible message, after the tool schemas, as an explicitly delimited
[Latch session instructions] block kept distinct from the user's own request
that follows it; a different session therefore does not invalidate the
system + tools prefix, and the provider can serve the first request of a
later session from cache. Canonical task state is rendered exactly once by
continuity and travels as a
final kernel-context user turn together with recalled originals and extension
context, so frequently changing state/evidence never invalidates the reusable
prefix. Tool schemas are stable and serialized before the messages. The
request signature is system + tools + messages, so within an epoch each turn
extends the previous request rather than rewriting it. The Anthropic adapter
merges that kernel-context turn into the preceding user message to keep roles
alternating; it also marks the system block as an ephemeral cache breakpoint
(an adapter-only control), while OpenAI-compatible endpoints rely on automatic
prefix caching.
Cache epochs are performance boundaries, not memory boundaries¶
The provider-visible conversation is organized into durable cache epochs.
Within one epoch every request is an exact append-only extension of the
previous one: no already-sent message is removed, reordered, or rewritten. The
system field and tool schemas are session-independent (session-specific
content is the first message), and kernel-owned context is
sent as durable [KernelContext] messages instead of a synthetic trailing
turn, so the reusable prefix does not break on ordinary turns.
A new epoch starts with a complete authoritative KERNEL STATE SNAPSHOT.
During the epoch the kernel appends deltas only when something materially
changes: a state update carrying the complete current canonical state at a
higher revision, an extension-context update, recalled original events, an
archival index update, or a re-ground instruction. Revision numbers make
supersession explicit: the highest revision of the current generation is
current truth; earlier revisions are retained provenance. Kernel messages from
older generations stay in the raw log but are not part of the provider-visible
epoch.
Rotation is deliberate and hysteretic. The recent_tokens configuration is
the conversation high-water mark, defaulting to 64,000 tokens and capped by
the available request budget. Epoch loading scans through kernel context
messages without charging their bytes to this threshold; they still count
toward the hard request budget. When an epoch exceeds it, one rotation keeps
the newest whole semantic units up to roughly three quarters of the budget and
emits a fresh snapshot, leaving a quarter-budget of growth headroom so a
saturated session rotates occasionally rather than every turn. Tool
transactions are never split, an oversized unit is kept whole, and
/compact is an explicit reset that starts a fresh epoch. Rotation never
deletes memory: evicted material stays in the event log, remains reachable
through FTS recall, and remains visible through the archival episode index.
Canonical task state stays authoritative. The append-only kernel history exists to preserve both provenance and cache locality, never to replace the state system: goal, constraints, decisions, supersession, hypotheses, questions, actions, validation requirements, evidence, failure lineages, and completion remain durable and are re-rendered into every snapshot.
Cache reuse may be sacrificed whenever semantic continuity requires it. If current truth would be crowded out, if a transaction would be split, or if unresolved work would be hidden, the epoch rotates (or grows) even though the prefix changes.
Incremental history access¶
Every raw event is retained in full; only how history is queried is
incremental. EventStore exposes narrow, indexed reads used by the hot
paths: last_sequence for watermarks, events_after/events_before/
events_between for bounded ranges, events_tail for bounded recent loads,
events_of_kinds for targeted replay (approvals, failed tool lineages, change
ownership, process starts), and latest_event_of_kinds for compact
boundaries. search_events retrieves FTS-matched rows directly instead of
loading and filtering the whole transcript. Bounded FTS selection takes the
newest matching rows first so a later correction remains recallable after many
repeated terms, then presents selected events in chronological order. The same
durable log always yields the same recall material.
Live supervision keeps sequence cursors, so each turn feeds only newly appended events to the progress supervisor and the live sink. Equivalence and stress tests assert that incremental episode indexing equals a full rebuild at every prefix and that a new turn over a large history reads only the working set and the new delta.
Durable transitions fail closed¶
Event persistence is the source of truth: a transition that must survive resume
never appears successful in live state unless its durable event committed.
Terminal tool results, kernel task-state updates, evidence, completion, and
permission resolutions propagate persistence failures and abort the run instead
of silently continuing. Evidence is persisted before it enters the live ledger,
and completion is remembered only after its durable announcement. Auxiliary
work — AI permission review, retry backoff, provider transport, and extension
RPCs — obeys the run's cancellation token, so Ctrl+C is never pinned by a
secondary call. Extension lifecycle stages are additionally bounded end to end
by the central ExtensionLifecycle policy (spawn, initialize,
registration/ready, ordinary RPC, shutdown, graceful exit). A missed deadline
kills and reaps the child, and startup observes the same cancellation token, so
a silent or broken extension can never block startup, RPC handling,
cancellation, or shutdown.
Source layout¶
The kernel keeps its stable public types and the run loop in agent.rs, with
one child module per responsibility: steering, request, permissions,
dispatch, agent_controls, kernel_tools, group_tools, validation, and
supervision.
Root graph/runtime ownership lives in agents/{supervisor,worker,graph,mailbox,
profile,group}.rs; group.rs owns the deterministic reducer, DAG validation,
and the root-scoped coordinator. context.rs owns the context-engine port
(request/budget/view vocabulary plus the ContextEngine trait) and
capability.rs owns the kernel-declared runtime capability vocabulary; the
Latch continuity algorithm stays a single module (continuity.rs) because
rollover, episode segmentation, and recall share one invariant, and implements
the port. Image ingestion, validation, and the artifact
media resolver live
in media.rs. Tools keep the
ToolExecutor facade in tools.rs and split policy, ownership,
process, files, write, and git into children. The TUI keeps the app
state and reducer in lib.rs with
transcript, markdown, chrome, theme, and runtime alongside the
existing composer, sidebar, diff, presentation, agents, and
session_picker modules.
Child modules are children of their owner, so private state stays private while
each file owns one concern.
Usage and cost¶
Usage keeps provider-reported categories distinct: total input, output,
cache read, cache write, and a normalized cache miss, each optional with
None meaning unreported. OpenAI/DeepSeek-style usage includes cache hits in
prompt_tokens, so the adapter records the explicit miss or derives
total-minus-hit; Anthropic reports uncached input directly as the miss. Cost
bills uncached input at the normal input price plus cache reads and writes at
their own prices, so cached tokens are never double-charged. Unknown
categories stay unknown rather than being guessed, and an estimate that
depended on an unreported category is marked partial.
Mode, Safety, Permissions, and the capability sandbox¶
Three orthogonal controls compose into one pipeline:
- Mode (
ASK/PLAN/WORK) gates whether mutation is eligible at all. - Safety (
Strict/Standard/Autonomous) maps the classified capabilities of a call toAllow/Ask/Deny. - Permissions (
AutoApprove/Human/AiReview) resolves anAskthrough a durable request/resolution pair and a single-useCapabilityGrantkeyed by kernel call id — never model-supplied.
safety::classify owns capability classification: workspace read/source/
metadata writes, Git metadata, external filesystem writes, network, remote side
effects, privileged and destructive operations, extension execution, and
unknown capabilities. External writes, Git metadata mutation, network, and
remote side effects always classify as Ask first, even under Autonomous +
All approved; hard deny (privileged/system-destructive) is independent of both
profile and resolver and cannot be granted. Explicit capability requests
(capabilities: ["network"]) are parsed from tool arguments; unknown names
become UnknownCapability -> Deny because no executable sandbox grant exists.
AiReview is a separate stateless provider call with no conversation history
and no tools. It receives a short task summary, workspace, the exact command,
the requested capability, and the escalation reason, and must answer strict
JSON {risk, reason}. low approves; medium/high/critical reject with
the one-sentence reason returned to the coding model; unparseable output
rejects conservatively. Non-command asks use human resolution rather than
fabricating a bash judgment.
Execution backend and mandatory platform sandbox¶
ToolExecutor and extension hosts now request commands through
ExecutionBackend. SandboxProfile remains the shared capability policy;
execution/shell.rs owns the model-facing shell guidance, tool descriptions,
unmodelled executable list, and conservative read-only shell classifier.
execution also owns search-tool discovery and platform-specific managed
process cleanup and search exit interpretation. tools/process.rs owns durable
process state and output, while workspace_path.rs shares alias-safe path
resolution and NTFS hardlink checks with the shell classifier and native file
tools. Adding a shell or toolchain adapter should change the execution
layer and its tests without adding shell branches to tool dispatch.
Linux delegates to SandboxRunner. Windows embeds its native runner and
compatibility DLL, verifies installed bytes, and launches cmd.exe through an
AppContainer token restricted again for writes and a kill-on-close Job Object.
There is no Git Bash, WSL, local-account, or unsandboxed fallback. The
Windows boundary records the native
security fixtures and platform limits. The launcher starts a trusted cleanup owner which holds the sandbox
job, temporary ACL grants and AppContainer profile. The owner observes launcher
termination through a synchronization-only process handle and drains the job
before revoking grants. The runner writes durable, identity-bound
ACL and resource intents and recovers stale state before accepting another
command. Sensitive ACL sealing is temporary and exact rollback is checked.
Read-only workspace commands use read_broker.{h,cpp} instead of mutating
source ACLs. The host-created message pipe and transaction mutex are duplicated
into the sandbox and its descendants. The broker independently checks the
requester's job and AppContainer SID, read-only access mask, root containment,
sensitive exclusions, final object path and hardlink aliases. It pins path
components against replacement, refuses reparses and case-sensitive directories,
then duplicates only the requested read rights. The compatibility DLL routes
opens and metadata queries; bypassing it grants no additional access. Runtime
bootstrap and writable roots still use recoverable ACL grants.
The recovery protocol documents the
module split, real owner-kill tests, and retained conflict cases. AppContainer
creation is pre-journaled by a unique name and derived SID; recovery verifies
the mapping before cleanup. Missing or changed objects fail closed when exact
recovery cannot be proven.
The same per-call transport mediates ordinary IPv4/IPv6 TCP/UDP sockets only
when the kernel granted Network. socket_broker.{h,cpp} owns socket policy and
transfer lifetime. Every request is authenticated against the exact Job Object
and AppContainer SID; Winsock duplication targets only that process. The host
closes its descriptor after recipient acknowledgment. Pending transfers are
bounded, dead recipients are reaped, and teardown closes every remaining source
socket after stopping the job. This enables loopback clients/listeners without
machine-wide exemptions or firewall changes. Raw sockets and privileged flags
are refused; Network remains a broad command-level authorization.
Fixed Git inspections and extension hosts share fixed_command; Linux quotes
each argument and explicitly execs the program inside Bubblewrap. Windows fixed
commands quote arguments and run through the same native runner. The Rust
toolchain grant selection, fixed-program aliases, staged-tool PATH entries,
and Python/Node/ripgrep/PowerShell 7 read roots live in execution/windows_runtime_tools.rs,
leaving windows_runtime.rs to assemble the capability request. Python,
Node, ripgrep, and PowerShell 7 runtimes are staged as read-only assets; a Chocolatey
ripgrep shim is resolved to the package's standalone executable before staging.
Python's standard library is one ZIP archive to avoid thousands of per-command
ACL changes. Read-only runtime roots grant only their root and immediate native
bootstrap files (.exe, .dll, .pyd), without inheritance; nested resources
use the scoped read broker. Production workspace writes use write_broker.{h,cpp}
through --filesystem broker; ordinary source trees receive no recursive grants.
The broker opens relative to pinned parents, validates root identities and every
hardlink, and transfers only requested data/delete rights. Shared broker_path.h expands
existing 8.3 aliases in policy masks and requests, including the existing prefix
of future create/rename targets, before path comparisons. Rename/link operations
remain host-mediated; directory namespace, ACL and owner capabilities are withheld.
Scratch and runtime bootstrap grants retain journaled recovery. Sensitive ACL
audits also detect broad Everyone/Users/enabled-Administrators/authenticated permissions, not just
package SIDs; they journal a deny for this exact package and remove package
allow routes before launch, or fail closed if sealing cannot be performed. PowerShell 7 is resolved from a native PATH installation, its
installed module path, or the standard Program Files location; Store launcher
aliases are not copied. Its complete runtime is staged under a source/managed-host
identity, and only staged modules enter PSModulePath. Fixed headless PowerShell
calls default to MTA because STA COM waits fail inside AppContainer; explicit
-Sta/-Mta arguments remain authoritative. The boundary still owns execution. The injected compatibility module initializes
each private console to UTF-8 before cmd caches its code page, so localized
directory output and Unicode filenames survive the UTF-8 tool protocol.
CLI startup and session Git observations also use fixed_command. Read-only
calls without external writable roots skip recursive Git metadata discovery;
they have no workspace write grant to mask. On Windows,
the ripgrep prerequisite check uses the same shim resolution as fixed-command
staging without starting a host process;
the search itself runs through the native boundary. Tool descriptions name the
actual platform shell, Bash on Linux and cmd.exe on Windows.
On Linux, the startup probe verifies
bwrap, unprivileged user namespaces, bind mounts, and the required namespace
set; failure is stored as an actionable refusal and shell/exec/validation/git
inspection all fail rather than running unsandboxed. A SandboxProfile binds
the host root read-only, mounts the workspace explicitly (read-only for
inspection; writable with .git remounted read-only unless GitMetadataWrite
was granted), adds call-scoped external writable roots, provides private tmpfs
/tmp and /run, masks home credentials, clears the environment, and isolates
user/PID/IPC/UTS namespaces plus network (re-shared only when the profile
grants it). Kernel-internal bookkeeping (git status, git show, drift
snapshots) and search run fixed, Bash-quoted inspection commands through the
same backend. Extension hosts start through the same
sandbox with a read-only workspace and network; their individual tool arguments
remain a cooperative boundary, documented rather than overclaimed.
Every command and extension profile carries the configured Latch state directory.
The sandbox resolves its real path and masks the whole directory after workspace
and external mounts. If secrets.toml is a symlink outside that directory, its
resolved target is masked too. The private /tmp and /run mounts already hide
paths there unless a workspace or granted external mount exposes them. Failure
to resolve the configured state directory refuses the command. Kernel-native
filesystem tools enforce the same boundary on the host: read_file,
read_image, and the search target refuse the resolved state directory and
any symlink alias into it, write/patch refuse to mutate it, and a recursive
search rooted above it excludes the protected tree with an rg glob instead of
failing. Credentials stay unreadable whatever the workspace layout.
On Windows, kernel-native file tools also reject multiply-linked files because
an NTFS hardlink has no canonical path identifying its state-directory alias.
read_file and read_artifact share one bounded text page policy: at most
64 KiB fetched and decoded, and at most 24 KiB / 8000 estimated tokens in the
ToolResult that is appended to history. Long lines continue with a byte cursor
and line number; large files cannot claim an exact whole-file hash or total
line count without crossing the IO bound. Small files keep their exact hash
for guarded edits. Binary and invalid UTF-8 pages fail before text reaches the
provider.
Non-interactive sessions record non_interactive denials; resume marks
unresolved requests resume_expired, and SafetyChanged/PermissionsChanged
events restore the exact policy the session ended with.
Change ledger and shell drift¶
Latch- and shell-owned changes persist across restarts: pre-change bytes go to
content-addressed artifacts referenced by FileChanged events, and
ChangeReverted tombstones keep a resumed ledger from re-applying undone
work. /undo peeks before popping, restores only when the file still matches
the recorded post-change hash, and refuses otherwise. Whole-file writes are
strict compare-and-swap: the base_hash must equal the current bytes exactly,
and a newer version any writer authored — including a concurrent root or child
agent — rejects the write as stale so the loser re-reads instead of silently
losing the winner's change. Exact patch replacement keeps the repairable
self-authored rule: drift onto a hash Latch itself wrote proceeds without a
forced re-read, but the replacement still has to match its old text exactly
once in current content, so it can neither merge nor blindly overwrite another
writer's change; only genuinely external modification trips the same stale
path. Workspace drift around
shell commands is classified honestly: Git workspaces get reversible Shell-owned
records where pre-content was capturable (captured dirty files, or the HEAD
blob for previously clean files) and explicit non-reversible markers otherwise;
non-Git workspaces get an honest "detection unavailable" marker. Pre-existing
dirty work is captured at startup and never conflated with Latch's changes.
Optional language intelligence¶
lsp.rs owns operator configuration, a single lazy client per session, and
advisory snapshot results; lsp/{protocol,query}.rs own bounded Content-Length
JSON-RPC, server-request refusal, document synchronization and semantic result
normalization. tools/language.rs integrates ordinary durable tool events,
read-only/no-network ExecutionBackend profiles and the existing file observation
ledger. External drift detected by LSP stales prior validation exactly as file
reads do. Durable cross-session mutation cursors invalidate disposable indexes
without reading full conversation history or introducing a new projection.
The optional schema and language list are fixed at session construction; process
startup/failure/stop never changes tools. Language lists live in session context,
not the stable provider system field. Children have independent lazy clients;
resume reconstructs them, retaining previous tool results in history. The
tools.lsp runtime declaration is workspace-scoped with read and extension-execution
permissions only. Unix caches one client, Windows closes it after every query
to avoid pinning native recovery. Session extension shutdown also closes LSP.
No server edit, command, diagnostic or index status grants kernel truth, writes
or validation. See LSP workflow and limits.
Providers¶
provider_request.rs bounds registry-built adapters with header (60s),
stream-idle (180s) and absolute attempt (1800s) deadlines, configurable under
[provider_request]. Adapter HTTP clients additionally bound new connections
(including TLS) to 15s, contained within the header budget rather than added to
it; reused connections do not restart this budget. Activity/heartbeat bytes
reset idle (not a thinking-duration limit), never the total
deadline. Cancellation drops the request future; child rebinding retains the
policy and adapter identity. The agent retries only recognized transient errors
before semantic output, with bounded exponential backoff and Retry-After;
permanent/unknown failures and absolute deadlines do not retry. HTTP status and
redacted diagnostics are extracted in provider.rs; wire serialization and
durable conversation schemas do not change. Shell/validation limits remain
independent. See model request deadlines and retries.
OpenAI-compatible chat completions and Anthropic Messages translate only at the
API boundary. Durable state remains provider-neutral. Assistant reasoning
(reasoning_content) is persisted and replayed verbatim by reasoning-capable
endpoints (DeepSeek, OpenCode Go) through a small wire profile; replay is
decoupled from tool-call structure so a defensive history transform can never
drop required reasoning state. Reasoning is never displayed in the transcript.
Tool outcome stays typed across the same boundary. The kernel records
ToolCompleted and ToolFailed as distinct durable events and the request
builder maps them separately, so ModelMessage.is_error carries the kernel's
own classification rather than leaving the model to infer failure from the
wording of command output. Anthropic Messages has a native tool_result.is_error
field and uses it. Chat completions and the Responses API define no such field,
so those adapters prefix the tool content with a short, stable, adapter-local
[latch:tool:ok] / [latch:tool:error] envelope and leave the tool output
itself unmodified; no unsupported JSON field is invented. The adapter-local
envelope is a wire-format workaround only — the provider-neutral
ModelMessage::is_error remains the kernel concept, and multimodal tool results
carry images unchanged in every transport.
Repository instruction precedence is CLAUDE.md, AGENTS.md, then
.latch/instructions.md; current user input follows them. Kernel invariants
override project text.
Multimodal image input¶
Image input is provider-neutral above the provider boundary. A durable
MediaRef (content-addressed id/sha256, MIME type, dimensions, artifact
path) names one immutable artifact in the session artifact store; bytes never
enter events, SQLite JSON, logs, or transcripts. EventPayload::UserMessage,
ModelMessage, and ToolResult carry media: Vec<MediaRef>, all
serde-defaulted, so text-only traffic is structurally unchanged and older
events deserialize with empty media.
media.rs owns validation and ingestion: format detection from actual bytes
(PNG/JPEG/WebP; GIF is rejected honestly because not every supported serializer
path handles it), structural header validation with a CRC check for PNG, the
5 MiB and 8000 px limits, and content-addressed write-once storage. The
read_image tool reuses the workspace read policy and feeds the resulting
reference into the tool result. The agent request builder carries user and tool
media into ModelRequest, and adapters resolve bytes to inline base64 only at
the wire boundary through a MediaBytesProvider rooted at the session's
artifact store, so replayed history keeps referencing the original immutable
artifact even if the source file changed or disappeared.
Wire serialization is per transport: OpenAI Responses emits input_image
content parts in user turns and in function_call_output.output arrays;
Anthropic Messages emits base64 image blocks ahead of the text in user turns
and nested inside tool_result.content; chat completions emits multimodal
image_url content for user turns and, because tool-role images are not
portable there, a terminal textual tool result followed by one adjacent user
observation turn so a tool transaction is never split. Encrypted-reasoning and
thinking replay are untouched by image input.
ModelDescriptor.input_modalities is the capability authority. Built-in
catalogs mark only officially documented vision models (the current OpenAI and
Anthropic catalogs, and DeepSeek Flash). OpenCode Go's exact
deepseek-v4.1-flash entry inherits image input from its documented
OpenCode catalog base model. Other Go models stay conservative text-only by default;
upstream vision support alone does not establish gateway support.
User ModelConfig.input_modalities overrides built-in metadata, and unknown
models stay text-only. The kernel fails locally before any provider request
when pending input or replayed history contains an image the effective model
cannot accept, and /model annotates image-capable models with vision.
Images are priced as estimated visual tokens in context accounting (never as
base64 text length).
TUI state surfaces¶
The transcript explains activity; a responsive right sidebar explains state.
Terminal layout stays in latch-tui::markdown and latch-tui::transcript:
styled text wraps by grapheme display width before gutters are added, with
continuations aligned to the body column after each bullet, number or status
marker. List paragraphs and nested items retain their own body columns.
Markdown tables parse inline styles before measuring and wrapping cells, then
pad the rendered spans for left/center/right alignment. Columns use three-cell
gutters; short columns retain natural widths and long descriptions share the
remaining viewport without a fixed width cap. Records have one blank row
between them, while each record's wrapped lines stay together. Tables also
keep one blank row before and after surrounding prose.
Top-level points have one blank row between them; headings have two preceding
blank rows and one following row. Transcript cells use one blank separator.
The viewport renders these physical rows directly and uses their count for
scrolling. The transcript caches physical rows for compact and raw views,
invalidating them on content changes and recomputing at the current width after
resize or sidebar changes. Scroll and blink frames clone only visible rows;
scroll offsets remain full-size even beyond 65,535 rows. Input/output events
are consumed independently of a 16ms redraw timer, which coalesces bursts
without dropping wheel or key events.
The sidebar divider is drawn separately with one glyph and explicit style
across every row, clearing any inherited emphasis or background.
Tool markers blink yellow on a 400ms TUI timer while running, then stay green
on success or red on failure. The frame alternates marker visibility without
relying on terminal blink support, changing widths, or writing durable events.
Activity indicators above the composer and in the sidebar use steady blue dots
with the same two-cell marker gutter as tool rows. Their labels and continuation
details start at the body column; tool result colors remain independent.
Command labels are bounded by the viewport and 96 display cells; successful
output previews use at most three visual rows. Ctrl+T shows the complete
command and retained output with hanging prefixes. These are presentation-only
views of the same live/replayed cells; durable history and shared reducers do
not change.
The sidebar reducer (latch-tui::sidebar) consumes the same durable events as
the semantic transcript, so live and resumed sessions agree: ContextStats
for the bounded working set, TaskStateUpdated/EvidenceCreated for
kernel-owned completion, ModelUsage for provider-neutral input/output and
optional cache categories (unknown stays —), and
FileChanged/ChangeReverted/ExternalFileChangeDetected/ShellMutationObserved
for Latch / Shell / Extension / External ownership. Optional user-configured
[models.<name>.pricing] yields a clearly labeled estimated cost; missing
components stay unavailable. The kernel forwards tool-appended durable events
to the live sink so live and replay observe identical event order.
The current-request label follows the latest durable RunStarted.prompt or
UserMessage; canonical TaskState.goal stays separate. Run outcome does not
imply task verification. The sidebar shows task, implementation,
validation, run, context utilization, usage, cost, and relevant changes;
/context presents low-level cache and budget accounting.
The composer (latch-tui::composer) is a real editor rather than a text field:
the complete buffer is wrapped into grapheme-safe visual rows, an independent
viewport offset tracks the visible window, and the terminal cursor is placed
only while its visual row is visible. PageUp/PageDown move through an
overflowing prompt and fall back to transcript scrolling when it fits; the
wheel routes by pointer position with mouse capture enabled by default, preventing
terminals and multiplexers from translating wheel events into composer history
keys. LATCH_MOUSE_CAPTURE=0 opts out; native text selection typically uses
Shift-drag while capture is active. Alternate-screen, paste, and mouse modes are
left symmetrically.
The TUI reserves two columns at each screen edge and one row above and below
on ordinary terminals, dropping these margins on very small screens. The
composer uses a muted rounded frame, internal padding and a two-cell prompt
gutter; its four corner cells retain the terminal background so the neutral
surface does not spill past the rounded outline. Its wrap width, cursor and
mouse viewport use the actual editor bounds.
The sidebar retains its quiet divider with padding on both sides of its text.
The idle composer keeps a three-row editor body when space permits, with
metadata, hints, and a workspace/version footer. Layout chrome drops before
the editor body shrinks on short terminals; multiline input grows within a
bounded viewport. The startup screen uses a small Latch title without a pixel
wordmark. The palette (latch-tui::theme) centralizes every
surface, status, and diff decision and degrades to semantic foregrounds without
backgrounds on ANSI-16. User messages, the framed composer, and action surfaces
share a neutral surface; assistant messages use the terminal background and a faint
gutter. Approvals and /safety//permissions selection render above the
composer. A compact status row reports running work derived from authoritative
state; running and completed tool cells remain visible in the transcript.
Child sessions surface through
latch-tui::agents: compact delegation/report cells in the root transcript, an
active child summary in the status row, and a bounded CHILDREN sidebar
section. Child transcripts never cross into the root.
Diffs are first-class: git_diff completions become a typed
DiffDocument (parser in latch-tui::diff) rendered with restrained semantic
colors. Red/green semantics only ever apply inside a parsed unified diff;
unparsed input falls back to raw, uncolored lines. /diff opens a full-width
inspector with independent scrolling, temporary sidebar collapse, and a raw
toggle. The sidebar appears automatically at ≥110 columns, toggles with Ctrl+B
or /sidebar, and collapses on narrow terminals unless explicitly opened.
Its width remains responsive (roughly 24–32%, clamped per breakpoint between
20 and 44 columns).
Resume¶
--resume is a user-level resume: the visible transcript replays from durable
events through the same formatter the live TUI uses (no reasoning, context
statistics, model usage, or raw task state), the effective mode resolves as
CLI --mode > the session's durable mode history > config default, prompt
history is rebuilt from user events, and task state, evidence, failure
streaks, progress supervision, and change ownership are restored without
re-executing anything or appending duplicate durable events. When the original
user prompt rotates out of the recent byte budget, the transcript is anchored
with a deterministic kernel continuation message rather than dropped, so a
mid-task window never gives the model amnesia.
Testing and CI¶
Testing follows two principles. Memory decides what the model needs to know; cache decides how cheaply we can send it — correctness and long-horizon continuity outrank cache locality. CI protects what Latch must never stop being. Local tests verify that the current implementation actually works.
Default CI runs Linux, Windows, and Apple Silicon macOS smoke jobs: formatting, offline installer checks, the kernel invariant tier, a real Linux Bubblewrap probe, the embedded Windows boundary test, Windows/macOS Web transport, macOS Seatbelt checks, and CLI build/start. Linux/macOS also launch the actual TUI through a PTY, exercise keyboard input and sandboxed tools, and upload real screen captures. Compilation has its own step before bounded runtime checks, with 20/35/30-minute Linux/Windows/macOS job limits. New pushes cancel superseded smoke runs on the same branch or PR. Caches separate smoke and full-validation builds; only main writes them.
The manual Full validation workflow retains the complete Linux release gate
(format, Clippy, invariants, workspace tests, release build), Windows native
security fixtures, and serial Windows Rust/runtime/integration/Web checks plus
a release build. Runs are serialized per ref without cancellation. Windows
format/Clippy, test compilation, runtime checks and release compilation have
separate steps and limits; native fixtures stay sequential on one runner.
Windows Web readiness allows 60 seconds for native startup rather than treating
the Linux 10-second correctness wait as a cross-platform latency requirement.
The deliberately selected invariant tier in
crates/latch-kernel/tests/invariants.rs is fast and deterministic
by construction — no bwrap, rg, python3, network, timing, or large
histories — and pins durable history as the source of truth, append-only cache
epochs that are not memory boundaries, canonical-state authority, the absence
of hidden destructive compaction, resume equivalence, kernel-owned
validation/evidence, safety hard-deny, steering protocol correctness,
deterministic provider serialization, and cache-accounting semantics.
Detailed correctness (providers, sandbox/command execution, snapshots,
extensions) runs in manual full validation and locally with
cargo test --workspace, and long-session / large-history stress tests (cargo test -p latch-kernel --lib continuity --
--nocapture) stay out of CI by convention. Tests are not moved between tiers
merely to make CI green, and passing CI alone is not sufficient for a
substantial change.
Agent Skills and MCP (v0.3.2)¶
skills.rs owns Agent Skills discovery and bounded resource reads. mcp.rs owns
stdio/Streamable HTTP clients, version negotiation, discovery and lifecycle.
agent/dispatch.rs owns permissions, pre-call validation invalidation and durable
results; agent/request.rs includes fixed schemas. The shared CLI session builder
loads operator-configured servers; child workers own independent clients.
See Skills and MCP for configuration and compatibility.
Windows MCP stdio uses initialized connections per call, validating the fixed catalog before execution and releasing the native recovery lock between calls. Linux stdio and HTTP retain their normal connection lifetimes.
Apple Silicon execution¶
execution/macos.rs implements the macOS Seatbelt backend using the system
/usr/bin/sandbox-exec, a deny-default profile, explicit writable roots and
backend-owned temporary scratch. Every shell, fixed command, extension and MCP
stdio process uses the existing execution port. Startup probes fail closed;
doctor only checks runner presence. Environment variables are allowlisted,
Latch state and conventional credentials are denied, Git metadata is protected
without its capability, and network access requires its capability. Cancellation
kills the sandbox process group. Web reuses the existing host and opens the
browser with /usr/bin/open. Qualification and limits are in MACOS.md.