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 around the live REPL prompt
 3    console         the terminal between two prompts: spacing, notices, errors
 4    render          markdown, tool indicators, todo lists, status lines
 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
12    sandbox         read-only confinement for execute_command
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    todo            the task list
17    image           format detection and transcoding
18    fs              private directories, atomic replace, file locks
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 the two terminal hosts (host/repl.rs, host/oneshot.rs) and by main.rs as tracing’s writer; it writes through console, which draws with render. render and console are reached only from host, cli and relay. 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.
src/cli.rs, src/cli/The clap definitions and one handler file per subcommand group: account, profile, session, history, mcp, tool, skills, memory, instructions, schedule, background. Everything that owns the stdout/stderr contract lives here.
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, BusyGuard, the CancelCell, the Sessions registry and its idle sweep, fork_and_lock, outcome claiming. host/scheduler.rs is HostHooks and run_wakeup, the out-of-band turn every host runs the same way. host/terminal.rs is what the REPL and one-shot share: Ctrl+C, the interruptible turn. host.rs keeps COMMANDS, the slash-command table the REPL offers and ACP advertises the for_editors rows of. host/repl, host/oneshot, host/acp and host/http are the hosts.
src/agent.rs, src/agent/The Agent and its options; turn.rs runs a turn and owns TurnInput, dispatch.rs executes tool calls and owns admit_tool_call, 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, 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 last_turn_at, capabilities and turn_in_flight; 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, todo list, 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, 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 LoginPrompt that only meka mcp login installs.
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; 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/profile.rsConfigFile, loading, environment substitution, ResolvedConfig, and the vocabulary every layer reads. 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.rsThe system prompt and build_turn_context, the block the model sees ahead of every user turn.
src/fs.rs, src/oauth.rs, src/sync.rs, src/text.rsPrivate directories and the file locks; PKCE, refresh and the loopback callback; 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 or an outcome delivery takes mark_busy; HTTP compact and rewind take claim_idle, which refuses while anything is in flight. The REPL 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, todos, 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 /messages, 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.
  4. Recovery. A failed request goes through TurnRecovery, which decides between a retry, a degraded resend and a reported failure. Compaction runs when the context gauge says so, when the model asks through context_compact, or when the user asks.
  5. Output. Text, thinking, tool indicators, approval prompts and elicitations all reach the user through the session’s Frontend. The REPL, 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: twenty-three entries today, so a current store reads PRAGMA user_version = 23, and HEAD_SCHEMA_FINGERPRINT in store/migrations.rs pins the columns of the twelve tables in HEAD_TABLES. The last two entries are sessions_have_an_inbox, which adds the inbox table, and messages_are_indexed_by_kind, which 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 them, 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, cwd, permission, approvals, profile, capabilities_json, token_id, additional_roots_json, subagent_spec_json, context_tokens, and the eight cumulative counters turns, input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens, redactions, redacted_images, redacted_bytes. idx_sessions_updated_at, idx_sessions_parent_session_id.
messagesstore/sessions.rsid, session_id, kind, content, created_at. idx_messages_session_id, idx_messages_session_id_kind.
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.
prompt_historystore/history.rsThe REPL’s input history: 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, mark_busy, claim_idle on the one in_flight celladmit_turn: HTTP POST /turn (under the process cap, host::http::state::admit_turn) and ACP session/prompt; mark_busy: scheduled fires and outcome deliveries in host/scheduler.rs; claim_idle: HTTP compact and rewind. The REPL and one-shot sample cancel.admit() directly.
Whether a second turn is refusedthe conversation mutex, try_lockHTTP POST /turn (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_partsevery host, before admission
Whether a tool call runs, is put to the user, or is refusedadmit_tool_call in agent/dispatch.rsinline dispatch, the checkpoint turn in agent/compaction.rs
What a tool call’s arguments are, and whether it detachestools::admit_argumentsdispatch, ahead of every tool
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_changeREPL /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; REPL /cd; HTTP create, PATCH and ?cwd=
Whether a session accepts an imageResidentSession::accepts_images, off the published profile’s visionHTTP POST /turn, 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 = 80; 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 readREPL and one-shot resume, serve re-attach (ensure_session_loaded), ACP session/load and session/resume
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; 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, HeldByCaller}meka session fork (Probe); REPL /fork through host::fork_and_lock (HeldByCaller); HTTP POST /fork and ACP session/fork, which hold a resident source still under HeldByCaller (refusing it mid-turn) and Probe a dormant one
What an HTTP caller may doscope::Scoped<R> as an extractorevery handler, by its signature
Whether a path may be writtenworkspace::WriteScopewrite_file, edit_file, scratchpad_save_file, execute_command’s confinement
Whether meka’s own directories may be readworkspace::private_read_refusal and resolves_into_privatetools/util.rs for the readers, find_files, search_contents
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
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 operationDelegationread_file, edit_file, write_file
Whether thinking is on for a requestThinkingOverride on CompletionRequestthe turn, the summarizer, the checkpoint turn
Where an image’s bytes reststore/blobs.rs: externalize_images on write, inline_blobs on readwrites: save_event, save_events_atomic, import_sessions, the fork’s row copy; reads: Store::load_conversation for every resume and the sub-agent follow-up; GET /messages serves the reference, GET /blobs/{hash} the bytes, and an export carries both
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 REPL, 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 REPL 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
Whether a skill’s name resolves outside meka’s own storeskills::refuse_foreign_write / refuse_foreign_delete, both on foreign_locationskill_write, 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.so/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.
  • REPL: console.error(&error), the Display on stderr.
  • 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 REPL 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.

  • REPL: console.error(&error), the Display on stderr.
  • One-shot and CLI: the Display on stderr and exit code 1; 130 when the turn was interrupted.

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.

OperationREPLOne-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}delete
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 turnCtrl+CCtrl+Csession/cancelPOST /{id}/cancelno

The REPL and the one-shot share COMMANDS in host.rs for what a slash command is; ACP advertises the three rows marked for_editors (/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 cwd or profile, DELETE, fork, compact and rewind check in_flight (or fail claim_idle) and return 409 with type https://meka.so/errors/turn-in-flight, through turn_in_flight_conflict and ProblemDetail::for_error(MekaError::TurnInFlight). A second POST /turn 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 the round boundary after a tool round’s results, in the same user message and 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

Six things implement or stand in for Frontend: ReplFrontend (host/repl/frontend.rs, also 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:

EventREPLACPHTTP streamHTTP blockingOne-shot JSONSilent
Notice info / warnconsole.notice, dim or warn-coloredagent-message chunk prefixed [meka] / [meka warn]notice event (NoticeView)notices[]notices[]dropped
McpProgressinline status linetracing::info!progress eventdroppeddroppeddropped
Compactednothing (/compact prints render::compaction_summary)info noticecontext.compacted eventdropped; GET /messages carries the markerdroppeddropped
Approval with nobody to askwarn approval_refused_without_asking, deny (REPL thread gone)asks the client; deny after APPROVAL_TIMEOUT, Canceled on cancelpermission_required 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 otherwiseas HTTP stream: a feed attendee answers a blocking turn’s prompt too; without one, warn notice, denywarn approval_refused_without_asking, denydeny; the notice goes nowhere
Elicitationasks through the REPL thread; warn elicitation_declined and decline when it is goneelicitation/create; warn elicitation_declined and decline when the client lacks the modewarn elicitation_declined, declinesametrait default: warn elicitation_declined, declinesame, dropped
Scheduled fire promptdim info notice on the consoleUserMessageChunkinfo notice into the streaminfo notice, drained after the turnno schedulern/a
Scheduled fire failureconsole.error; “interrupted” annotation on a cancelwarn notice scheduled job '<id>' failed: ...; info on a cancelschedule.fired webhook, status completed, canceled or failed; nothing on the frontendsame webhookno schedulern/a

The scheduled-fire rows come from each host’s HostHooks (show_prompt, finished) rather than its Frontend, in host/repl.rs, host/acp/schedule.rs and host/http/schedule.rs.

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 (provider/registry.rs, host/assembly.rs) and MEKA_MOCK_PROVIDER_SCRIPT names the JSON script it plays back.

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/repl_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.