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.88 | 0.89 |
|---|---|
POST /v1/sessions/{id}/turn | POST /v1/sessions/{id}/turns, same body; a detached turn’s Location is unchanged |
GET /v1/sessions/{id}/messages | GET /v1/sessions/{id}/history for a transcript, GET /v1/sessions/{id}/context/messages for what the model sees |
GET /v1/sessions/{id}/stream | GET /v1/sessions/{id}/feed |
GET /v1/stream | GET /v1/feed |
GET /v1/sessions/search?q= | GET /v1/sessions?q=, beside any listing filter |
GET /v1/sessions/{id}/tasks | GET /v1/tasks?session=<id> |
DELETE /v1/sessions/{id}/tasks/{task_id} | DELETE /v1/tasks/{task_id} |
GET /v1/schedule | GET /v1/jobs |
POST /v1/sessions/{id}/schedule | POST /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=markdown | gone: 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.
| Where | 0.88 | 0.89 |
|---|---|---|
The conversation’s ETag, a 412, turn.finished, turn.failed, turn.canceled | revision, total | position; the ETag is "<position>" |
| A message of the conversation | turn_label, paged by offset | gone: turn_id and position, paged by before and after |
POST /rewind body | turns, optional | turn_id, required |
POST /rewind answer | turns_removed, messages_before, messages_after | the rewind history item |
POST /compact answer | source, memories_written, kept_recent, messages_before, messages_after | the compaction history item |
conversation.rewound | revision, total, turns_removed | session_id and the rewind item’s fields |
context.compacted, a summary’s compaction | replaced_count | gone |
Blocking POST /turns answer | turn_id, session_id, stop_reason, usage, messages | the 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}/context | message_count, totals | gone: position; a session’s spend is turn_count and usage on its record, its turns summed, a sub-agent’s on its own |
| Session record | approvals_pending | requests_pending, and turn_count and usage beside it |
| A session search hit | excerpt | excerpt and excerpt_position |
| A memory search hit | snippet | excerpt |
GET /v1/profiles, GET /v1/profiles/{name}, meka profile list --format json | active | default |
tool_call.progress | server_name, tool_name, tool_use_id | id, the call’s |
meka session show --format json prints the session record’s new fields too.
| 0.88 event | 0.89 event |
|---|---|
permission_required | approval.requested |
permission_resolved | approval.resolved |
elicitation_required | elicitation.requested |
elicitation_resolved | elicitation.resolved |
progress | tool_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
readon 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-subscriptionprofile sealed names that endpoint, and is sent back to it alone; reasoning from anywhere else names none and is sent nowhere. Naming it readsconfig.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.
| Surface | 0.80 | 0.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.75 | 0.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:
| Surface | 0.75 | 0.76 |
|---|---|---|
| Run flag | --writable-root <PATH> | --additional-workspace-directory <DIR> |
| Run flag | none | --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 API | cwd | working_directory |
| Export archive | additional_roots | additional_workspace_directories |
GET /v1/sessions filter | cwd=<path> | working_directory=<path> |
| Export archive | format_version 9 | format_version 11 |
| Store | sessions.cwd, sessions.additional_roots_json | working_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_promptandnewline_after_promptdescribed a stream the screen no longer writes. Delete them fromconfig.toml.--render-modeandMEKA_RENDER_MODEare 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_promptandshow_context_in_promptare replaced bystatus_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 --oneshotand 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--oneshotor the HTTP API instead. !commandoutput 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.
/historyand/clearare gone, anddisplay.resume_show_recentwith 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. Deleteresume_show_recentfromconfig.toml.- A slash command’s answer is a panel, not rows of the transcript.
/help,/status,/usageand every listing open in place of the composer until Esc closes them; a bare/permission,/approvalsor/profileis 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;alwaysandneverare 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.59 | 0.60 |
|---|---|
read_file | file_read |
write_file | file_write |
edit_file | file_edit |
find_files | file_find |
search_contents | file_search |
execute_command | shell_execute |
fetch_url | web_fetch |
render_image | image_render |
load_tool | tool_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
-
Run the script against your config, first as a dry run, then with
--apply. It needs Python 3.11 and thetomlkitpackage (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 placeIt finds meka’s config the way meka does, honoring
MEKA_CONFIG_DIR;--config PATHpoints it at a copy instead.--self-testchecks the script against its own fixture and exits. -
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.bakfor a store 0.45 left), in ten ledger steps. The first creates the REPL’sprompt_historytable where a store lacks one, a no-op otherwise. The other nine:sessions.providerbecomessessions.profile, andprovider_credentialsbecomesaccount_credentials, keyed by account, both renames of what was always there; anapprovalscolumn is added, everyasksession becomesnonewith 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 ownturn_contextblock; every image’s bytes move out of its message row intoblobs, leaving a reference; every column and index takes one naming rule, with the JSON in two of them following suit; arepairrow’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 spelledcanceled. 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.dumpreplays the whole ledger, and if no default profile can be resolved when it does, the frozen 0.44 step warns with its old--provideradvice; 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
| Before | After |
|---|---|
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_seconds | request_timeout = "30s", connect_timeout, read_timeout, by value; a 0, which meant the default, is removed |
[mcp].grace_seconds = 3, connect_timeout_seconds = 30 | grace = "3s", connect_timeout = "30s", by value; a 0 becomes "0s", which grace accepts and connect_timeout refuses at startup |
[mcp].strict | default_required, same meaning |
[session].retention_days = 30 | retention = "30d", by value |
[thinking].budget_tokens | budget, 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 providersuite is gone.meka account add/login/list/removemanage accounts and their credentials;meka profile add/set/use/list/removemanage profiles.meka account usage/whoami/statsare where they were, and take--profile <name>instead of a positional name.account addtakes--backendwhereprovider addtook--type. - The prompt is a flag.
meka "text"ismeka -p "text", and-p -reads the prompt from stdin. There is no positional prompt, someka unknowncommandis an error rather than a session. --provideris--profile, long form only:-pis the prompt.--format jsonon a--oneshotrun prints one object for the turn; see One-shot mode.- HTTP API: the
providerfield onPOST /v1/sessions,PATCH /v1/sessions/{id}and every session response isprofile, andGET /v1/providersisGET /v1/profiles, whose rows carryaccountandbackendin place oftype. A session export archive’sproviderfield isprofile, and itsformat_versionis 2, so an archive written by 0.45 is refused by version; re-export it from a migrated store. - ACP: the
configOptionsentryproviderisprofile. - REPL:
/provideris/profile, and/statusshows the profile with its account. - One spelling per value.
--permissionandMEKA_PERMISSIONtake a level’s full name (n,r,w,uare gone), and--render-mode,MEKA_RENDER_MODEand[display].render_modetaketermimad,syntectorraw(richis 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 undocumentedtextspelling of--format plainis gone too. - A
[permissions].defaultorenabledentry 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 rewritesask; anything else you spelled yourself. - The
askpermission level is gone, replaced by theapprovalsswitch beside the level: a call above the level is refused, or put to you when the switch is on./approvals on|offin the REPL,approvalsonPOST /v1/sessionsandPATCH /v1/sessions/{id}, and the ACP config option of the same name set it;[permissions].approvalsis what a new session starts with. The store migration turns anasksession intononewith approvals on, which asks about every call asaskdid. An approved call now runs at the session’s level, so an approved write atreadlands only under the workspace roots whereaskwrote 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_contextcontent block, first, and the words are atextblock.GET /v1/sessions/{id}/messagesreturns the block typed, so a client readingcontent[0].textas the prompt now reads the context; take thetextblocks. A migration splits every stored turn once. - Image bytes live in a
blobstable. 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 ablobslist 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 reportsmedia_typeandhash, andGET /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
--permissionno 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_timeoutandread_timeout,[mcp].graceandconnect_timeout,[session].retention("30d"); the script converts the_secondsand_dayskeys by value, and"0s"is refused where zero is meaningless.[mcp].strictisdefault_requiredand[thinking].budget_tokensisbudget, both converted.MEKA_MCP_STDIO_CONCURRENCYandMEKA_MCP_HTTP_CONCURRENCYare gone: set[mcp].stdio_concurrencyandhttp_concurrency(3 and 20 by default, zero refused).MEKA_MCP_TOOL_TIMEOUTtakes a duration such as10m, 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,--formatandmcp add --transportrefuse case variants (Read,JSON),session exportandGET /v1/sessions/{id}/exportdrop themdalias ofmarkdown,mcp add --authtakesoauth,client_credentialsorclient_credentials_jwtas the[auth]block spells them (the hyphenated forms are gone), and[mcp].default_permission, a server’spermissionandtool_permissions, and[tools].tool_permissionsrefuse 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 thannull,display_summaryincluded;GET /v1/health/readyreportsprofile_configured(wasprovider_configured); thepermission_requiredevent carriesinputand 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 422session-not-drivable; a fork of a session another meka process holds answers 409session-locked; a session’stitleis the first user words with whitespace collapsed, cut at 80 characters, andmeka session showlabels ittitle(wasopening). Four status codes move: a[web]orbase_urlmisconfiguration is a sanitized 500 (was a 422 naming the operator’s path), and a session lock meka cannot open is 500 (was 409session-locked); meka’s own request-ceiling refusal is 422 with the newtyperequest-too-large(was 502provider);GETandPATCH /v1/sessions/{id}answer 404 or 500 for a row they cannot read (was 200 withprofile: ""); andPOST /v1/sessions/{id}/schedulewith 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(wasInternalError);session/set_config_optionrefuses a profile switch while a turn is in flight instead of writing the row and deferring;session/new,load,resumeandforkrefuse acwdthat 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 carryrawInputwith 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, notReadFile); the prompt is headed[approval]and takesalwaysandnever. - 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(wasnot-empty) inschedule_create,POST /v1/sessions/{id}/scheduleandmeka schedule add; stored jobs are converted by the store. canceled, onel, on every wire meka owns. Matchturn.canceledas the SSE terminal event,https://meka.bot/errors/turn-canceledas the problemtype, andstatus == "canceled"in task views (GET /v1/sessions/{id}/tasks,DELETE .../tasks/{task_id}),task_listoutput andschedule.firedwebhook bodies; thereasonvalues are unchanged. The store rewrites its stored task rows on first open (the tenth ledger step). ACP’sstopReason: "cancelled"and MCP’snotifications/cancelledare 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.
| Tool | Before | After |
|---|---|---|
conversation_search | regex (boolean) | is_regex |
conversation_read | count | limit |
find_files | pattern | glob |
fetch_url | max_length | limit |
agent_followup | agent | id |
agent_delete | agent | id |
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
- 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.
- 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.
- 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
| Conversion | What it changes | If you skip it |
|---|---|---|
| A. Memories | The 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 blocks | A 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_result | Content 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.