Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

ModuleHolds
src/main.rsDispatch only: parse arguments, pick a host or a CLI handler, map the exit code.
build.rsThe 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.rsThe 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.rsWhat 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.rsThe 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.rsPrivate 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.

  1. Admission. A ResidentSession counts 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 with admit_turn, which hands back a TurnGuard and samples the cancel epoch; HTTP wraps it in host::http::state::admit_turn to add the process-wide cap. A scheduled fire, an outcome delivery or an inbox turn goes through HostHooks::admit, which under serve counts on the same cap and steps back for the next tick when it is full; HTTP compact and rewind take claim_idle, which refuses while anything is in flight. The screen and the one-shot drive one session from one thread, so they sample cancel.admit() themselves and take the conversation lock.
  2. 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_parts is the empty-prompt rule, raising MekaError::EmptyPrompt before admission.
  3. The loop. Agent::run_turn appends one user message of two blocks: a TurnContext block holding everything meka injected (permission and environment context, world state, budget, background outcomes, the resume notice) and a Text block 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 a CompletionRequest and dispatches every tool call the response carries. admit_arguments type-checks the call’s arguments and takes out background, the flag that detaches it; admit_tool_call decides run, ask or refuse. Each call gets a ToolContext: 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, in prompt::render_tool_discovery: summaries while the section fits 8 KB, names past it, and past 200 names a count that points at tool_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_call lives in tools, where tool_search and tool_load consult it to say what a call would do before the model makes it.
  4. Recovery. A failed request goes through TurnRecovery, which decides between a retry, a degraded resend and a reported failure. A rejection that MekaError::rejects_thinking_signature names, Claude rejecting a thinking block sent back, is answered first, by a resend of the conversation since the last compaction with Message::without_claude_thinking applied, recorded as an Event::Unsign that 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::fallback opens that profile’s breaker in the ProviderRegistry, 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 through context_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 calling context_replace falls 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.
  5. 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’s PermissionForwardingFrontend forwards its approval prompts and notices to its parent’s; SilentFrontend answers 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.

TableOwnerColumns and indexes
sessionsstore/sessions.rsid, 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.
messagesstore/sessions.rsid, 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.
turnsstore/turns.rsid, 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_credentialsstore/credentials.rsaccount, credentials_json, updated_at.
mcp_credentialsstore/credentials.rsserver, kind, secret, updated_at; keyed by (server, kind), so a client secret and its refreshable bundle coexist.
blobs, message_blobsstore/blobs.rshash, media_type, bytes, size_bytes, created_at; and message_id, hash for which message rows reference a blob. idx_message_blobs_hash.
scratchpad_entriesstore/scratchpad.rssession_id, name, content, created_at.
scheduled_jobsstore/schedule.rsid, 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_tasksstore/background.rsid, session_id, tool, label, status, outcome, scratchpad_entry, started_at, finished_at, announced_at, delivered_at. idx_background_tasks_session_id_status.
memories, memories_ftsstore/memory.rsid, 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_entriesstore/recall.rsThe 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.

RulePredicateDoors
Whether a turn may startResidentSession::admit_turn and claim_idle on the one in_flight celladmit_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 refusedthe conversation mutex, try_lockHTTP POST /turns (try_lock_owned, 409), ACP session/prompt (InvalidParams)
Whether a session is idle enough to evicthost::session::idle_on, behind ResidentSession::is_idle_given and Sessions::sweep_idlethe HTTP GC and the ACP idle sweep
Whether a prompt is emptyTurnInput::from_parts for typed words, TurnInput::sent for blocksevery host, before admission
Whether a memory write may grow the inline tierstore::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 measurememory_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 refusedtools::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 advisoryinline dispatch, the checkpoint turn in agent/compaction.rs
Whether a session row is wholestore::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 leftthe 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 showsmessages.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 outthe 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 rewrittenAgent::reset_conversation_markers: the accepted-prefix marker, the world snapshot, the read tracker, the schema advisories, the gauge and its rowcompaction, 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 placeAgent::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 memorya 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 nameshost::apply_recorded_profile, under the conversation lockACP session/prompt and its fires, HTTP POST /turns, POST /compact and its fires, through HostHooks::prepare
Whether a session is a sub-agent’sstore::spawned_session_sql in SQL; SpawnTerms::describe for the sentence every refusal starts withevery 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 takeEXPIRABLE: a root by its own age, spared while a pin or a job sits anywhere in its treethe retention sweep, meka session remove --older-than-days
How old a message row is, and which rows a turn sweep may takeROW_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 itStore::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 locksStore::lock_tree: the session and every sub-agent under it, since the cascade takes themthe 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.tomlconfig::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 detachestools::admit_argumentsdispatch, ahead of every tool
What a blank or wrong-typed argument meanstools/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 ledgerevery built-in’s string and list arguments
Where a process is started, and what it is handed for memoryprocess::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 ledgerthe 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 servesSharedDeps::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_instructionsevery 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 readResolvedConfig::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 ledgerevery 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 cleartools::effective_permission: the [tools.tool_permissions] override, else the tool’s owndispatch, the gate, tool_load’s listing, the catalog
What level a scheduled job runs atscheduler::live_permission, reading the session row through admit_recordedthe fire door, the wake watcher, meka schedule show
Whether a recorded level still appliesEnabledPermissions::admit_recordeda 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 admitEnabledPermissions::disabled_level, raising MekaError::DisabledLevelSharedPermission::try_set behind /permission, ACP session/set_mode and its permission option; HTTP create and PATCH
Whether a profile name is configuredconfig::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 costsStore::update_session taking a SessionPatch; the policy is host::record_session_changescreen /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 spelledworkspace::accept_cwd; cwd_filter for a listing filterACP session/new, load, resume, fork; screen /cd; HTTP create, PATCH and ?cwd=
Whether a session accepts an imageResidentSession::accepts_images, off the published profile’s visionHTTP POST /turns, ACP session/prompt
A session’s titleConversation::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 residentStore::open_session_row: lock first, then readscreen 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 bestore/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 backmemory::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’ rulesmeka memory export, meka memory import
Which tier a memory write lands in, and whether the name is free therestore::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 meantmemory_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 holdsStore::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 historyevery host resume through host::hydrate_conversation, the sub-agent follow-up
ForkingStore::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 doscope::Scoped<R> as an extractorevery handler, by its signature
Whether a path may be writtenworkspace::WriteScopefile_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 partworkspace::private_read_refusal and workspace::ReadFencetools/util.rs for the readers, file_find, file_search
Whether a backend reads a profile or account keyBackend::reads_profile_key and reads_account_key (in config.rs)profile add, profile set, account add, the load-time warning
Which account a profile billsconfig::account_forconfig validation, the provider registry
Whether an MCP server’s config may be sent at allServerEntry::refused, a field set at constructionevery connect door in mcp/connector.rs, ServerEntry::reconnect
How an MCP connection opens, and which protocol era it speakshandler::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 afterwardsthe 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 itServerEntry::adopt_connection, which refuses a connection once a shutdown has begunthe first connect, the cold-start retry, a reconnect
Where a server’s tools/list_changed and resources/updated gohandler::refresh_tools_after_list_changed, handler::record_resource_updatethe 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_requiredhandler::RoundsMcpTool::call_tool_once, mcp::read_resource, mcp::get_prompt
Whether a fire’s session is still residentHostHooks::still_residentscheduled fires and outcome deliveries, once the lock is won
What a frontend answered for a file operationDelegationfile_read, file_edit, file_write
Where an image’s bytes reststore/blobs.rs: externalize_images on write, inline_blobs on readwrites: 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 holdsLogRow::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 entriesthe 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 spentits 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 tooGET /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 nothingtext::unknown_nameevery refusal by name: profiles, accounts, MCP servers, configuration options, scopes, gates
How a terminal shows a time, a size, an idtext::format_timestamp with Precision, text::format_size with KIB and MIB, text::ID_PREFIXevery listing and status line; the wire keeps RFC 3339 and raw byte counts
The Authorization value for a tokentext::bearerevery backend that sends one, and store/credentials.rs
What a sticky approval answer coversStickyApprovals in frontend.rsthe screen, ACP and HTTP frontends
How long a host waits for an approval answerAPPROVAL_TIMEOUT in frontend.rs (30 minutes)the ACP and HTTP frontends; the screen has a human and no timeout
How a notice serializesNoticeViewSSE notice, the blocking response, the one-shot JSON report
Whether an upstream’s own words may reach a callerhost::relay_provider_errors, bounded by error::bounded_upstream_bodyProblemDetail::for_error’s provider_response, acp_error_for’s data
How much of a reply is readerror::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 noneevery provider reply, the token exchange included
Whether a skill’s name resolves outside meka’s own storeskills::refuse_foreign_write / refuse_foreign_delete, both on foreign_locationskill_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 storestore/migrations.rs alonenothing 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_error in host/http/errors.rs. SessionNotFound is 404 (session-not-found); TurnInFlight and SessionLocked are 409 (turn-in-flight, session-locked); EmptyPrompt, DisabledLevel, ProfileNotConfigured, Usage and Config are all 422 under invalid-body, so a client tells them apart by detail; RequestTooLarge is 422 under its own request-too-large, because meka refused it and there is no provider response behind it. The type URIs live under https://meka.bot/errors/.
  • ACP: acp_error_for in host/acp.rs. Everything the caller can act on is InvalidParams (-32602) with the Display as data; everything else is InternalError.
  • Screen: an error item in the transcript, the Display text.
  • One-shot and CLI: the Display on 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.

OperationScreenOne-shotACPHTTPmeka session
New sessionat launchat launchsession/newPOST /v1/sessionsno
Resume-c, -r at launch-c, -r at launchsession/load, session/resumeimplicit re-attach of a dormant id on any requestno
Fork/forknosession/forkPOST /{id}/forkfork
Deletenonono (session/close releases only)DELETE /{id}remove
Rewind/rewindnonoPOST /{id}/rewindrewind
Compact/compactautomatic onlyautomatic onlyPOST /{id}/compactno
Export/export (Markdown)nonoGET /{id}/exportexport
ImportnononoPOST /v1/sessions/importimport
Profile switch/profile--profile at launchsession/set_config_optionPATCH profileno; meka -r --profile repins
Level switch/permission, Shift+Tab--permission at launchsession/set_mode, config optionPATCH permission, liveno
Approvals/approvalsconfig onlysession/set_config_optionPATCH approvals, liveno
List, show/session, /statusnosession/listGET, GET /{id}list, show
Cancel a turnEsc, Ctrl+CCtrl+Csession/cancelPOST /{id}/cancelno

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 PATCH naming working_directory or profile, DELETE, fork, compact and rewind check in_flight (or fail claim_idle) and return 409 with type https://meka.bot/errors/turn-in-flight, through turn_in_flight_conflict and ProblemDetail::for_error(MekaError::TurnInFlight). A second POST /turns on the session gets the same 409. The detail names what was refused (doing), and a session_id member carries the id. A PATCH naming only permission or approvals writes the cells and the row without waiting, the same three steps ACP takes below. POST /v1/sessions/{id}/inbox never refuses for a turn in flight: it writes an inbox_items row (store/inbox.rs) and wakes the driver (host/http/inbox.rs). The row reaches the model at one of four places, all in Agent::run_attributed_turn so no host door can forget one: a steer or an interrupt at 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, stamped appended_at by the same transaction (save_events_atomic_marking_inbox); an interrupt the 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 is delivered where last_accepted_len is stamped, which is the request the provider accepted, and FrontendEvent::InboxDelivered is 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_interrupts polls the inbox once a second (INTERRUPT_POLL_INTERVAL) and cancels the child when an interrupt row is pending: polled rather than signaled, so a parent’s agent_steer on a running worker needs no registry to reach it. The call returns as it does on a stop, and absorb_interrupt reads the tokens to tell the two apart. What streamed is kept through without_tool_use, the items follow as the next user message, and the loop continues 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 and withdraw_unanswered_prompt takes 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_mode and the permission and approvals options of session/set_config_option write 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 option try_locks the conversation first and refuses with InvalidParams (TurnInFlight { doing: "switch profile" }) so that a switch refused for a turn in flight leaves the row where it was. A second session/prompt is refused InvalidParams (TurnInFlight { doing: "prompt" }), and so is a session/fork of the session (TurnInFlight { doing: "fork the session" }), which try_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:

EventScreenACPHTTP streamHTTP blockingOne-shot JSONSilent
Notice info / warna dim • row, or a yellow ! rownotice 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
McpProgressthe running call’s progress line under its rowtracing::info!tool_call.progress eventdroppeddroppeddropped
CompactionStarted / CompactionFailedthe working row’s word while nothing more specific is said; a failure is the command’s or the turn’s to reportcompaction_update in_progress, then failed or cancelled, for a client that advertised session.compaction; otherwise a think card opened, then closed as failedcontext.compacting / context.compaction_failed eventsdroppeddroppeddropped
Compactedthe →← 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 alikecompaction_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 doescontext.compacted eventdropped; GET /history carries the record on the compaction’s item, GET /context/messages on the summarycompactions[]dropped
CheckpointCallCompleted (a compaction checkpoint’s call finished)dropped: the compaction’s row shows the checkpoint from its record once it landsa finished tool_call under <compaction id>/<n>, or the update that completes the card the call’s approval prompt openeddropped: GET /history carries the recorddroppeddroppeddropped
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 startedthe card of one that had started closed failed, a terminal it opened endeddropped: the turn’s terminal settles itdroppeddroppeddropped
Approval with nobody to askwarn approval_refused_without_asking, deny (the screen is gone)asks the client; deny after APPROVAL_TIMEOUT, Canceled on cancelapproval.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_TIMEOUTas HTTP stream: a feed attendee answers a JSON request’s prompt too; without one, warn notice, deny, the same capability asidewarn approval_refused_without_asking, denydeny; the notice goes nowhere
Elicitationasks the screen; warn elicitation_declined and decline when it is goneelicitation/create; warn elicitation_declined and decline when the client lacks the mode or two sessions have calls in flight on the serverwarn elicitation_declined, declinesametrait default: warn elicitation_declined, declinesame, dropped
TurnOpened (the level a turn opened under)the turn’s prompts take the level’s color on their bardropped: the protocol has no place for itdropped: the turn’s record carries itthe in-flight turn’s permissiondroppeddropped
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 drawnUserMessageChunk per itemdropped: inbox.delivered says it, and the messages hold its wordsdroppeddroppeddropped
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 typedUserMessageChunk 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 echoedmessage.appended event (MessageBody), re-issued with resumed: true to a client joining mid-turnthe opening, first in messages[]a paragraph breakdropped
Scheduled fire failurea red ■ row; ! Interrupted on a cancelwarn notice scheduled job '<id>' failed: ...; info on a cancelschedule.fired webhook, status completed, canceled, failed or not_run; nothing on the frontendsame webhookno schedulern/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.