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

Upgrading

Most upgrades are a binary swap: stop every meka process on the store (a meka serve unit, an open screen, an editor’s meka acp), replace the executable, and carry on. An upgrade that changes the store’s schema migrates it on the first open, and refuses while another meka process of 0.87 or later has the store open, since that process would go on writing the shape the migration replaced; an older process goes unseen, which is why it is stopped first. This page covers the upgrades that are more than a swap.

Copying a store

The store under MEKA_DATA_DIR runs in WAL mode, so the most recent writes, including a schema migration, can sit in meka.db-wal beside meka.db until SQLite checkpoints them. A copy that takes meka.db alone can therefore carry a schema version its tables have not caught up with. meka checks for that on open and refuses the store rather than running against it. Copy the -wal and -shm companions with the file.

0.88 to 0.89

Nothing in config.toml is renamed. The store moves forward on its first open: the table of the screen’s recall entries, prompt_history, becomes recall_entries, and every copy a compaction kept records the turn and the time it was said, read from the row it copies, behind the automatic backup. A copy 0.88’s turn retention already left standing alone has no row left to read, and stays under the turn and time of the compaction that wrote it. 0.88 refuses a store 0.89 has opened; downgrading means restoring the backup the upgrade took. Session archives are format_version 22, since a compaction run between turns is now a turn of its own and a copy a compaction kept records where it was said; a 0.88 archive (21) imports as it is, and 0.88 refuses a 22.

The problem type URIs moved to meka.bot. Every error document meka returns now names a type under https://meka.bot/errors/ where it used to sit under https://meka.run/errors/, and the store moves the error an inbox item recorded the same way. The identifier after the last slash is unchanged, as are the HTTP status, the title and every other member of the document. A client that matches the whole URI must update its table; one that reads only the final segment is unaffected.

meka history is meka recall. meka recall list and meka recall clear do what the old pair did, and meka history is no command at all. The JSON listing is {"recall": [...]}, and an empty one says No recall entries.

The HTTP API tells a conversation two ways. Its history is what was said: every turn, the ones a compaction summarized included, each once where it was said, without what a rewind took back. It is GET /v1/sessions/{id}/history, paged by before and after a position, the place of a row in the session’s log. Its context is what the model sees now, GET /v1/sessions/{id}/context/messages. A transcript reads the history and follows the feed; the context is what the model sees, and a transcript drawn from both shows the turns a compaction kept twice. See History and Context.

0.880.89
POST /v1/sessions/{id}/turnPOST /v1/sessions/{id}/turns, same body; a detached turn’s Location is unchanged
GET /v1/sessions/{id}/messagesGET /v1/sessions/{id}/history for a transcript, GET /v1/sessions/{id}/context/messages for what the model sees
GET /v1/sessions/{id}/streamGET /v1/sessions/{id}/feed
GET /v1/streamGET /v1/feed
GET /v1/sessions/search?q=GET /v1/sessions?q=, beside any listing filter
GET /v1/sessions/{id}/tasksGET /v1/tasks?session=<id>
DELETE /v1/sessions/{id}/tasks/{task_id}DELETE /v1/tasks/{task_id}
GET /v1/scheduleGET /v1/jobs
POST /v1/sessions/{id}/schedulePOST /v1/jobs, with the session as session_id in the body
DELETE /v1/schedule/{job_id}DELETE /v1/jobs/{job_id}
/v1/memory, /v1/memory/{name}/v1/memories, /v1/memories/{name}
GET /v1/sessions/{id}/export?format=markdowngone: render the history, or run meka session export

GET /v1/sessions/{id}/export answers the JSON archive alone; a format parameter is ignored, so a client that asked for Markdown, or took it as the default, is handed the archive.

Where0.880.89
The conversation’s ETag, a 412, turn.finished, turn.failed, turn.canceledrevision, totalposition; the ETag is "<position>"
A message of the conversationturn_label, paged by offsetgone: turn_id and position, paged by before and after
POST /rewind bodyturns, optionalturn_id, required
POST /rewind answerturns_removed, messages_before, messages_afterthe rewind history item
POST /compact answersource, memories_written, kept_recent, messages_before, messages_afterthe compaction history item
conversation.rewoundrevision, total, turns_removedsession_id and the rewind item’s fields
context.compacted, a summary’s compactionreplaced_countgone
Blocking POST /turns answerturn_id, session_id, stop_reason, usage, messagesthe turn record (id, source, started_at, permission, ended_at, status, stop_reason, error, reason, profile, usage), refusal_text, messages as history items, notices
GET /v1/sessions/{id}/contextmessage_count, totalsgone: position; a session’s spend is turn_count and usage on its record, its turns summed, a sub-agent’s on its own
Session recordapprovals_pendingrequests_pending, and turn_count and usage beside it
A session search hitexcerptexcerpt and excerpt_position
A memory search hitsnippetexcerpt
GET /v1/profiles, GET /v1/profiles/{name}, meka profile list --format jsonactivedefault
tool_call.progressserver_name, tool_name, tool_use_idid, the call’s

meka session show --format json prints the session record’s new fields too.

0.88 event0.89 event
permission_requiredapproval.requested
permission_resolvedapproval.resolved
elicitation_requiredelicitation.requested
elicitation_resolvedelicitation.resolved
progresstool_call.progress

A rewind names the first turn to drop. Send {"turn_id": "<id>"} to POST /rewind, the id of the first turn to take back, read from GET /v1/sessions/{id}/turns or a history item’s turn_id: the conversation goes back to how it stood before it. A body with turns is a 422, and so is a turn the context does not hold, one a compaction summarized say. A retried rewind names a turn that is gone and is refused, rather than taking another. If-Match takes the ETag of whichever listing was read. The screen’s /rewind and meka session rewind still count turns back from the end.

A branch is one fork. POST /v1/sessions/{id}/fork with turn_id copies the conversation as it stood before that turn, where 0.88 forked and then rewound the copy; a fork gives the turns it copies ids of their own, so a turn id of the source names nothing on the copy.

A blocking turn’s messages carry thinking whenever it was recorded. supports_reasoning_stream governs only the thinking.delta events. A bot that joins the text blocks of the assistant messages needs no change.

A withdrawn prompt stays in the history. message_withdrawn on a terminal says the prompt left the context, so a client that resends still reads it; one that dropped the prompt from a transcript keeps it there, as the screen does.

The one-shot JSON report and ACP lose replaced_count too. A compaction in the one-shot report’s compactions and in ACP’s _meta.compaction carries the same fields context.compacted does, without it.

0.87 to 0.88

Nothing in config.toml is renamed, but display.status_row and display.turn_end_summary refuse a fact listed twice: name it once. The store moves forward on its first open, and 0.87 refuses a store 0.88 has opened; downgrading means restoring the backup the upgrade took. The conversions need nothing from you:

  • Every turn records the permission level it ran under. A turn written before takes the level the first of its own messages that names one told the model, its opening in a turn meka wrote; one where none does takes its session’s level, or read on a session at a level meka does not know.
  • A tool result written before records nothing of how the screen draws it, so a resumed session draws those calls as 0.87 did: an edit’s diff from its arguments, a write with none, and a failed command as failed, its exit code in its output.
  • A compaction written before records no checkpoint turn, so its row opens to the memories its checkpoint wrote, by name, and its summary. The summary is read out of the message the compaction wrote, up to the first section meka adds after it, so a summary that held one of those headings itself shows only what came before it.

Session archives are format_version 21. A 0.87 archive imports as it is, its turns and compactions converted the same way.

An ACP client that did not advertise session.compaction is shown each compaction as a tool_call of kind think, closed under the sentence for what set it going, where 0.87 told it in a [meka] chunk or a notice. A client that matched those for a compaction should match the card, or advertise the capability and read compaction_update.

0.86 to 0.87

Nothing in config.toml is renamed. The store moves forward on its first open, and 0.86 refuses a store 0.87 has opened; downgrading means restoring the backup the upgrade took. The conversions need nothing from you:

  • A cron job written before names no time zone and keeps running in the machine’s.
  • Reasoning a chatgpt-subscription profile sealed names that endpoint, and is sent back to it alone; reasoning from anywhere else names none and is sent nowhere. Naming it reads config.toml’s profiles, so an upgrade of a store holding sealed reasoning refuses while the file cannot be read.
  • The copies of the recent turns earlier compactions kept are linked to the rows they copy, so history shows each such turn once; a copy whose row a turn sweep has already taken, or that no row matches, stays in history as the record of what it says.
  • The session search index is rebuilt once.

Session archives are format_version 18. A 0.86 archive imports as it is: its kept copies are linked the same way, and its sealed reasoning names no endpoint, since the archive does not say which.

Two environment values, a handful of flags, two OpenAI request shapes, two HTTP shapes, an ACP field and a tool’s argument types are refused or read differently.

openai-responses asks for encrypted reasoning, a reasoning summary and a cache key. Every request carries include: ["reasoning.encrypted_content"], reasoning.summary and a prompt_cache_key naming the session, as OpenAI’s own client sends them. A model that does not reason refuses the first two, on OpenAI’s own API as well (gpt-4.1, gpt-4o): set encrypted_reasoning = false and reasoning_summary = false on its profile (meka profile set). An endpoint behind base_url that refuses one is answered by turning that one off the same way, and prompt_cache_key = false in the account’s table of config.toml.

openai-chat-completions asks for usage on every streamed request (stream_options.include_usage), where it asked only some endpoints. An endpoint that refuses the field is answered by stream_usage = false in the account’s table of config.toml.

A relative MEKA_CONFIG_DIR or MEKA_DATA_DIR is refused. meka stops at startup naming the variable, where it used to fall back to the platform directory with a warning and so ran against the real store. Set each to an absolute path.

meka serve and meka acp refuse the flags that belong to a run. Both refuse --prompt, --skill and --oneshot, and meka serve refuses the directory flags too, where they were silently ignored. Remove them from the unit file or the editor’s agent command.

meka profile add and meka profile set refuse a flag the account’s backend never sends, as meka account add already did, where they wrote a key nothing reads. Drop the flag; nothing is written when it is refused.

Feed event ids start from the clock. Each meka serve process issues ids above the clock’s microseconds at its start, so a Last-Event-ID saved before a restart is read as a gap (a feed.gap with no count) rather than as a position in the new process’s feed. A client that handles feed.gap by re-reading GET /messages needs no change; one that compared ids against small numbers should treat them as opaque, increasing integers.

GET /v1/instructions shows path and source to a token with config:r or config:w alone. A token with sessions:r still reads the text. Grant config:r to a client that needs to know where the file is.

ACP: a prompt response’s usage is the turn’s, as the protocol defines it, where it was the session’s running total. An editor that showed it as the session’s cost should sum it across turns.

A tool’s boolean, number and object arguments are read strictly. A boolean is true or false, a number an integer (2.0 counts as 2, as JSON Schema has it), an object an object (web_fetch’s headers an object of strings), or the field is left out. Anything else, a string such as "true" or "3", a fraction, a negative where a count is asked for, or an object spelled as a JSON string, is refused naming the field, so a model corrects the call rather than having it read as false, as the default or as absent. memory_search read a quoted limit as the number it spelled, and now refuses it.

0.85 to 0.86

Nothing is renamed and no key is refused; one archive version moves.

Session archives are format_version 17. A 0.85 archive imports as it is; a 0.86 archive can record where a session stopped sending Claude its earlier thinking, which 0.85 refuses to read by its version.

0.84 to 0.85

One config key is retired, and the store gains a table and a column, which the first run adds on its own; the HTTP API grows a surface and four of its shapes change, a start refuses a profile that cannot run, two shell commands refuse where they warned, and an ACP client sees two things differently.

The installation is managed over HTTP, under two new scopes. /v1/accounts, /v1/profiles, /v1/mcp and PUT /v1/instructions do what meka account, meka profile, meka mcp and the instructions file do in a shell, and a change made through them is served at once; an edit made in a shell is served by the next of them, or by POST /v1/config/reload. They sit behind config:r and config:w, which no existing token holds: grant them to the token that administers the installation and to no other, since config:w is shell on the host. GET /v1/config names where the installation is (config_path, data_dir), when config.toml was last read and written, and config_changed, whether an edit made in a shell awaits POST /v1/config/reload, which reads the file again; every write of the server’s own ends with the same whole read. The server now starts with no profile configured, so an installation can be set up over the API; the refusal moved to the session that names none, and over HTTP it names POST /v1/accounts and POST /v1/profiles rather than the shell’s commands. Adding a second profile names the one that was alone as default_profile, in a shell as over HTTP, where it used to leave the installation with no default; and removing the default while two or more profiles would remain is refused, in a shell as over HTTP, where meka profile remove used to warn and unset it. Point default_profile elsewhere first, with meka profile use or PATCH /v1/profiles/{name} and "default": true. meka account add refuses --client-id and --oauth-token-url on a backend that never reads them, where it dropped them with a warning, as it already refused --interleaved-thinking.

Every profile must be able to run. A start, a reload and every write of the installation refuse a config.toml holding a profile whose account is not configured, that names no model, or whose max_output_tokens does not exceed its thinking budget, where only the default was held to it. The refusal names the profile; repair it with meka profile set or remove it.

An MCP credential is bound to the endpoint it was stored for. A connect presents OAuth tokens to the exact url they were issued for alone, and a bearer or a client secret to the scheme, host and port of the url it was stored with, so a credential never follows a server to another host. The first run binds every credential already stored to its server’s url as config.toml states it then, which is where it has been presented all along, so nothing changes for a server that stays put; it stops, naming the file, while a credential is stored and config.toml cannot be read, and naming the server while a credential is stored for one whose url holds a ${VAR} the environment leaves unset, since a binding made from the literal would hold the credential for a URL nothing reaches. The binding is made from that first command’s environment: a variable it holds differently from the server’s own runs, or a ${VAR:-default} it leaves to its default, binds the credential where its expansion points, and the server’s connect then refuses it, naming the origin it was stored for. Run the first command after the upgrade with the environment the server runs under. Storing a secret for such a server, with meka mcp add, meka mcp login --auth-token-stdin or --client-secret-stdin, or over the API, is refused the same way where the variable is unset. A server whose url moves to another host afterwards fails to connect, saying so, until it is logged in again or its secret stored again. meka mcp get and GET /v1/mcp/{name} list as credentials the kinds presented to the url, and the kinds held for another endpoint under credentials_for_another_endpoint, with the origin each was stored for; credential_origin and credential_origin_matches_url are gone.

resource is retired from [mcp.servers.auth]. A client_credentials or client_credentials_jwt token request names the server’s own url as its resource, as the MCP authorization spec requires, so the key would only restate it or contradict it; a file that still carries it is refused at startup, naming the key and its line. Delete the line.

GET /v1/mcp is an object, and a server’s auth is the block. GET /v1/mcp answers {"servers": [...]}, with orphaned_credentials beside it when a credential is stored under a name no entry claims; a client that read the bare list reads servers. GET /v1/mcp/{name} and meka mcp get --format json carry every key the entry states, and auth is the block as written, {"type": "oauth", ...}, where it was the type string alone; required and deferral are what the server runs under, and inherited names those the entry leaves to [mcp].

OpenAPI: the responses body’s outcome schema is ResponseOutcome. It takes the three answers an elicitation takes (accept, decline, cancel) beside the four an approval takes. A client generated from the spec regenerates; one that sends the approval answers is unaffected.

context.measured on the session feed. After every provider round, and after a compaction or rewind, the feed says what is in context (used, and window once known), the figure GET /v1/sessions/{id}/context answers with, so a client that polled it for a gauge can follow the event instead. A client that ignores events it does not know needs no change.

An ACP client’s gauge moves per round, and a background report is a notice. usage_update arrives after every provider round, and once on session/new, session/load, session/resume and session/fork with what the session’s row recorded, rather than at a turn’s end. Finished background work is no longer pushed as a user_message_chunk carrying the report the model read: a client that advertised session.notices gets a notice titled in the screen’s words, with the task’s label as its description and the tasks under _meta.background; one that did not gets the same words as a parenthesized agent_message_chunk, which is also what a session/load replays where the report landed. A client that matched on the [Background task header reads the notice or the chunk instead. A scheduled job’s prompt is still a user_message_chunk.

0.83 to 0.84

Nothing is renamed; one value is refused where it never applied, the store is migrated on first open, and one wire shape grows.

[shell].sandbox_backend takes this platform’s values only. A value from another platform was ignored on macOS and Windows; it is now refused: shell_execute at read fails and startup warns, naming the values this platform takes. Linux takes bubblewrap, bubblewrap-landlock and landlock, Windows takes appcontainer and restricted-token, and macOS takes none. A config.toml shared across machines leaves the key unset, or sets it per machine through MEKA_SANDBOX_BACKEND. Windows now chooses its backend by probe: a build without the AppContainer API, or whose API speaks schema 1.0, warns once at startup that the restricted token is in use and what it cannot hold; pinning sandbox_backend = "restricted-token" accepts that and silences the warning, as pinning landlock does on Linux, and on a schema 1.0 build pinning sandbox_backend = "appcontainer" takes the AppContainer with network = "host" held as remote.

A Windows 11 build with the process security environment API runs commands in an AppContainer. On 26H2, and on 24H2 and 25H2 from the August 2026 update as Microsoft lists them, read and workspace now confine a command through the OS’s own environment: meka’s config directory and store are unreadable, confine_reads and network hold for the shell as they do for the file tools, and no ACE is written to your directories. Four limits of the mechanism come with it: cd into a read-only directory fails (git’s -C and Python’s os.chdir included; the initial directory, PowerShell’s Set-Location and cd into a workspace directory work), msys2 bash.exe cannot start, a removable FAT32 volume and a mapped network drive are out of reach, and an app execution alias such as the Python install manager’s python.exe does not start, while the real executable runs by its path. Pin sandbox_backend = "restricted-token" to keep the previous mechanism. On both backends a confined command can no longer read or set the clipboard, drive other windows or inject input.

Every compaction records what it was. The boundary a compaction writes now carries what set it going (reactive, proactive, manual, requested, emergency or retention), which strategy wrote the summary, the memories the checkpoint wrote and whether the recent turns were kept, so the screen’s row, the editor’s entry and the HTTP marker say the same thing live and on a replay of the log. A boundary written before 0.84 recorded none of that, and nothing in the store says, so the migration gives every one of them the commonest case: an automatic compaction at the ceiling (reactive), written by the checkpoint, with no memories written and the recent turns kept. A resumed older session therefore replays each earlier compaction as Autocompacted the conversation after context usage crossed the ceiling., whatever set it going at the time.

Session archives are format_version 16. An archive written by 0.79 through 0.83 is brought forward on import, its boundaries given the same record.

context.compacted says more, and has company. The stream event and the compaction object on GET /messages carry trigger, memories_written and kept_recent beside source, replaced_count and generation; the event also names its compaction_id. context.compacting opens a compaction under that id before any request is made, and context.compaction_failed closes one that did not land. The one-shot JSON report lists the turn’s compactions under compactions.

An ACP client sees the compaction it asked to see. One that advertised session.compaction gets compaction_update, in_progress then completed with the summary, in place of the [meka] chunk; one that did not gets the advisory, a notice if it advertised session.notices and the chunk otherwise, now in the screen’s words (The agent compacted the conversation. rather than Compacted the conversation: 545 messages replaced by a summary (checkpoint, compaction 8)). A client matching on the old sentence should match on [meka] alone or advertise a capability.

0.82 to 0.83

Nothing is renamed and no key is refused; one thing changes what a memory does.

load decides how a memory reaches the context; priority is importance alone. A memory stored at priority 0 before 0.83 is migrated to load: inline, so what sat in every turn’s context stays there. A memory written from now on is index unless the write says inline or search, whatever its priority: memory_write’s load, meka memory add --load, or load on PUT /v1/memory/{name}. Every existing memory is marked verified as of its creation, since nothing has verified it since; memory_search’s unverified filter and --unverified count from there.

0.81 to 0.82

The Swagger UI is gone. /v1/docs answers 404 whatever [serve].docs says, and the key still serves /v1/openapi.json. To browse the API, import that URL into one of the tools the HTTP API page lists.

0.80 to 0.81

Two keys move to the section that governs them. config.toml refuses the old key, naming its line; move it.

Surface0.800.81
config.toml[session].subagent_max_depth[subagents].max_depth
config.toml[session].send_class[display].send_class

A profile’s keys are ordered by how many backends read them. meka profile add and meka profile set write max_request_bytes after max_output_tokens and deferred_tools after vision, and the next write to a profile puts an existing one in that order. Nothing is renamed and no value changes.

0.78 to 0.79

Nothing is renamed and no key is refused; two things change what a running deployment does.

Every MCP connect opens with a protocol probe. A server that answers it is spoken to as MCP 2026-07-28; one that does not gets the initialize handshake it had. A stdio server from before the probe existed may swallow it and answer nothing afterwards, so such a server is given up to ten seconds, or half of [mcp].connect_timeout when that is less, on its first connect in a meka process before a fresh child gets initialize; the verdict is kept for the process, so later connects pay nothing. See Connection lifecycle.

Session archives are format_version 15. A 0.78 archive imports as it is; a 0.79 archive carries the allow-list keys on every sub-agent spec, which 0.78 refuses to read by its version.

0.77 to 0.78

A blocking POST /turn holds its turn. A request with stream: false whose client disconnects now stops the turn at its next round boundary once the connection has been gone for [serve] stream_reattach_grace, the way a dropped stream always did, and the turn ends with reason: "abandoned". Until 0.77 such a turn ran to its end and the client read the reply out of GET /messages. A client that cannot hold a connection for the length of a turn sends detach: true and reads the turn from the resource the 202 names, or follows the session feed; see Holding a turn. A feed subscriber counts as a listener, so a client that keeps the feed open is unaffected.

The message a turn opens with is on the feed as message.appended. A scheduled fire’s prompt and a background report that opened a turn used to reach the session feed as an info notice holding the text; they now arrive as message.appended, the message as GET /messages types it, under the turn’s id, and so does every other turn’s opening. A client that displayed those notices reads the scheduled and background_report blocks of the new event instead; a client that logs notices stops seeing prompt bodies in them. See the session feed.

A blocking POST /turn answers with the opening message first. messages begins with the user-role message the turn opened with, as the history reads it back, where it used to begin with the assistant’s first message. A client that read messages[0] as the reply filters by role.

Session archives are format_version 14. A 0.77 archive imports as it is; a 0.78 archive names why a canceled turn stopped and which fallback profile served a turn, which 0.77 refuses to read.

0.76 to 0.77

Nothing is renamed and no key is refused; three things change what a running deployment does.

Deferred tools are declared natively on the official endpoints. A profile on claude-subscription, chatgpt-subscription, or anthropic-messages and openai-responses without a base_url now declares every deferred tool from the first request with defer_loading, and a load no longer rewrites the prompt cache. Set deferred_tools = "append" on a profile to keep the 0.76 shape. A profile behind a base_url keeps append unless it states native.

The first request of every live session rewrites its prompt cache once. The tools array and the shape of a successful tool result both change, so a session resumed on 0.77 reads nothing back on its first request and everything from the second.

A claude-subscription session has no MCP meta-tools. The mcp_resource_* and mcp_prompt_* tools leave the request and the catalog on that backend, because its endpoint refuses their names; see Deferred tools. They are back the moment the session moves to another profile.

0.75 to 0.76

A sub-agent’s scope is three fields. agent_spawn’s writable_roots and readable_roots are gone. A call names working_directory, where the sub-agent works, and may add additional_workspace_directories, more of its workspace with the working directory’s rights, and additional_readable_directories, what it reads beyond its workspace. A call naming none of the three gives the sub-agent the parent’s whole scope, as before; a list needs working_directory beside it, and "." is the parent’s own directory. Stored specs are converted when the store is opened: the first writable root becomes the working directory and the rest the workspace list, and the readable roots become the readable list beside the directory the sub-agent was spawned in. A sub-agent that had only readable roots no longer follows the parent’s other directories. The common calls, before and after:

0.750.76
{"writable_roots": ["/a"]}{"working_directory": "/a"}
{"writable_roots": ["/a", "/b"]}{"working_directory": "/a", "additional_workspace_directories": ["/b"]}
{"readable_roots": ["/b"]}{"working_directory": ".", "additional_readable_directories": ["/b"]}
{"writable_roots": ["/a"], "readable_roots": ["/b"]}{"working_directory": "/a", "additional_readable_directories": ["/b"]}

A list no longer runs the sub-agent at workspace on its own: it runs at the parent’s level unless permission lowers it, and a list beside unrestricted is refused. A permission above the parent’s is refused rather than clamped. The result of a spawn and of a follow-up opens with Agent:, Permission: and the scope the sub-agent holds, in the lines the environment block uses; a script that read agent: from the first line reads Agent:.

A sub-agent’s reads follow its spawn. [permissions].confine_reads governs root sessions alone. A sub-agent reads what its spawn call stated, or what its parent reads when it stated nothing, on every installation. A parent whose reads are not confined hands reads anywhere only by stating no scope.

Every surface says directory. One table of the renames:

Surface0.750.76
Run flag--writable-root <PATH>--additional-workspace-directory <DIR>
Run flagnone--additional-readable-directory <DIR>
memory export--dir--directory
config.toml[shell].readable_paths[shell].additional_readable_directories
config.toml[skills].extra_paths[skills].additional_directories
Session JSON, HTTP APIcwdworking_directory
Export archiveadditional_rootsadditional_workspace_directories
GET /v1/sessions filtercwd=<path>working_directory=<path>
Export archiveformat_version 9format_version 11
Storesessions.cwd, sessions.additional_roots_jsonworking_directory, additional_workspace_directories_json

What meka relays into a user message has its own block. An inbox item, a scheduled job’s fire and a background report were text blocks, or for a report a section of the turn_context block; they are inbox, scheduled and background_report blocks now, on GET /messages, in a blocking turn’s messages and in an archive, each carrying text as the model read it and the facts a client labels it by. The store converts what it holds on the first open, and a resumed screen shows an item as the labeled prompt it was and a fire or a report as the stage direction the live screen said, where it used to show meka’s header as something you typed.

The account views take the account. meka account usage, whoami and stats take the account by name, as login does, and lose --profile; named nothing, they ask the account the default profile bills, or the one account. An account with no profile answers. profile leaves their output, plain and JSON.

The two config keys are refused by name, with their line, until renamed. An archive of version 10 or older is converted on import. The store’s columns are renamed on the first open, behind the usual backup; a hand-written query names the new ones. ACP’s additionalDirectories keeps its protocol name, and /cd keeps its verb.

0.73 to 0.74

A one-shot run prints the answer alone, as markdown. meka --oneshot writes the last text block the model wrote to stdout when the turn ends, as source rather than rendered, terminal or pipe alike, and nothing else: no tool indicators, no thinking, no session id line and no usage line, on either stream. --format json is unchanged and carries the session id, the calls and the usage; -v logs the process. A script that read the session id from stderr reads it from the JSON report instead.

Three keys are gone, and the file refuses them by name. [display].tool_params and [thinking].show_content chose what a tool call or a thinking block showed before you expanded it. The screen expands and collapses either on a click or a key, so a call starts with its summary and a thinking block collapsed, and a one-shot run shows neither. [display].max_width capped the width meka laid text out at; the screen uses its window and a one-shot run its terminal. Delete the three from config.toml.

[display].show_token_usage is a fact of turn_end_summary. The screen now closes every turn with a row saying when it ended and how long it took, and the tokens used are one of the facts that row can state. show_token_usage = true becomes turn_end_summary = ["time", "duration", "usage"]; the old key is refused by name.

0.72 to 0.73

meka session delete is meka session remove. The session command now uses the verb every other object command uses; its flags are unchanged. A script or a cron line that runs meka session delete --older-than-days <DAYS> changes the one word. Tool names such as agent_delete, the HTTP DELETE routes and [serve].delete_on_idle keep theirs.

0.71 to 0.72

The interactive terminal is a screen, not a line editor. meka without --oneshot now takes over the terminal the way an editor does: the conversation fills the screen, the composer stays at the bottom, and you can type while the agent works. What that changes for an existing setup:

  • Six [display] keys are gone, and the file refuses them by name. render_mode, input_style, newline_before_prompt and newline_after_prompt described a stream the screen no longer writes. Delete them from config.toml. --render-mode and MEKA_RENDER_MODE are gone with the first; a one-shot run now renders markdown when stdout is a terminal and writes the source as written when it is a pipe, so a script gets the words without asking. show_path_in_prompt and show_context_in_prompt are replaced by status_row, a list of what the status row shows in order. The default shows the path; to have the gauge back, list it: status_row = ["path", "profile", "context", "tasks", "subagents"].
  • Answers no longer go to stdout in interactive mode. The screen draws on one stream. meka --oneshot and the HTTP API are the doors for a script, and both keep their contracts.
  • A terminal is required. A pipe on stdin or stdout is refused with a line naming --oneshot. A harness that drove the old prompt through pipes drives --oneshot or the HTTP API instead.
  • !command output is captured into the transcript rather than the child taking the terminal. A command that needs a terminal of its own runs in a shell beside meka.
  • The syntect and raw views are gone. The screen renders markdown; a thinking block or a tool call expands to its whole content on a click or a key, so the source views have nothing left to show that the screen cannot.
  • /history and /clear are gone, and display.resume_show_recent with them; the file refuses the key by name. The screen opens on the last page of the session’s log and reads older turns as you scroll up, however long the session is, so there is nothing left for either command to do. Delete resume_show_recent from config.toml.
  • A slash command’s answer is a panel, not rows of the transcript. /help, /status, /usage and every listing open in place of the composer until Esc closes them; a bare /permission, /approvals or /profile is a picker. A command is no longer echoed as a prompt.
  • The approval prompt is a picker. The four answers are picked with the cursor, their keys (y, a, n) or their numbers; always and never are no longer typed. Esc denies.
  • Every row has a glyph. [tool name(...)], Thinking..., Error:, (interrupted) and the bracketed usage line are replaced on the screen by one register: •, ! and ■ in column 0 and the text at column 2. A one-shot run keeps the old spellings on its stream, with one change: consecutive tool indicators at the summary level now have a blank line between them, as every other pair of blocks does.

Two keys are new: display.mouse, on by default, and session.send_class, steer by default. Interactive mode describes the screen.

0.70 to 0.71

A hole in a feed is a feed.gap event, no longer a notice. A client that resumed a session feed, a sub-agent’s feed or the server feed from a Last-Event-ID the replay ring no longer reached was told so by a warn notice, and so was one that fell behind while connected. The first case is now a feed.gap event, with session_id on a session’s feed and dropped when the server can count the hole; a client that matched the notice’s text, or treated a notice without a turn_id as a hole, switches on the event name instead. The second case no longer reaches a client: a reader that falls behind is caught up from the ring, and only a POST /turn stream whose client was the sole reader still ends, with the sse-lag failure as before. notice now carries only what the agent or a provider said. See Falling behind.

0.69 to 0.70

The HTTP API changes shape in the places below. The store gains a table of turns and a column naming each message’s turn, the three todo tools become the checklist tools, every compaction boundary takes a checklist, and the message meka writes to send the model back to work (after a reply with no visible text, or with checklist items open) becomes a block of its own; the migration runs on open and needs nothing from you, and an older archive converts on import.

todo_write, todo_edit and todo_read are checklist_add, checklist_edit and checklist_read, and the list is binding. A turn can no longer end while an item on the list is pending or in progress: the model is sent back to its open items, up to three times in a row without a tool call, before the turn is allowed to end with them open. There is no title and no replace-all; items have ids, checklist_edit changes one at a time, and completed and canceled take an item off the list, the latter with a reason. Checklist has the rule. The old names in a session’s history, in a sub-agent’s denied tools and in an archive are renamed on open and on import; a list from before 0.70 is not recovered, since it never survived a restart anyway. A [tools] filter or a skill that names a todo_* tool needs the new name.

The per-turn context no longer carries the list. The model sees it in every checklist_* result, in the nudge, and through checklist_read; compaction copies the open list into the summary.

A blocking turn’s response carries the turn’s messages, and final_text and tool_calls are gone. messages now holds what the turn added to the conversation, in the shape GET /v1/sessions/{id}/messages reads it back: the assistant’s messages with their text, thinking and tool calls, and the tool-result messages that answered them. The reply a bot shows is the text blocks of the assistant messages, joined; a tool call is a tool_use block and its result the tool_result block on the user-role message after it.

last_turn_at is gone from the session record; last_turn replaces it. The old field named the last turn of any outcome, only while this server held the session. last_turn is the session’s latest turn, recorded for every host’s turns, and says how it ended: status, ended_at, and a stop_reason or an error. GET /v1/sessions/{id}/turns lists them all.

turn_id on a message is the turn’s id; the positional label is turn_label. A message’s turn_id was a dense label (t_0001, …) derived from its position; it is now the id of the turn that added the message, the one turn.started announced and GET /v1/sessions/{id}/turns lists, and the label moved to turn_label unchanged. A client that groups by the label reads turn_label; one that joins the feed to the history reads turn_id.

used_percent is gone from GET /v1/sessions/{id}/context. Divide used by window; both are present exactly when the percentage was.

vision left GET /v1/info for GET /v1/profiles. Read it on the entry of the profile a session runs on; the entry marked active is the one a session gets when it names none.

GET /v1/sessions/{id}/schedule is gone. List one session’s jobs with GET /v1/schedule?session=<id>; creating a job keeps its route.

DELETE /v1/schedule/{job_id} and DELETE /v1/sessions/{id}/tasks/{task_id} take the full id. A prefix matches nothing and answers 404. meka schedule cancel and the task_cancel tool keep taking the short form.

subagent.activity is gone from the HTTP feed. A sub-agent’s tool calls are no longer summarized under the parent’s agent_spawn call; while the parent runs a sub-agent, the sub-agent’s own id answers GET /v1/sessions/{id}/stream with its feed, every call in full. The permission_required a sub-agent’s call parks still arrives on the parent’s feed, now naming the sub-agent in subagent_id. ACP clients are unaffected.

GET /v1/sessions/{id}/stream on a sub-agent’s id answers 409, not 422. While the parent runs the sub-agent it is the sub-agent’s own feed; otherwise it is 409 subagent-not-running, a type of its own, where 0.69 answered 422 session-not-drivable. A client that branched on the 422 to recognize a sub-agent reads parent_id on the record instead.

Session archives are format_version 9. Each session carries its turns, each event names the turn that added it in turn_id, so an import keeps a session’s turns under ids of its own, each compaction boundary carries the checklist as it stood, and a nudge is a block of its own. An archive of version 3 through 8 still imports, with no turns and no turn named where it had none, an empty checklist on its boundaries, and every nudge it wrote as text taken as the block.

0.67 to 0.68

headers_helper is a path beside config.toml, or absolute. A relative name is resolved against the directory config.toml is in and nowhere else; it is no longer tried against the working directory meka was launched from, and a bare name is no longer searched on PATH. A helper that lived beside the config keeps working; one that relied on the launch directory or on PATH needs its path written out.

An MCP server whose url or args name an unset ${VAR} is refused. The literal used to be sent to the other side; a key in a query string or a token on a command line is a credential, so these fields now fail closed as env and headers always did. Set the variable, or give it a default with ${VAR:-default}. A reference the grammar does not accept (an unclosed ${, an empty name, a default that opens another reference) is refused the same way in every field that reaches the other side. To hand a child shell a variable it expands itself, write it without braces (args = ["-c", "srv --token $MY_TOKEN"]); meka leaves that alone.

[serve] values take ${VAR:-default} and no longer take $$. The one ${VAR} grammar serves every field of config.toml; $$ in a [serve] value is now two dollar signs, and $${VAR} is a $ followed by the expansion.

meka mcp add, remove, enable and disable refuse to write a config.toml that would not read back, as account and profile always have. A file broken elsewhere is fixed by hand first; an entry whose own keys broke the file is still removable.

[background] max_tasks = 0 is refused at startup while enabled = true. It offered background to the model and refused every call; set it to 1 or more, or turn the switch off.

A session expires as a tree. The retention sweep and meka session delete --older-than-days judge a root by its own age and take its sub-agents with it, and keep every sub-agent of a root that is pinned or owns a scheduled job. Before, an old sub-agent under a kept root was deleted on its own and agent_followup on it then failed. Every delete now locks the session with its sub-agents: meka session delete <id>, --all and the servers’ deletes refuse while another process is running a sub-agent under it, where they took the sub-agent from under that process.

GET /v1/sessions/{id}/stream loads an evicted session only for sessions:w. A token with sessions:r alone attaches to a session the server has loaded and is answered 409 (/errors/session-not-loaded) otherwise, where it used to revive the session and hold it, under its cross-process lock, for as long as the stream stayed open. A bridge that subscribes before it submits already holds sessions:w; a dashboard that only reads keeps reading resident sessions.

Below unrestricted, the in-process readers refuse a process’s own /proc entries. file_read and file_search refuse /proc/<pid>, /proc/self and /proc/thread-self, whose environ, cmdline, fd and mem carry what a process was given; the system-wide files (/proc/meminfo, /proc/cpuinfo, /proc/mounts) stay readable.

A skill’s bundled files in meka’s own store are readable at every level. file_read and file_search reach skills/ under the config directory below unrestricted, where they refused it; the rest of the directory stays private, and the shell sandbox still masks all of it, which the header a skill is served with now says.

interleaved_thinking on an account. anthropic-messages still sends the interleaved-thinking beta whenever thinking is on; [accounts.<name>] interleaved_thinking = false, or meka account add --interleaved-thinking false, withholds it for an endpoint that refuses the header. Nothing changes for an account that does not set it.

Wire changes. schedule.fired carries status: "not_run" for a job whose session could not be opened for it, where it said "failed"; turn.started carries source: "compaction" for the checkpoint turn POST /compact runs, with a turn.finished that closes it; meka account whoami --format json carries auth.expires_at as RFC 3339 rather than Unix seconds; DELETE /v1/sessions/{id} answers 409 while a scheduled or background turn is taking the session, as fork and PATCH do. A script that matched refusals from meka mcp, schedule, background, memory or skill on configuration error: will not match them any more: they print what happened, without that prefix. meka session list -n 0 and meka session search -n 0 show every session where they showed none, and meka --format json <subcommand> is refused where the flag was ignored.

0.66 to 0.67

Session archives are format_version 6. Each session’s stats carries a turn_position, the pair the Claude subscription wire reports, or null for a session that numbers nothing. An archive written by 0.59 through 0.66 still imports, brought forward at the door. A session that already had turns before this release never numbers them, on the store and in an archive alike, because nobody numbered those; every session created from now on does.

0.65 to 0.66

A shell under Landlock alone has no temporary directory. 0.65 gave each Landlock-confined command a private directory under /tmp, named by TMPDIR. That was the one thing meka wrote outside its own store below unrestricted, and it is gone: mktemp, Python’s tempfile, gcc and patch fail at read under Landlock as they did through 0.64. Install bubblewrap, whose private /tmp is a tmpfs inside the sandbox and touches nothing real, or add the directory a tool needs as a writable root at workspace.

Command output is bounded at 64 MiB and never spooled to disk. A command that prints more than that across stdout and stderr is stopped, and the result carries everything it printed up to the stop. The capture files that used to hold an overflowing stream, under the platform cache directory or MEKA_DATA_DIR/command-output, are no longer written; any left from earlier releases can be deleted. A command killed at its timeout now keeps what it printed before the kill.

0.64 to 0.65

Landlock on its own needs ABI v9 (kernel 7.1). Below that the kernel cannot refuse a connect() to a Unix socket on disk, so a shell at read could reach D-Bus and systemd-run --user and have them act on its behalf. meka now reports the backend unusable on such a kernel instead of warning, and shell commands at read fail until a usable backend exists. Install bubblewrap, which works on any kernel with user namespaces; on a kernel of 6.12 or newer the command also runs inside meka’s Landlock ruleset there. Affected as of this release: Debian 13, RHEL 10, Ubuntu 24.04 and 26.04 LTS without Bubblewrap.

Session archives are format_version 5. Every field is written, and the ones that used to default when absent are now required: blobs on the archive, and approvals, additional_roots and profile on each session. An archive written by 0.59 through 0.64 still imports, brought forward at the door, so only an archive written by hand has to change.

The checkpoint_text compaction source is gone. POST /v1/sessions/{id}/compact and the context.compacted stream event report source as checkpoint or summarizer only. A checkpoint turn that ends without calling context_replace now falls back to the summarizer instead of having its closing text taken as the summary. A client matching on the old value should treat it as summarizer, which is what those compactions now produce.

file_read and scratchpad_read take start and end. Both name the first and last line to return, counted from 1 and inclusive, the way a regex match reports its line; offset and limit are gone. file_read counted offset from 0, and scratchpad_read took both as bytes. A call that still passes the old names reads from the top. A scratchpad_read reply now ends on a whole line and names the line to continue from rather than a byte offset, and returns as much as fits by default. A client that read file_read’s offset to show a location should read start, which needs no adjustment.

0.63 to 0.64

The first open builds the session search index. meka session search reads a full-text index over the words of every conversation, and the first launch of 0.64 fills it over every session in the store, which takes a moment on a large one and is logged at info. The index is a few percent of the store’s size. Nothing else changes about the store’s contents.

A title or a pin does not move updated_at. PATCH /v1/sessions/{id} used to move updated_at for any field; title and pinned leave it alone, so renaming a session does not reorder the listing or change which session meka -c continues. A client using updated_at to detect changes sees a title change only in title.

Pinned sessions are listed first. GET /v1/sessions, ACP session/list and meka session list put pinned sessions ahead of the rest, newest pin on top; the unpinned sessions keep their order. A next_cursor taken from a 0.63 server is refused by 0.64 as invalid; start the listing again.

0.62 to 0.63

An inbox item’s source is optional. An item posted to POST /v1/sessions/{id}/inbox without a source used to be given one by meka: the token’s description, else client. It now has none. GET /v1/sessions/{id}/inbox omits the field for such an item, so a client that reads source as required must treat it as optional; a client that sends one sees no change. The model reads such an item under [Message arrived <time>] rather than [Message from <source>, arrived <time>], and the notice an interrupt raises says “Interrupted the answer to read a message.” with no name. Send a source if you want the model to know who is writing: who sent a message is the client’s to say.

The system prompt states how the harness works and nothing else. It no longer tells the model what it is (“a general-purpose agent”), what to do (“complete the user’s task”), or how to conduct itself. The conduct sentences that left, for an instructions file that wants any of them back, where they were always yours to set:

  • “Explain consequential or destructive actions before proceeding.”
  • “Recover from routine tool errors when possible; report unresolved failures and their impact.”
  • “Lead with the outcome and relevant evidence; keep progress updates brief.”
  • “Batch independent work; sequence dependencies and changes to shared state.”
  • “Apply relevant skills and server guidance within these constraints.”
  • “Do not poll or duplicate running work.” and “Keep work needed for your answer in the foreground.”
  • “Prefer to finish or checkpoint work before then.” and “save essential state with the available tools before another pass” (the context window)
  • “Check here before creating one, so you do not duplicate.” (scheduled jobs)
  • For a sub-agent: “Complete the assigned task without follow-up questions. Report the outcome, relevant evidence or verification, and unresolved limitations.”

What stays is what the model needs to use the harness: the permission levels, how tools, background work, scheduling, memory and the context window behave, and that replies are rendered as Markdown.

0.59 to 0.60

Every built-in tool is named <noun>_<verb>. The noun is the class a tool belongs to and the verb its one operation, so the tools that act on one thing sort as one block. Nine names change:

0.590.60
read_filefile_read
write_filefile_write
edit_filefile_edit
find_filesfile_find
search_contentsfile_search
execute_commandshell_execute
fetch_urlweb_fetch
render_imageimage_render
load_tooltool_load

todo is three tools. todo_write takes title and items and replaces the list, todo_edit takes set and updates statuses by task number, todo_read takes nothing. The single tool’s “items then set in one call” is two calls now.

tool_search is new: keyword search over every registered tool, deferred ones included, that says for each whether the current permission level allows a call.

What is converted for you, the first time the store is opened and behind the automatic backup: the calls in every stored session (so a resumed session keeps the tools it had loaded and its history shows one set of names), the denied_tools of every sub-agent, the tool a finished background task reports, and the tool a scheduled job’s gate calls. A session archive written by 0.59 (format_version 3) still imports; new archives carry format_version 4.

One narrow case to know about: the conversion reads every stored message that can hold a call, and a prompt you typed that was, in its entirety, the JSON of a tool call under an old name is converted like a call. Prose around such JSON leaves it alone.

What is not: a [tools] or [subagents] list, a tool_permissions key, a skill, standing instructions, or a client that matches on a tool name must be edited. The startup warning and the model’s unknown-tool reply suggest the new name when the old one is a reordering of it (read_file, write_file, edit_file, render_image, load_tool) or a family’s bare noun (todo); the other four are named only here.

0.58 to 0.59

meka is licensed under the GNU Affero General Public License, version 3 or later. Releases up to 0.58.0 were published under MIT and remain so. From 0.59.0 on, conveying meka or a modified copy, and offering a modified copy to users over a network, carries the AGPL’s obligation to make the corresponding source available.

The problem type URIs moved to meka.run. Every error document meka returns now names a type under https://meka.run/errors/ where it used to sit under https://meka.so/errors/. The identifier after the last slash is unchanged, as are the HTTP status, the title and every other member of the document. A client that matches the whole URI must update its table; one that reads only the final segment is unaffected.

0.54 to 0.55

GET /v1/sessions/{id}/stream is the session’s event feed and no longer ends with a turn. It used to rejoin the current turn and close after that turn’s terminal; it now carries every turn on the session, whoever started it, for as long as the connection is held, and opens the feed of a session that has not streamed yet instead of answering 404. A client that read the stream to its end must stop at the terminal it was waiting for. POST /turn with stream: true is unchanged and still closes after its own terminal. The replay ring spans turns, so a Last-Event-ID from an earlier turn resumes across them rather than reporting a gap.

Every SSE event carries turn_id and session_id, and turn.started carries source. A client that compared an event’s data whole sees two extra members; one that read named fields is unaffected.

Scheduled fires and background-outcome turns are on the feed. They used to be invisible over HTTP until GET /messages. Nothing changes on the webhooks: a fire still posts schedule.fired and nothing else.

agent_steer is a new built-in tool. A session that lists tools by name, or a skill that denies agent_spawn to take the lifecycle tools with it, now covers five names rather than four.

0.53 to 0.54

The store renames its tables and columns in place. The upgrade runs on the first open, behind the usual pre-migration backup, and needs nothing from you. A tool that reads meka.db directly sees scratchpad_entries for tool_outputs, messages.kind for messages.role, background_tasks.tool and scratchpad_entry for tool_name and scratchpad_name, mcp_credentials.server for server_name, blobs.size_bytes for size, and the session counters without their stat_ prefix; the provider_credentials view and memories.last_read_at are gone. Sessions has the full layout.

Three surfaces say the new names too. GET /v1/sessions/{id}/tasks and the task.finished webhook carry tool and scratchpad_entry in place of tool_name and scratchpad_name. A memory’s creation stamp is created_at on GET /v1/memory and in --format json, and created in a file meka memory export writes. A session archive holds its scratchpad entries under scratchpad_entries rather than tool_outputs and is format_version 3; meka session import refuses an older archive, so export it again from the meka that wrote it, or rename the key and set the version in the file by hand.

0.52 to 0.53

[session].context_messages is gone. Delete the key from config.toml; a file still carrying it is refused at startup, naming the key and the line. It cut every request to the newest 200 messages, and a conversation that outgrew it lost its oldest messages from the request with no summary in their place: context usage fell instead of climbing, and the agent forgot what it had been told before the cut. Every request now carries the whole conversation, and the context ceiling ([session].context_ceiling_percent, 90% of the profile’s context_window by default) with auto-compaction is the only bound. A long session reaches the ceiling and compacts where it used to slide; with auto_compact = false it grows until the provider rejects a request.

Nothing in the store changes. A session already past 200 messages resumes with all of them in the request, so its first turn back may compact.

The container image is gone. ghcr.io/k4yt3x/meka receives no new tags, and the Dockerfile went with it. What the image was for, running the agent unrestricted against a disposable filesystem, is what mekabox does with the meka installed on the host, inside a stock archlinux:latest container with your config mounted read-only; install the binary from the release archive or with Cargo and run the wrapper. The wrapper itself moves from contrib/container/mekabox to scripts/mekabox, so a link or PATH entry that named the old path needs the new one.

0.49 to 0.50

meka tools is meka tool. Every top-level command names the object it manages in the singular, and this was the one that did not. meka tools list is meka tool list; the flags and the JSON envelope are unchanged. meka mcp tools <name> keeps its name, since it lists the tools of a server rather than managing tools.

/tasks is /task in the REPL, for the same reason: /skill, /memory and /schedule are singular. /task, /task show <id>, /task cancel <id> and /task cancel --all do what the plural did. The HTTP route /v1/sessions/{id}/tasks is unchanged.

0.46 to 0.47

search_web is gone. It scraped DuckDuckGo’s HTML and was turned away by the bot detection more often than not. Web search now comes from an MCP server, which packages one engine’s key, quota and response shape without meka embedding any of them:

meka mcp add exa https://mcp.exa.ai/mcp

A [tools] list that still names search_web warns at startup and otherwise works. Existing sessions that called it resume unchanged: their tool results are text, and nothing re-runs them.

0.45 to 0.46

The store migrates itself, as every release since 0.43 has. config.toml does not, and this release changes its shape: a [providers.<name>] profile is now an [accounts.<name>] table plus a [profiles.<name>] table, default_provider is default_profile, and the ask permission level is gone. A config in the old shape is refused at startup, naming the first key meka does not know, rather than read with a guess at what it meant. The conversion is a one-shot script, migrate-0.45-to-0.46.py, attached as an asset to the 0.46 release.

Beyond the config shape, this release renames several tool parameters, changes a handful of HTTP fields and status codes, and makes ACP answer InvalidParams where it answered InternalError. Everything a client, a skill or a script could depend on is listed below under “What else changed” and “Tool parameters”, each with its remedy. Run the script, launch once, and work down those two lists for anything you automated.

Order

  1. Run the script against your config, first as a dry run, then with --apply. It needs Python 3.11 and the tomlkit package (pip install tomlkit), which is what lets it keep every comment and the order of everything it does not touch.

    python3 migrate-0.45-to-0.46.py            # prints a diff; writes nothing
    python3 migrate-0.45-to-0.46.py --apply    # rewrites config.toml in place
    

    It finds meka’s config the way meka does, honoring MEKA_CONFIG_DIR; --config PATH points it at a copy instead. --self-test checks the script against its own fixture and exits.

  2. Install 0.46 and launch it once. The store migrates on that open, behind an automatic copy beside it named for the schema version it came from (meka.db.v9.bak for a store 0.45 left), in ten ledger steps. The first creates the REPL’s prompt_history table where a store lacks one, a no-op otherwise. The other nine: sessions.provider becomes sessions.profile, and provider_credentials becomes account_credentials, keyed by account, both renames of what was always there; an approvals column is added, every ask session becomes none with it on, and a root session that never recorded a level (one an ACP client created) adopts [permissions].default; each stored turn’s inline <context> preamble becomes its own turn_context block; every image’s bytes move out of its message row into blobs, leaving a reference; every column and index takes one naming rule, with the JSON in two of them following suit; a repair row’s own thinking blocks take that rule’s tag too, which the step before it passed over; a root session still without a level after all that takes [permissions].default, and a launch that cannot read the file refuses here rather than skipping the stamp; and a stopped task’s stored status is spelled canceled. Four of these walk every message row, so a store with years of image-heavy sessions takes a moment on that first launch and grows a copy of the same size beside it. A store restored from a .dump replays the whole ledger, and if no default profile can be resolved when it does, the frozen 0.44 step warns with its old --provider advice; read it as --profile.

Run the script before you launch 0.46, not after. A meka launched against an unconverted config warns that it cannot read the file and then refuses whatever needed it; only the commands that edit it through toml_edit (meka account remove, meka profile remove, meka mcp remove) and the ones that read the store alone still run. The store migrates on that launch only if no step needs the file. A root session that never recorded a level needs [permissions].default from it, and a migration that cannot read the file refuses and rolls back rather than stamping nothing, so the store keeps its 0.45 shape until the launch after the script has run.

What the script converts

BeforeAfter
default_provider = "work"default_profile = "work"
[providers.work] with type, base_url, client_id, oauth_token_url, device_id[accounts.work] with backend in place of type, and the other four unchanged
[providers.work] with model, context_window, max_output_tokens, effort, vision, thinking, thinking_budget, max_request_bytes, redact_thinking[profiles.work] with account = "work" and eight keys unchanged; redact_thinking = true becomes thinking_display = "redacted" and false becomes "summarized"
[permissions].enabled containing "ask""none" in its place
[permissions].default = "ask"default = "none" and approvals = true
[web].request_timeout_seconds = 30, connect_timeout_seconds, read_timeout_secondsrequest_timeout = "30s", connect_timeout, read_timeout, by value; a 0, which meant the default, is removed
[mcp].grace_seconds = 3, connect_timeout_seconds = 30grace = "3s", connect_timeout = "30s", by value; a 0 becomes "0s", which grace accepts and connect_timeout refuses at startup
[mcp].strictdefault_required, same meaning
[session].retention_days = 30retention = "30d", by value
[thinking].budget_tokensbudget, same value

Every profile becomes one account and one profile of the same name, so nothing you named changes its name and every session still resolves. Two old profiles on one login stay two accounts with two copies of the credential; merge them by hand if you like, by pointing both profiles’ account at one and running meka account remove on the other once nothing names it. A key the script does not know is carried into the profile table and reported, where meka will refuse it by name; a duration key whose value is not a whole number is left under its old name and reported, with the same result.

What else changed

  • The meka provider suite is gone. meka account add/login/list/remove manage accounts and their credentials; meka profile add/set/use/list/remove manage profiles. meka account usage/whoami/stats are where they were, and take --profile <name> instead of a positional name. account add takes --backend where provider add took --type.
  • The prompt is a flag. meka "text" is meka -p "text", and -p - reads the prompt from stdin. There is no positional prompt, so meka unknowncommand is an error rather than a session.
  • --provider is --profile, long form only: -p is the prompt.
  • --format json on a --oneshot run prints one object for the turn; see One-shot mode.
  • HTTP API: the provider field on POST /v1/sessions, PATCH /v1/sessions/{id} and every session response is profile, and GET /v1/providers is GET /v1/profiles, whose rows carry account and backend in place of type. A session export archive’s provider field is profile, and its format_version is 2, so an archive written by 0.45 is refused by version; re-export it from a migrated store.
  • ACP: the configOptions entry provider is profile.
  • REPL: /provider is /profile, and /status shows the profile with its account.
  • One spelling per value. --permission and MEKA_PERMISSION take a level’s full name (n, r, w, u are gone), and --render-mode, MEKA_RENDER_MODE and [display].render_mode take termimad, syntect or raw (rich is gone). The flags and the config key refuse anything else; the two variables warn and fall through to the next source, as they always have. The undocumented text spelling of --format plain is gone too.
  • A [permissions].default or enabled entry naming a level meka does not have is refused at startup, with the line, the way an unknown key is, instead of being dropped with a warning. The script rewrites ask; anything else you spelled yourself.
  • The ask permission level is gone, replaced by the approvals switch beside the level: a call above the level is refused, or put to you when the switch is on. /approvals on|off in the REPL, approvals on POST /v1/sessions and PATCH /v1/sessions/{id}, and the ACP config option of the same name set it; [permissions].approvals is what a new session starts with. The store migration turns an ask session into none with approvals on, which asks about every call as ask did. An approved call now runs at the session’s level, so an approved write at read lands only under the workspace roots where ask wrote anywhere; raise the level if an approved call needs the reach. See Permissions.
  • A user message is two blocks. What meka injects ahead of the words for a turn (permission and environment context, todos, catalog changes, background outcomes, the resume notice) is its own turn_context content block, first, and the words are a text block. GET /v1/sessions/{id}/messages returns the block typed, so a client reading content[0].text as the prompt now reads the context; take the text blocks. A migration splits every stored turn once.
  • Image bytes live in a blobs table. The migration moves every inline image out of its message row and leaves a reference by content hash, so a screenshot read twice is stored once. A session export carries a blobs list with the bytes its sessions reference, and an archive that references a blob neither it nor the store holds is refused. Over HTTP an image block reports media_type and hash, and GET /v1/sessions/{id}/blobs/{hash} serves the bytes.
  • A scheduled job runs at its session’s recorded level and nothing else. The polling process’s own --permission no longer stands in for a session row that records no level; every surface records one at creation, and the migration stamps the configured default on any older root row that never got one. A sub-agent’s row now records its level too.
  • Every config duration is a humantime string. [web].request_timeout, connect_timeout and read_timeout, [mcp].grace and connect_timeout, [session].retention ("30d"); the script converts the _seconds and _days keys by value, and "0s" is refused where zero is meaningless. [mcp].strict is default_required and [thinking].budget_tokens is budget, both converted. MEKA_MCP_STDIO_CONCURRENCY and MEKA_MCP_HTTP_CONCURRENCY are gone: set [mcp].stdio_concurrency and http_concurrency (3 and 20 by default, zero refused). MEKA_MCP_TOOL_TIMEOUT takes a duration such as 10m, not milliseconds; a bare number is ignored with a warning and the default of ten minutes applies.
  • One exact spelling per value, everywhere. --permission, --render-mode, --sandbox-backend, --format and mcp add --transport refuse case variants (Read, JSON), session export and GET /v1/sessions/{id}/export drop the md alias of markdown, mcp add --auth takes oauth, client_credentials or client_credentials_jwt as the [auth] block spells them (the hyphenated forms are gone), and [mcp].default_permission, a server’s permission and tool_permissions, and [tools].tool_permissions refuse a level meka does not have at startup, naming the line, where they warned and ignored it.
  • HTTP API: an unloaded session whose row records no level omits permission (it sent ""); every optional field is omitted rather than null, display_summary included; GET /v1/health/ready reports profile_configured (was provider_configured); the permission_required event carries input and stays answerable for 30 minutes (was 60 seconds); a body that fails to parse says which field on every endpoint; POST /v1/sessions/{id}/responses/{request_id} at a sub-agent’s id answers 422 session-not-drivable; a fork of a session another meka process holds answers 409 session-locked; a session’s title is the first user words with whitespace collapsed, cut at 80 characters, and meka session show labels it title (was opening). Four status codes move: a [web] or base_url misconfiguration is a sanitized 500 (was a 422 naming the operator’s path), and a session lock meka cannot open is 500 (was 409 session-locked); meka’s own request-ceiling refusal is 422 with the new type request-too-large (was 502 provider); GET and PATCH /v1/sessions/{id} answer 404 or 500 for a row they cannot read (was 200 with profile: ""); and POST /v1/sessions/{id}/schedule with scheduling disabled is 404 (was 422).
  • ACP: a locked session, a sub-agent’s id, a profile the config no longer has and every other refusal the caller can act on answer InvalidParams (was InternalError); session/set_config_option refuses a profile switch while a turn is in flight instead of writing the row and deferring; session/new, load, resume and fork refuse a cwd that is not an existing directory and record it canonically; tool-call and permission titles read <tool_name> <argument> (read_file src/x) and permission requests carry rawInput with a JSON content block.
  • Terminal output: every timestamp is local time with its UTC offset (2026-09-07 14:03 +02:00), sizes print as MiB, KiB or B, and every listing command takes --format json, printing the HTTP API’s record shapes. Tool-call indicators and the approval prompt show a tool’s real name (read_file, not ReadFile); the prompt is headed [approval] and takes always and never.
  • Skills you wrote that name a renamed tool parameter (next table) or the old [ask] prompt must be edited by hand; meka does not rewrite skill files.
  • A gate’s pointer test is not_empty (was not-empty) in schedule_create, POST /v1/sessions/{id}/schedule and meka schedule add; stored jobs are converted by the store.
  • canceled, one l, on every wire meka owns. Match turn.canceled as the SSE terminal event, https://meka.bot/errors/turn-canceled as the problem type, and status == "canceled" in task views (GET /v1/sessions/{id}/tasks, DELETE .../tasks/{task_id}), task_list output and schedule.fired webhook bodies; the reason values are unchanged. The store rewrites its stored task rows on first open (the tenth ledger step). ACP’s stopReason: "cancelled" and MCP’s notifications/cancelled are those protocols’ own spellings and stay.

Tool parameters

Six built-in tool parameters are renamed so that one name means one thing across the catalog: is_regex for a boolean, glob for a glob, limit for a result cap, id for an identifier.

ToolBeforeAfter
conversation_searchregex (boolean)is_regex
conversation_readcountlimit
find_filespatternglob
fetch_urlmax_lengthlimit
agent_followupagentid
agent_deleteagentid

A call spelling the old name is missing its required parameter (glob, id) or, where the parameter was optional, has it ignored in favor of the default. search_contents gains a limit (1 to 100, default 100) beside its unchanged pattern.

meka does not rewrite what names these. A skill under the skills directory (~/.config/meka/skills/<name>/SKILL.md) that spells out a find_files or agent_followup call must be edited by hand, and a scheduled job whose gate calls one of these tools with the old argument must be recreated. Past calls in a session’s history keep the old names, which is harmless: the model reads the current schema on its next turn.

The sections below predate 0.46 and use its old names: --provider is --profile, meka provider … is meka account … and meka profile …, the positional prompt is -p, and the ask level is none with approvals on.

0.43 to 0.44

A binary swap, and the store migrates itself as promised below, unless you authenticate an MCP server with auth_token or client_secret, which are no longer config keys. Read the next section first if you do; meka will refuse to start otherwise. Then the behavior changes below, worth reading before you resume an existing session or run a scripted meka, several of which apply only if you run meka serve or meka acp.

MCP secrets moved out of config.toml. auth_token on a server, and client_secret in a [mcp.servers.auth] block, are gone. Both were secrets sitting in a plaintext file people commit and sync; they now live in the store beside the OAuth tokens, which is where the login credentials have always been.

meka cannot move them for you. The store migrates itself because it has a ledger recording what it has already done; config.toml has none and may be older or newer than the binary at any moment, so a key left behind is a parse error naming the key and the line rather than a value silently ignored:

$ meka mcp list
Error: database error: schema migration 3 ('sessions_name_their_provider') failed: Invalid
parameter name: cannot record a provider for 4 carried-forward session(s) while config.toml
cannot be read; fix the file and start meka again. The store is unchanged

The parse error itself is a warning just above it, naming the key and the line:

WARN meka: failed to read config.toml, so no profile can be adopted for older sessions:
configuration error: failed to parse …/config.toml: TOML parse error at line 12, column 1
   |
12 | auth_token = "…"
   | ^^^^^^^^^^
unknown field `auth_token`, expected one of `name`, `transport`, …

Two messages because two things are stuck: the file will not parse, and the migration that has to name a profile for your existing sessions cannot ask it which one. Fixing the file fixes both, and nothing has been written in the meantime: “The store is unchanged” is literal, and the copy taken before the attempt is still beside your store. (On an installation with no sessions to carry forward, only the parse error appears.)

For each server, delete the line and store the secret instead. Which command depends on which key you deleted, and the two are alternatives, not a sequence: a bearer belongs to a server with no [auth] block, a client secret to one that has it.

$ # for a server whose `auth_token` you deleted (no [auth] block):
$ pass show api-token | meka mcp login api --auth-token-stdin

$ # for a server whose [auth] block's `client_secret` you deleted:
$ pass show acme-secret | meka mcp login acme --client-secret-stdin

meka mcp get <name> then lists the kinds it holds without printing any of them. --auth-token and --client-secret are gone from meka mcp add for the same reason: an argument is visible in ps output and in the shell history of every user on the machine. Use the -stdin forms, which add also takes.

If you were using auth_token = "${API_TOKEN}" to keep the token out of the file, a header does the same job and still expands: headers = { Authorization = "Bearer ${API_TOKEN}" }. Storing it is the better answer, since it survives without the variable being set.

Nothing else about a server moves. env, args and headers stay in config.toml with ${VAR} expansion, because they configure a process or a request and merely may contain a secret.

isolated scheduled jobs are gone; every job fires in the session that created it. The mode ran a job’s turn in a fresh session rather than the conversation that made it, to avoid replaying that conversation’s history. Only meka serve ever honored it: the REPL and ACP already ran such a job in the open conversation, with a warning, so for two of the three hosts nothing changes at all.

Existing jobs are not deleted and do not need touching. The store drops the column and the job keeps its schedule and its prompt, firing into the session it belongs to from then on.

What it cost is why it went. The fire inherited the creating session’s authority (its permission level, its working directory, its profile, its MCP servers) and dropped the conversation, which is where anything you told the agent that never reached a memory or an instructions file lives. Its result landed in a session nothing linked to, and the turn could not even cancel its own job, because schedule_cancel resolves against the session it is running in.

meka acp and meka serve clients: POST /v1/sessions/{id}/schedule now refuses isolated with a 422 naming the field, rather than accepting and ignoring it. GET /v1/schedule and the schedule.fired webhook no longer carry it either.

If you were relying on the mode, an external timer does the same job with the level and profile stated outright instead of inherited (0.44 syntax):

meka --oneshot --permission read --provider work "summarize today's alerts"

Often a gate is the better answer: it means a frequent job takes no turn at all on the ticks where nothing happened, which saves more than skipping the history did.

A session another one spawned is driven only by its parent. POST /v1/sessions/{id}/turn answers 422 for a sub-agent’s id, meka -r <sub-agent-id> refuses by name, and a scheduled fire aimed at one does the same. Both agent builders now check, rather than the scheduling door alone.

What this closes is that a sub-agent’s restrictions live in its spawn record, which those builders never read: the [subagents] denials it was created under, its memory and instruction grants, and the permission ceiling its spawn call set. Driving one from a host therefore ran a conversation that was deliberately given narrow tools with the full built-in set at the host’s level. agent_followup was and remains the door that reconstructs those terms, so nothing meka does for you changes.

Reading a sub-agent is untouched: meka session export, GET /v1/sessions/{id}/messages and meka session list --include-children all still serve it.

Forking one does not promote it, and that is the other half of the change. A fork of a sub-agent now carries parent_session_id and the spawn terms, so the copy is a sibling under the same parent rather than a new root; without that, POST /v1/sessions/{id}/fork was a one-call way around the refusal above, handing back a live session over a sub-agent’s whole conversation with none of the terms it was spawned under. The two doors that have to hand back a live session therefore refuse a sub-agent’s id up front: POST /v1/sessions/{id}/fork answers 422, and ACP’s session/fork answers InvalidParams. meka session fork still makes the copy: it takes no runtime, and the copy is readable like any other sub-agent. Forking an ordinary session is unchanged. If you want a sub-agent’s conversation as a root session of your own, copy the text out rather than expecting a command to promote it.

meka session list --long is gone, along with the columns it showed. If a script parses that output, it needs updating; the default columns are unchanged.

/cd with no argument returns to the directory meka was launched from, not $HOME. /cd ~ still goes home. The old behavior made a bare /cd a surprising way to leave the project you were working in.

render_mode = "silent" is gone, as are --render-mode silent and MEKA_RENDER_MODE=silent. Delete the setting: termimad is the default.

A config still carrying it fails to parse, naming the value and the line, and --render-mode silent is refused by clap. MEKA_RENDER_MODE=silent is the quiet one: an unreadable value there has always been dropped in favor of the next source, so it falls through to your config file or the default rather than saying anything.

It never did what it says. It suppressed the model’s answer and nothing else, so a run under it printed the session id, the reasoning line, tool indicators, todo lists, notices and token usage, and dropped the one thing you were waiting for. Both things it might plausibly have meant are shell redirections that already work, and work the right way round: meka … 2>/dev/null keeps the answer and drops the chrome, meka … >/dev/null 2>&1 drops both.

SSE thinking.delta now carries one chunk of reasoning per event. It used to send one event per completed block, so a client that opted into supports_reasoning_stream and rendered each event as a whole block will now show fragments. Concatenate the deltas to rebuild the block, exactly as you already do for assistant_text.delta. A client that concatenated needs no change, and one that never set the capability sees nothing new. A turn the provider answered without streaming still arrives as a single delta, so there are no two shapes to tell apart, and stream: false still reports each block whole in thinking.

One consequence worth planning for: a session receiving reasoning gives up its retry on a transient provider failure, because the deltas have already reached you and a second attempt would repeat them. Leave supports_reasoning_stream off if you would rather have the retry.

meka session delete refuses ids given alongside --all. It used to take both and quietly do the wider thing, so meka session delete "$ID" --all with $ID unset deleted every session and then reported the empty id as a failure: a complete wipe reported as an error. Naming sessions and asking for all of them are two different requests; say one or the other. --older-than-days has conflicted with both for the same reason since 0.44.

Every command taking a session, job or task id now accepts a unique prefix of one, which is what the listings print. Full ids still work, so nothing that already worked stops. An ambiguous prefix is refused with the candidates named, and an empty one matches nothing rather than the only row: meka schedule cancel "$JOB" with $JOB unset used to cancel whatever job was alone.

meka mcp logout <name> clears every credential that server holds, not only its OAuth tokens. If you were using it to drop a stale token from a server that also has a stored bearer or client secret, you will now need to store that again with meka mcp login.

A scheduled job is refused on a sub-agent session. POST /v1/sessions/{id}/schedule answers 422 if the session was spawned by another. Sub-agents never had the schedule_* tools, so no job meka created can be affected; what this closes is a client planting one directly, which would have woken the sub-agent without the tool restrictions or memory grants it was spawned under.

ACP session/load and session/resume refuse a sub-agent’s id. Both used to take the session’s lock, rewrite its cwd, retire its background work and replace its roots before failing with Internal error; both now decline with InvalidParams before touching anything, naming the parent to use agent_followup from. An editor that stored a sub-agent’s id from session/list gets a clear refusal instead of a mutated row and an opaque failure. Over HTTP the same holds for every write-side endpoint: POST /v1/sessions/{id}/turn and its neighbors refuse before taking the sub-agent’s lock or marking its background tasks interrupted.

A session that carries spawn terms is refused even when its parent is not in the store. That shape has one source, and it is a pair of documented commands: meka session export <sub-agent> --format json followed by meka session import. The archive’s parent_id points outside it, so the import re-roots the row while copying the spawn terms faithfully. The result reads as a sub-agent’s conversation to every door, so meka -r on it, and POST /turn, /fork, /schedule and PATCH against it over HTTP, all refuse. meka -c skips it and meka session list shows it only under --include-children, both so nothing offers you a session it will then decline. If you were using export-then-import to promote a sub-agent into a standalone session, that no longer works; there is no supported replacement, because the tools and permission ceiling a sub-agent ran under live in the terms its parent set and nothing outside that parent can reconstruct them. The conversation itself stays fully readable, and importing a whole tree (the root and its sub-agents together) is unaffected, since each child keeps its parent.

This upgrade deletes the pre-migration copy 0.43 left, and keeps one from now on. Before it migrates, meka copies the store aside; until now nothing removed those, so a full duplicate of your whole history accumulated per schema-changing release. From 0.44 a fresh copy supersedes the one before it. In practice that means meka.db.v1.bak in your data directory, the copy of your pre-0.43 store, is removed on this upgrade and replaced by a copy of your pre-0.44 one. If you want the older file, move it somewhere else before upgrading.

Two things worth knowing about what is kept. Peak disk during an upgrade is higher than the steady state, because the new copy is written before the old ones go: budget for the store plus every copy already beside it plus one more, and expect to settle back at twice the store. And the copy is taken per schema-changing upgrade, not per release, so one file can span several versions if you skip some.

What keeping only the newest copy costs, stated plainly. The copy you hold is of the store after the migration before this one. So it undoes the most recent conversion and nothing earlier: if a migration converts something wrongly, you do not notice, and you then take another schema-changing upgrade, the only copy predating the fault is gone. That is a real limitation rather than a technicality, and it is the reason to move a copy of your own aside if a particular upgrade worries you. It is accepted because the alternative was an unbounded pile of full-size duplicates, whose cost is certain where this one is conditional on a bug outliving a release.

A resume now starts at the level the session recorded. Both CLI hosts do this: the REPL and meka --oneshot -c / -r. The scripted one is where a silent change matters most, since a --oneshot run that passes no --permission used to start at the config default and now starts at whatever the session was last set to. A session you created with --permission unrestricted comes back at unrestricted without the flag. Before, the row said one thing and the run did another; every other surface already read the row, and these two were the ones that did not. Pass --permission on the resume to move it. A level that is no longer in [permissions].enabled is not granted: the session drops to the configured default with a warning.

A session now runs on the profile it was created with. Every existing session is recorded as running on your current default profile, which is what they were in fact running on, so nothing moves. From here meka -p openai then meka -c stays on openai. If nothing could be resolved when the migration ran (no profile configured yet), sessions are left without one and say so; resume such a session once with --provider <name> to record it. The migration says which profile it recorded and on how many sessions; run once with -v if you want to see it.

A 502 from meka serve now carries the provider’s own response text, as a provider_response member on the Problem Detail. It used to be withheld and written only to the server log.

The reason for the change is that the redaction defended less than it appeared to: meka acp has always handed the same text to its client, so withholding it on HTTP left the text just as public while making the one surface quieter. What it cost was the upstream’s error type, which is the one part of a failed turn a client can act on.

Know who can read it before you leave it on. An upstream refusal can name your account with the provider, your organization, and your rate-limit posture. Submitting a turn takes sessions:w, but the failure is also carried by the terminal turn.failed event, and re-attaching to a stream takes only sessions:r, so a read-only token sees it too. If you issue read-only tokens to people who may observe a session but are not entitled to the account behind it, set [serve] relay_provider_errors = false. Nothing else changes: detail carries the same sentence either way, and with the key off the member is simply absent.

The 503 for a required MCP server that is down is not affected and still reports only the server names. That reason is meka’s own subprocess text and has carried a command line and its filesystem path, which is a different disclosure and not one this key governs.

GET /v1/info no longer returns provider or model. Read them from GET /v1/providers instead, which lists every configured profile with its name, its type (the backend), its model, and active: true on the one a session gets when it names none. The old fields held the default profile’s backend under the name provider, while provider on POST /v1/sessions names a profile, so a client that read one and posted it to the other got a 422. They were duplicates of the active row besides.

If you ran a 0.44 development build, your store repairs itself on the next run. One such build removed a migration from the middle of the ledger instead of appending its reversal. user_version is a positional index, so that renumbered every later step, and a store sitting between the hole and the new head skipped a step it had never run while stamping itself current. The symptom was every MCP connection failing with no such table: mcp_credentials after a migration that reported success.

Nothing is needed from you: an appended step recreates the table and carries the old MCP credentials into it, because a store already stamped past the missed step is only reachable by appending. Released 0.43 stores were never affected; they sit at the baseline and migrate straight through.

--model, --base-url, --thinking and --thinking-budget are gone. A profile is an indivisible bundle: the backend, the endpoint, the credential keyed to it, the model, and every model-tied setting. A session selects one by name and records that name. A flag that moved one field of the bundle left the rest behind, so --model against a profile stating context_window = 1000000 ran a 200K model while gauging its context against a 1M window, and never auto-compacted.

Change a setting on the profile (0.44 syntax):

meka provider set work model claude-opus-5
meka provider set work effort --unset

Or make a second profile and select it with --provider, which is now the only provider flag on a run. meka provider add has a flag for every profile field except device_id, which meka resolves and persists itself, so one command creates a whole profile (0.44 syntax):

printf '%s' "$ANTHROPIC_API_KEY" | meka provider add fast \
    --type anthropic-messages --model claude-haiku-4-5 \
    --context-window 200000 --api-key-stdin

The thinking budget is per profile. [providers.<name>].thinking_budget takes precedence over [thinking].budget_tokens, which stays as the installation-wide fallback and needs no edit. The global was previously cross-checked against a per-profile max_output_tokens, so a profile could be refused over a number stated nowhere in it, and told to fix it by lowering a value every other profile also read.

meka acp and meka serve refuse -c and -r. Both name one run’s session, and a long-lived host has no such thing: it creates one per session/new or per POST /v1/sessions, each naming its own profile. They used to be accepted and quietly misapplied: -c / -r switched off the default-profile check a host with no default needs most. Over HTTP, name a provider on POST /v1/sessions; under ACP, session/new creates on the host’s default and session/set_config_option moves it. --provider is still accepted, because it selects which configured profile the host defaults to, which is a property of the host rather than of one session.

Also on the HTTP side, and not a break: PATCH /v1/sessions/{id} with a body naming only a provider now works on a session that is not loaded, which is how you move one whose profile has left config.toml. It takes the session lock to do it, so if you run more than one meka on the same store, send it to whichever process has the session; another one answers 409 session-locked rather than moving a row the running host would ignore.

0.42 to 0.43

Nothing to do. Start 0.43 and it brings the store forward itself, on the first open, before anything reads it.

This is the first release that migrates its own store, and from here on that is the rule: upgrades from 0.43 onward are a binary swap, whatever the schema does.

What it changes, if you want to know what happened. A scheduled job’s gate used to be two columns, gate_command and gate_fire; it is now gate_kind plus a JSON gate_spec, which is what lets a gate call a read-only tool instead of a shell command. And a due job is now claimed by leasing it rather than by consuming its row, which adds claimed_by, claimed_until and attempts, so a host that crashes mid-delivery no longer loses the occurrence, or for a one-shot the whole job. Each gate’s stored baseline is preserved, so a changed gate does not fire spuriously on its first evaluation afterwards.

Before it writes anything, meka copies the store to meka.db.v1.bak beside it. That doubles the space the store takes until you delete it, which is worth knowing if yours is large. Start with -v once if you want the exact path in the log; the copy is otherwise silent. It records the version it was taken at, so if you ever restore it, the next start migrates it again correctly rather than mistaking it for a store that is already current.

The whole thing is one transaction, so an interruption leaves the store exactly as it was rather than half-converted. Running two hosts at once is fine: the first takes the schema lock and the second waits, then finds nothing to do.

Coming from 0.41 or older, run migrate-0.41-to-0.42.py once first, as described below. 0.43 recognizes a 0.41-shaped store and refuses it by name rather than converting it into something still unreadable, and it changes nothing when it does.

A gate that cannot be read

Rare, and worth knowing the shape of. If a job’s gate was already unreadable under 0.42 (a hand-edited row, or a gate_fire value meka never wrote), it cannot be converted, because there is nothing to convert it from. Such a job never fired under 0.42, and it does not fire under 0.43 either: the migration leaves it in the same refused state rather than guessing at what it meant or deleting it. It is logged once, by id, at warn.

The consequence is that the row stays inert and invisible, as it already was: it will not appear in meka schedule list and meka schedule cancel cannot reach it. Recreate the job if you still want it. The original row is in the backup 0.43 took, meka.db.v1.bak. Note that from 0.44 a later schema-changing upgrade deletes that file, so put a copy somewhere of your own if you want to keep it.

0.41 to 0.42

A store written by 0.41 needs five conversions before 0.42 reads all of it. They are performed by migrate-0.41-to-0.42.py, a one-shot script attached as an asset to the 0.42 release. Download it, run it once, and you are done with it.

This one stays a script, and 0.43’s own store migration does not replace it: 0.42 carried no migration code to reach back with, and conversion B below has to guess. 0.41 recorded nothing about which provider a thinking block came from, so the script tells them apart by the shape of the blob, and it reports what it read before it writes. A guess wants a human reading the counts, which is the one thing a migration that runs on every start cannot offer.

Order

  1. Run 0.41 once, before you replace it. It brings a store from an older release fully up to date; 0.42 carries no migration code and cannot.
  2. Install 0.42 and launch it once. This is what creates the tables the script writes into, so it is not an arbitrary step you can move: run the script against a store that predates 0.42 and it stops with an explanation rather than guessing.
  3. Run the script, first as a dry run, then with --apply.
python3 migrate-0.41-to-0.42.py            # reports what it would change; writes nothing
python3 migrate-0.41-to-0.42.py --apply    # does it

Read the dry run before you apply it. Conversion B in particular reports how many thinking blocks it read as Claude’s and how many as OpenAI’s, and 0.41 did not record which was which. If those counts do not match the providers you actually used, stop: the blocks it could not place are left alone, but the ones it places wrongly are not recoverable from the row afterwards.

The dry run is the only place to read that. Its per-class counts and its warning about a session holding both kinds describe the write it is about to do, so once the blocks are converted a later run has nothing left to report about them.

Between steps 2 and 3 the store is live but incomplete: memories are absent from the agent’s index, and any session affected by conversion E below is already broken. Step 3 is part of the upgrade rather than cleanup to get to later.

The script finds meka’s own directories by default, honoring MEKA_CONFIG_DIR and MEKA_DATA_DIR; --root, --skills-root and --database point it at a copy instead. --self-test checks the script against its own fixtures and exits, touching nothing of yours.

What it converts

ConversionWhat it changesIf you skip it
A. MemoriesThe Markdown files under <config>/memory/ become rows in the store’s memories table, which is where 0.42 reads memories from. The files are read, never written or deleted.The memories are simply not there. The files are untouched on disk, so nothing is lost and the import still works whenever you get to it.
B. Thinking blocksA stored block’s bare signature becomes an opaque object naming which provider it belongs to: signed for a Claude signature, sealed for OpenAI’s encrypted reasoning. 0.41 wrote both to the same field and recorded nothing about which was which, so the script tells them apart by the shape of the blob and reports the counts before it writes. A blob it does not recognize is left exactly as it is.The block loses its opaque half, so that reasoning stops being replayed to the provider. The session still loads and still runs; it just resumes without the chain of thought behind those turns.
C. A skill’s version: / author:Both move from the top level of a SKILL.md’s frontmatter under metadata:, keeping their names, which is where the Agent Skills spec puts them.Nothing. meka reads a top-level version: and author: permanently, because Claude Code’s plugin skills declare version: there. This conversion is cosmetic.
D. A skill’s priority:Moves under metadata: and is renamed to meka-priority:.The skill silently drops to the default rank of 5. A rank is read from metadata.meka-priority and nowhere else, so the [Skills] index comes out in a different order and its cap drops different skills. Nothing warns.
E. A stored tool_resultContent held as a bare JSON string becomes a list of typed blocks, [{"type": "text", "text": ...}].The affected session breaks. The row will not deserialize, so it is dropped as the session loads, which orphans the tool_use it answered, and the provider then refuses the next turn.

The two that matter

A and B announce themselves: a memory you saved is missing from the index, or a thinking block is not replayed. Both are recoverable by running the script later.

D and E are the ones that damage silently. D changes which skills the [Skills] index shows first and which its cap drops, with nothing on screen to say the rank it used was not the one in your file. E can leave a session unusable: it loads cleanly, and then the next turn is refused by the provider because a tool_use in the history has no matching result. See Sessions if you have already met that error.