Internals
A map of the source tree for contributors: how the modules depend on each other, how one turn
travels through them, where state lives, which rules are enforced by a single named predicate, and
what each host offers. The code is the reference; this page says where to look. Every name here
was checked against the tree when it was written, so if a name is missing, grep for it before
assuming the page is right.
Layering
The tree reads top-down. Each module calls the ones below it and never the ones above or beside it,
and tests/layering.rs scans every crate:: path in production code to keep it that way. Every
top-level module has a rank there; an edge may only point at a strictly greater rank. The tolerated
list is empty. One upward edge is by design, tools/subagent.rs building an Agent, because a
sub-agent is an agent.
rank module holds
0 main argument parsing; picks a host or a cli handler
1 host the layer between a host and the agent, and the four hosts under it
2 cli clap definitions and one handler file per subcommand group
2 relay tracing output routed to the screen, or to stderr
3 installation the doors that edit config.toml and the credential rows: accounts, profiles, MCP servers
4 render the screen's rows and markdown layout, the approval block, status lines, the one-shot's answer
5 agent the turn loop, tool dispatch, compaction, recovery
5 view the JSON record shapes --format json and the HTTP API share
6 tools the Tool trait, the registry, the built-ins, MCP tools as Tools
7 prompt the system prompt and the per-turn context block
8 session the materials a session is built from, and its live cells
9 scheduler the sweep that claims due jobs and hands a Wakeup to a host
9 background tool calls the agent starts and does not wait for
10 mcp the MCP client: connections, published tools, progress, auth
11 provider the Provider trait, wire types, the registry, one backend per API
12 frontend the Frontend trait, its events, and the two generic frontends
12 skills skill discovery and loading
12 instructions the standing instructions file
12 oauth expiry, refresh, the refresh lock, PKCE, the person's side of a browser login
12 sandbox read-only confinement for shell_execute
13 workspace cwd, roots, the write fence, private-directory refusals
13 tokens token estimates for the gauge between provider reports
14 store every SQL statement, behind one connection owner
15 schedule what a scheduled job is: parsing, gates, the scheduler's memory
16 conversation the event-log conversation and its title
16 stats per-session counters
16 config config.toml, ResolvedConfig; profile.rs holds accounts and profiles
16 memory the memory entry
17 entry what skills and memories share: an indexed entry
17 permission levels, the enabled set, the shared cell, the approvals switch
17 checklist the checklist: items, states, the open rule
17 image format detection and transcoding
18 fs private directories, atomic replace, file locks
18 process the one door for every process meka starts: behind meka for memory
19 error MekaError
19 sync locks that outlive a panic
19 streams raw stderr
19 paths the config and data directories
20 text pure text helpers: widths, columns, sizes, timestamps, unknown_name
relay is installed by main.rs as tracing’s writer; the screen drains it, and every other
run writes it to stderr. render is reached only from host and cli. text is the lowest leaf so that error
can render MekaError::ProfileNotConfigured through text::unknown_name.
Put new code where its callers already are: a type read by store and host belongs in store or
below, never in host. A new module fails the test until it is given a rank.
| Module | Holds |
|---|---|
src/main.rs | Dispatch only: parse arguments, pick a host or a CLI handler, map the exit code. |
build.rs | The Windows icon resource, and MEKA_GIT_REVISION: the commit the binary was built from, -dirty included, which the screen’s banner shows beside the version; absent outside a checkout unless the build’s environment sets it. |
src/cli.rs, src/cli/ | The clap definitions and one handler file per subcommand group: account, profile, project, session, recall, mcp, tool, skills, memory, instructions, schedule, background. Everything that owns the stdout/stderr contract lives here; account, profile and mcp are the prompts, flags and listings over the doors in src/installation/. |
src/installation.rs, src/installation/ | The installation’s editor, shared by the CLI and the HTTP API: account.rs writes [accounts.<name>] under the config file lock, moves a credential on a rename, and holds the browser login that acquires one; profile.rs writes [profiles.<name>] and takes a ProfileChanges; mcp.rs writes a [[mcp.servers]] entry, takes a ServerChanges, stores a server’s secrets and runs its OAuth login behind a LoginPrompt. Patch<T> is the one tri-state every edit is spelled in: a key kept, removed or set. No prompt and no request body reaches it. |
src/host.rs, src/host/ | host/assembly.rs builds a session: SharedDeps, build_session_agent, hydrate_conversation, resolve_profile_switch, record_session_change. host/session.rs runs one: ResidentSession, TurnGuard, the CancelCell, the Sessions registry and its idle sweep, fork_and_lock, outcome claiming. host/scheduler.rs is HostHooks, run_wakeup, the out-of-band turn every host runs the same way, spawn_outcome_poller, the one background-outcome poller both servers spawn, and TurnSource, who started a turn. host.rs keeps wait_for_shutdown_signal, and host/assembly.rs apply_recorded_bindings, which both servers call ahead of every turn, POST /compact included, so a session runs on the profile and the project its row names. host/terminal.rs is what the screen and one-shot share: Ctrl+C, the interruptible turn. host.rs keeps COMMANDS, the slash-command table the screen offers and ACP advertises the rows with an editor_help of. host/tui, host/oneshot, host/acp and host/http are the hosts; host/tui/model.rs is the transcript the screen draws from (a bounded window of the session’s log plus this run’s tail), host/tui/view.rs how each item is drawn and the viewport, host/tui/panel.rs the panel a command’s answer, a picker or the approval prompt opens in, host/tui/screen.rs the task that owns the terminal; host/http/feed.rs is the session feed, the SSE channel and replay ring HttpFrontend produces into and the stream handlers read. |
src/agent.rs, src/agent/ | The Agent and its options; turn.rs runs a turn in phases (open_turn, run_rounds, settle_turn) and owns TurnInput, dispatch.rs executes tool calls and owns run_admitted_call, the one sequence a resolved call goes through, compaction.rs summarizes, recovery.rs decides what a failed request becomes. |
src/view.rs | The record shapes --format json prints and the HTTP API serves, each defined once with its From conversion from the store or config type it shows: SessionView, ProfileView, ProjectView, AccountView, McpServerView, McpToolView, ScheduledJobView, GateView, MemoryDetail, ToolView, SkillView, SkillDetail. A host adds what only it can answer around the shared core by #[serde(flatten)]: the HTTP SessionResponse flattens SessionView under capabilities, turn_in_flight, inbox_pending, requests_pending and checklist; the CLI’s InstalledSkillView and ConfiguredToolView flatten a core under what only a terminal should see, such as a path on this machine. Every Option field is omitted when absent, never null. |
src/session.rs | What a session is made of: CoreMaterials and SessionMaterials (what every agent and registry of a session is built from), SessionCells (permission, cwd, roots, session id, the project, checklist, the published profile, the context gauge, background tasks, the frontend, the session lock slot, a pending compaction), ToolSite (the four cells a built-in reads), AgentOptions and CompactRequest. No host and no SQL. |
src/provider.rs, src/provider/ | The Provider trait, the wire types, MessageAccumulator, the registry of profiles, and one backend per API: anthropic/messages.rs and anthropic/subscription.rs over anthropic/shared.rs; openai/chat_completions.rs, openai/responses.rs and openai/subscription.rs, the last two over openai/responses_wire.rs. Every streaming backend reads SSE through provider/sse.rs; the refresh-once rule for a rejected subscription credential is oauth::send_with_one_refresh; provider/mock.rs is the scripted provider the test suites drive. |
src/tools.rs, src/tools/ | The Tool trait, ToolContext, admit_arguments, the registry and its builders, the gate toolset, the built-in tools, and mcp_adapter.rs: each remote MCP tool as a Tool, and the registry as a subscriber to the client’s tool lists. A built-in reads the session it serves through one ToolSite. |
src/mcp.rs, src/mcp/ | The MCP client: ServerEntry per server, the connector and its reconnects, the client handler, policy.rs (which of a server’s tools register, load eagerly, and at what level, as pure config policy), subscriptions.rs (what the model subscribed to, applied to every connection the entry adopts: per-URI requests on a 2025-11-25 connection, one subscriptions/listen stream on a 2026-07-28 one), each server’s tools published to whoever subscribes, progress and resource-update routing on McpClientContext, and auth. It never names a registry and never talks to a terminal: an interactive login goes through the oauth::LoginPrompt installed on the client context, which only a login run with a person at hand provides. |
src/store.rs, src/store/ | The store. migrations.rs is the append-only ledger; locks.rs the per-session file claim; backup.rs the copy taken before a migration; export.rs the archive format; projects.rs the projects table and the one transaction a rename or a removal is; every other file owns the statements for one table family. |
src/schedule.rs, src/schedule/ | Scheduled jobs as a domain: parsing, gates, and SchedulerMemory (schedule/memory.rs), the per-process record of which refusals have been reported. No SQL. src/scheduler.rs, above the store, is the sweep that fires them. |
src/config.rs, src/config/ | ConfigFile, loading, expand_env_vars (the one ${VAR} grammar), ResolvedConfig, and the vocabulary every layer reads; mcp.rs and serve.rs hold the [mcp] and [serve] schemas. profile.rs is Backend, an account and a profile as written, a profile as resolved through its account, select_profile and require_profile. Live services and probes are resolved by the host, not here. |
src/prompt.rs, src/prompt/world.rs | The system prompt and build_turn_context, the block the model sees ahead of every user turn; world.rs renders the world state, whole or as the diff against what the model was last shown. |
src/fs.rs, src/oauth.rs, src/sync.rs, src/text.rs | Private directories and the file locks; PKCE, refresh, the loopback callback and SubscriptionCredential, the one token-refresh-and-rejection machine both subscription backends run; the one place a poisoned lock is recovered; and every pure text rule the surfaces share. |
One turn
A host admits the turn, the agent runs it, and everything the user sees comes back through a
Frontend.
- Admission. A
ResidentSessioncounts its work in one cell,in_flight, and holding the conversation mutex is what “a turn is in flight” means. HTTP and ACP admit a typed prompt withadmit_turn, which hands back aTurnGuardand samples the cancel epoch; HTTP wraps it inhost::http::state::admit_turnto add the process-wide cap. A scheduled fire, an outcome delivery or an inbox turn goes throughHostHooks::admit, which underservecounts on the same cap and steps back for the next tick when it is full; HTTP compact and rewind takeclaim_idle, which refuses while anything is in flight. The screen and the one-shot drive one session from one thread, so they samplecancel.admit()themselves and take the conversation lock. - Input. The host builds a
TurnInput: the typed prompt or the outcomes it carries, images, and the retention, which the scheduler sets per job and the HTTP turn takes from the request.TurnInput::from_partsis the empty-prompt rule, raisingMekaError::EmptyPromptbefore admission. - The loop.
Agent::run_turnappends one user message of two blocks: aTurnContextblock holding everything meka injected (permission and environment context, world state, budget, background outcomes, the resume notice) and aTextblock holding the words as typed. Providers render the first as text ahead of the second; exports,GET /history, replays and the title read the words. It then sends aCompletionRequestand dispatches every tool call the response carries.admit_argumentstype-checks the call’s arguments and takes outbackground, the flag that detaches it;admit_tool_calldecides run, ask or refuse. Each call gets aToolContext: the session id, the tool-use id, the prompt id, the frontend and the cancellation token. Every request carries the conversation whole; the context ceiling and compaction are its only bound. The world state’s[Tool discovery]index is bounded like the skills and memory indexes, inprompt::render_tool_discovery: summaries while the section fits 8 KB, names past it, and past 200 names a count that points attool_search. Root agents and sub-agents share this per-turn renderer over their own registries. Workers receive permitted server instructions and granted memories here; their system prompts carry the role and granted standing instructions, without repeating active tool descriptions. Skill updates use the same bounded renderer as the initial skill index. The execution context states the active profile’s image-input capability and whether the host can deliver later turns.admit_tool_calllives intools, wheretool_searchandtool_loadconsult it to say what a call would do before the model makes it. - Recovery. A failed request goes through
TurnRecovery, which decides between a retry, a degraded resend and a reported failure. A rejection thatMekaError::rejects_thinking_signaturenames, Claude rejecting a thinking block sent back, is answered first, by a resend of the conversation since the last compaction withMessage::without_claude_thinkingapplied, recorded as anEvent::Unsignthat names no message; a compaction’s checkpoint and summarizer do the same to their own copies. Once a profile’s retries are spent on an error that says the profile cannot serve (MekaError::is_profile_unavailable),agent::fallbackopens that profile’s breaker in theProviderRegistry, binds the next profile in its chain the turn has not tried, breaker or not, and re-sends under the retry’s own no-output guard; the turn’s end restores the named profile and its row records the one that served. The published profile cell holds both the named profile and the one serving, so a worker spawned mid-turn binds to the named one and a repin ends the substitution. Compaction runs when the context gauge says so, when the model asks throughcontext_compact, or when the user asks. A checkpoint’s request carries the turn’s own tool list, so it shares the turn’s cached prefix; its guidance names the subset it may call, and a call to the rest is refused at dispatch. Every summary, from the checkpoint or the summarizer, has the same four sections: rules and authorizations, commitments and their state, facts established, next action; a compaction-specific focus adds emphasis within them and drops none. The messages received during the summarized turns are copied into the summary message by code, newest first within a third of the verbatim budget when there are any, so what a person sent never depends on the summary. A checkpoint that ends without callingcontext_replacefalls back to the summarizer; its closing text is never taken as the summary. Every compaction restores full live context before the agent’s next request, including memory, discovery, permission, and execution guidance. The world snapshot advances only after the replacement is saved. Finished background outcomes are not delivered again during this rebuild. - Output. Text, thinking, tool calls, approval prompts and elicitations all reach the
user through the session’s
Frontend. The screen, the one-shot run, ACP and HTTP each implement it once; a sub-agent’sPermissionForwardingFrontendforwards its approval prompts and notices to its parent’s;SilentFrontendanswers a call with nobody behind it.
Where state lives
Non-secret settings live in config.toml. Secrets live in the store. Environment variables are
operational only. The store is one SQLite file, meka.db under MEKA_DATA_DIR, opened by Store,
and its shape is whatever the migration ledger says it is: a current store’s PRAGMA user_version
is the number of entries in the ledger, and HEAD_SCHEMA_FINGERPRINT in store/migrations.rs pins the
columns of the tables in HEAD_TABLES. The count is the ledger’s to state: the_ledger_is_append_only digests every entry, so read the ledger rather than this sentence for what the last ones do. The entries after messages_are_indexed_by_kind give sessions a title, a pin and a search index, fold every diacritic in the search indexes, and number turns. Before them, sessions_have_an_inbox adds the inbox table and messages_are_indexed_by_kind indexes a session’s rows by kind so a resume finds its last compact_boundary row and the context block counts them without reading the whole log; before those, names_follow_the_vocabulary gives every table, column and index the name the vocabulary uses and drops the provider_credentials view and a column nothing read. That view existed for one replay: the frozen sessions_name_their_provider reads the name when no default profile resolves, so classify_by_shape classifies a store that lost its user_version and has no such view at the version this entry leaves it, past that step, rather than at the baseline. Before it, sessions_record_their_context_tokens adds the occupancy a resume checks its first turn against, background_tasks_spell_canceled_with_one_l rewrites a task status an earlier meka stored as cancelled, and root_rows_take_the_default_level_once_the_config_reads stamps [permissions].default on a root row that still records no level and refuses to migrate while config.toml cannot be read.
| Table | Owner | Columns and indexes |
|---|---|---|
sessions | store/sessions.rs | id, created_at, updated_at, parent_session_id, working_directory, permission, approvals, profile, capabilities_json, token_id, additional_workspace_directories_json, subagent_spec_json, context_tokens, title, pinned_at, the eight cumulative counters turns, input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens, redactions, redacted_images, redacted_bytes, and the position of the last turn opened, prompt_index and turn_index, both NULL on a session that numbers nothing. idx_sessions_updated_at, idx_sessions_parent_session_id. |
messages | store/sessions.rs | id, session_id, kind, content, created_at, turn_id. idx_messages_session_id, idx_messages_session_id_kind. The writer names the turn; the store never infers one from the table. |
turns | store/turns.rs | id, session_id, source, started_at, ended_at, status, stop_reason, error_type (the kind’s name, never a URI), detail, the four usage counters. idx_turns_session_started_at, which the listing’s per-row last_turn subquery leans on: a second per-row join would be the one to measure. |
account_credentials | store/credentials.rs | account, credentials_json, updated_at. |
mcp_credentials | store/credentials.rs | server, kind, secret, updated_at; keyed by (server, kind), so a client secret and its refreshable bundle coexist. |
blobs, message_blobs | store/blobs.rs | hash, media_type, bytes, size_bytes, created_at; and message_id, hash for which message rows reference a blob. idx_message_blobs_hash. |
scratchpad_entries | store/scratchpad.rs | session_id, name, content, created_at. |
scheduled_jobs | store/schedule.rs | id, session_id, kind, spec, prompt, gate_kind, gate_spec_json, gate_last_output, gate_permission, claimed_by, claim_expires_at, attempts, created_at, last_fired_at, next_fire_at. idx_scheduled_jobs_next_fire_at, idx_scheduled_jobs_session_id. |
background_tasks | store/background.rs | id, session_id, tool, label, status, outcome, scratchpad_entry, started_at, finished_at, announced_at, delivered_at. idx_background_tasks_session_id_status. |
memories, memories_fts | store/memory.rs | id, name, description, tags, body, priority, created_at, updated_at, read_count. idx_memories_priority_created_at. The FTS index and its triggers are rebuilt to this build’s definition by reconcile_index on every open, so they are outside the ledger’s fingerprint. |
recall_entries | store/recall.rs | The screen’s recall: id, command_line, created_at. |
Per-process state that is not a table has an owner too: the scheduler’s memory of which refusals it
has already reported is the SchedulerMemory on the Store; MCP progress routing and resource
updates live on the McpClientContext; the per-path write locks are the WriteLocks on the host’s
SharedDeps, cloned into every session’s CoreMaterials; a frontend’s sticky approval answers are
its StickyApprovals, in memory and never persisted.
The doors
Most defects that survive review are one rule enforced at one entry point and forgotten at a sibling. The rules below are each written once and called from every path that reaches the thing they guard. When adding a path, call the predicate rather than restating the rule.
| Rule | Predicate | Doors |
|---|---|---|
| Whether a turn may start | ResidentSession::admit_turn and claim_idle on the one in_flight cell | admit_turn: HTTP POST /turns (under the process cap, host::http::state::admit_turn) and ACP session/prompt, and through HostHooks::admit the scheduled fires, outcome deliveries and inbox turns in host/scheduler.rs (under the same cap in serve); claim_idle: HTTP compact and rewind. The screen and one-shot sample cancel.admit() directly. |
| Whether a second turn is refused | the conversation mutex, try_lock | HTTP POST /turns (try_lock_owned, 409), ACP session/prompt (InvalidParams) |
| Whether a session is idle enough to evict | host::session::idle_on, behind ResidentSession::is_idle_given and Sessions::sweep_idle | the HTTP GC and the ACP idle sweep |
| Whether a prompt is empty | TurnInput::from_parts for typed words, TurnInput::sent for blocks | every host, before admission |
| Whether a memory write may grow the inline tier | store::memory::inline_budget_refusal, judged after the statement inside the immediate transaction of MemoryStore::write and write_body, against [memory] inline_max_bytes; WorldSnapshot::with_inline_max_bytes lays the band out by the same measure | memory_write, memory_edit, meka memory add, meka memory edit, PUT /v1/memories/{name} |
| Whether a tool call runs, is put to the user, or is refused | tools::admit_tool_call, called through Agent::run_admitted_call, the one sequence from the level read at the enforcement site to the tool’s own refusal, the approval prompt and the schema advisory | inline dispatch, the checkpoint turn in agent/compaction.rs |
| Whether a session row is whole | store::whole_session_sql, false while a fork or an import copies its rows in short transactions behind the row; Store::delete_unfinished_copies, through the startup sweep, takes one a crash left | the listing, session search (through SessionReach::sql_term), last_session_id, find_sessions_by_prefix and all_session_ids, the session counts of a project and a profile, and the turn sweep; a reader that names an id needs none, since a copy’s id is its copier’s until whole; the profile rename guard counts an unfinished copy, which runs on its row once whole; the deleting doors take every row |
| Which rows history shows | messages.copy_of, the row a copy a compaction kept copies: written by Store::save_compaction (link_kept_copies, which matches each kept message to the message of the view the compaction replaces that holds it, each the row it is shown as, so one the view rewrote in place pairs with the row it was said in and a turn two compactions kept is a copy of a copy, and a reader follows the chain to the row it stands for: Store::original_positions for the context, taken_rows for a rewind), carried by a fork and an archive (as a position), and cleared by search::release_copies_of when the turn sweep takes that row, which also indexes the copy’s words; beside it messages.said_turn_id and said_at (SaidStamp), the turn that said a kept message and when, which every copy carries from the row it pairs with and keeps once it stands alone, so the history, the context, a rewind or a branch by turn and a turn’s messages name that turn and time, while the row’s own turn_id and created_at, the turn that wrote the copy, date it for the turn sweep and mark turn ends on the screen; RowRange::History (Store::load_history), the log pages and the search index’s indexed_words leave a linked row out | the screen’s and ACP’s replays, conversation_search, conversation_read, the Markdown export, session search; the view, a rewind, a fork and the archive read every row |
| What forgets the conversation’s old shape when the log is rewritten | Agent::reset_conversation_markers: the accepted-prefix marker, the world snapshot, the read tracker, the schema advisories, the gauge and its row | compaction, screen /rewind, HTTP POST /rewind, a reload of what the store holds (Agent::match_the_store) |
| Whether a count taken on one view of the conversation cuts the other in the same place | Agent::match_the_store, which replaces a resident conversation with the store’s when the two hold different numbers of messages; Agent::match_the_store_after_a_failed_save asks only once a write failed (Agent::note_save, which every save helper of the agent passes through, sets store_behind), and a reload records where the store’s view first differs from what memory had sent (unaccepted_from), the furthest back a turn’s degrade then reaches until a request is accepted; a withdrawal whose save fails is undone in memory | a rewind of a resident session through Agent::rewind_resident (screen /rewind, HTTP POST /rewind); after a failed save, the start of every turn, a compaction between turns, HTTP POST /compact and the background-outcome poller |
| Whether a turn runs on the profile its row names | host::apply_recorded_profile, under the conversation lock | ACP session/prompt and its fires, HTTP POST /turns, POST /compact and its fires, through HostHooks::prepare |
| Whether a session is a sub-agent’s | store::spawned_session_sql in SQL; SpawnTerms::describe for the sentence every refusal starts with | every listing, meka -c, session search, the inbox driver; the refusals of the HTTP fork, the ACP fork, POST /responses, a job on it, and every builder |
| What the retention sweep may take | EXPIRABLE: a root by its own age, spared while a pin or a job sits anywhere in its tree | the retention sweep, meka session remove --older-than-days |
| How old a message row is, and which rows a turn sweep may take | ROW_AGE_SQL in store/sessions.rs (its turn’s end, else its turn’s start, else its own stamp), and only below the session’s latest compact_boundary row; a copy of a row it takes becomes the row history shows, and a turn’s record stays while a copy says it | Store::delete_expired_turns at startup, hourly and at each turn’s start; view_holds_a_turn_older_than, which decides whether a retention compaction has to run first |
| What a delete locks | Store::lock_tree: the session and every sub-agent under it, since the cascade takes them | the sweep and --all (over roots, through delete_the_unattached_among), session remove <id>, agent_delete, the servers’ deletes of a session they have not loaded |
What ${VAR} means in config.toml | config::expand_env_vars, one grammar (${VAR}, ${VAR:-default}; an unclosed ${, an empty name or a nested default is reported as malformed and left as written); what a miss or a malformed reference costs is the caller’s: [serve] refuses, an MCP server is refused when it sits in args, env, url or headers, and mcp add skips its probe | [serve] tokens and URLs, every MCP server field |
| What a tool call’s arguments are, and whether it detaches | tools::admit_arguments | dispatch, ahead of every tool |
| What a blank or wrong-typed argument means | tools/util.rs: require_str, optional_str, their _allowing_empty forms and string_or_list (blank is “not given”, a wrong type is refused by name, a blank list entry is dropped), with text::deserialize_blank_as_none for a serde-parsed tool; tests/layering.rs fails on a raw read outside its RAW_ARGUMENT_READERS ledger | every built-in’s string and list arguments |
| Where a process is started, and what it is handed for memory | process::spawn (and process::prepare with process::adopt where rmcp makes the spawn): the highest oom_score_adj on Linux, a job capped at the machine’s memory on Windows; tests/layering.rs lists every spawn site in its PROCESS_STARTERS ledger | the shell tool, the screen’s !command, schedule gates, stdio MCP servers and an HTTP server’s headers_helper, the Bubblewrap smoke test, the confined Windows command |
| How a host that serves the installation writes it, and what it serves | SharedDeps::write_installation, the one sequence: under installation_doors, installation_as_written parses the file once and refuses one that fails to parse, holds an MCP server entry the manager refuses, or, when it changed since the last whole read, anything ResolvedConfig::validate refuses; the edit decides on that parse and runs its door, which undoes its own file write when a later step fails, the file being served anyway when it moved; serve_installation then reads the file whole, holds it to everything startup holds a file to every time (SharedDeps::servable: ResolvedConfig::validate, the [web] client, and on meka serve the [serve] resolution, so an edit from outside landing between the door’s write and this read is never served unchecked), replaces the catalog with the [thinking] budget and [session].context_window its profiles complete with, under the provider cache’s lock so no build from the old catalog is cached after it (ProviderRegistry::resolve), resolves device ids, brings the manager to the file’s entries (restarted when the connection differs, installation::mcp::same_connection, or when the store’s count of the server’s credential writes moved, TokenStore::mcp_credential_generations; reconfigured in place when the tool policy alone differs, McpClientManager::reconfigure_server), records the read for config_changed, and reads the instructions again, keeping the last text when they cannot be. A credential write counts only when it changes a row. A browser login obtains its credential outside it, the MCP flow against an in-memory store, and commits through it, refused when the account or the connection it began on changed (account::commit_login, mcp::commit_login); once its commit has begun, nothing that ends logins settles it. Every MCP credential row records the endpoint it was stored for, and a connect presents it there alone (mcp::auth::presentable: OAuth tokens at their canonical resource URL, a bearer or a client secret on its origin); a credential is never bound to a url the environment leaves unresolved (installation::mcp::endpoint_of); a revocation is read before a clear and before any secret the same edit stores, carries the client secret stored for the grant’s own origin alone, and is sent after the write commits (mcp::auth::Revocation, #[must_use], so a door that drops one does not compile clean). A profile rename repoints the live cells of every resident session and running sub-agent (PublishedProfile::rename_profile, RunningSubagents::rename_profile), refuses only what would run on another profile (Store::sessions_run_on_profile_name: a root on the name, a sub-agent pinned to it), and undoes exactly what it moved (Store::undo_profile_move). The typed refusals’ HTTP wording lives in ProblemDetail::for_error alone. PUT /v1/instructions takes the same lock through write_instructions | every write of the installation over HTTP: accounts, profiles, MCP add, change, remove, secret and login commits, logout; POST /v1/config/reload, the write with no edit; the screen’s /mcp login and /mcp logout |
Where a MEKA_* variable is read | ResolvedConfig::resolve in config.rs, with paths.rs for the two that locate the config file; tests/layering.rs keeps every other file out through its ENVIRONMENT_READERS ledger | every operational override: the permission level, the standing instructions, the sandbox backend, the render mode, the MCP tool timeout, and the debug-only mock switches |
| The level a call must clear | tools::effective_permission: the [tools.tool_permissions] override, else the tool’s own | dispatch, the gate, tool_load’s listing, the catalog |
| What level a scheduled job runs at | scheduler::live_permission, reading the session row through admit_recorded | the fire door, the wake watcher, meka schedule show |
| Whether a recorded level still applies | EnabledPermissions::admit_recorded | a resume on every host (host/assembly.rs, host/http/reattach.rs, host/acp/session.rs), the scheduler |
| The refusal for a level the set does not admit | EnabledPermissions::disabled_level, raising MekaError::DisabledLevel | SharedPermission::try_set behind /permission, ACP session/set_mode and its permission option; HTTP create and PATCH |
| Whether a profile name is configured | config::require_profile, raising MekaError::ProfileNotConfigured | --profile and default_profile selection, resolve_profile_switch and the resume repin (host/assembly.rs), HTTP create and PATCH, the provider registry, meka profile use |
| What a session’s row may move to, and what a failed write costs | Store::update_session taking a SessionPatch; the policy is host::record_session_change | screen /permission, /approvals, /cd; ACP session/set_mode and session/set_config_option; HTTP PATCH; the profile switch and resume reconciliation. A patch that fails to write the level, switch, directory or roots is warned about and the command continues; one carrying a profile fails the request. The one writer below host is the sub-agent tool recording the profile a worker ran on, which warns. |
| Whether a working directory is accepted, and how it is spelled | workspace::accept_cwd; cwd_filter for a listing filter | ACP session/new, load, resume, fork; screen /cd; HTTP create, PATCH and ?cwd= |
| Whether a session accepts an image | ResidentSession::accepts_images, off the published profile’s vision | HTTP POST /turns, ACP session/prompt |
| A session’s title | Conversation::title (the first user Append in the log with a non-blank Text block that is not a redaction placeholder or harness note, whitespace collapsed, TITLE_CHARS = 200; TITLE_ROW_WHERE_SQL is the same rule in SQL) | SessionSummary, the HTTP turn response, ACP’s title update (from the resident log after a first prompt, from the row on a load, resume or fork, since a hydrated log begins at the last boundary) |
| Making a persisted session resident | Store::open_session_row: lock first, then read | screen and one-shot resume, serve re-attach (ensure_session_loaded), ACP session/load and session/resume |
| Whether a project exists, and what its name may be | store/projects.rs: Store::require_project refuses a name no row has with the names that do; add_project and rename_project hold a name to entry::validate_entry_name and refuse one taken under case folding; remove_project refuses while a memory or a root session names it | --project on a run, /project open, POST/PATCH /v1/sessions, the ACP option, meka memory add --project, session import, memory import, and the turn start of a session whose row names one |
| What a memory export is, and how it reads back | memory::render_memory writes a file and memory::memory_from_export reads one, with split_memory_file undoing the framing; MEMORY_EXPORT_FORMAT_VERSION in export.toml, brought forward by store::migrations::bring_memory_file_forward; MemoryStore::import writes the snapshot in one transaction under the write doors’ rules | meka memory export, meka memory import |
| Which tier a memory write lands in, and whether the name is free there | store::memory::resolve_write_tier, inside MemoryStore::write’s transaction over every tier: a named tier that holds the name is an update, an unnamed one keeps the one visible row’s tier or creates in the view’s own, naming the tier of the one visible holder moves it, a global and a project memory never share a name, and a name in several projects is refused where no one row is meant | memory_write, meka memory add, PUT /v1/memories/{name}; the view each door passes is MemoryView, the one predicate every read, update and delete by name carries |
| What a resident session’s log holds | Store::load_conversation: the rows from the last compact_boundary on (load_view_events), images inlined, orphaned tool_use dropped by a repair it saves, so the store replays the view memory holds; the whole log is read only by load_events for the readers that want history | every host resume through host::hydrate_conversation, the sub-agent follow-up |
| Forking | Store::fork_session_locked with SourceLock::{Probe, Probed, HeldByCaller} | meka session fork (Probe); screen /fork through host::fork_and_lock (HeldByCaller); ACP session/fork, which holds a resident source still under HeldByCaller (refusing it mid-turn) and Probes a dormant one; HTTP POST /fork, which holds a resident source the same way and probes a dormant one first (Store::probe_fork_source, passed as Probed), so its If-Match and turn_id read the source the copy takes |
| What an HTTP caller may do | scope::Scoped<R> as an extractor | every handler, by its signature |
| Whether a path may be written | workspace::WriteScope | file_write, file_edit, scratchpad_save_file, shell_execute’s confinement |
Whether meka’s own directories, or a process’s own /proc entries, may be read, with the skills store as the readable part | workspace::private_read_refusal and workspace::ReadFence | tools/util.rs for the readers, file_find, file_search |
| Whether a backend reads a profile or account key | Backend::reads_profile_key and reads_account_key (in config.rs) | profile add, profile set, account add, the load-time warning |
| Which account a profile bills | config::account_for | config validation, the provider registry |
| Whether an MCP server’s config may be sent at all | ServerEntry::refused, a field set at construction | every connect door in mcp/connector.rs, ServerEntry::reconnect |
| How an MCP connection opens, and which protocol era it speaks | handler::http_lifecycle (probe, then the initialize fallback on the same connection) and handler::stdio_probe (the probe alone, on a child the connector discards when it finds a legacy server, the verdict kept in ServerEntry::legacy_era); ServerEntry::protocol_version afterwards | the stdio and HTTP arms of connector::connect_server, auth::connect_http_with_oauth; subscriptions.rs wherever the era changes the mechanism |
What becomes Connected, and what rides onto it | ServerEntry::adopt_connection, which refuses a connection once a shutdown has begun | the first connect, the cold-start retry, a reconnect |
Where a server’s tools/list_changed and resources/updated go | handler::refresh_tools_after_list_changed, handler::record_resource_update | the client handler’s notification arms (a 2025-11-25 server’s unsolicited notifications) and the listen stream’s pump in subscriptions.rs (a 2026-07-28 server’s) |
The rounds a request takes when a 2026-07-28 server answers input_required | handler::Rounds | McpTool::call_tool_once, mcp::read_resource, mcp::get_prompt |
| Whether a fire’s session is still resident | HostHooks::still_resident | scheduled fires and outcome deliveries, once the lock is won |
| What a frontend answered for a file operation | Delegation | file_read, file_edit, file_write |
| Where an image’s bytes rest | store/blobs.rs: externalize_images on write, inline_blobs on read | writes: PreparedEvent::from_event on the caller’s thread, then store::sessions::insert_event, the one door into messages, which every writer takes (save_event, save_events_atomic, a rewind, a withdrawal, import_sessions); the fork copies rows that came through it; reads: Store::load_conversation for every resume and the sub-agent follow-up; GET /history and GET /context/messages serve the reference, GET /blobs/{hash} the bytes, and an export carries both |
| What a session’s history holds | LogRow::said over the session’s stretches (Said: a message, a compaction, a rewind, a joined prompt, or a row that says nothing), the one classifier the screen, ACP’s replay and the history call, Store::rewound with is_rewound (the runs of history rows a rewind took, a kept copy standing for the row it copies, and the prompts a turn took back), and the log pages, which leave out the copies a compaction kept; history_entries and Store::history_page in store/sessions.rs compose them into entries | the screen’s pages, ACP’s session/load replay, GET /v1/sessions/{id}/history, a blocking turn’s messages (Store::turn_entries), the answers of compact and rewind (Store::history_entry), conversation_search and conversation_read (Store::history_messages), session search’s matches |
| What a session has run and spent | its rows of turns: a turn’s row closes in settle_turn with its rounds and what the compactions it ran spent, and one that ended before its row was written gets it from settle_unopened_turn when its compactions spent or wrote anything (Agent::compaction_spend, which record_compaction_spend fills), and a compaction run between turns opens a row of its own under its compaction id (open_compaction_turn, close_compaction_turn) that its boundary names; the record’s turn_count and usage are those rows counted and summed (SPENT_COLUMNS). The /status counters (SessionStats) are the screen’s alone: a sub-agent’s spend and a turn that never finished reach them too | GET /v1/sessions, a session’s record and its events, GET /turns, meka session list and show, agent_list’s turns=, the screen’s turn-end rows |
| The sentence for a name that matches nothing | text::unknown_name | every refusal by name: profiles, accounts, MCP servers, configuration options, scopes, gates |
| How a terminal shows a time, a size, an id | text::format_timestamp with Precision, text::format_size with KIB and MIB, text::ID_PREFIX | every listing and status line; the wire keeps RFC 3339 and raw byte counts |
The Authorization value for a token | text::bearer | every backend that sends one, and store/credentials.rs |
| What a sticky approval answer covers | StickyApprovals in frontend.rs | the screen, ACP and HTTP frontends |
| How long a host waits for an approval answer | APPROVAL_TIMEOUT in frontend.rs (30 minutes) | the ACP and HTTP frontends; the screen has a human and no timeout |
| How a notice serializes | NoticeView | SSE notice, the blocking response, the one-shot JSON report |
| Whether an upstream’s own words may reach a caller | host::relay_provider_errors, bounded by error::bounded_upstream_body | ProblemDetail::for_error’s provider_response, acp_error_for’s data |
| How much of a reply is read | error::MAX_REPLY_BYTES, through error::read_whole_reply for a whole reply and provider::sse::drive for one streamed event; a redirect is refused ahead of either by provider::succeeded, since provider::build_http_client follows none | every provider reply, the token exchange included |
| Whether a skill’s name resolves outside meka’s own store | skills::refuse_foreign_write / refuse_foreign_delete, both on foreign_location | skill_write, skill_edit, skill_delete, meka skill add/remove, PUT/DELETE /v1/skills/{name}; the ForeignSkill it hands back renders with the path for a local reader and without it on the wire |
| Whether an older meka wrote the store | store/migrations.rs alone | nothing else may know |
One error, one mapping per host
MekaError carries the refusals the doors raise: SessionNotFound, TurnInFlight { doing },
EmptyPrompt, DisabledLevel { level, enabled }, ProfileNotConfigured { name, known },
RequestTooLarge, beside SessionLocked, SessionNotDrivable, Usage and Config. Each host
maps the enum once, and no handler rewrites the sentence:
- HTTP:
ProblemDetail::for_errorinhost/http/errors.rs.SessionNotFoundis 404 (session-not-found);TurnInFlightandSessionLockedare 409 (turn-in-flight,session-locked);EmptyPrompt,DisabledLevel,ProfileNotConfigured,UsageandConfigare all 422 underinvalid-body, so a client tells them apart bydetail;RequestTooLargeis 422 under its ownrequest-too-large, because meka refused it and there is no provider response behind it. ThetypeURIs live underhttps://meka.bot/errors/. - ACP:
acp_error_forinhost/acp.rs. Everything the caller can act on isInvalidParams(-32602) with theDisplayasdata; everything else isInternalError. - Screen: an error item in the transcript, the
Displaytext. - One-shot and CLI: the
Displayon stderr and exit code 1; 130 when the turn was interrupted.
Two classes never travel verbatim, on either wire. Installation is meka’s own sentence about
the operator’s setup – a [web] client that will not build, a base_url shape a backend refuses
– so it names a path or an endpoint from config.toml that no caller can act on: HTTP answers a
sanitized 500 and ACP an InternalError with the text in the log, while the screen and the CLI print
it, since their reader is the operator. The web client is built once in build_shared_deps, so on
every host a bad [web] block fails the process at startup rather than surfacing per session.
And an upstream’s own response text travels only when [serve] relay_provider_errors says so, which
host::relay_provider_errors reads once for both hosts. for_error attaches it as
provider_response; acp_error_for appends it to data; both bound it with
error::bounded_upstream_body. An MCP connector’s reason, a Database and an Io never travel at
all, on either.
What each host offers
The hosts do not offer the same operations, and the gaps are recorded here rather than filled.
“Run” is meka with -p, -c, -r, --profile and --permission; the one-shot is that run
with --oneshot.
| Operation | Screen | One-shot | ACP | HTTP | meka session |
|---|---|---|---|---|---|
| New session | at launch | at launch | session/new | POST /v1/sessions | no |
| Resume | -c, -r at launch | -c, -r at launch | session/load, session/resume | implicit re-attach of a dormant id on any request | no |
| Fork | /fork | no | session/fork | POST /{id}/fork | fork |
| Delete | no | no | no (session/close releases only) | DELETE /{id} | remove |
| Rewind | /rewind | no | no | POST /{id}/rewind | rewind |
| Compact | /compact | automatic only | automatic only | POST /{id}/compact | no |
| Export | /export (Markdown) | no | no | GET /{id}/export | export |
| Import | no | no | no | POST /v1/sessions/import | import |
| Profile switch | /profile | --profile at launch | session/set_config_option | PATCH profile | no; meka -r --profile repins |
| Level switch | /permission, Shift+Tab | --permission at launch | session/set_mode, config option | PATCH permission, live | no |
| Approvals | /approvals | config only | session/set_config_option | PATCH approvals, live | no |
| List, show | /session, /status | no | session/list | GET, GET /{id} | list, show |
| Cancel a turn | Esc, Ctrl+C | Ctrl+C | session/cancel | POST /{id}/cancel | no |
The screen and the one-shot share COMMANDS in host.rs for what a slash command is; ACP advertises
the three rows with an editor_help (/mcp, /status, /usage) as available_commands.
Mid-turn
Only HTTP and ACP can receive a request while a turn holds the session, and the two answer differently by design.
- HTTP refuses what needs the turn to end. A
PATCHnamingworking_directoryorprofile,DELETE, fork, compact and rewind checkin_flight(or failclaim_idle) and return 409 withtypehttps://meka.bot/errors/turn-in-flight, throughturn_in_flight_conflictandProblemDetail::for_error(MekaError::TurnInFlight). A secondPOST /turnson the session gets the same 409. Thedetailnames what was refused (doing), and asession_idmember carries the id. APATCHnaming onlypermissionorapprovalswrites the cells and the row without waiting, the same three steps ACP takes below.POST /v1/sessions/{id}/inboxnever refuses for a turn in flight: it writes aninbox_itemsrow (store/inbox.rs) and wakes the driver (host/http/inbox.rs). The row reaches the model at one of four places, all inAgent::run_attributed_turnso no host door can forget one: asteeror aninterruptat a round boundary, after a tool round’s results or after a reply that would have ended the turn, in the same user message or the one that follows the reply, stampedappended_atby the same transaction (save_events_atomic_marking_inbox); aninterruptthe moment it lands while a provider call is in flight; any class at the next turn’s opening, after the words, whoever started the turn; or as the opening message of a turn the driver starts (host::scheduler::run_inbox_turns) when the session is idle. The item isdeliveredwherelast_accepted_lenis stamped, which is the request the provider accepted, andFrontendEvent::InboxDeliveredis what the feed and the webhook relay. - An interrupt is decided by the loop, never by a host. Each provider call runs under a child
of the turn’s token, and
watch_for_interruptspolls the inbox once a second (INTERRUPT_POLL_INTERVAL) and cancels the child when aninterruptrow is pending: polled rather than signaled, so a parent’sagent_steeron a running worker needs no registry to reach it. The call returns as it does on a stop, andabsorb_interruptreads the tokens to tell the two apart. What streamed is kept throughwithout_tool_use, the items follow as the next user message, and the loopcontinues in the same turn. Nothing streamed means the items join the user message that was cut, as a withdrawal and a re-append in one write, so the log still ends on an appended turn opening andwithdraw_unanswered_prompttakes the items back with the prompt if the turn then fails before the provider accepts anything. A tool round is never cut. - ACP applies the level and the switch, and refuses the rest.
session/set_modeand the permission and approvals options ofsession/set_config_optionwrite the cells without taking the conversation mutex, so an editor toggle takes effect on the very next tool call, and then record the row. The profile optiontry_locks the conversation first and refuses withInvalidParams(TurnInFlight { doing: "switch profile" }) so that a switch refused for a turn in flight leaves the row where it was. A secondsession/promptis refusedInvalidParams(TurnInFlight { doing: "prompt" }), and so is asession/forkof the session (TurnInFlight { doing: "fork the session" }), whichtry_locks the conversation the same way and holds it across the copy.
Frontends
Seven things implement or stand in for Frontend: TuiFrontend (host/tui/frontend.rs, the
screen), PlainFrontend (host/oneshot/frontend.rs, the plain one-shot), AcpFrontend
(host/acp/frontend.rs), HttpFrontend
(host/http/http_frontend.rs, which records for the blocking response and broadcasts to SSE
through host/http/sse.rs), JsonFrontend (host/oneshot.rs, --format json), SilentFrontend
and PermissionForwardingFrontend (frontend.rs; the latter forwards notices and approval prompts
to the parent’s frontend and drops the rest). Where they differ:
| Event | Screen | ACP | HTTP stream | HTTP blocking | One-shot JSON | Silent |
|---|---|---|---|---|---|---|
Notice info / warn | a dim • row, or a yellow ! row | notice at severity info / warning when the client advertised session.notices, else an agent-message chunk prefixed [meka] / [meka warn] | notice event (NoticeView) | notices[] | notices[] | dropped |
McpProgress | the running call’s progress line under its row | tracing::info! | tool_call.progress event | dropped | dropped | dropped |
CompactionStarted / CompactionFailed | the working row’s word while nothing more specific is said; a failure is the command’s or the turn’s to report | compaction_update in_progress, then failed or cancelled, for a client that advertised session.compaction; otherwise a think card opened, then closed as failed | context.compacting / context.compaction_failed events | dropped | dropped | dropped |
Compacted | the →← Context compacted rule, which opens to the record’s headline and departures, the checkpoint’s rounds and the summary as written, live and on a replay of the log alike | compaction_update completed with the summary and the record in _meta.compaction, or the card closed under the record’s headline for a client without the entry, its first report on a replay, which then sends the record’s checkpoint calls as CheckpointCallCompleted does | context.compacted event | dropped; GET /history carries the record on the compaction’s item, GET /context/messages on the summary | compactions[] | dropped |
CheckpointCallCompleted (a compaction checkpoint’s call finished) | dropped: the compaction’s row shows the checkpoint from its record once it lands | a finished tool_call under <compaction id>/<n>, or the update that completes the card the call’s approval prompt opened | dropped: GET /history carries the record | dropped | dropped | dropped |
ToolCallDropped (a call nothing will run: its round was cut or recovered from, or a retried stream did not bring it again) | the call’s row goes, found by its id and whether it had started | the card of one that had started closed failed, a terminal it opened ended | dropped: the turn’s terminal settles it | dropped | dropped | dropped |
| Approval with nobody to ask | warn approval_refused_without_asking, deny (the screen is gone) | asks the client; deny after APPROVAL_TIMEOUT, Canceled on cancel | approval.requested event while a streaming client or a feed reader with attend=true is there; deny after APPROVAL_TIMEOUT, Canceled when the last of them leaves; refused with a warn notice otherwise, unless the session declared answers_prompts_on_return and no request holds the turn, in which case it parks and waits out APPROVAL_TIMEOUT | as HTTP stream: a feed attendee answers a JSON request’s prompt too; without one, warn notice, deny, the same capability aside | warn approval_refused_without_asking, deny | deny; the notice goes nowhere |
| Elicitation | asks the screen; warn elicitation_declined and decline when it is gone | elicitation/create; warn elicitation_declined and decline when the client lacks the mode or two sessions have calls in flight on the server | warn elicitation_declined, decline | same | trait default: warn elicitation_declined, decline | same, dropped |
TurnOpened (the level a turn opened under) | the turn’s prompts take the level’s color on their bar | dropped: the protocol has no place for it | dropped: the turn’s record carries it | the in-flight turn’s permission | dropped | dropped |
InboxAppended (inbox items added behind a round’s results, a nudge or a prompt an interrupt cut) | each item another client posted drawn as a prompt where it lands; one the screen enqueued itself is already drawn | UserMessageChunk per item | dropped: inbox.delivered says it, and the messages hold its words | dropped | dropped | dropped |
MessageAppended (the message a turn opens with, whoever opened it, or one an interrupt appends whole) | a dim • Scheduled job <id> fired: … or • Background task <id> … row per fire or report block; an inbox item another client posted is drawn as a prompt with its (steer) note, one the screen enqueued itself is already drawn; the person’s words are drawn as typed | UserMessageChunk per scheduled or inbox block; a background_report block is said as what reported, a notice with the tasks under _meta.background or a stage-direction chunk; the editor’s own words are not echoed | message.appended event (MessageBody), re-issued with resumed: true to a client joining mid-turn | the opening, first in messages[] | a paragraph break | dropped |
| Scheduled fire failure | a red ■ row; ! Interrupted on a cancel | warn notice scheduled job '<id>' failed: ...; info on a cancel | schedule.fired webhook, status completed, canceled, failed or not_run; nothing on the frontend | same webhook | no scheduler | n/a |
The scheduled-fire failure row comes from each host’s HostHooks (finished) rather than its
Frontend, in host/tui.rs, host/acp/schedule.rs and host/http/schedule.rs. The fire’s
prompt takes no hook: the agent emits MessageAppended where it appends a turn’s opening, which is
the one place every door passes, so a report riding a client’s turn is announced as a fire’s is.
The ACP frontend sends each tool call under an id CallIds (host/acp/frontend.rs) gives it, since
a provider may give one id to every call or reuse its ids each round: the provider’s own id the
first time the session sends it, else one minted with a token unique to the frontend, and always a
minted one when the session was opened as EditorThread::Inherited (resume, fork, a reopen after
the idle sweep), whose thread the editor already holds. Every event about a call after its
ToolCallStarted, an approval prompt’s PromptedCall::Announced included, goes under that id, the
oldest still running under the provider’s id, and session/load’s replay goes through the same
CallIds, so a live call after it is told apart from the replayed ones. A sub-agent’s activity goes
under the id its agent_spawn card had when the sub-agent started (AcpFrontend::for_subagent),
since a background sub-agent outlasts its call. A call ToolCallDropped names, or one still running
at TurnEnded, is closed failed, unless a session/cancel stopped the turn, which the client
marks; a detached command streams no output (OutputRelay::for_call), its call having returned.
Building
Two Cargo features shape a build. serve, on by default, is meka serve: the HTTP API and the
dependencies only it needs, so --no-default-features builds a meka without one; src/host/http.rs
is behind cfg(feature = "serve"). mock-provider compiles in provider/mock.rs, the scripted
provider the test suites drive every host with; debug builds carry it regardless, and CI enables it
so a release-profile build is testable too. A shipped artifact is built without it. At run time,
MEKA_MOCK_PROVIDER=1 selects that provider on every host and MEKA_MOCK_PROVIDER_SCRIPT names
the JSON script it plays back; both are read by ResolvedConfig::resolve, like every other MEKA_*
variable, and acted on by host::build_shared_deps.
The integration crates are gated on the same fact: tests/acp.rs and tests/cli.rs carry
#![cfg(any(debug_assertions, feature = "mock-provider"))]; tests/serve.rs and
tests/multiprocess.rs add feature = "serve"; tests/tui_pty.rs adds unix. Only
tests/layering.rs runs unconditionally. Unit tests open the store with Store::for_test(), an
in-memory database at the current schema; Path::new(":memory:") is spelled out because None
means the default path. The integration crates share tests/harness/support.rs, whose Install is
a temporary root with a config directory, a data directory and a work directory; Install::env
points a meka command at it (MEKA_CONFIG_DIR, MEKA_DATA_DIR, HOME, XDG_*,
MEKA_MOCK_PROVIDER=1, the script when one was written) and Install::meka(args) builds one.
The exact gate CI runs is in AGENTS.md under “Build gate”; run it before declaring a change done,
since clippy and rustdoc deny warnings there and not locally. The one-shot conversion for a 0.45
config.toml, migrate-0.45-to-0.46.py, is not in the repository: like the 0.42 script before it,
it is attached to its release as an asset by hand and carries its own --self-test fixture.