Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Sessions

Sessions persist your conversation so you can resume later. Each session has an id and lives in the store, a SQLite file.

How sessions work

  • A session is not created when meka starts. It is created lazily when you send the first message.
  • Its id is printed to stderr when it is created, resumed and left, so you can note it for later. Each banner has a switch under [display]; the creation banner is off by default.
  • Sessions include the full conversation: your inputs, the agent’s responses, and tool call results.

Resuming a session

Continue the last session

meka -c

This resumes the most recently updated session. -c takes no value; give an opening prompt with -p: meka -c -p "and now add tests". With --project <name> it is the most recently updated session on that project, or a new one on it when there is none.

From a list

meka -r

With nothing after -r, the screen opens on a picker: the fifty newest root sessions in a table, the title first, then the directory, the profile and when the session was last touched. A long directory is cut from its start so its tail stays, and a narrow terminal drops the directory column, then the profile. / then typing filters the list, j and k move the pick, Enter resumes the pick, and Esc leaves you in a new session. The same picker opens from a running session with /resume, and /resume <id> switches to a session by id or unique prefix; either way the lock moves with you and the session you left stays as it was. meka -r --project <name> opens the picker over that project’s sessions alone.

By id

meka -r 550e8400-e29b-41d4-a716-446655440000

The agent loads the previous conversation and continues from where you left off. --project <name> beside an id moves that session onto the project, rewriting its row, as --profile repins its profile.

By id prefix

If the value passed to -r isn’t a whole id, meka treats it as a leading prefix and looks up sessions whose id starts with it. This avoids having to copy the entire id:

meka -r 550e            # works if exactly one session starts with `550e`
meka -r 5               # likely ambiguous; meka lists matching ids and exits

When a prefix matches multiple sessions, meka prints the matching ids (most-recent first) so you can disambiguate. Type a few more characters until the prefix is unique.

What a resume restores

A session records what it runs on, and a resume brings all of it back:

  • The profile. A session started with --profile openai resumes on openai, whatever default_profile says. This matters beyond the surprise: a thinking block is tagged with the backend that produced it and is not replayed to a different one, so resuming on another profile could silently discard the reasoning the conversation recorded, and a different account would be billed.
  • The permission level, and the approvals switch. A session created at unrestricted resumes there without the flag, and one that had /approvals on comes back asking.

Everything the profile and its account state comes with it: the model, the endpoint, the context window the gauge and auto-compaction measure against, and whether images may be attached. A session records the profile’s name, not a copy of its settings, so editing the profile with meka profile set moves every session on it. Two sessions on one meka serve can sit on profiles with different windows and each is measured against its own.

Naming --profile on a resume repins the session: the row is rewritten, so it keeps that profile from then on rather than for one run. --permission repins the same way.

You can also change the profile mid-session: /profile <name> on the screen, PATCH /v1/sessions/{id} with {"profile": "..."} over HTTP, or the Profile picker in an ACP client. Each rewrites the row.

That PATCH is also how you rescue a session over HTTP when its profile has left config.toml: a body naming only a profile moves the row without building an agent for it, so it works on a session that cannot currently run. From the CLI the equivalent is meka -r <id> --profile <name>.

Switching profile mid-conversation is allowed and is your call. From the next turn the model no longer sees the reasoning recorded under the old profile, for the reason above.

What a resume does not restore

--additional-workspace-directory and --additional-readable-directory are not restored, because they belong to the process rather than the session; pass them again. See permissions for why recording it would be wrong.

A resumed session opens in the directory it recorded, not the one your shell is in. meka -c from anywhere reopens the session where it was working, and /cd is the only thing that moves it. This is deliberate: at workspace the working directory is the writable boundary, so adopting the shell’s would silently widen it: resume a project session from $HOME and the whole home directory becomes writable, with a scheduled job able to fire before you could react. If the recorded directory has since been removed, meka warns and opens where you are. To get back to your shell’s directory, run /cd with no argument.

A resume restores the conversation, not the world it ran in. The messages come back verbatim, which means the agent reads its own earlier tool calls and can reasonably assume their effects still hold. Two kinds of state do not survive the process that made them:

  • Which files have been read. meka tracks reads in memory so file_edit can refuse to write over a file the agent has not seen. A new process starts with that record empty, so the first edit to any file asks for a file_read first.
  • Anything an MCP server was holding. A loaded database, an authenticated session, a subscription: these belong to the server’s process, not to the conversation, and a reconnect drops them. meka has no way to model what a given server keeps open.

Everything else is restated in the per-turn context on every turn regardless (permission level, working directory, tool catalog), the checklist is read back from the conversation, and background tasks that were running deliver an interrupted outcome, so none of those can go stale unnoticed.

Because the second kind is unknowable from meka’s side, the first turn after a resume carries a [Session resumed] note telling the agent to re-establish rather than assume. It appears once and is not repeated. There is nothing to configure.

Session locking

Only one meka instance can be attached to a session at a time. This prevents race conditions from concurrent writes.

  • The lock is taken the moment the session row exists, which for a brand-new session is at the start of its first turn rather than the end. A second invocation launched while that turn is still running is refused like any other.
  • If you try to resume a session that is locked by a running meka process, you will get an error.
  • If the locking process has exited (crashed or was killed), meka detects this and allows you to take over the lock.
  • Under ACP (meka acp), the lock is released as soon as the editor disconnects: closing the connection (stdin EOF) or sending SIGTERM/Ctrl-C makes meka acp exit, so the session can be reopened immediately.

Fallback profiles

A session runs on the profile its row names, and that is where every turn starts. A profile can name other profiles to serve a turn from when it cannot, in order, with fallbacks:

[profiles.main]
account   = "anthropic"
model     = "claude-opus-5-5"
fallbacks = ["backup"]

A turn falls back when the profile’s upstream is still failing once the retry budget is spent (an outage, a rate limit, a dropped stream), or when it refuses for quota or billing by a signal it documents (a 402, a quota code on a 429, the Claude subscription’s exhausted usage window). It does not fall back on a context overflow, an invalid request, a tool refusal or an authentication failure, which another profile would not fix, and never once any of the reply has been shown, the same guard a retry has. The request is re-sent unchanged to the next profile, and the rest of the turn stays there, so a turn never alternates between backends.

The fallback serves one turn. The session keeps naming its own profile, meka session list and GET /v1/sessions/{id} keep saying so, and the next turn starts on it again, unless its breaker is open: a profile that failed over is skipped at the start of later turns for five minutes, or until the upstream’s stated reset when that is sooner, never longer, and the turn starts on the first fallback whose breaker is closed, or on the session’s own when every profile in the chain is skipped. The breaker decides only where a turn starts. Once a turn is running and its profile fails, it moves to the next profile in the chain it has not tried, breaker or not, wrapping round to the ones its start skipped, so a turn fails only once every profile in the chain has refused it, and an answer from any profile closes that profile’s breaker. The breaker lives in the process, shared by every session and sub-agent it runs, and is gone with it. A sub-agent spawned while its parent’s turn is on a fallback is bound to the parent’s own profile, as any spawn is, and its turns start by the same breaker.

The list is a preference order. Every turn starts on the first profile in it, the session’s own included, whose breaker is closed, so a session comes back to the profile you would rather run on as soon as it answers again, rather than staying where the last turn happened to land. Each switch rebuilds the prompt cache at the new profile once, and since a turn never alternates, that cost falls at a turn boundary and never inside one.

Every fallback is visible: a warning in the log naming both profiles and the error, a notice on the screen and the session feed (Profile 'main' is unavailable; this turn runs on 'backup'.), and the turn’s record carrying the profile that served it, on GET /v1/sessions/{id}/turns, on the turn’s terminal event, and in an export, so a cost question later has an answer. A turn the session’s own profile served records nothing. When the whole chain refuses, the turn fails on the last profile tried with that profile’s error. A compaction’s checkpoint and summarizer fall back the same way, and a compaction run between turns puts the named profile back when it ends.

A fallback crosses backends when the profiles do. A thinking block is tagged with the backend that produced it and is not replayed to a different one, so from the switch the model no longer sees the reasoning recorded under the earlier profile, which is the same loss a PATCH to another profile takes.

A fallback must be able to carry what the session holds: the same vision, and a window and a request cap no smaller (fallbacks). While a fallback serves, the session keeps gauging against its own profile’s window. A profile with vision off, reached by a switch rather than a fallback, refuses a turn while the context holds an image.

Storage location

Sessions live in the store, a SQLite file at a platform-specific location:

PlatformPath
Linux~/.local/share/meka/meka.db ($XDG_DATA_HOME/meka/meka.db)
macOS~/Library/Application Support/meka/meka.db
Windows%APPDATA%\meka\meka.db

What else is in that directory

meka.db-wal and meka.db-shm are SQLite’s own companions to the open store, not backups. The locks/ subdirectory holds the lock files meka uses to keep two processes off one session and off one schema change; it is empty of anything worth reading.

You may also find one meka.db.v<version>.bak. meka copies the store aside before it changes the schema, and keeps exactly one such copy: the next schema-changing upgrade takes a fresh one and removes the one before it, so the copies do not accumulate. Expect the data directory to settle at roughly twice the size of the store, and to peak higher than that during an upgrade, since the new copy is written before the old one goes.

To restore one, stop every meka process and copy it over meka.db. It records the schema version it was taken at, so the next start brings it forward again rather than mistaking it for a current store. Because only the newest is kept, restoring undoes the most recent schema-changing upgrade and nothing before it; move a copy of your own aside if you want to go back further.

An interrupted upgrade can leave a meka.db.v<version>.bak.partial behind. That is a copy that never finished, so it is not restorable and nothing removes it; delete it whenever you like.

Anything else you put in this directory is yours and meka leaves it alone, including a file whose name merely resembles the above.

Store schema

The three tables below are the conversation itself, with messages_fts, the full-text index over the words of messages that meka session search reads. The store holds seven more, which the features that own them document: scheduled_jobs (scheduling), background_tasks (background work), memories and its memories_fts full-text index (memory), recall_entries (the screen’s recall), and account_credentials and mcp_credentials (secrets, never in config.toml).

sessions, one row per session:

ColumnTypeDescription
idTEXT (UUID)Primary key
created_atTEXT (RFC 3339)When the session was created
updated_atTEXT (RFC 3339)When the session was last updated
parent_session_idTEXT (UUID)The session that spawned this sub-agent, or NULL
working_directoryTEXTWorking directory the session is in; moved only by /cd, ACP, or PATCH
permissionTEXTPermission level a re-attached session resumes with, and the level a scheduled gate is re-checked against
approvalsINTEGERWhether calls above the level are put to the user for approval
capabilities_jsonTEXTPer-session capability flags, for HTTP re-attach
token_idTEXTBearer token that created the session, for HTTP
additional_workspace_directories_jsonTEXTThe workspace beyond working_directory, as a JSON list
subagent_spec_jsonTEXTThe terms a sub-agent was spawned under
turns, input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens, redactions, redacted_images, redacted_bytesINTEGERThe cumulative counters behind /status
prompt_index, turn_indexINTEGERThe position of the last turn opened, which the Claude subscription wire reports; both NULL on a session that numbers nothing, which is one that had turns before the columns existed
profileTEXTProfile the session runs on. Never NULL, though a row carried forward from a store that predates the column can hold ''
context_tokensINTEGERContext occupancy the provider last reported, which a resume checks its first turn against; NULL until a turn records one, and after a rewind
titleTEXTThe title a user set, or NULL for a session labeled by its first words
pinned_atTEXT (RFC 3339)When the session was pinned, or NULL

blobs and message_blobs: image bytes by SHA-256 content hash, and which message rows reference them. A message row holds a reference in place of the bytes, so a screenshot read twice is stored once, and deleting a session removes the blobs nothing else references. An export carries the bytes its sessions reference, and GET /v1/sessions/{id}/blobs/{hash} serves them over HTTP.

Locks are OS file locks under the data directory, not a column: a row cannot record a crashed process’s PID and lock a session forever.

messages, one row per message in a session:

ColumnTypeDescription
idINTEGERAuto-incrementing primary key
session_idTEXT (UUID)Foreign key to sessions.id
kindTEXTuser_blocks (a turn: its turn_context and text blocks and any images), user (a plain text message meka wrote), assistant, tool_results, compact_boundary, repair, redact, or unsign
contentTEXTMessage content (plain text or JSON)
created_atTEXT (RFC 3339)When the message was saved

scratchpad_entries, one row per entry:

ColumnTypeDescription
session_idTEXT (UUID)Part of composite primary key
nameTEXTPart of composite primary key
contentTEXTThe stored content
created_atTEXT (RFC 3339)When the entry was created

Scratchpad entries are scoped to a session. Two sessions can have entries with the same name. Entries are preserved across compaction but deleted when a session is deleted.

History retention

meka never deletes sessions unless you ask it to. Conversation history isn’t reproducible, so there is no default cleanup by age and none at all by size.

If you do want a time window, set it explicitly:

[session]
retention = "30d"   # delete sessions not updated in 30 days

With that set, meka deletes matching sessions when the agent starts, and then every hour for as long as meka serve, meka acp or the screen stays up, and says so at warn level, so a deletion you configured is still a deletion you see. A session another meka process has open is spared, and so is the one you are resuming with -c or -r, however old it is; so is a pinned session, and one with a scheduled job ahead of it. Unset (the default) keeps everything forever.

To prune on demand instead, remove on your own schedule:

meka session remove --older-than-days 90   # same window, run when you choose
meka session remove <id> [<id>…]           # specific sessions
meka session remove --all                  # everything

Removing a session takes its messages, scratchpad entries, and any sub-agent children with it.

A session is locked from the moment it exists: the lock is taken before the row is written, so a sweep in another terminal cannot catch it in between. That holds for new sessions, for sub-agent sessions, for forks made from any surface, and for the root of an imported archive while it is written. Copying a conversation holds it still too: every fork door (meka session fork, /fork, POST /v1/sessions/{id}/fork, ACP session/fork) and meka session export refuse a session another process has open, because a copy taken mid-turn ends on a user message the model never answered and restores as an unusable session. meka session rewind has always done this.

No deletion touches a session another meka process has open. Naming one by id fails and says so; --all, --older-than-days and the startup sweep skip it and report how many they left behind. This matters most for the startup sweep, because only turns bump a session’s timestamp (resuming does not), so a screen left at its prompt past the window looks expired while somebody is sitting in front of it.

Turn retention

A session that is used for months, an assistant behind meka serve say, keeps every conversation it ever had unless told otherwise. turn_retention sets a window on the turns inside a session, where retention sets one on sessions:

[session]
turn_retention = "90d"   # delete turns that ended over 90 days ago, once summarized

The sweep takes only what a compaction has summarized: the turns before the session’s latest compaction, since the summary is what stands in for them, and never the turns the model is still working from. When one of those is older than the window, the next turn compacts first, keeping no rounds verbatim and quoting none, and the sweep takes it afterward; turn_retention therefore needs auto_compact on. It runs at startup, at the start of each turn for that turn’s session, and hourly while a long-lived host stays up, and reports what it took at warn level.

A turn goes whole: its messages, its record, its words in the search index and the images only it carried. A turn the latest compaction kept verbatim stays in the model’s view as the copy the compaction wrote, and history shows that copy, under the turn and at the time it was said, once the turn’s own rows are gone; a turn a compaction cut into keeps only the rounds the copies hold. Inbox items and background outcomes go once they have been delivered or withdrawn for longer than the window. Scratchpad entries and memories stay, since they are the agent’s own notes rather than the conversation. A fork copies what has survived, and an import brings in turns the next sweep will date like any other. What remains of a deleted turn is whatever the compaction summary said about it, which is the model’s memory and the only thing retention cannot take.

See Config file for details.

Context management

Every request carries the whole conversation as it stands, tool calls and their results included. Nothing is dropped by count: the context ceiling and compaction are the only bound. Once the occupancy the provider last reported passes context_ceiling_percent of the profile’s context_window (85% by default), auto-compaction summarizes the older turns and keeps the recent ones verbatim, between turns and between the tool rounds of one turn. See [session] for the switch and the percent.

The full history stays in the store either way, unless turn_retention is set. After a compaction a request carries the summary and the kept tail, and the tool catalog, skill list and MCP server instructions are restated in full on the next turn.

With auto_compact = false the conversation grows until the provider rejects a request for exceeding its window, which fails that turn; /compact summarizes on demand.

Compacting a session

When a session becomes too long, /compact replaces the older turns with a summary. A token-budgeted tail of recent messages may be kept verbatim, without splitting tool calls from their results. If too little older history remains to summarize, the whole conversation is summarized instead; an unanswered trailing prompt is still kept.

The kept tail is written after the summary as copies the model alone reads. Everything that shows the session’s history (the screen on a resume, an editor’s reload, GET /v1/sessions/{id}/history, conversation_search and conversation_read, the session search and a Markdown export) shows each kept turn once, where it was first said, and shows the copy, under the turn and at the time it was said, once turn retention has taken the turn’s own rows. A kept message the view had rewritten on its way to the model (thinking it stripped, an image it replaced) is shown as it was said, and once retention has taken that row, as the model last saw it.

Whoever writes the summary, it has four sections: the rules and authorizations the user stated, in their words; each commitment and its state (done, pending, blocked, or superseded) and what must happen before the next; the facts established, each with its source; and the next action. A /compact <instructions> focus adds emphasis within those sections and never drops one.

The messages received during the summarized turns are not left to the summary. meka copies the most recent ones into the summary message as they were written, newest first within a third of the verbatim budget, oldest first when read, each quoted line by line; the kept tail takes the other two thirds, or the whole budget when there is nothing to copy. A message relayed from the inbox keeps the header naming who sent it. The one that overflows the budget keeps its beginning; conversation_read reaches all of it. Nothing already in the kept tail is copied twice, and a summary’s own quotes are never quoted again by the next compaction. The reason is the one loss an agent cannot notice: a restriction stated in a later turn and left out of a summary is gone from the window, and the agent cannot search for a rule it does not know it lost.

By default the summary is written by the agent itself, in a checkpoint turn that runs before anything is discarded. The agent gets its real system prompt, its memory index, the full conversation with its images, and the turn’s own tool list, and is told its context is about to be replaced. The request is the turn’s request with the checkpoint instruction appended, so the provider answers it from the turn’s prompt cache rather than reading the conversation again. It saves whatever must outlive the window (memory_write for facts and decisions that should still be true in a future session, the scratchpad for working material), then calls context_replace with the summary.

This matters because compaction is the one moment information is destroyed, and before this it was also the one moment the agent could not act. The alternative, a separate summarizer call, knows nothing about who the agent is or what it is for.

A checkpoint can save, but not act. It may call the memory, scratchpad, conversation-history and read-only search tools and checklist_read, and nothing else; the rest of the catalog rides along for the cache’s sake, and a call to any of it is refused: no shell, no file writes (scratchpad_save_file included), no sub-agents, no scheduling, no MCP. The delete tools are excluded too, since deleting is not saving and a mistaken delete in an unattended checkpoint is unrecoverable, and so are the checklist edits, since a checkpoint’s calls are not part of the conversation and the list is what the conversation’s calls add up to. A tool disabled in [tools] stays disabled here.

You can say what to keep:

/compact keep the auth refactor decisions, drop the debugging

The screen marks it with a rule, →← Context compacted, which opens to what set it going, the checkpoint’s calls (each memory it wrote among them) and the summary the agent now reads. An editor shows it as a compaction entry, or as a card where it has none, with the checkpoint’s calls after it. A one-shot run logs one line, shown with -v, which names what was written, because memories are durable and instance-scoped:

Compacted the conversation on user request. Wrote 2 memories: deploy-pipeline-quirks, api-rate-limits.

Note that an automatic compaction runs a checkpoint too, unattended, and can write memory without anyone watching.

Compaction preserves scratchpad entries, copies the open checklist into the summary verbatim, and re-injects environment context so the agent isn’t disoriented afterwards. The tool catalog, skill list, and MCP server instructions are restated in full on the next turn, since the messages that carried them may have been summarized away. Tools loaded via tool_load stay loaded; the deferred-tool active set is snapshotted into the compaction boundary. If a detail was dropped, the model can conversation_search / conversation_read the full pre-compaction history, which stays on disk.

Internally, compaction does not delete pre-compaction rows from the store. It appends a compact_boundary row to the messages table; the materialized view is reconstructed from the event log, so the persisted log itself stays append-only until turn retention, if set, takes the rows a boundary has superseded. A resume reads that log from its last compact_boundary row: nothing before it can reach the view, so opening a session costs the same however long its history is. conversation_search, conversation_read, GET /history, a rewind and an export still read the whole log.

When the summarizer runs instead

A standalone summarizer, with no tools and none of the standing instructions, is the fallback. It runs when:

  • The compaction is an emergency one, i.e. the provider has already rejected the request for exceeding the window. A checkpoint turn re-sends that same conversation, so it would be refused identically; the summarizer strips images and truncates long blocks, which is what lets it get through.
  • The checkpoint turn fails or ends without calling context_replace. tool_choice isn’t available across meka’s backends, so the call can’t be forced, and a turn that ends on prose instead is not trusted to have written a summary: that prose is as often an answer to the conversation, or a sentence cut off at the checkpoint’s round limit, as a summary, and nothing can tell which. Memories the checkpoint wrote before it ended are kept either way.
  • compact_checkpoint is off.

The summarizer runs under the profile’s thinking setting, as every request does. A summary that comes back with no text, its whole reply thinking, is asked for once more with the nudge a turn gets; a second empty one fails the compaction.

[session]
compact_checkpoint = true   # default

Turning it off leaves the standalone summarizer to write every summary, which saves one model call per compaction at the cost of the agent having no say in what survives.

Auto-compact

When auto_compact is enabled (default: true), meka automatically compacts the conversation once it is past context_ceiling_percent of the context window (default: 85%). The check runs at three points. At the start of a turn it reads the last reported usage, which a resumed session takes from its row, so the first turn after a resume is checked against the real number. Before the first request it projects the request from an estimate, so a turn whose own input jumps over the ceiling is compacted before it is sent. And after every round of tool results inside a turn it reads that round’s reported usage, so a long tool loop overshoots the line by one round rather than by the whole loop; the turn’s most recent rounds may be kept verbatim within the budget, its earlier ones are summarized with the history, the request the turn is answering is quoted after the summary as the user wrote it, and one crossing is answered once, until a later measurement reads under the line again. As a last resort, if the provider still rejects a request for exceeding the context window, meka compacts once and retries the turn instead of failing.

[session]
auto_compact = true
context_ceiling_percent = 85  # default
context_window = 200000  # optional override

Agent-initiated compaction

The agent doesn’t have to wait for the ceiling. context_compact asks for a compaction before the agent’s next step: it runs once the current batch of tool calls finishes, and the turn then carries on against the summary. With the default keep_recent, a retained tail may keep the most recent rounds within the verbatim budget, cut at a call so no call is parted from its result, and everything before that, the history and the current turn’s earlier rounds alike, goes into the summary; the request the turn is answering is quoted after the summary as the user wrote it, so the turn continues against the user’s words rather than a paraphrase. One compaction per turn: a further request once the first has run is ignored, and the agent can ask again on a later turn.

context_compact(instructions: "the day's work is in memory now", keep_recent: false)

keep_recent: false requests no verbatim tail. The checkpoint’s own context_replace has the last word, and without a checkpoint the tail is kept, since nothing was saved. When the tail goes, the summary and the restored live context carry the work forward; an unanswered trailing prompt is still preserved. That is the difference between compacting and turning the page, and it’s what makes a “start of a new day” routine work: a scheduled job at midnight can write the day’s diary to memory, then compact clean, instead of carrying yesterday’s context forward indefinitely.

The request is parked rather than applied where it is made: a tool cannot rewrite the conversation the agent loop is holding. It is drained at the next boundary between rounds, once the batch’s tool results are in, which is what lets the rest of the turn run against the summary.

What the agent sees

Once a turn has been measured, the per-turn context block carries a [Context budget] line reporting occupancy and the ceiling compaction fires past:

[Context budget]
Using ~84k of 200k tokens (42%). Auto-compaction at 85%.

What happens at the ceiling (a summary between turns or between two tool rounds, which loses detail while recent rounds may be kept verbatim, and what it omits stays reachable through conversation_search and conversation_read) is stated once in the system prompt rather than on every turn. With turn_retention set, that sentence bounds the reach by the window, and says that older turns are deleted and that what must outlive them belongs in memory. The context block then carries the date the record starts at, which moves every turn:

[Retention]
Turns older than 90 days are deleted; nothing before Thu, 16 Jul 2026 09:12:34 +0800 is on record.

The date is in the same zone and form as the block’s own Date: line, so the two read against each other.

The agent is expected to budget its own reading and to decide when a task will fit, so it needs the same number the harness uses. Without it, those are guesses. The line is suppressed when the window is unknown, and on the first turn of a session, when there is no measurement yet rather than a genuine zero.

It rides the per-turn context block rather than the system prompt because it changes every turn and the system prompt is the cached prefix.

From the second compaction onward the line also reports how many have happened, since a summary of a summary has lost considerably more than a first pass:

This conversation has been summarized 3 times, so early detail is now several
removes from what was said; write anything that must last to memory rather than
relying on it surviving another pass.

Because that block is rendered once per turn, it does not move while the agent works. During a long tool loop, which is exactly when context moves fastest, it is stale. context_check reports the live figures on demand: occupancy, headroom in tokens, the fixed overhead compaction cannot reclaim, how much of the recent conversation would survive verbatim, and the compaction count. Refreshing the pushed block instead would rewrite a message the provider’s prompt cache already covers, invalidating it on every iteration; a tool result appends at the tail and is cache-safe.

Listing sessions

To see past sessions:

meka session list

This shows a table with each session’s id, last update time (local, with its UTC offset), profile, project (- for none, and the column only once a project exists), and its title: the one you set, else the words of the first message. Pinned sessions come first, newest pin on top, marked with *; the rest follow most recently updated first:

ID        Updated                  Profile   Project  Title
7c1d9e02  2026-02-20 18:04 +00:00  work      -        * Research: LLM security blind spots
550e8400  2026-03-14 12:00 +00:00  work      webdev   How do I implement a binary search tree?
a1b2c3d4  2026-03-13 09:30 +00:00  personal  -        Fix the login page CSS

--project <name> lists the sessions on one project; --project "" those on none.

The ID column shows as much of each id as distinguishes it from the others on screen, widening only if two would otherwise read the same. Every command that takes a session id (meka -r, export, show, fork, rewind, remove) accepts any unique prefix, so what you see is normally what you retype. An ambiguous prefix is refused and every match listed, rather than acted on.

Uniqueness is computed over the rows on screen, while the commands resolve against every session in the store. A listing narrowed by -n, or one hiding sub-agent sessions (they are hidden unless --include-children is given), can therefore print a prefix that a wider set makes ambiguous. That fails closed: the command refuses and names both ids, so nothing is acted on and the full id is one copy away.

For the whole id, and the working directory and permission the table has no room for:

meka session show 550e8400

By default the 20 most recent sessions are shown. Use -n to change, and -n 0 to show every session; meka session search takes the same flag:

meka session list -n 50

Sub-agent transcripts are hidden by default, so the listing stays the conversations you started. Add --include-children to see them too:

meka session list --include-children

The Profile column names the profile, which is the whole story: a session records a profile name and nothing else, so the model and endpoint it runs on are whatever that profile and its account currently say. meka profile list shows them.

Both commands take --format json. The listing becomes {"sessions": [...]} and show one object, each session carrying id, created_at, updated_at, profile, project, title, approvals, turn_count and usage (the turns the session has on record and what they spent, its compactions included), and, when the row records them, working_directory, permission, parent_id, pinned_at and last_turn: the fields GET /v1/sessions returns under the same names, less what only the server holding the session can answer (capabilities, turn_in_flight, inbox_pending, requests_pending, checklist). Ids are printed in full, and an empty store is {"sessions": []}.

meka session list --format json | jq -r '.sessions[] | "\(.id) \(.profile)"'
meka session show 550e8400 --format json | jq .working_directory

Titling a session

A session is labeled by the words of its first message, which is not always what you want to find it by later. A session opened on a skill or an MCP prompt is labeled by the words typed with it, or by its name when none were, never by the body it expanded to. Give it a title of your own:

meka session set 550e8400 title "Research: LLM security blind spots"

The title is what every listing shows from then on: meka session list, the web and editor clients, and GET /v1/sessions, all under the same title field. It is one line, at most 200 characters, and whitespace runs are collapsed. Clear it with an empty string, and the session goes back to being labeled by its first words:

meka session set 550e8400 title ""

From the screen, /title <text> titles the session you are in and bare /title shows the current one. Over HTTP it is PATCH /v1/sessions/{id} with {"title": "..."}. Titling a session does not move its updated_at, so it stays where it was in the listing and meka -c still continues the session you last used, not the one you last renamed. A fork carries the title with it, since it holds the same conversation, and an export does too.

Pinning a session

A session you keep coming back to can be pinned, which lists it first, ahead of every unpinned session, on every surface:

meka session set 550e8400 pinned true
meka session set 550e8400 pinned false

Pinned sessions are ordered among themselves by when they were pinned, newest on top, so the shelf does not reshuffle as you use them; unpin and pin again to move one to the top. meka session show prints when a session was pinned, the JSON forms carry it as pinned_at, and the listing marks the title with *.

A pin is also a keep: the [session].retention sweep and meka session remove --older-than-days leave a pinned session alone whatever its age. A session expires as a tree, by the age of its root: a pin or a scheduled job anywhere in the tree keeps the root and every sub-agent under it, and a root that expires takes its sub-agents with it. meka session remove <id> and --all still do what they say. Like a title, a pin does not move updated_at and does not change which session meka -c continues. A fork is a new session and starts unpinned; an export carries the pin.

Searching sessions

When you remember what a conversation was about but not which session it was, search the words:

meka session search "blind spot timestamp"
ID        Updated                  Title                             Match
7c1d9e02  2026-02-20 18:04 +00:00  Research: LLM security blind...  The paper's point is that a timestamp in the prompt lets the...
3e0a77b1  2026-01-08 09:12 +00:00  what does the blind spot pape...  what does the blind spot paper say about timestamps

What is searched is what was said: your messages, the assistant’s replies and compaction summaries. Tool calls, tool results and the model’s thinking are not, so a file the agent read does not make its session a match for every word in it; conversation_search, which the agent runs over the sessions it names, still covers those. The agent has this search too, as session_search, over the sessions it may read. A session whose title holds every word is listed first, newest first among those; the rest are ranked by how well their best message matches. Every word must appear; when nothing holds them all, the words are tried as prefixes, so zebra okap still finds the session about zebras and okapis; then sessions holding any of the words are listed, then any as a prefix. Words are matched by their stem, so deployed finds deploying, and every diacritic is folded, so viet finds Việt. Chinese, Japanese, Korean and other scripts written without spaces are matched character by character, so a word is found inside a sentence and a query of one character works. What the matching does not do: an Arabic word is matched as written, article prefix included; a German compound is not split into its parts; Thai is matched by its consonants alone. A message a rewind took back is not found, since the history no longer shows it.

Each row shows the session’s title and the line of the best-matching message the words were found on, cut to fit; a line taken from a compaction summary is marked (summary). -n sets how many sessions to show (default 20), --include-children adds sub-agent sessions, and --format json answers {"sessions": [...]} with each session’s record plus excerpt and excerpt_position, the position of the message the excerpt was taken from, which GET /v1/sessions/{id}/history?before=<excerpt_position + 1> ends a page with. Over HTTP it is GET /v1/sessions?q=.

The search reads an index the store keeps beside the conversation, a few percent of its size, kept in step with every message written and checked on every open. The first launch after upgrading builds it over every existing session, which takes a moment on a large store and is logged.

Exporting a session

You can export any session as a Markdown file:

meka session export 550e8400-e29b-41d4-a716-446655440000

This writes session-550e8400-e29b-41d4-a716-446655440000.md in the current directory with the full conversation. User and assistant messages are rendered as Markdown sections, while tool calls and results are wrapped in collapsible <details> blocks. The export always covers the entire session, including turns that were later hidden from the model by compaction (each compaction point is marked with its summary).

To write to a specific file:

meka session export 550e8400-e29b-41d4-a716-446655440000 -o conversation.md

To print to stdout (for piping):

meka session export 550e8400-e29b-41d4-a716-446655440000 -o -

JSON (structured, round-trippable)

Pass --format json for a structured export instead of rendered Markdown:

meka session export 550e8400-e29b-41d4-a716-446655440000 --format json

This writes session-<id>.json, a lossless dump of the session’s event log (including input images and compaction boundaries), its cumulative stats, its profile and project, and scratchpad entries. The archive carries format_version: 22, and an import refuses any other version rather than guessing at its shape, except an archive written by 0.59 or later (format_version 3 to 21), which is brought forward as it is read. An event that is a copy a compaction kept names the position of the event it copies (copy_of). Unlike Markdown, a JSON export also includes any sub-agent child sessions spawned during the conversation, and it can be re-imported with meka session import. It deliberately contains no credentials: API keys and OAuth tokens live in separate tables and are never part of an export.

Importing a session

Recreate a session from a JSON export:

meka session import session-550e8400-e29b-41d4-a716-446655440000.json

meka assigns the imported session (and any sub-agent children) new ids so they can’t collide with existing sessions, then prints the new root session id. Resume it like any other session:

meka -r <new-id>

Read from stdin with -:

cat session.json | meka session import -

The import preserves the full conversation, per-message timestamps, cumulative stats, scratchpad entries, the title and the pin, and the name of the profile the session ran on. That name is all an archive carries about the profile: the settings themselves come from whatever [profiles.<name>] and its account say on the installation importing it. An archive naming a profile this installation does not configure, on its root or on a sub-agent spawned with one, is refused rather than restored as a session every resume would refuse; meka --profile <name> session import moves every session in the archive onto a configured profile instead, the same explicit act that moves a session on resume. An archive that names no profile adopts this installation’s default; if nothing can supply one, because no default_profile is set and several profiles are configured, the import is refused: name one with meka --profile <name> session import. An archive whose messages reference an image it does not carry and this store does not hold is refused as malformed before anything is written.

updated_at is stamped to the import time rather than restored from the export, so that restoring an archive older than a configured retention window isn’t undone by the retention sweep on the next launch. created_at still carries the original.

Forking a session

Branch off an existing conversation without disturbing it:

meka session fork 550e8400-e29b-41d4-a716-446655440000

The copy starts with the original’s full conversation and continues from there under a new id, which is printed on stdout so it can be captured:

meka -r "$(meka session fork 550e8400-e29b-41d4-a716-446655440000)"

Use it to try a different direction from a known-good point, to run a throwaway question against a large accumulated context, or to keep a conversation you’re about to compact.

What the copy carries: the full event log, scratchpad entries, working directory, permission level and approvals switch, additional workspace roots, cumulative stats, and the title, since it names the same conversation. What it does not: sub-agent sessions (the sub-agent’s result already sits in the parent conversation as a tool result, so the copy is complete without them), the pin, since the copy is a new session, and the timestamps, which are stamped fresh.

A fork of an ordinary session records no link back to the one it came from; it is a root session like any other. A fork of a sub-agent is the exception: it keeps that sub-agent’s parent and spawn terms, so the copy is a sibling under the same parent rather than a promotion to a session of its own, and it is continued through agent_followup like any other sub-agent.

Forking copies what has been committed to the store, and only between turns. A session another meka process has open is refused rather than copied (see Session locking), and so is one with a turn in flight in the process that holds it: POST /v1/sessions/{id}/fork answers 409 turn-in-flight and ACP session/fork answers InvalidParams. The user message is persisted before the model is called, so a copy taken mid-turn would end on a prompt with no reply and restore as an unusable session. Cancel the turn or wait for it.

The same operation is available from the screen as /fork, which switches you into the copy and leaves the original where you branched (an always or never given at an approval prompt stays with the original; see Permissions); over HTTP as POST /v1/sessions/{id}/fork, whose turn_id branches the copy before a turn; and over ACP as session/fork.

Fork or export/import?

Both produce a runnable copy under a new id. Reach for fork to branch a conversation you’re working on, and for export + import to move a session between machines or keep an archive. Export/import also copies sub-agent transcripts and preserves created_at, because an archive should restore whole.

Both copy a long session in short transactions, so another meka process writing to the store meanwhile is not held up for the length of the copy. The copy appears in listings, searches and meka -c only once it is whole. One that a crash interrupted is deleted the next time a meka host starts, unless another process still holds it.

Rewinding a session

Drop the most recent turns from a session:

meka session rewind 550e8400-e29b-41d4-a716-446655440000
meka session rewind 550e8400-e29b-41d4-a716-446655440000 -n 3

The cut lands on a turn boundary, so a tool call is never separated from its result. A compaction summary is never taken, since it stands for turns no rewind can give back: a session whose context holds fewer turns after its summary than -n asks for is refused with nothing to rewind. Nothing is deleted: the dropped turns stay in the event log and still appear in meka session export, marked at the point of the rewind. The model simply stops seeing them.

The command takes the session lock, so it refuses to run while a screen, meka serve, or meka acp holds the session; that process has its own copy of the conversation in memory and would write over the rewind on its next turn. On the screen use /rewind instead. Under ACP there is no in-session equivalent, so close the session in the editor and run this command; over HTTP, POST /v1/sessions/{id}/rewind takes a live session back to before the turn its turn_id names.

Its main use is recovering a session a provider has started rejecting. A provider validates the whole conversation on every request, so one piece of content it rejects fails every later turn too.

meka repairs a rejection caused by content added since the last request the provider accepted, and repairs a mislabeled image on resume, but anything older than that needs rewinding past. That window is usually the current turn, and it reaches back into the previous one when a turn failed mid-tool-loop and left it unaccepted. A compaction widens it to the whole conversation, because a compaction replaces that conversation wholesale and nothing in the result has been accepted yet. The repair escalates: first it removes the attachments the turn added and leaves everything else alone, and only if that is refused as well does it empty the turn’s tool calls, moving each call’s arguments into the result that reports it and replacing the result’s body with an explanation. The second step exists because a tool result is usually text, which the first step cannot touch, and because a call’s own arguments can be what the provider objected to. A step the provider then accepts is not counted as spent, so the cheap one stays available if the turn is refused again later.

Neither step changes the shape of the conversation: a tool call stays a tool call and its result stays its result, marked as an error. That is deliberate. Removing one half of a pair is the one thing every provider refuses outright, so a repair that could do it might turn a recoverable rejection into a permanent one. The model sees a failed tool call, which it already knows how to read, with the arguments it sent quoted in the failure so it can tell which call not to repeat.

Nothing a repair removes is deleted. The log is append-only (within turn retention, when set), so the superseded messages stay on disk and meka session export renders them above a marker saying what replaced them. Use --format json to get a removed attachment back: the markdown export writes each message as a person said it, an invoked skill or prompt as the invocation rather than the body it expanded to and an attached file by its mention, and leaves image blocks out. Only the conversation the model sees is changed.

Whatever is removed is restored untouched if the retry carrying it is refused too. That restore is what bounds the risk, and it bounds it only in that direction: each step spends a fresh retry sequence rather than a single request, and a step whose retry succeeds keeps the loss, so the trigger is deliberately narrow. The words you typed are never rewritten by either step, though an image you attached to that prompt is exactly what the first one removes, replacing it with a note. Notes meka inserts into a conversation are prefixed [meka harness].

Both steps run whether the provider answered 400 or spent every retry on a 5xx. A gateway in front of a model reports a payload its own decoder choked on as a server error, which is indistinguishable from being overloaded, so meka honors the retries in full and treats a refusal that outlives them as one the content may explain.

On the 5xx path it does one thing more before touching anything. The retry sequence is short by design (two attempts across three seconds of backoff), which an ordinary overload outlasts, so meka waits eight seconds and sends the same request one last time. If that succeeds the outage was the whole story and nothing is lost; if it fails too, the reading that the body is the problem has been earned rather than assumed. The wait is paid only by a turn that was otherwise about to start deleting things, and only once per run of consecutive failures: a request the provider accepts makes it available again, since a later refusal is about work the earlier wait never saw. A 400 skips it, because the provider has already read the body and said no.

Nothing outside those two shapes degrades at all, because a degraded retry that succeeds only because the network came back would keep the loss. Excluded, then: a dropped connection, which never delivered the request for anything to judge; a 429, which is a statement about rate rather than about what was sent; a failure that arrives partway through a stream, after some of the answer has reached you, both because re-sending would print it twice and because the stream cannot tell an overload from anything else; and anything at all once the retries have not been exhausted. If a step it did try does not help, it says so and points here before the turn fails.

One cause of that refusal has its own fix. A session recorded by 0.41 can hold a tool_result whose content is a bare JSON string, a shape meka does not read: the row is dropped as the session loads, which leaves the tool_use it answered unanswered, and the provider rejects the next turn over the mismatch. Run the one-shot upgrade script, which converts those rows in place, rather than rewinding past a turn you wanted to keep.

One rejection reaches past the window those steps work in, and meka answers it on its own. Claude ties each thinking block to the conversation ahead of it, the tool list included, and on the accounts where Anthropic enforces that check it rejects a request that sends a block back after that conversation changed: most changes to the tool list, a compaction that keeps recent turns verbatim behind its summary, or a repair above can each make every later request fail. When Claude rejects a thinking block that way, or one whose signature it cannot verify at all, meka sends the conversation again without its thinking, back to the last compaction. The model no longer gets that reasoning back, and the turn goes on. Nothing on disk is rewritten: an accepted resend is recorded in the log as the point the thinking stopped being sent, so a resume sends the same, and an export marks that point and still shows the reasoning’s readable text. A rejected one restores the thinking, and the steps above take their turn. A compaction does the same for its own requests, the checkpoint’s and the summarizer’s. On an account that does not enforce the check the rejection never comes, and nothing changes.

Removing sessions

Remove specific sessions by id:

meka session remove 550e8400-e29b-41d4-a716-446655440000

Remove multiple sessions at once:

meka session remove 550e8400-e29b-41d4-a716-446655440000 a1b2c3d4-e5f6-7890-abcd-ef1234567890

Remove every session not updated in the last N days:

meka session remove --older-than-days 90

This is the manual counterpart to retention, and spares what the sweep spares: a pinned session, and one with a scheduled job ahead of it. It can’t be combined with ids or --all, and 0 is refused: it would match everything.

Remove all sessions:

meka session remove --all

--all takes no ids of its own: naming some sessions and then asking for every session are two different requests, and it refuses rather than quietly doing the wider one. It means every session, pinned ones included.

Recall

Separate from your saved conversations, meka keeps a rolling list of the prompts you type on the screen, its recall, so Up-arrow recall and Ctrl+R reverse-search work across runs (shell-style). This is distinct from a session (a stored conversation), which the screen reads back as you scroll up.

List recent recall entries (oldest first; -n 0 shows all), one per line, or as {"recall": [...]} with --format json; an empty list says No recall entries. on stderr:

meka recall list
meka recall list -n 100
meka recall list --format json

Clear it entirely:

meka recall clear

Managing sessions via SQLite

You can also manage sessions directly through the store’s SQLite file. For example, to list all sessions:

sqlite3 ~/.local/share/meka/meka.db \
  "SELECT id, created_at, updated_at FROM sessions ORDER BY updated_at DESC;"