Skip to content

Sessions and persistence

Sessions retain conversation, request configuration, agent state, and captured filesystem changes across saves and resumes. Project sessions publish state on their execution target; file-backed sessions use their own storage authority. Compaction, Rewind, and Fork each preserve different parts of that state.

Directive session boundaries

Settled directive work remains a complete first-class turn in its execution session segment. Its canonical user, response, and (tool . id) properties are saved through the normal transcript/GPTEL_BOUNDS path so resume and MevView use the ordinary renderer. Paired hidden directive-boundary audit records retain the directive id, action, and canonical turn identity; ordinary-chat request assembly finds those boundaries in gptel's temporary prompt copy and marks the enclosed body gptel 'ignore, including tool spans.

The workspace-owned implementation attempt or discussion turn also retains the exact submitted request, terminal result, and matching (:session-id ... :turn ...) checkpoint; implementation attempts additionally retain the authored-request snapshot, capture timestamp, capture metadata, and patch. Bash/Eval/execution-control or agent activity that may mutate files outside the ordinary tool snapshot seam is retained as an explicit capture gap; such an attempt is never labeled complete, and Rewind surfaces the gap before restoring. This bounded duplication separates durable follow-up/inspection context from transcript presentation. Every accepted request reserves its turn identity before tools run, and that same identity keys pre-turn snapshots, transcript metadata, the prompt/Rewind index, and the workspace record. A directive reuses its bound live or persisted session. If that session no longer exists, rebinding requires explicit confirmation and does not rewrite historical checkpoints.

Sessions materialize lazily and save at turn settlement, including errors and aborts. Compaction rotates segments rather than rewriting in place.

Transcript bounds are serialized without Lisp printer length or depth limits. On restoration, sanitation of malformed bounds keeps the surviving ranges anchored while rewriting the property drawer, so hidden metadata does not become visible merely because the drawer changes size. Dotted or circular bounds lists are discarded at the malformed level (the whole bounds list, an entry, or a range), retaining valid sibling ranges. Entry property names must be symbols. Missing ranges in an already truncated transcript cannot be recovered from the bounds alone.

Request cancellation is terminal before cleanup callbacks run. Later canceller registrations run immediately rather than reopening the request. Permission admission rejects that cancelled request even if cancellation happened before there was a queue entry or provider review to drain. Public abort uses the same terminal cancellation boundary while preserving the request reservation and file snapshots until pending durable settlement finishes.

Artifacts, whiteboards and documents live in the workspace artifact store, not in a session. A session persists only the ids of the artifacts attached to it, so Resume, Save As, Fork and Rewind neither copy nor roll them back. See shared editing for item leases, pending browser drafts, operation retries, and native exports.

Conversation compaction has its own doc in compaction.md. This page describes the session persistence contract that compaction relies on.

A root data buffer owns one live session epoch. Fresh initialization emits SessionStart(startup), restoration emits SessionStart(resume), and the epoch ends with one SessionEnd: kill-buffer when the data buffer is killed, or exit when Emacs exits with the buffer still live. Successful /clear and root compaction start clear and compact context epochs inside that same live epoch; they do not emit SessionEnd. Their hook context is appended as a new snapshot and is consumed by the next accepted root input, except automatic compaction attaches compact-start context to its already-pending request.

A failed fresh initialization retains neither the named data buffer nor its companion view. Buffer-local cleanup hooks unwind any lifecycle state installed before the failure, and retrying the same session name starts from a new buffer. mevedel-chat-discard-buffers owns this cleanup for both fresh initialization and failed browser-requested session creation. It attempts every cleanup hook despite individual errors, then forces remaining buffers closed without queries.

Plan Summary is a root compaction and follows this rotation and context epoch contract, in the planning session for Here and in its Worktree fork for Worktree.

Session identity and display names

Every new root session has a stable ID before it is saved, in the form 2026-07-19T09-47-bf87b5dd793a. Creation checks saved and live identities; materialization refuses to reuse an occupied directory. Explicit display names are independent of that ID. Unnamed sessions initially display the ID, including Conversation and Worktree forks. Chooser labels include IDs so repeated titles remain distinguishable.

First/latest user-message previews use the canonical transcript prompt index for both live saves and restore repair publications. Harness mailbox deliveries (MAIL, RESULT, execution and monitor events) are structural transcript spans, not authored prompts, even though providers receive them in the user role. The chooser and collaboration lobby consume these sidecar previews; naming uses the separately recorded authored input below.

The first accepted model-bound authored prompt starts one asynchronous title request using the naming workload, which defaults to the fast tier. It runs as an isolated text request in a private buffer (a short-lived native conversation for Claude), with no tools, conversation history, skill/file/hook expansion, or context injection. Directive-created sessions use the authored directive text. Local commands, rejected submissions, automatic continuations, retained agents, and transient conversations do not trigger naming.

Naming sends at most 2,000 input characters, requests a descriptive title in the user's language (1–4 words), and enforces at most four whitespace-separated words and a 60-character display limit. These limits do not apply to manual names. The title request inherits the session's streaming choice and collects streamed chunks until it completes, so a backend that rejects a non-streaming request can still answer it. The provider has 30 seconds to reply, counted from submission of the prompt. Provider startup is bounded separately by its own lifecycle (30 seconds for a Claude native conversation), so a slow start does not consume the reply time; the deadline stops when the reply arrives, even if the title then waits for publication. The sidecar's :naming-state is pending, attempted, or explicit. An automatic attempt changes pending to attempted and records its bounded, property-free authored input in :naming-input before inference; a saved consumed attempt is not retried on resume. Only attempted may carry input, and it may be nil when none was recorded, as in migrated sessions. Explicit creation, manual rename, and Save As establish explicit ownership and drop the input. Fork children start pending without input; compaction keeps it.

When the configured naming provider is positively unavailable, the title request falls back once to the root session's own backend and model, with the session's reasoning effort when that model supports it. Unavailability is established only by structured evidence: the provider's own readiness check (stale Codex credentials, a missing Claude runtime), a Claude launch check (missing subscription sign-in or an outdated executable), HTTP 401/403, the provider codes invalid_api_key and model_not_available, or ACP's authentication-required error during startup (its code is also JSON-RPC's generic server error, so it does not count once the prompt was sent). A timeout, cancellation, unusable reply, unknown model or other configuration error, or publication failure never falls back, and nothing falls back to an identical provider. The fallback changes neither the session's provider nor its preset.

M-x mevedel-retry-session-naming repeats a title request from a chat or view buffer, using the recorded input. When no input was recorded it asks for a description; a prefix argument always asks. It never substitutes the session picker's first-message preview or conversation history. Retry replaces a title request in flight, requires session mutation authority, and refuses explicit names and transient conversations.

A successful /clear cancels the old segment's naming operation and persists pending for automatically managed names with the new segment. The existing title remains visible until the next accepted authored prompt supplies its replacement; old history is not naming input. Explicit names survive clear.

A /clear also closes a chat: the run of segments between two clears, while compaction rotates segments within one chat. The sidecar's :chapters records, in the same commit, the closing chat's last segment, its title at that moment (nil under an explicit name, which titles every chat alike) and when it closed; a failed clear records nothing. An untitled chat is labelled by its first prompt. Fork and Rewind keep only the chats closed before their target segment, so a later clear appends in order; Save As keeps all, and restoring an earlier published head restores the record with the rest of its sidecar. The resume picker counts chats rather than segments and names the earlier ones, newest first, from the sidecar alone; it reads no segment files. The collaboration viewer uses these same chat descriptors to group lazy archived-segment disclosures and mark the live conversation. Owner links can rename through Session settings or the lobby; both use the native naming writer, preserve the session ID, and update room titles and connected lobby listings. Compaction does not rearm naming. A failed clear leaves naming eligibility unchanged; failures after a committed transition do not roll it back. Keyboard quit is deferred during clear publication and recovery so it cannot separate a durable commit from its bookkeeping; target I/O can delay cancellation. An explicit name, manual rename, or root-buffer teardown also cancels pending naming. Every terminal attempt is one session-naming telemetry event: renamed on success, otherwise the failure outcome with a categorical :error-class (provider, unavailable, aborted, validation, timeout, cancelled, or the local error symbol). The event covers the whole operation, including a fallback: :fallback t marks one, and the outcome is the last provider's. :trigger is automatic or retry, and a failure adds its :stage (start, startup, or response). cancelled reports an attempt dropped before dispatch because the transport was unavailable; an attempt the user ends by renaming the session, retrying, or closing its root buffer records nothing. Provider failures add the provider's status, structured error type, and error code; provider message text is never recorded. Failures keep the previous title (initially the ID) without interrupting the conversation and leave a session-naming recovery notice. For the host and owners it names each attempted provider, the stage, a bounded cause, and the retry command; an HTTP provider's message text is never shown, as it can echo credentials. Other collaboration readers see only "Automatic session naming failed". A successful title, manual rename, or Save As name clears the notice. Transport and publication scheduling defer metadata work until it can run safely.

mevedel-rename-session accepts readable Unicode names and normalizes whitespace. It updates the persisted name and buffer/view presentation, without moving files. Save As creates a new identity with the explicitly supplied display name.

The closed sidecar schema is v0.5.15, including recovery issues, retained input and pause state, naming input, closed chats (:chapters), the ids of attached store artifacts (:attached-artifacts), and, for an artifact's own conversation, that artifact's id (:dedicated-artifact). Other schemas are rejected by the runtime loader; older sessions are converted only by the explicit migration. Recovery records contain issue messages, never credentials. Every retained-agent registry entry includes :working-directory: nil means no override, while an absolute target-native directory string retains that agent's own working directory without a client-specific TRAMP prefix. Resume qualifies the stored path through the current execution target before rebuilding the live record and its lazy conversation invocation. It does not infer an override from a transcript file's location. Save As copies the override with the retained registry. Conversation and Worktree forks still discard retained-agent identities under ADR 0062. Queued follow-ups retain text, attribution, scope and attachment grants. Live submission objects are excluded. Undelivered steering reloads as requiring review, with automatic delivery paused; a restart never resurrects a submitted request.

Persistence flow

Project-session publication has one authoritative commit point: the published head. Preparing a generation does not make it visible to readers. A failure before head publication leaves the prior head authoritative; a committed head remains published even if later lease normalization needs recovery.

flowchart TD
    A[Writable owner prepares a generation] --> B[Write immutable artifacts and manifest]
    B --> C{Ownership and candidate valid?}
    C -- No --> D[Refuse publication and retain prior head]
    C -- Yes --> E[Commit new published head]
    E --> F[Readers validate referenced manifest and bytes]
    F --> G[Restore data and render the view]

File-backed sessions use the separate PID-lock profile described below. File snapshots are captured at mutation boundaries; saving persists that evidence. Compaction, Rewind, and Fork use the persistence contracts described in their sections.

Session persistence

Persistence is split by ownership. mevedel-session-codec.el owns the closed sidecar schema and validation; mevedel-session-checkpoint.el owns the committed root projection, per-conversation mailbox consumption, and deferred ToolCall retirement. mevedel-session-artifacts.el owns paths, artifacts, snapshots, and segment writes; mevedel-session-rewind.el owns restore plans and the Rewind transaction; and mevedel-session-fork.el owns Fork/Worktree projection and publication. mevedel-session-naming.el owns display names and background title requests. mevedel-session-collection.el owns idle publication scans and their session lifetime. mevedel-session-persistence.el remains the lifecycle, resume, listing, locking, and cleanup facade used by callers.

Sessions auto-save lazily and at turn settlement under <workspace-root>/.mevedel/sessions/<timestamp>-<12-hex-suffix>/. Ordinary model turns and awaited fork-skill turns share one successful-turn transaction. It advances the turn, records the token baseline, saves before request teardown, runs Stop, ends request-local policy scope and the request, and schedules queued follow-up delivery. Error and abort settlement also save before request teardown, preserving partial responses and request-local file checkpoints. mevedel-abort additionally saves from cleanup that runs even if cancellation signals an error or quit. Opening, auto-saving, aborting, or closing a fresh zero-byte conversation does not materialize it. Partial first-turn text and already materialized sessions remain eligible for saving, including a previously saved session rewound empty.

When transport or event-loop breaks defer final-patch generation or settlement, the data buffer remains busy until that terminal continuation completes or is cancelled. Abort stops the provider but preserves the request reservation and file snapshots needed by already pending settlement. The pending holds nest and release on failure as well as success. Repeated or obsolete callbacks cannot tear down a replacement request; captured patch and directive-attempt evidence belongs to the old request, not the new request's active presentation.

Emacs's native auto-save-hook checkpoints modified root and retained-agent data buffers through their respective persistence writers. It follows Emacs's normal auto-save scheduling, queues one coalesced opportunity per buffer, and allows input between saves. Emacs auto-saves only after keyboard input, so while any root or agent request is in flight a repeating timer runs the same checkpoint every mevedel-session-checkpoint-interval seconds (default 30); request admission starts it, and it stops on the first tick that finds no request in flight. Unattended Goal and agent turns therefore checkpoint mid-turn too; nil leaves mid-turn checkpoints to Emacs auto-save alone. Each opportunity waits for transport availability and pending input; settlement or an explicit save can absorb a queued checkpoint. Exit cancels queued opportunities and flushes synchronously. Failed writes remain eligible for retry even when the transcript write succeeded and only the sidecar failed. Views and read-only inspection buffers do not write. Project-agent checkpoints commit the sidecar along with retained transcript writes in one publication so resume can see them; root auto-save also commits outstanding batches even when its text is unmodified. These checkpoints preserve unfinished text without marking a turn completed.

The 30-second timer is not a minimum interval between all saves: native Emacs auto-save adds opportunities according to its own input/idle settings. Retained agents also debounce saves after tool-result cycles by mevedel-agent-conversation-save-debounce (default 2 seconds of idleness), cancelling an earlier pending timer. These sources share modified-state rechecks, and the warm-local checkpoint path permits at most one accepted job per buffer; there is no fixed global maximum checkpoint frequency. The timer and debounce values above are defaults, not a measurement of a running session.

Checkpoint preparation retains derived transcript structure, response bounds, prompt units and audit spans per data buffer. Each consumer owns a change frontier established by paired modification hooks; unchanged prefixes are reused, and known leading metadata writes rebase body-relative coordinates. An unobserved edit, changed source or restriction, reordered hooks, or an overlay with modification hooks invalidates the relevant proof rather than trusting cached text. Cloned buffers do not share proof ownership. Incomplete controls and block-depth lookahead also prevent reuse across their unresolved dependency. These are disposable computation caches, not durable state or lease authority. Generated transcript storage disables font-lock and JIT fontification: the rendered view owns normal presentation, and JIT's silent fontified writes would invalidate otherwise valid change proofs after each edit. Raw transcript inspection (mevedel-view-toggle-transcript or switching buffers) and the temporary data-buffer display behind gptel-menu remain unfontified. Enabling gptel on storage also unregisters its Org JIT annotator and disables JIT again, because that registration otherwise turns JIT back on during restore/activation. The stream bridge announces gptel's silent string insertions and finishes their proof before post-stream observers run. An arbitrary transformer or observer that edits the source still invalidates the proof; no hook allowlist exempts foreign edits. Foreign code that redirects point or the current buffer also invalidates the proof, since gptel inserts at the location its transformer leaves behind. The writer still freezes the complete transcript, sidecar and checkpoint candidate together, with the same success-only adoption and modified-tick check. Synchronous saves freeze encoded bytes; automatic warm-local root checkpoints capture native characters and encode in the later staging phase described below. Large lines, unresolved controls, full payload encoding and garbage collection can still make a phase exceed an event-loop budget; asynchronous target publication does not itself bound this main-thread work.

Committed conversation checkpoints

Live session state may advance before its corresponding transcript is durable. Ordinary sidecar-only writes therefore use the last committed root projection for :current-segment, :total-turn-count, :prompt-index, :file-snapshots, :chapters, :first-user-message, and :latest-user-message. Agent activity, mail arrival, or another metadata update cannot publish a newer root turn or segment without its matching transcript. The committed first-message preview is owned by this same projection, not a second cache or a sidecar readback.

Mailbox records have durable identities, independent of their payload text. Delivery removes records from the live root or retained-agent mailbox, but the checkpoint owner retains them for that receiving conversation until its transcript commits. Ordinary sidecars merge those pending records back into the persisted mailbox. Consumption itself does not synchronously save the transcript or add an acknowledgement publication. Canonical hidden delivery receipts let restore suppress an already represented record without confusing two different messages with identical text.

A writer freezes transcript bytes together with an opaque checkpoint candidate: the matching root projection, that conversation's captured consumption IDs, and its deferred ToolCall retirement IDs. Only that candidate's IDs may be excluded from its sidecar. Mail delivered after the freeze remains pending for a later checkpoint. Candidates travel with staged sidecar artifacts through queueing, failure, and retry; serialization or a queued save's return does not install them. Project sessions install the candidate only when its actual published head commits. File-backed sessions install after the matching fixed transcript and sidecar writes succeed. Restore and committed-state adoption seed a new checkpoint epoch, invalidating candidates from the previous owner state.

Before a queued sidecar commits, its frozen data is projected against the current committed root, without rereading advanced live fields. This prevents an older metadata-only or agent save from rolling back a predecessor's root checkpoint. During outstanding publication work, bounded retirement proofs also prevent stale queued sidecars from restoring consumed mail or retired ToolCalls after a rotation has archived their receipts or tool rows. Proofs survive failed publication and retry, and are released when no active, queued, staged, or recoverable publication remains; they are not session-lifetime identity history.

Root, ordinary retained-agent, and terminal retained-agent writers mark a buffer clean only if its full modification tick still matches the freeze. Text, mail or property-only gptel changes made during publication stay modified, so the next save cannot retire a mailbox record without persisting its transcript or silently omit newly changed response bounds.

Terminal retained-agent state and its RESULT remain tentative while the final checkpoint is prepared. Mail delivery and native continuation paths cannot consume or inject that provisional result. A pre-commit failure keeps the prior agent activity and transcript authoritative, without leaking the tentative result into a later root checkpoint.

ToolCall audit checkpoints identify their owning root or retained-agent conversation. A settled checkpoint may be removed from live state only when its canonical tool row already exists in that conversation's transcript; the owner retains the full row until the matching transcript checkpoint commits. Cold recovery inserts missing rows only into their owning conversation and never resumes an interpreter continuation. ToolCall start and settlement updates remain durable sidecar writes, rather than transcript-retirement events.

Closing a root data buffer saves it before releasing session ownership. On Emacs exit, retained-agent transcripts (including pending debounced saves) flush before root session state and before ownership is released. A failed save or background cleanup does not skip other buffers; failed saves produce a warning. A forced process kill or power loss cannot run exit hooks: recovery is limited to the last successful checkpoint. Unavailable or unwritable storage can still prevent saving.

Layout:

.mevedel/workspace-id                 ; project-owned portable workspace identity
.mevedel/sessions/2026-04-23T14-30-a9f23d74c019/
  session.meta.el                    ; non-authoritative current sidecar cache
  .lease/                            ; project-session portable lease generations
    00000000000000000001.el         ; current renewable ownership record
  .publications/
    generation-a1b2c3/
      000001.data                   ; immutable artifact bytes
      manifest.el                  ; logical path -> bytes + SHA-256
  .recovery/                       ; target-side project recovery only
    recovery-a1b2c3...el            ; marker, written after recovery bytes
    a1b2c3.../                      ; manual repair material
  segment-0001.chat.org              ; logical name: finalized at compact #1
  segment-0002.chat.org              ; logical name: finalized at compact #2
  segment-0003.chat.org              ; logical name: current/live
  hook-log.el                        ; one hook execution plist per line
  permission-log.el                  ; permission/request diagnostic plists
  repair-log.el                      ; redacted tool-input validation telemetry
  telemetry-log.el                   ; correlated lifecycle events, one plist/line
  diagnostics/run-*/                 ; resource reports; local sessions only
  file-history/                      ; optional materialized backup cache
    <sha256-of-raw-file-contents>
  local/                              ; lazy session-owned shared resources
    plans/current.md                 ; mutable Plan draft/proposal
    plans/accepted-*.md              ; immutable accepted plans
  tool-results/                      ; retained oversized tool output and media
    executions/*.log[.gz]             ; logical execution output, raw or gzip
  agents/                            ; logical agent transcripts; physical
                                     ; numbered compaction recovery archives

Project sessions use the portable authority profile on both local and TRAMP targets: .lease/ and immutable .publications/ are authoritative, while the fixed sidecar is a discovery cache. Root and canonical agent transcripts, file history, and instruction snapshots exist as logical manifest entries without an additional fixed copy. Old fixed transcripts are ignored, including their modification times; this change does not delete them. Numbered agent compaction archives still receive a physical recovery copy before the live buffer is rewritten. File-workspace sessions use the separate pid-lock profile and .lock; they do not create a lease or publication tree. A session directory containing both control artifacts, or a persisted authority profile that disagrees with its workspace category, is rejected rather than guessed.

Retired local execution output may use lossless gzip storage behind stable artifact://executions/*.log handles. Compression runs outside the editor and does not rewrite old sessions. Save As copies retained gzip evidence just like raw evidence; Fork continues to exclude execution history, and cooperative ownership transfer leaves the same stored evidence in place. A receiving TRAMP client reads compressed output through the same resource-storage seam, not through a persistent inflated cache. This representation is retained output, not a replacement authority for portable session state or published artifacts. See execution output storage and artifact addresses.

The data buffer is locked to bare org-mode so gptel-org--save-state can round-trip text-property bounds via GPTEL_BOUNDS. Entering the mode suppresses all major-mode hooks, Org startup UI, global-minor-mode attachment, and Local Variables processing. The transcript is generated authoritative storage rather than a normal Org editing surface, and model output must not activate project or user configuration. The companion view owns presentation. The sidecar holds session-wide state that doesn't live in the buffer text: permission rules, exact or recursive session resource grants, tasks, prompt-index (driving the rewind picker and latest resume preview), :file-snapshots (per-turn pre-turn checkpoints of tracked files, including absent markers and known capture gaps; post-turn copies remain alongside them for historical Fork), workspace identity, :working-directory, fork lineage (:fork-type, :forked-from-session-id, :forked-from-turn, and :forked-from-fork-point-id), and Worktree Fork origin (:worktree-source-root, :worktree-directory, :worktree-branch, and :worktree-base-commit), and :agent-transcripts presentation metadata and the explicit :agent-registry containing retained paths, frozen configurations, activity, mailboxes, and conversation locations. It records only the selected :preset-name; resume rebuilds mevedel variables from that currently registered trusted preset, so sidecar data cannot name or populate buffer locals. It also records the session's baseline :model-provider and explicit :reasoning-effort, independently of implementation overrides, and the durable ToolCall audit checkpoint. The checkpoint contains the ToolCall call and bounded child audit, never an interpreter continuation. A Session Fork also copies the source session's permission mode, sandbox mode, session permission rules, and resource grants at the fork point. Parent and child then diverge independently. Other gptel buffer-local settings are request-time state only; mevedel does not persist them as Org properties. An Agent summary selection is persisted only in the child transcript as a labelled <task-background> block before the authoritative Agent Task. The parent sidecar and tool result retain only provider/model/effort metadata, not the generated summary text.

Selecting a session model clears retained root model-unavailable failures, including failures reported by request dispatch or native readiness. The next send validates the selected provider normally. Other recovery holds and the pending-input failure pause remain until their own recovery actions resolve them.

External conversation references

Every Claude launch checks the ACP client, Node.js, Python, CLI and adapter versions and the subscription login while the adapter starts; the connection is admitted, and its prompt sent, only after both succeed (overlapping them took a warm launch from about 500 to 380 ms); M-x mevedel-claude-code-setup runs the same check, plus the packaged MCP bridge, without a model request. Installation, maintenance and user steps are in the README. Status commands run asynchronously, each with a ten-second timeout, and are cancelled with their owning request. Successful version checks are reused while the resolved executable and its enclosing package metadata stay unchanged. Every launch still checks the login through claude auth status --json with inherited API billing routes removed, and accepts only a first-party claude.ai Pro, Max, Team or Enterprise subscription; authentication is never cached. A logged-out status exits nonzero with valid JSON and still reports the login action.

Composer sends check provider readiness before the prompt enters the transcript. Codex credentials are checked directly. Claude readiness is learned from root turn startup and never refuses a send: its own launch is the check, and the next send is the retry. A startup that failed before its prompt shows the cause in the echo area and as an informational recovery issue; a browser retry checks without a prompt and reports when Claude is ready. Changing the executable, adapter, directory, model or CLAUDE_CONFIG_DIR, and login or runtime changes, forget the result.

Failures after request admission but before ACP startup, including an invalid tool roster, use the ordinary failed-turn settlement. They record the cause, release the busy state and sleep inhibitor, and leave the next send available as an explicit retry; they do not launch the provider or replay the prompt.

The Claude Code provider routes root and directive submissions through ACP. The session's :external-conversations metadata maps conversation scopes to the engine, native conversation ID, local installation directory, machine name and last known state. The installed Claude agent retains model history; mevedel retains the canonical transcript and tool effects. No credentials or process handles are stored in the sidecar. A prompt the Claude CLI refuses because another process holds its credential refresh lock is re-sent once after 30 seconds, provided it reported no tool call; every other prompt failure ends the turn.

Sonnet, Opus, Fable and Haiku aliases remain available and resolve through Claude's current catalog. Each ACP session initialization refreshes an in-memory model list from the adapter's configOptions, before any prompt is sent. Additional reported models appear in the normal picker. The selected model's effort menu uses the reported levels. Before connection, Sonnet, Opus, Fable and Haiku offer low, medium, high, xhigh and max from Claude's documented alias capabilities. Other configured or persisted model IDs resolve with staged effort symbols without joining the catalog; dispatch rejects any ID the session's catalog omits. Collaborators can pick only offered model labels. Startup applies the selected level through session/set_config_option and waits for acknowledgement before prompt dispatch. The adapter reads the user's Claude settings for its initial permission mode regardless of the isolated setting sources; startup resets any other reported mode to default the same way first, since mevedel's own pipeline authorizes every tool call. Unsupported effort falls back to Claude's native default, with a visible notice and a reset of the still-matching session selection. Missing models, an adapter selecting a different model, or failure to apply the selected effort fail visibly before the prompt. Model capabilities belong to the Claude backend and do not overwrite gptel API model metadata. A compatible model change resumes the same native history with the newly selected model.

Claude submissions expand mentions through the shared permission and resource resolver. PNG, JPEG, GIF and WebP attachments become native ACP image blocks; selected gptel media contexts use the same path. Repeated references to the same image in one submission send its bytes once. PDF and audio input are not advertised by this provider. An ACP peer without image capability rejects image submission before dispatch.

Selected text files and buffer regions use gptel's context collector and gptel-context-string-function, including its asynchronous callback form. gptel-use-context keeps its system/user placement meaning. System placement joins the native system baseline; user placement is delivered and restored as described in native context delivery. The authored system prompt is evaluated before asynchronous formatting so later editor configuration changes cannot alter that submitted prompt.

The admitted ACP turn owns the preparation wait. Interruption during this wait settles it before any native process starts. Late or repeated callbacks cannot start another process or change the next composer draft. A formatter returning a value other than text or nil fails the turn before launch.

An admitted first root turn records native ownership as unstarted until the agent returns a real conversation ID. This state has no installed history. Aborted preparation or failed startup can therefore be retried, including after save/reopen, by starting a fresh native conversation.

Attachment deduplication is committed only at the prompt receipt described in native context delivery. Canonical transcripts keep the submitted references and delivery reminders, without base64 blobs. Directive and child inputs expand independently of root mention history; a child follow-up can attach the image again without marking it as delivered to the root.

Before dispatch, the native ID and in-flight state are published while the session transport is idle. A successful terminal response records ready; failure or interruption records uncertain. Restore also converts a saved in-flight record to uncertain. Opening a session does not start its external process. The next root submission creates a process and resumes the stored root ID. Each directive request instead starts an isolated native conversation from its existing selected prompt: complete local discussion, selected attempt, planning feedback, or the implementation handoff. Resuming a previous hidden native history would reintroduce evidence that these prompts deliberately exclude. Directive identities are therefore never retained.

An uncertain conversation receives the same current-state reconciliation guidance as an interrupted native request, plus an explicit no-replay instruction, in its next prompt. For the root it is a shared pending event, so enabled configured reminders do not add a second copy. Like other prompt deliveries it must be acknowledged before further tools or successful settlement. It establishes delivery of recovery guidance, not proof that every prior effect has been inspected or undone.

Editing earlier response or prompt text in a raw root or retained child transcript marks its native history as diverged. This state survives saving and reopening; sending another Claude turn refuses before prompt dispatch and names mevedel-claude-code-recover-history as the next action. Explicit recovery detaches the native history and continues from the edited, labelled excerpt. Excerpt recovery includes the effective compaction summary once, read from its authoritative root or child bounds. Editing summary text cannot hide it behind the wrapper's incidental gptel properties; replaced older archives remain available for inspection rather than being replayed as current context. A persisted submitted-input boundary also protects prompts interrupted before any model output. Editing the unsent draft after that boundary and any later response does not mark divergence. Each child stores this boundary relative to its private transcript body, with segment identity zero; native child compaction rebases it to the retained summary and tail. Root boundaries use the numbered root segment. Editing a child does not mark the root or another child divergent.

A native call identity is admitted once per turn; a repeated identity in the same turn is rejected before the pipeline. Call identities are not persisted. New calls remain subject to ordinary permissions and current-state reconciliation; matching tool arguments or text alone is not evidence that two calls are the same event. No pending approval or operating-system process is resurrected by these records.

Native root and child turns offer the same Plan and Goal tool visibility as gptel requests; the fixed native roster's Goal tools are described under Goal tools. Calls still face current authority.

Public abort retains the existing settlement reservation until ACP acknowledges cancellation or its timeout ends the connection. This preserves the partial transcript, terminal accounting and uncertain history marker before admission is released for another request.

Before a directive transcript boundary opens, its complete prior transcript is published; terminal settlement publishes the closed directive frame and durable activity together. This works for both portable and PID-lock sessions. Root history guards consult root references and completed ordinary prompt entries, not the count of shared directive turns. A directive provider override does not switch the root engine. An installation/machine mismatch or missing native history prevents continuation while leaving the transcript readable.

M-x mevedel-claude-code-recover-history explicitly detaches an unavailable root or retained-child native reference. It requires mutation authority, a writable session, no active root or child turns, and no active Goal. It persists the detachment before returning; a failed publication restores the previous reference and context acknowledgements in memory. Uncertain target publication still requires the ordinary publication recovery before new work. Transcripts, tool effects and chronology remain intact. No native process or Goal continuation starts from this command. The next normal root send or child follow-up starts a new native conversation from labelled current-transcript evidence, never an exact resume. Until the replacement native identity is published, a failed startup keeps that excerpt continuation for the retry. Child recovery uses only that child's private conversation; directive prompts already reconstruct their selected context on every turn and need no recovery selection. Failed native startup retains the original diagnosis and names this command as an option when history is unavailable.

Same-machine editor restart preserves the root provider and each retained child's native identity, frozen configuration and transcript. Opening the restored session starts no native process; root submission and explicit child follow-up resume their respective histories. The ordinary Goal restore rule still applies: a saved active Goal becomes paused. Learned path-instruction scopes survive reopening for native conversations. Each prompt re-sends only instructions whose current contents differ from their acknowledged hash, including explicit withdrawals of removed files; retained native history holds the rest. Opening the session acknowledges nothing.

The ordinary model picker can change root engines. Selection alone keeps the native reference intact. Sending through gptel detaches and persists that reference before dispatch; gptel reconstructs the current segment through its normal transcript serializer. Native reasoning remains readable in the transcript but is not replayed as signed API thinking. Archived segments are not flattened back into the request.

Sending through Claude after local history starts a new native conversation with a labelled excerpt continuation. The excerpt uses current-segment evidence, excludes independent directive and shared-item turns, and marks shortened tool output. Historical calls are evidence, not a replay queue. Subsequent Claude turns resume the new native ID. This also applies when a request-only workload policy changes engines while the saved selection remains different.

Engine transitions invalidate root mention deduplication and path-instruction acknowledgements. Known paths remain persisted with a nil hash until their complete current content reaches the new engine's receipt boundary. This keeps nested instructions available across startup failure and reopen. Child acknowledgements remain independent. Current guidance and memory use the normal gptel payload rule or Claude system/prompt receipt, not the previous engine's acknowledgements.

A session retaining a native identity in its root or any child conversation rejects cooperative control transfer, Save As, Fork, Rewind and Redo. Manual transcript compaction and the side-conversation command consider only the root conversation, and are also unavailable with the external provider selected. Unstarted and released markers hold no native history and restrict nothing. These commands cannot treat a copied transcript as an equivalent native model history. The adapter's native fork extension alone does not map mevedel's chronological checkpoints and independent conversation scopes to native message boundaries. Direct commands and menu actions report the external-history restriction before arming a fork or changing state. Save As refuses before asking for a name or saving the parent, and Redo refuses before reading the published-head choices. Cooperative control requests refuse before creating a request; an owner rejects requests from a client with stale native history information instead of automatically granting them. Direct acquisition of an unheld or expired lease preserves native identities just as ordinary resume does. The next send checks the native installation and history, so recovery after an Emacs crash does not require copying or discarding native history. Read-only following remains available.

The gptel HTTP request-controls menu is unavailable for a selected external provider, including direct gptel-menu, prefixed send and mevedel's bridge menu. It refuses before opening the transient or changing bridge return state, and points to mevedel's model and tools menus. Model and reported effort choices remain supported there. Temperature and output-limit controls in gptel's HTTP menu do not configure Claude's native loop. Sending from a busy Claude composer queues steering for the running turn, as on HTTP providers; explicit queued follow-ups and interruption remain available. Steering reaches the native turn at its next tool batch, or as a further prompt in the same turn when the native prompt ends first (see Native context delivery). A native turn cannot wait at a held boundary: steering still held by the Pending Inputs cockpit or an unresolved interaction when the turn succeeds becomes the first follow-ups, in submission order. A failed or interrupted turn marks it Needs review, as on HTTP.

Native context delivery

Each launched Claude turn puts the recipient's complete selected observations into the native system prompt and disables the SDK's first-prompt snapshot, so a resumed launch uses the newly composed baseline. Everything else reaches native history through one of two receipt boundaries. Transmission, ACP send completion, a hook preview or an offloaded file never counts as delivery.

  • Prompt. A submitted prompt carries the user's text and images, configured reminders collected once with pending root hook context, the initial direct-child roster, queued mail in full, recovery guidance for an uncertain history, path instructions whose current contents differ from their acknowledged hash, and user-placement selected context. User-placement text and its selected images are sent once per native conversation and content hash; a changed selection or new conversation sends them again. A block is received only when the SDK's top-level user-message echo for that conversation contains its exact text; images also need one echoed image each. Claude Code resizes and may re-encode an image beyond its dimension or size limits before echoing it, so an echoed image is matched by presence, not by bytes or MIME type. Text receipts alone do not establish image delivery.
  • Tool-batch hook. After each native tool batch, a PostToolBatch hook carries selected observations changed since the last accepted snapshot, queued turn events, newly available direct children, the root turn's deliverable steering and mail. It is received only by a successful SDK hook_response for that event whose output matches exactly. A receipt can trail the next tool boundary; that boundary leaves its updates queued.

Receipt runs the captured commits, such as reminder firing marks, mention deduplication and roster marks, and consumes only the captured events and mail; items queued or replaced meanwhile stay pending. A unique correlation marker exists only on the wire. The recipient's transcript records the accepted batch once, as the shared injection record followed by any delivered mail blocks. Receipt establishes SDK acceptance into conversation context, not model understanding or retention through later compaction. Unreceived mail stays unread; consumed mail remains in the persisted mailbox until a matching receiving-transcript checkpoint commits its removal, so process loss before that commit can redeliver mail without losing its only copy. Except for a mail-only hook batch, an unreceived delivery fails the next tool call and turns a successful terminal response into an error.

Root steering joins a delivery only while it is not held; a turn that ends at a boundary takes none. Each entry is expanded like an HTTP steering prompt and sent after a fixed header telling the model to treat it as direct user input, because hook context is not a user message. Receipt consumes the entry, commits its mention, skill and prompt-submission effects, and records its transcript text and hook audits as user input; the header stays on the wire. Steering left when a native prompt ends successfully, with no continuation pending, is sent as a further prompt in the same turn and received by its exact echo, so the turn settles once. Steering that carries images cannot ride hook text: it takes the continuation prompt below at its tool batch, with the images after the body text. Receipt then also requires at least one echoed image per steering image; the echo of the text alone delivers nothing.

Claude's inline hook output limit is 10,000 UTF-16 code units. A hook update beyond it, including a single mail message, or one carrying steering images, stops the native prompt at that boundary. The same admitted turn then sends a continuation prompt containing the complete captured body and requires its exact echo before more tools. Native compaction's SessionStart hook cannot stop the prompt, so while a continuation is pending a PreToolUse hook denies any attempted call and stops the prompt; a stop alone does not prevent the call. The conversation, MCP endpoint, transcript and per-turn call identities stay the same; usage accumulates across prompts (Goals) and the turn settles once. Pause, a user boundary stop and cancellation take precedence: they discard the unsent continuation while its underlying events stay queued. A failed native prompt is not retried.

Native compaction keeps the system baseline. A SessionStart(compact) hook restores, through the hook receipt, the observations that differ from that baseline (including updates acknowledged before compaction), user-placement selected text, the full direct-child roster, a child's current sample-limit guidance, active root Plan guidance, eligible accepted-plan references and the current contents of path instructions already learned by that conversation, broad to narrow with local overrides last. Removed files explicitly withdraw their guidance. Compaction clears the selected-context mark, so the next prompt re-sends selected images. Root, retained children, fresh directives and isolated shared-item turns keep separate path-instruction acknowledgements; a child never imports sibling or root scopes. Fresh directive and shared-item turns also expand mentions independently of the root's delivery history and do not acknowledge mention delivery for the root. An oversized restoration takes the continuation prompt above. Isolated shared-item turns leave root mailbox messages queued for the next root turn rather than consuming them into an unrelated item conversation. Retained-agent sample accounting at these boundaries is described under agent turn limits.

Session-owned local state

local/ is created lazily when the first durable write to a session-owned work:// descendant succeeds. It is shared by the root and its retained-agent tree, survives save, resume, and rename, and is removed when its owning session is cleaned up. A Conversation or Worktree Fork copies it into independent child state; Rewind leaves it unchanged. Local files are not workspace snapshots, touched files, instruction discovery, LSP inputs, directive patch captures, or Git summary inputs. An ephemeral request without durable session ownership cannot create or mutate it.

local/plans/ is the shared durable plan namespace. The parent and retained agents use work://plans/... for current and accepted plans. Working notes, findings, contracts, and handoffs default to workspace-owned work://shared/, outside session persistence. These shared files survive session cleanup and are not copied by Fork; sessions in the same workspace use the same files. Separate workspace roots have separate shared areas. Shared files are temporary: memory consolidation removes them once their knowledge is extracted. Accepted archives use canonical accepted-TIMESTAMP.md names, so every managed plan is addressable.

local/plans/ is the one exception to plain Fork copying, because it is managed plan state rather than free-form local content. A Fork drops the copied local/plans/ subtree and re-adds only the accepted artifact that was already accepted at the fork point, after re-verifying its recorded hash. The child therefore starts without an inherited current-plan draft.

Session-owned work:// descendants, artifact://, agent://, and history:// addresses belong to the session's execution target. Client-local skill and memory roots retain their origin, while MCP addresses use the current configured connection; no resource address changes a session's target. See address-to-resource.md.

The workspace identity is one opaque 64-character lowercase hexadecimal value stored in .mevedel/workspace-id when the first session is materialized. The sidecar's nested :workspace record stores that :workspace-id together with :target-native-root; the top-level and per-agent :working-directory fields are target-native as well. None stores a client-specific TRAMP prefix or the process-local workspace registry id. Resume binds the record to the currently opened workspace, verifies its project-owned identity, maps saved target-native paths to the current target root, and constructs a fresh immutable execution target. A missing identity is an error. An identity mismatch requires explicit confirmation; declining aborts resume, while accepting binds the conversation to the opened workspace and discards copied session permission rules, resource grants, and additional roots. The next save records the opened workspace's identity. Superseded sidecar shapes are not migrated during resume; the explicit migration runs separately.

The package release is 0.5.0; its persisted session format is independently v0.5.15. The top-level :authority-mode, :ptc-checkpoints, and execution-target incarnation are required by that session format: project sessions persist portable, while file-workspace sessions persist pid-lock. Portable project sessions always persist a non-empty :target-incarnation, including local sessions; the local and TRAMP probes use one canonical boot-id, machine-id, PID-1 start, and hostname payload, without client-specific TRAMP spelling or Emacs build details. Resume restores the persisted baseline and compares it with the fresh observation before readiness and authority admission, so a reboot or replacement target cannot inherit old grants. A replacement probe stages the observation without changing that durable baseline. The fence then runs as one transaction: it first revokes exact session grants, then commits a sidecar marker containing the new incarnation and no exact session grants; only that successful commit acknowledges the replacement. The workspace store in .mevedel/permissions.el is outside this contract: it is deliberate configuration shared through version control between machines, so a reboot or a different host leaves it untouched, and a grant there that does not apply on the current target matches nothing. Resume with an acquired lease runs that transaction immediately, so a restored session already carries the acknowledged new incarnation; request admission runs it for replacements observed later. Publication failure leaves admission blocked and retryable.

Admission observes the fingerprint with one target command rather than a full readiness probe. Environment, capabilities, and sandbox facts are fixed for the life of a connection, so they are probed when the session opens, when the connection is replaced, and on mevedel-retry-target-readiness. A failed observation falls back to the full probe, which settles the blocked readiness that admission reports.

The full probe also reports the target's container, VM or WSL kind from /.dockerenv, /run/.containerenv, /proc/version and, when installed, systemd-detect-virt. It is one extra shell command per probe, cached with readiness, so the system prompt's <env> block shows a Virtualization: line for local and remote targets without probing per request. A failed or unexpected answer omits the line and never blocks readiness.

Durable mutation's incarnation fence checks target identity and reachability, not the current mode's child-confinement requirement. This lets a session leave Edits when remote confinement is unavailable without skipping ownership or replacement checks. Request and child-execution admission still enforce the selected mode's confinement requirement. The fence reuses the cached confinement mode so an unchanged connection does not alternate between full readiness probes. Unavailable child confinement still requires a fresh identity observation; other readiness failures remain blocking.

A lost settlement records the target process-group identity, so a mutating request re-proves that group against the target before it is refused: an affirmative dead clears the block with no user action, while live, unreachable, or ambiguous keeps it. If the transport is already active, the request stays blocked rather than nesting a target probe; the next mutation attempt retries the proof. The durable latch restored across a restart carries no process identity and still needs mevedel-retry-target-readiness, which every refusal names.

The unsettled-mutation latch is ADR 0098; the one portable authority profile is ADR 0100.

Portable control roots are physical target directories, not ordinary pathname-checked caches. Lease generations, transfer requests and fences, and specialized recovery markers are read and mutated through a pinned target-side parent descriptor; symlinked roots or final entries fail closed. Each control operation opens its parent directory, proves the opened directory is the requested physical path, and then works only through that descriptor.

One target process carries a program of such operations rather than a single one. Every operation in a program opens and re-proves its own parent, so a program is exactly as pinned as the same operations run one at a time, while costing one round trip instead of one per operation. A program stops at the first operation that does not succeed and reports the rest as skipped, which is what lets a caller state a precondition as a verify operation its writes depend on: the proof and the write it guards share one process, which narrows the window between them to two adjacent syscall sequences rather than a network round trip. It does not remove the window -- another client can exclusively create the next generation in between -- so a lease commit decides who won from what it observes after its write, and exclusive creation is the generation-election primitive. Callers that need a guarded critical section can supply a physical lock directory; target-side flock then serializes that program against other users of the same lock. Journal and memory mutation use this separate boundary. Each operation reports ok, conflict, absent, mismatch, failed or skipped, so a caller reproduces the per-operation nil-versus-signal contract of the single-operation wrappers. For two to 32 independent unbounded reads, the same control program can carry regular files together in a GNU tar stream. It keeps every proved parent descriptor open, refuses symlink leaves, and disables inherited TAR_OPTIONS. Emacs decodes the stream without extracting files and requires the expected ordered regular members, valid header checksums and successful transfer status. The target streams encoded bytes without buffering the archive in a shell variable. Unavailable or unsuccessful transfer and rejected archives are discarded and retried with fresh ordinary reads in a second target process; it never supplies partial success to the caller. Bounded reads and mutations retain their ordinary execution path. Source byte/hash checks still belong to their existing callers. Content payloads and listings travel base64-encoded inside a NUL-framed request and response, because filenames and content both contain bytes a shell cannot pass through a command substitution literally. Numeric request fields travel as digit strings and are validated on the target without a decoding subprocess. Oversized stdin requests include each encoded field's byte length. Required writes that use a temporary destination stream large payloads through dd and base64, then require the trailing framing byte before rename or link commits them. Client framing joins the encoded fields once, without first copying each large payload into a separate operation string. Optional operations and plain append decode their field before execution. The operation decoder owns its shell variables, avoiding repeated large copies through shell function argument lists. Parent proofs, per-operation results, and failed-precondition ordering remain the same for both request carriers. An operation marked optional does not end its program, which is how ensuring a directory that may already exist shares a round trip with the write that needs it. Target diagnostics are encoded through a separate pipe, so a tool writing to stderr cannot present itself as a result.

Control programs combine related proofs, reads, and mutations in one dispatch. Interpreter paths are cached per target; whole-process failure invalidates them for a later lookup, while an operation-level refusal does not. ASCII request fields travel as arguments when shell-quoted physical lines fit 3 KiB, individual fields fit 96 KiB, and the total fits 512 KiB. Wrapped payloads can occupy several short lines. Fields whose raw byte length already exceeds the individual bound select a stdin carrier before ASCII classification or shell quoting; quoting cannot make them fit. Larger requests or non-ASCII fields travel as UTF-8 bytes on stdin. On targets that support direct-async spawns (single-hop ssh and scp, with mevedel-transport-remote-direct-async enabled), a private channel receives the script and then the request, costing no command on the shared connection. Elsewhere, an input file carries the request, and TRAMP copies it to the target at about twenty commands. The carrier changes without splitting the program. See ADR 0101 for these bounds and their transport rationale. Reads never create control directories: an absent transfer mailbox holds no requests, so a polling observer performs no target mutation. Publication proves lease ownership immediately before every artifact write and once after the last one; those proofs may share a control program. A proof counts for the next write only while no target write came between: the reservation that opens a publication proves ownership for its first fixed write, and a batch that commits proves it after its last write through the head commit's exact-generation check, with only the immutable generation written in between. Those adjacent steps share one program. The reservation's program continues with the batch's first fixed write, behind a verify-latest that stops it when a newer lease generation exists; the publisher then decides from the listing and writes in its own program, as before. The head commit's program opens with the immutable generation and its manifest, so a taken generation name stops it before the lease record is touched and another name is tried. A committing sidecar is read back from its private local staging copy, not through a target program, and the local target's hostname comes from /proc. A local sidecar publication thereby takes three programs instead of six. Enumerating a workspace's sessions obeys the same budget. Every candidate directory's control artifacts are probed in one program and every lease directory is listed in one more, and both observations are then shared with the reads they feed: a candidate proved free of a PID lock is recorded in the durable transaction that spans the listing, so the assertions each publication read repeats cost nothing, and each listing is handed to that read in place of its own listing round trip. Lease records, manifests, and sidecars are then read in bounded batches rather than separate programs for each session. Capturing a manifest canonicalizes its session root once and qualifies each validated relative artifact name beneath it. It does not repeatedly canonicalize the shared directories for every artifact. Actual reads still prove physical containment on the target and verify the required artifact digests. Portable lease expiry and transfer deadlines use the target filesystem clock at whole-second resolution, so clients with skewed wall clocks cannot change one another's authority and lease durations are configured in whole seconds. A lease observation reads that clock in the same process as the records it inspects, and one durable transaction may answer from a reading it already took for that control directory for at most one second before taking another. Nested transactions keep separate readings for different control directories, so one target's clock never supplies another target's deadline. Local elapsed time only decides when to read the target again and never becomes a time value, so no deadline is ever derived from a client clock. A client reclaims its own expired lease without a prompt, because renewal cannot run inside blocking target I/O and confirming a takeover from oneself is meaningless; the exclusive creation of the next generation still refuses the reclaim when another client claimed the lease meanwhile. Taking over another client's expired lease still requires explicit confirmation. The renewal heartbeat performs that reclaim itself: a machine that suspends longer than the lease duration stops its timer but not the target clock, so it wakes with its own record already expired and nobody else claiming it. Reporting a lost lease there would cancel the heartbeat for a condition the next mutation heals anyway, so renewal retakes the record instead and the session keeps its authority across standby.

Portable project durability publication

Portable project session state remains authoritative under the target workspace's .mevedel/ directory. Before the first target-side state write in a live Emacs client, mevedel discloses the exact state directory, the stored data categories, and that mevedel does not encrypt them. Acceptance is remembered once per execution target for that Emacs process; another client or restart discloses again.

Completed-turn saves and segment transitions stage their complete critical artifact batch locally before target I/O. Root and canonical agent transcripts skip duplicate fixed writes and are stored in immutable publications. Remaining fixed files use nearby target temporary files plus atomic rename; these caches are not the durable snapshot boundary. Numbered compaction archives retain their physical recovery copy. A successful session-local batch without a sidecar marker retains its staged source locally. A later batch with exactly one session.meta.el artifact marked :commit-marker t merges those retained artifacts with the current batch in order; duplicate logical paths use the last write. Artifacts outside the session directory remain ordinary fenced writes and never enter the session snapshot.

A save whose durable state is byte for byte the committed snapshot performs no target transaction at all: the candidate artifacts are hashed against the committed manifest, and when every one matches, the save records nothing and updated-at is not even stamped, because stamping it first would make the sidecar differ on every call and defeat the comparison. When some artifacts differ, only those are published. Payload comparison runs once per save and its selection is reused for publication, avoiding a second encoding/hash of a changed transcript. Synchronous root saves freeze the encoded bytes after save hooks, using the buffer's file coding system. Automatic warm-local root saves instead freeze native characters and encode once while staging; asynchronous digests and comparison then use the staged bytes. Both paths preserve the captured coding system and original line endings. An omitted artifact keeps its existing manifest entry, so the logical snapshot is unchanged while whole per-artifact ownership proofs are avoided. The commit marker always publishes, because it is the transaction's commit point rather than a payload. mevedel-save-session publishes regardless, for a user who wants a snapshot rather than a record of a change.

Repeated captures within a turn reuse that file's checkpoint version and timestamp. Backup names are the full SHA-256 of raw file contents, so identical pre/post images share bytes across files and turns. Checkpoint records retain their individual paths and versions. New bytes receive immutable publication storage; previous heads keep their original bytes. Portable saves remove logical file-history/ entries no longer named by the checkpoint index, including files changed and then restored to their pre-turn contents. Portable saves write these snapshots only into immutable publications, without an additional fixed copy. Rewind and WorktreeFork read the committed artifact; fork staging can still materialize a fixed cache. Existing caches are not pruned by this change. PID sessions remove unindexed content-addressed backups only after the replacement sidecar commits. A failed sidecar write therefore leaves the previous checkpoint bytes available; cleanup failure is diagnostic and retried on a later save. Snapshot contents remain raw and owner-only. Readers resolve the names in the checkpoint index; saving does not rewrite existing archived history.

Sessions no longer publish artifacts/... logical entries. The artifact migration moves the content of a session published before the store into it and drops those entries from the converted copy.

Retained idle agents remain registry entries until their first conversation access; resume eagerly hydrates only active abandoned turns needed for partial-response recovery. See agent persistence.

The artifact resolver verifies immutable bytes without requiring a live lease. It checks lease ownership only when a newer staged candidate exists, immediately before using those local bytes. Losing ownership excludes staged content and leaves the captured publication as the read source.

The marker transaction copies the merged logical artifacts into a unique, never-overwritten directory below .publications/, records each target-native session-relative logical and published path plus its SHA-256, and creates the immutable manifest.el last. That directory, its artifacts and its manifest are one target program, so the whole generation costs one round trip and the manifest is still the last byte written inside it. Updating the exact current lease generation's :publication-head to that manifest is the only commit point. Every fixed cache write precedes this commit, and no fixed or shared state is written after a failed one. Consequently, a reader sees either the complete old manifest or the complete new one even if newer fixed caches already exist. A lifecycle replacement such as Rewind may put :replace t only on the marker, which starts from an empty manifest rather than overlaying the preceding snapshot.

An uncached read validates the manifest and all logical/control paths, returns qualified immutable paths, and verifies the selected sidecar bytes eagerly. Other artifact digests are verified only when those bytes are consumed, so session listing costs one manifest plus one sidecar rather than the whole conversation. A nil head means the session is unpublished or incomplete. The live owning session may resolve its newest retained local staged source before falling back to its captured committed manifest; readers never treat the fixed caches as authority.

Cooperative control transfer uses immutable generation-specific request and decision records below .lease/requests/. Resolving request, decision, and release-fence paths does not create their directories. Only protocol writes create mailboxes or fence directories; inspecting a released lease or an absent decision leaves those control directories absent. Each lease records its open transfer generation. Ordinary rotations preserve it, so every contender uses the same exclusive request path even when a lease rotation lands between lookup and creation. A successful rejection rotation opens the next transfer generation; the decision alone does not. The owner pairs a decision with that request's generation, and the pending request remains the only requester across ordinary lease rotations. A request must still name the current lease's client as its owner, so one left over from an earlier ownership cannot cross a release. Granting a request does not move authority: the owner drains existing work, commits one final publication, and releases with a short requester-only fence. New mutation admission is refused while draining; the named requester acquires only after observing the released successor, then reloads the committed sidecar and transcript before its buffer becomes writable. The requester also reads its exact immutable decision, so a rejection ends the pending state and exposes a fresh request action. The fence records the later generation actually released, while retaining the request's identity; retries therefore remain idempotent after the owner's normal generation drift. A failed refresh releases the new lease and leaves the requester read-only. Sidecar, transcript, target, and instruction restoration are staged before the live session or buffer advances, so a failed follower refresh remains retryable. View-owned drain predicates survive committed-state adoption. Transfer state is transient session state and is not serialized in the sidecar. A view polls that state every mevedel-view-control-transfer-poll-seconds, or every mevedel-view-control-transfer-remote-poll-seconds when the session lives on a target; each poll reads the lease head, the target clock, and the mailbox, so the interval trades handoff latency against time on the one target connection the user's own work also needs. A local owner with no transfer in flight first compares a native stamp of its request mailbox with the one its last empty poll saw, and skips the target program while it is unchanged: a requester adds a file to that flat directory, and lease renewal, not this poll, detects a lost lease. Before that check, each open local view spawned a shell every five seconds. The remote default is much longer because that cost is real only there, and because a command in flight is also a window in which a foreign process sentinel can issue its own remote operation on the same connection — see Transport reentrancy.

New mutation admission, including guest prompt delivery, observes transfer state without draining or releasing the session. When that observation needs a target program, its lease, clock and mailbox reads carry the recovery-root and mixed-authority probes too. The ordinary authority gate consumes those observations within the same admission transaction; it still checks recovery, lease expiry and target incarnation. Thus an uncontested cold local or remote admission with an owned lease needs one control program, as does a warm local admission whose mailbox poll is skipped. A live request, recovery or acquisition may need more. If the lease directory does not yet exist and the program skips the added probes, the ordinary gate performs them before first acquisition; skipped probes are never treated as evidence of absence. No authority observation is cached across admissions. A newer grant or release handled during a yielding read cannot be overwritten by the older poll. A durable timeout decision still supersedes a nested refresh of that same undecided request. Admission rechecks the local transfer fence and live lease binding after its authority checks return, including a manual release without transfer state.

While a transfer is in flight — a request outstanding on either side — both sides poll at mevedel-view-control-transfer-active-poll-seconds instead. The idle cadence is chosen for a connection nobody is waiting on, and a handoff composes three separate waits: the owner noticing the request, the grant deadline, and the requester noticing the release fence. Paying the idle interval for each turns a thirty-second handoff into minutes. Both sides return to the idle cadence as soon as the transfer settles, so the timer is re-armed per tick rather than fixed when the view is set up.

An unanswered request is granted by the owner's own poll once mevedel-session-transfer-prompt-timeout passes, so control can be taken from a machine nobody is sitting at. That window runs from the moment the owner could first see the request, not from when the requester wrote it: a poll interval can be longer than the timeout, and measuring from the requester's clock granted requests the owner had never displayed. Worst-case latency is therefore one poll interval plus the timeout, and the person at the owner always gets the full timeout to press Keep. That requires the owner's Emacs to be alive and polling; an owner that has died instead lets its lease expire, and the successor takes over through the expired-lease path. Grant is not release: the owner still drains, publishes, and releases, so a request against a busy owner sits in quiescing until the work it is waiting on finishes. Only the owner can say what that work is, so the owner's surface names the blocker and the requester's says just that it is waiting.

mevedel-take-control, also bound to the read-only interaction's r key, is the one command for the requester's side. It routes on lease state: an unheld or expired lease is acquired directly, because there is no owner to ask and the lease layer's own confirmation is the whole negotiation; a held one is requested. mevedel-release-control is the reverse, saving and publishing before the lease goes and leaving the buffer read-only and following.

Entering read-only mode reports itself as a message rather than a warning. The buffer states its own authority in the interaction zone and the cockpit header for as long as it holds, so a warning window would repeat durably visible state and take the frame at the moment the user is watching the handoff. A lease that fails to renew or is lost still warns: that is a problem, not a transition. Neither side is privileged: the machine that just handed control away is a non-owner like any other and can ask for it back.

Following a session owned elsewhere

A buffer whose session is being written somewhere else — a joined client, or a host that has handed control away — advances through the owner's committed publications while mevedel-session-follow-published is non-nil, which is the default and is read per buffer so one session can opt out through mevedel-toggle-follow. mevedel-refresh-session performs the same read on demand.

Updates are per publication, which means whole turns rather than streaming tokens: a non-owner sees what the owner has committed, never work in progress. The publication head in the lease record names the owner's current generation, so an owner that has published nothing new costs one lease observation and no artifact reads. When it has advanced, the follower reloads the committed sidecar and segment through the same path a granted transfer ends with, minus the lease and the write enable. A locally modified buffer is never advanced: those edits are exactly what the transfer path refuses to discard. Like lease renewal, that poll performs no target I/O while another TRAMP operation is in progress or while a publication owns the bounded window: Emacs runs timers and process filters wherever the main loop waits, including inside a TRAMP operation, and the owner poll may itself publish. The next tick observes the same durable state once the transport is free.

Transport reentrancy

Target I/O started while another remote operation is already in flight does not merely fail. TRAMP refuses the reentrant call, or the nested command consumes the running command's pending output and returns an answer belonging to something else — an absent lock reads as present, and durable state derived from that is wrong. Every caller reachable from a timer, a process filter, or redisplay therefore asks mevedel-transport-busy-p first and defers through mevedel-transport-run-when-idle.

That predicate combines two signals with complementary blind spots. Advice on tramp-file-name-handler maintains a dynamic depth counter spanning the whole of any operation this Emacs started through a file name, including operations belonging to other packages: a mode line that stats a remote file during redisplay opens exactly this window. When the outermost handler returns, the same advice re-arms package-owned deferral timers created while TRAMP had timer-list temporarily bound away; otherwise the pending entry would retain a timer that can never fire. TRAMP's own per-connection lock property covers callers that reach the connection without a handler frame, but only for the instants it is held — TRAMP releases it and runs timers between the send and the read, which is the window the depth counter exists to see. TRAMP has no global in-operation flag, so a plain variable check silently never fires.

Neither signal sees a remote process on a separate connection, so the predicate is necessary rather than sufficient. mevedel-session-control-fs therefore also refuses outright, with mevedel-session-control-fs-busy, any control operation issued while the transport is busy: a visible failure is recoverable, a fabricated answer is not.

Transport integration follows both package and TRAMP lifecycle. Unloading TRAMP cancels deferred work and detaches the handler advice; reloading TRAMP restores it while mevedel remains active. mevedel-uninstall cancels the same work and keeps later TRAMP reloads detached until mevedel-install runs again; deferred producer callbacks arriving in that interval are dropped.

The predicate only governs this package. It cannot stop the reverse case, which is just as destructive: an idle timer belonging to a syntax checker or a mode line does not consult it, and TRAMP's wait loop yields to timers with a command in flight, so that timer sends its own command on the same connection and consumes the reply this session was waiting for — our lease records arrive at its parser, and the record we read belongs to it. Control operations, and the whole save transaction around them, therefore run inside mevedel-transport-with-exclusive-connection, which suspends timers for the duration, exactly as TRAMP does around its own critical sections. A timer the body arms is re-armed on exit rather than lost, and a with-timeout opened inside it still fires, because the bound lists are the ones Emacs consults while the body runs; only timers that existed beforehand are held. Restoring an idle timer preserves whether it may run in the current idle period, so incremental cleanup continues without requiring a new user command.

Suspension stops timers, not process sentinels. accept-process-output with JUST-THIS-ONE suppresses other processes' output but still dispatches their status changes. A package performing remote I/O from a sentinel can therefore nest inside a mevedel command and receive TRAMP's reentrancy refusal. The remote control-transfer poll limits how often a command is in flight. See ADR 0101 for the failure that motivated this boundary.

For packages such as projectile that advise temporary-file deletion with remote project discovery, the following configuration suppresses that discovery while TRAMP is already active. mevedel-transport--depth is non-zero while a TRAMP operation is on the stack:

(with-eval-after-load 'projectile
  (define-advice delete-file-projectile-remove-from-cache
      (:around (fn filename &optional trash) skip-inside-remote-operation)
    (if (and (bound-and-true-p mevedel-transport--depth)
             (> mevedel-transport--depth 0))
        nil
      (funcall fn filename trash))))

The cache update is skipped only inside that window; a delete outside one maintains the cache as before, and the entry skipped is stale until the next projectile-invalidate-cache.

Note what the test is not. Whether FILENAME looks remote decides nothing: the file a sentinel deletes is typically a local temporary, and it is projectile's project root that is remote, so the truename crosses the connection however the argument is spelled. The same mistake is available inside this package, and the transport's own state is the only reliable answer to "will this reach the target".

Turn settlement waits for transport ownership for the same reason. gptel drives it from a process sentinel, which Emacs may dispatch from inside an unrelated remote operation. Only the turn commit — the single-use reservation fence, which touches no target — runs synchronously there; the publishing steps wait for an idle transport. Cheap steps share a five-millisecond callback budget; expensive steps return to the event loop before the next step. Durable saving and journal checkpointing are separate steps. One admission hold covers the entire ordered chain, so ending the request still follows saving and checkpointing and a new request cannot overtake either. Transport cancellation and source-buffer death retire the continuation and release its hold. Individual publication transactions and checkpoint operations remain synchronous.

Without a prefix, M-x mevedel lists persisted workspace sessions before creating a buffer, after sweeping expired sessions and locks left behind by dead Emacsen. Each candidate carries the action its authority produces, alongside Start new session:

Lease state Label Outcome
already open in this Emacs Switch switch to the live buffer
available Resume acquire the lease, writable
foreign Join read-only, following the owner
expired Take over confirmed takeover

The verb comes from the lease alone. Whether the workspace is local or reached over TRAMP is a property of the client's vantage point, not of the session: two machines see one session directory through different path spellings, and what decides the outcome is whether anybody is writing it. The row's annotation names the machine involved — held by desktop, lease expired, was laptop — from the lease record's :host, since the client id is opaque and per-process and can only answer "is this me, now". A lease without diagnostic host metadata displays unknown.

Joining an active writer opens only its last committed publication and exposes Request control in the view. Expired takeover still requires the ordinary explicit confirmation. Starting an independent session while another one is held — joined or open here, not merely resumable or expired — warns that both share project files and points to a Worktree Fork for isolation. Accepting this general advisory suppresses it for the rest of the Emacs process, across workspaces; refusing or quitting still cancels the start and leaves the advisory unacknowledged. Lease takeover and destructive-action confirmations remain per action. An expired lease does not warn: its writer is gone, which is the reason that row offers a takeover in the first place.

A failure before the manifest-head compare-and-set retains the current and previously retained staged sources as one transient local recovery, surfaces Publication pending in session status, and blocks request and mutation admission until mevedel-session-publication-retry succeeds or the user explicitly runs mevedel-session-publication-abandon. Once the compare-and-set succeeds, the transaction is durable even if the final publishing to active lease normalization fails: the client fails closed with visible lease loss, consumes the transaction sources, and does not create recovery or republish the committed bytes. Normal session-buffer closure and Emacs exit are refused while recovery is pending, so releasing a lease cannot silently turn the only local recovery into an unreachable temporary directory. If publication is queued reentrantly while a live publisher is being released, lease release converts the queued and preceding uncommitted batches into the same pending-recovery state instead of deleting their staged sources.

An incomplete portable-project Rewind rollback has a stronger recovery boundary. Before the failure is reported, its local repair directory is copied into a unique directory below the session's target-side .recovery/ control tree and a recovery-*.el marker is atomically written last. The marker makes manual recovery durable across client loss: restore, lease takeover, and every later mutation admission refresh it and remain blocked until the user explicitly abandons the recovery. Any client that can inspect the session can therefore find the named target-side bytes. The client-local directory is removed only after the marker is installed; if target installation fails, it is retained and the warning reports both the failed target install and its exact local path. Target recovery is not a publication artifact: .recovery/ is excluded from immutable publications, Rewind materialization, forks, and Save As clones. Explicit abandonment is destructive and removes the target marker and bytes (or the retained local fallback) while holding the session lease. A successful rollback leaves no recovery tree. Abandonment records the approved deletion in the marker before removing bytes. If cleanup stops partway through, the marker still blocks mutation and explicit abandonment can retry, even after the payload directory is gone. Missing bytes without that recorded intent remain an invalid recovery marker.

Critical publication changes the owned generation to publishing and reserves a one-hour ownership window before each fixed artifact write. The head commit that ends a drain, with nothing remaining or queued, returns the generation to active with its ordinary expiry in the same write, so the window closes without a separate finishing write; work queued after it reserves again before writing. Timer callbacks perform no target I/O while publication is active, avoiding reentrant TRAMP calls; the serialized publisher renews before each such write that no fresher proof precedes, and after the final write of a batch that does not commit. An owned lease is reserved directly; a separate renewal first runs only for a lease that needs reclaiming. A batch that writes no fixed file and carries no marker -- agent and segment transcripts, file history, tombstones -- only stages its bytes for the next commit and reserves nothing. When a renewal would reproduce identical lease bytes, it verifies the target bytes and observes the generation listing without rewriting the record. A fresh transaction clock reading also avoids another target clock marker; reuse never extends that reading's one-second lifetime. Ownership proof cadence, takeover checks and the authoritative manifest commit are unchanged. The publication entry point shares this transaction scope across admission, reservation and commit, including direct agent publications. Its clock and recovery observations expire with the call; nested root saves retain their existing scope, and clocks remain keyed by target directory. Tombstones and immutable-only file-history artifacts do not cause fixed writes or per-entry renewals. The final proof and the immutable manifest commit still check ownership. If one uninterrupted target filesystem operation exceeds that window, the next ownership check fails closed and preserves local recovery. Another client may take over an expired publishing generation only after an explicit prompt warns that a critical write may still be in flight and asks the user to confirm that the prior client is stopped.

The same bounded reservation wrapper covers long synchronous lifecycle I/O, such as target-side copy, restore, and rename. It suppresses timer target I/O and checks final ownership but does not itself publish, commit, or drain queued artifacts.

Warm-local asynchronous mailbox publication

Delivering a background command's completion or monitor batch to its owner's mailbox and automatic root or agent transcript checkpoints do not block the editor on target programs. Only a materialized local portable session qualifies: its save path is local, its authority is portable, a committed publication with a sidecar exists, its root buffer is live, and it is not stopping. Cold materialization, remote targets, file-backed and PID-lock sessions use the synchronous publication above. Every other acknowledged mutation is synchronous too; agent mail, spawn, RESULT and settlement drain the gate first (below) and then commit as before.

The mailbox append is one session-owned publication job that holds publication-active-p from acceptance to acknowledgement, so renewal timers stay quiet and cooperative transfer stays blocked. Its phases are admission, freeze, reservation, generation, head commit, adoption, acknowledgement and cleanup. Each target phase is a pinned control program launched without waiting; its result is stored by the sentinel and the next phase is scheduled from a timer through the idle transport, never from the sentinel. The transaction's clock, asserted-directory and recovery cells are job-owned and rebound only while that job advances, so the bounded clock freshness check applies across event-loop turns instead of becoming an authority cache; the job's own bounded lease proofs run between phases and fail closed after expiry or ownership loss.

Local control-program jobs retain their staged payload files until their delivery timer cleans up, before invoking the callback. The sentinel performs no file deletion or buffer teardown. A stopped-early program's unused payloads are removed too; cleanup failure reports a diagnostic without losing the stored outcome. Emacs exit also cleans up jobs whose delivery never ran and retries failed staged-file deletions. A hard process kill cannot run exit cleanup.

Admission is one program carrying the clock, the lease listing and the bound generation's record, the transfer-request listing and the PID-lock and recovery-root probes. It admits only the warm case: an owned, newest generation with remembered bytes, no recovery marker, no live transfer request naming this client, and no quiescing or released state. Anything else fails the job, acknowledges the failure, and leaves the producer holding its output; a later synchronous attempt reports the reason. Freeze runs only after admission, under the gate: it identifies the records (a record already present is dropped), marks the new ones :uncommitted, prepends them to the live queue and captures the fresh sidecar. The reservation program carries the first fixed write, the head-commit program carries the immutable generation ahead of the lease update, and that update returns the lease to active. The commit point is the lease head, as for synchronous publication.

An automatic root checkpoint, whose payload is the whole transcript, splits its freeze from the work that used to run inside it. Freeze runs under the gate and only captures: it takes the transcript's native characters as one string with the buffer's coding system recorded, the sidecar, a copy of the committed manifest entries, the comparability flag, the forced-save flag and the timestamp. For multibyte transcripts it defers encoding as well as hashing; unusual unibyte buffers retain their explicit-encoding contract. The next idle turn stages: each artifact is written once, directly from its string through write-region in a fundamental temporary buffer with an explicit coding system and annotation hooks disabled, so no live buffer state can change the staged bytes. There is no eager encoded transcript payload retained for comparison; write-region still encodes and allocates on the main thread. Staging also projects the staged sidecar through its checkpoint owner. A further turn runs one asynchronous file-hash control program over every staged source that has no digest yet, across the job's retained earlier batches as well; the digests must be well-formed lowercase SHA-256 values or the job fails and rolls back. No transcript digest is computed on the main thread, and the generation phase refuses to hash for itself. Only then does the selector run, using the digests and the frozen snapshot, never live fields: it keeps payloads whose digest differs from the frozen manifest, stamps and rewrites the small staged sidecar -- the checkpoint-projected local file, which is the frozen authoritative snapshot, not a fresh live projection -- computing that sidecar's own digest, and returns the batch to publish. A nil selection is an exact no-op and completes the job without a reservation; it is refused while earlier retained batches are pending. Every staged batch, including pruned ones, belongs to the job until acknowledgement or failure, and a pruned original is not kept as recovery.

A save first runs its save hooks, serializing property-derived metadata such as GPTEL_BOUNDS into characters. It may then skip transcript capture, staging and hashing only when an acknowledged proof says nothing differs: the buffer, owner epoch, file name, character modification tick, coding system, multibyte state, restriction, and the digest of the transcript's manifest entry must all equal those recorded when an earlier save was acknowledged, and no publication is queued or pending. Because it compares the post-hook character tick, only changes that leave the saved characters alone can be skipped over; a parser frontier that advanced before a failed save proves nothing. The proof is recorded only after adoption, and only if the buffer's ticks and identity still equal the frozen state, so edits made after capture stay modified. The synchronous publication path is unchanged.

The 2026-10-20 compiled-copy replay used the complete 95-file real session copy (27,795,709 bytes), restoring a 1,069,346-byte root, plus a synthetic 3,504,146-byte scaling fixture that retains the original control/fork records. Each row below has six samples; latency and phase instrumentation are separate runs. Every sample checked authoritative saved bytes and successful acknowledgement. Times are milliseconds, with a 5 ms heartbeat:

Fixture/edit Prepare median Stage median ACK median Heartbeat gap median / max
Natural response tail 9.07 5.31 141.36 12.81 / 15.84
Natural new turn 10.13 4.98 143.34 14.51 / 61.05
Synthetic response tail 10.55 15.36 162.93 17.54 / 51.90
Synthetic new turn 11.89 14.52 159.65 16.87 / 57.19

Before the preparation changes, prepare medians were 34.77/34.85 ms natural and 78.14/77.37 ms synthetic. All 20 non-GC latency samples now have heartbeat gaps below 20 ms (largest 17.94 ms); four samples with 36.92-45.66 ms of observed GC exceed it. The uninstrumented latency samples do not establish the exact GC stacks. A separate phase run directly observes a 40.35 ms GC inside preparation and a 51.40 ms freeze phase; its other phase maxima are below 20 ms, with staging at most 17.78 ms. The unconditional 20 ms goal is therefore still unmet, not explained away as unrelated rendering.

Acknowledgement is slower than with prefix reuse alone: medians increase by 32.33-37.16 ms (25-36%). Compared with the original baseline, natural ACK is 17.67-22.86 ms slower while synthetic ACK is 17.02-26.32 ms faster. Digest phase dwell is 41.88-44.24 ms median, including worker execution and idle handoff, while its main-thread submission is 0.83-0.90 ms. This trades earlier durability for editor responsiveness; it does not improve both unconditionally.

GC settings remained 800000 / 1.0, without explicit collection. Allocation does not show an overall improvement: cumulative string-character counters increase roughly 0.75-0.77 million per checkpoint versus prefix reuse alone (instrumentation contributes), and independent memory profiling reports about 13.08/13.38 MB natural and 33.25/33.40 MB synthetic allocated per checkpoint, versus 10.47/10.70 MB and 32.91/33.09 MB respectively. These are allocated amounts, not retained heap. Encoding still allocates inside write-region. The remaining transcript-sized copies are the immutable native capture and the staging write's C-side encoding. Synthetic freeze/stage/selector spans allocate about 4.06-4.09 / 3.63 / 0.43 million Lisp string characters per checkpoint (inclusive counters; nested spans overlap). A byte-checked region write from a frozen temporary buffer still allocates the full encoding in this runtime and showed no low-risk allocation advantage. No second main-thread whole-transcript digest remains; async digest submission allocates about 12,000 string characters and hands over file paths, not transcript contents. The replay proves neither a hard latency bound for arbitrary transcripts nor graphical typing/scrolling latency; the running editor was not reloaded.

On success the records become readable, the job's batches are consumed, the producer's acknowledgement runs (execution bookkeeping completes), and only then, unless the session is stopping, the owner is woken. Cleanup failures after the head committed warn but do not change the outcome. On a definitive failure before the head committed, the job normalizes a reserved lease, removes only its own record identities from the live queue, preserves unrelated arrivals, discards its staged batch, and acknowledges the failure. The producer keeps its unread output and may deliver again. An execution's terminal result retries with a stable record identity, a hash of its owner and execution id. Monitor batches stay pending, with their lines retained, until acknowledged, and are retried only after a definitive rollback; a retry builds new records, so a monitor batch's mail identities are not guaranteed to repeat. An uncertain outcome retains the job instead of retrying (below). Acknowledgement happens exactly once, including after a synchronous-fallback carrier.

An error or lost result at or after the head commit is not decided locally. The job reads the authoritative lease head: the candidate head means adopt and acknowledge success; the prior head means roll back as above; any other head, or one that cannot be read, retains the job as unresolved, records the frozen batches as pending publication and keeps the mail identities unchanged, because a successor head may already contain them and rolling back could acknowledge the same execution twice. mevedel-session-publication-retry repeats the reconciliation and mevedel-session-publication-abandon ends the job as failed. Both are refused while the job is still running.

While a job is accepted its session's mutation gate is held. Work that would change acknowledged state or capture a sidecar queues as a preparation continuation that runs, outside the current callback, before it mutates or captures anything: acknowledged mailbox arrivals, deferred terminal settlement, autosave and the observational registry save. Queued entries hold no frozen snapshot, so a rolled-back job cannot be resurrected by a sidecar frozen earlier, and a committed job cannot be erased by one frozen before it. A failed deferral is reported to its owner and never silently dropped. Explicitly synchronous operations -- manual and root saves, transcript and segment writes, spawn registration and activation, settlement publication, and a registry save that requires a commit -- drain the gate first, so none reports queued work as committed; an unresolved job makes them fail with the recovery instruction. Transcript streaming and composer edits are not gated.

Automatic checkpoints use that same job, not a separate writer. Their queued preparation rechecks modified state, the save-failed cue and retained batches under the gate, then runs the existing root or agent transcript capture. A synchronous save ahead of the preparation can absorb it without freezing or publishing anything. One accepted checkpoint per buffer coalesces repeated requests until acknowledgement. The frozen batch contains its changed transcript, instruction and file-history artifacts, and a sidecar marker last. If an agent has no canonical transcript to prepare, acknowledgement releases the gate and hands the save to its synchronous writer; it does not clear the save-failed cue as though a transcript had been published. The marker's checkpoint candidate is validated before publication and installed only after authoritative head adoption, including reconciliation. This retires only the consumption and ToolCall receipts covered by those frozen transcript bytes. Ancillary fixed files share the asynchronous reservation program; the transcript, instruction and file-history payloads remain immutable-only.

Checkpoint acknowledgement runs the same success-only finish as a synchronous save. It clears the buffer's modified flag only if its character-change tick still matches the frozen bytes; model output or user edits during publication remain modified for the next checkpoint. A definitive pre-head checkpoint failure retains the frozen batch as pending publication before reporting the save-failed cue. Further checkpoints do not blindly retry it: explicit retry or abandonment resolves recovery first. Retry installs the frozen candidate without repeating the failed acknowledgement. Unlike failed mailbox delivery, checkpoint recovery does not discard its frozen bytes. Explicit manual saves, settlement and close/exit remain synchronous and drain accepted checkpoints before saving or releasing authority. A synchronous agent writer drains before entering checkpoint exclusion or capturing its transcript. Pending recovery blocks close/exit even when no job remains in flight, until explicit retry or abandonment resolves it. Explicit abandonment deletes the discarded staging files without retaining the failed job's checkpoint again. A canceled save hook or continuation is handled as an asynchronous failure, not re-signalled from its timer; acknowledgement and cleanup cannot strand the next preparation. Before capture exists, cancellation leaves modified text and the save-failed cue for a later checkpoint rather than inventing recovery bytes.

Retained-agent buffer kills run a local query that saves unpublished transcript bytes first and refuses the kill if publication is failed or unresolved. The existing session exit query likewise drains accepted jobs and refuses pending recovery; there is no second agent exit query. If teardown bypasses the buffer query and its final save still fails, the kill hook writes the unpublished transcript to a temporary recovery file and warns with its path. Cold, remote and file-backed checkpoints keep their previous synchronous path.

Gate ownership is explicit. A queued preparation runs with a fresh token that is held in a session slot only while it runs and is passed to the preparation as an argument; only a call carrying that token may reenter the gate. No dynamically bound exemption exists, so an unrelated timer or callback running during a preparation inherits nothing. The drain fails fast, with an error naming an active operation, when a nested transport frame is in flight, a preparation is running on the stack, or a synchronous publication on this stack owns the gate: waiting there could not let the work advance. Otherwise it pumps events in 10 ms steps and signals after 30 seconds (mevedel-session-publication--barrier-seconds). That limit is checked between pumps, so it is not a hard bound on a single blocking operation inside one.

A preparation therefore never runs arbitrary callbacks under its token. Notifications -- waiter wakes, root and agent wake offers, and the reoffer after a vetoed close -- never enter the mutation preparation queue. Each session owns one coalesced first-in-first-out set of pending notifications and at most one timer. A notification runs at once only when the publication gate is idle (no job, preparation or active publication), the transport is idle and nothing is already pending; otherwise it joins the set, so a later acknowledgement cannot overtake an earlier one. The timer starts at 50 ms and doubles up to 500 ms while the gate or transport stays busy, then flushes the set in order, only while the root buffer is live and the session is not stopping. A waiter callback can resume tool code and gated operations, so it runs with no token and fresh transaction observations, and defers or drains like any other caller. A failing notification warns and does not stop the rest. Temporary stopping during a close query retains actual suppressed offers without a timer. If the close is vetoed, reopening admissions re-arms that same FIFO once; an idle close creates no timer. A close that proceeds cancels the timer and discards the queue during session teardown, without running callbacks.

After the checkpoint preparation changes, the original mailbox workload was rerun with compiled code: a synthetic 6000-turn local portable session, one 4000-character result per sample, six samples per mode and a 5 ms heartbeat. Asynchronous submission returned in 0.196-0.460 ms and acknowledgement arrived 105.028-115.540 ms later, with a maximum heartbeat gap of 6.533-7.159 ms, against 63.907-70.177 ms of synchronous blocking (heartbeat 63.977-70.228 ms). The earlier async ranges were 8.2-10.2 ms heartbeat and 75.4-120.1 ms ACK; the rerun shows no blocking regression on this workload, not universally faster acknowledgement. A separate phase-instrumented pass measured at most 1.674 ms per async advance and 1.704 ms per containing timer; these are not added to the uninstrumented latency values.

An immediate explicit drain waited 111.696-119.674 ms. A deliberately stalled admission with a 0.25-second drain limit signalled at 250.244 ms without acknowledging or discarding the job; releasing the callback completed it, with ACK at 355.000 ms from submission. Composer text and point survived all 38 samples across latency and phase passes. This original recipe explicitly collects garbage before each sample; no sample contained GC, unlike the streaming-checkpoint replay above. Possible independent verifier load was not excluded. One synthetic batch fixture, small samples, no live or graphical measurement; this is not a hard bound on arbitrary filesystem latency, sidecar size or GC.

The automatic-checkpoint follow-up was measured separately with compiled code on a cp -a copy of a real session (95 files, 27,795,709 bytes), never the live session itself. The restored root held 1,069,346 bytes and the selected agent about 1.34 MB. After draining pending view-batch and coalesced-render timers before each edit (not every observer queue), six samples per mode compared the same frozen code's synchronous writer with its async checkpoint entry. A 5 ms heartbeat covered submission, the acknowledgement wait and 12 ms afterwards; no observer was suppressed. Ranges in milliseconds:

Checkpoint Synchronous heartbeat gap Async submission Async acknowledgement Async heartbeat gap
Root 94.42-104.04 0.08-0.11 105.45-118.50 40.35-46.18
Agent 81.11-122.33 0.07-0.30 89.42-210.48 21.67-57.44

The largest synchronous agent sample included 41.35 ms of GC; the paired asynchronous samples had none. Async publication removes target-program waits, not main-thread transcript capture: a separate profile measured root preparation at 37.61 ms (prompt indexing 15.57 ms, gptel state saving 13.20 ms including 10.43 ms of property normalization), and agent preparation at 14.06 ms. These inclusive components must not be summed. The root therefore does not meet a 20 ms responsiveness target. A repeated agent run also reproduced a 116.90 ms gap without GC: a restored-view batch callback took 113.55 ms, including a 74.79 ms full-rerender reset resolving an archived continuation prompt on a cold cache. This is a restore/first-render cost, not checkpoint publication; a warm live-editor probe took 0.5-0.9 ms. The delayed cold render can overlap the acknowledgement wait or follow it, so the smaller paired gaps do not bound all visible stalls. These are copied-session timer measurements, not graphical input latency. They precede the cancellation, abandonment and buffer-kill guard corrections; those lifecycle corrections were validated separately, not remeasured here.

Closing the root buffer stops new admissions and drains accepted jobs before its final save and lease release; Emacs exit does the same for every live session before execution teardown, then waits for control programs still in flight. A buffer-close query drains an owned root before teardown begins, and a query that vetoes the close reopens admissions. A job that stays unresolved refuses the close or exit for that session: it is neither saved nor released, so explicit recovery or abandonment remains possible. A lease release requested while a job runs is performed when the job completes.

Every committed publication, turn settlement, and lease-acquiring restore schedule publication collection without scanning history on the foreground path. The coalesced job reads at most eight generations per idle slice, yielding after 50 ms between reads. Publication scheduling also covers a long first root turn: agent checkpoints must not accumulate until that root turn finally settles. Existing sessions use the same full scan on their next publication or lease-acquiring restore. Slices run one idle second apart: each is a wakeup, and at 0.2 s a job that deleted a few files woke the editor three or four times a second for fifteen seconds after every turn. A wait armed while the editor is already idle is counted from then, and input during it restarts the count at the next idle period. Counting it on top of the idleness so far meant a slice armed during a long absence needed that much idle time again once the user returned. A single target read can exceed that budget. For local Linux sessions with more than sixteen uncached generations, a batch Emacs child prepares the compact immutable generation and sidecar observations. The editor caches those answers and resumes its ordinary scan; subsequent scans reuse them. The child performs no deletion and needs no session lease. Cancellation stops it, and late replies cannot revive a cancelled job. Finishing the scan yields before marking retained files. Pending input prevents all collection target I/O, including ownership checks and deletion after a completed scan. Busy transport and pending, queued or active publications defer work, retrying after twice the previous wait, up to eight seconds; an active request does not, since a Goal keeps one active for hours and deferring to it once left a 25-hour Goal session with 3,067 unreclaimed generations (4.9 GB). Closing the root or losing its lease cancels the job. Generations are immutable, so a moved head during the scan only adds the new generations to summarize. After planning, a moved head adds every manifest the plan has not seen to the retained set before any further listing or deletion, and pending deletion batches are filtered against the grown retained set. Only the lease owner publishes and each new generation carries its manifest, so nothing new is ever a deletion candidate. Each job ends with a publication-collection telemetry event. After a complete scan, retained manifests are validated one per step and mark their exact referenced files. All obsolete manifests are retired before any unreferenced payloads are removed, so interruption cannot leave discoverable heads whose bytes have been collected. Each deletion batch contains at most eight operations. Wholly unreferenced generation directories need no listing and share a batch. Up to eight partially retained generations share one listing program, avoiding a separate target process for each directory. Up to eight marking, enumeration, or synchronous deletion steps share the same 50 ms slice budget; an asynchronous deletion ends the slice immediately. Steps that read manifests or list generations check local state and cost only their own read. A deleting step is one program whose first operations prove, on the target, this client's lease generation bytes, a target clock before the lease expiry, and the planned journal pin directory's exact entries and pin bytes. On targets that support direct-async spawns that program runs in the background; the job waits for it, and its report continues the job from a timer outside any remote command. A failed proof deletes nothing and ends the plan quietly; the next publication plans afresh. Other failures warn without breaking settlement or restore.

Collection follows references rather than age: manifests can retain unchanged bytes in older generations. It reads every published sidecar, using cached immutable generation/sidecar facts, and retains the current head, settled-turn representatives, journal capture pins, the newest mevedel-session-publication-keep-recent-generations (default 3), and their referenced artifact files. Referencing one file in an older generation does not retain its obsolete manifest, sidecar, or unrelated files. Artifact-only directories remain eligible for later collection when their last reference disappears. Superseded mid-turn heads are collectible even before their turn settles unless selected by the grace window or pinned. An unreadable journal pin or retained manifest blocks collection before any deletion.

Generation assembly compares staged artifact digests with the preceding manifest after merging queued writes. Equal payloads, including sidecars, reuse their immutable files rather than copying them again. Replacement snapshots use only their supplied artifacts; they do not inherit dropped entries.

The generation cache retains only referenced generation names, the sidecar locator, and first-transcript size. Full artifact indexes are discarded after validation and extraction; caching every generation's repeated index would keep hundreds of megabytes of redundant Lisp objects alive. Retention receives its own copies of the compact reference lists so destructive list operations cannot change later observations. Authority reads still verify the actual publication.

Ordinary transcript readers have no read pins. The recent-generation grace window protects typical following, but a reader of an older non-boundary head can lose its bytes and receive an absence or hash failure. It never silently falls back to mutable caches. ADR 0072 records the retention tradeoffs and measurements.

Terminal retained-agent state in an already-materialized portable project session uses the same seam: finalization first updates the in-memory transcript metadata and registry, then publishes the agent transcript followed by the session sidecar as one batch. A mid-batch failure therefore leaves one retryable recovery and blocks later mutation. Transcript allocation may shallowly create the session directory before the first root turn settles, but the first acknowledged agent registry or mailbox mutation forces a full non-fork-point root snapshot. The sidecar therefore exists before mevedel reports the child publication or wakes a mailbox consumer; the root turn's later DONE publication still establishes its first stable fork point.

After that first snapshot, acknowledged registry and mailbox changes publish the sidecar with any retained transcript/artifact batches, without copying the root transcript. The checkpoint owner supplies the committed root fields even when live turn metadata has advanced before its deferred settlement save. The acknowledged mutation is therefore reachable without pairing newer root state with older transcript bytes.

An acknowledged agent mutation refuses reentrant publication queueing: its caller returns only after that batch changes the immutable head. Once the head has changed, later lease normalization or buffer save-hook failures are diagnostic cleanup failures rather than grounds to roll live agent state back.

Strict metadata and archived-transcript updates use the publisher's same required-commit contract. Reentrant calls fail before staging or queueing any bytes, leaving the caller free to retry after the active publication. A post-commit cleanup failure remains diagnostic because the new head is already authoritative.

Mailbox delivery consumes records after inserting their transcript blocks successfully. Acknowledgement itself performs no publication: the owner retains the removed records and merges them into ordinary persisted mailboxes until the receiving root or retained-agent transcript commits. A failed checkpoint leaves that pending consumption intact, without removing newer arrivals. A crash may redeliver mail but cannot leave a committed removal without its delivered text. Empty acknowledgements do no work.

Transcript-checkpoint preparation excludes nested transcript writers for the same session, including segment rotations and fresh chapters. Ordinary sidecar-only mutations remain admissible through committed projection. Terminal checkpoint preparation additionally excludes other writers, including observational sidecar writes from save hooks. Only the owning writer's final prepared publication is admitted, so tentative terminal activity and RESULT state cannot escape through another sidecar. Other sessions remain independent; the exclusion unwinds on failure. Ordinary consumption needs no such exclusion: committed root projection and pending records keep sidecar-only writes coherent while the live conversation advances.

Observational agent persists are not acknowledged mutations. Activity transitions (blocked/waiting flavors and their release) and explicit mailbox discards debounce into one sidecar-only registry save (mevedel-session-persistence-save-agent-state-soon, landing as mevedel-session-artifacts-save-agent-registry): the sidecar carries the agent registry the persist is about, and the transcript segment is committed at settlement, so the observational path never saves the segment or scans snapshots. A portable session publishes the sidecar alone as a one-artifact commit and the manifest overlay keeps every other committed entry; a portable session whose sidecar is not yet committed skips the save until the next critical commit. A synchronous acknowledged commit absorbs a pending one, and Emacs exit flushes the rest. Recovery treats every active activity identically, so a crash inside the debounce window costs at most a stale activity flavor or a repeated explicit discard. See ADR 0112.

Diagnostic streams (telemetry, hook, permission, and repair logs) reach a remote target as one pinned append operation carrying only the delta. The append works in place, so a crash mid-operation can tear one trailing line of a stream nothing reads at resume. A flush admits once -- recovery refresh and lease check -- and each append then runs in a program that first verifies the exact lease bytes this client last committed and lists the lease directory; a failed proof retains the content for the next flush, and a newer live generation ends the client's assumption. No lease record is rewritten for diagnostics. Publication is held active meanwhile, so a critical publisher reached from a sentinel queues and is published through the ordinary reserved path afterwards.

Local Fork and Rewind retain their same-filesystem directory transactions and rollback trees. Portable project lifecycle commits use the immutable publication head instead. A portable project fork acquires one fresh child lease in hidden staging, publishes a complete :replace t child snapshot, then moves the staged tree with its .lease/ and .publications/ control state while the bounded lease is reserved. The child's save path changes immediately and restore retains that same generation; it is never released and reacquired at a discoverable path. The replacement manifest contains the selected transcript, accepted-plan evidence, retained agent transcripts, file-history artifacts, and any other logical entries of the Source's committed manifest, never its fixed caches.

Portable project Rewind does not rename or exchange the session directory. It materializes only logical committed artifacts, excludes .lease/ and .publications/ by construction, restores project files under a bounded lease reservation, then commits the complete rewound snapshot with one :replace t sidecar marker against the current generation and head. A pre-commit failure revalidates the same authority before rolling project files back. Incomplete rollback fails closed visibly and retains recovery bytes; the control trees and current lease generation are never copied, moved, or replaced.

Portable project Rename commits only the display-name metadata against the current publication head. The session ID, directory, lease generation, and artifact paths stay fixed. A failure before commit restores the in-memory name; a failure after commit retains the committed name and reports the failure.

Portable project Save As captures the parent's current committed publication through the mevedel-session-save-as.el transaction while holding its lease. That module materializes only the manifest's logical artifacts in hidden child staging, acquires a fresh child lease before writing them, commits one complete replacement publication, and moves the staged child into discoverability before releasing the parent. Adoption target-verifies that already-owned child lease, transfers its exact path, generation, and renewal responsibility into the live session, and only then releases the old parent path; it never reacquires child authority after live mutation. The child therefore starts with one publication and no copied lease, publication-history, recovery, lock, or staging controls; work is proportional to current logical state rather than the parent's retained history. Its in-memory session is built by the same explicit slot policy as every Save As path: logical containers are deep-copied, the workspace and immutable execution-target identity are shared, and all request, queue, publication, transfer, and lease runtime is reset. Pre-commit failure, including cancellation or another nonlocal exit, removes staging and releases any child lease while leaving the parent unchanged. Once the marker commits, later finalization failure retains the independently resumable child and reports the failure instead of rolling it back. If the final staging-directory rename itself fails, session listing recognizes that hidden committed child by its verified publication; the live buffers and lease stay attached to that recoverable path.

Pending input, its delivery pause and failure pause, and recovery issues live in the sidecar. Queue and pause changes are written by the coalesced sidecar-only save, never a transcript save. Queued follow-up delivery writes one durable dispatching mark before dispatch and nothing after it; the turn's own saves record the delivered prompt. A restart reloads a dispatching entry as interrupted input requiring review, so a submitted prompt is never replayed. Live submission objects and edit state are not persisted. There is no queue-size cap.

A failed or aborted root turn pauses follow-up delivery only when steering for that turn was left undelivered or a queued follow-up's dispatch may have reached the transcript; those entries need review in the Pending Inputs cockpit. The next root request clears a pause that no reviewable entry still justifies, and the pause banner shows only while entries remain.

A saved model selection that no longer resolves uses mevedel-model-fallback-provider, or the host default when that option is nil, with a notice; without either, history stays readable while requests wait for a model choice.

That fallback is for the ordinary session baseline only. An accepted implementation policy resolves its exact provider/model and effort without fallback; a failure leaves Direct retryable or pauses active Goal work.

Recovery issues are informational unless an owner that re-checks them reports them as blocking: Codex login, a missing preset (cleared by applying any preset) and a saved model with no available fallback (cleared by selecting a model). Blocking issues refuse root requests. Provider failures, failed agents, naming, effort and fallback notices are informational; the next root request clears them, because that request is the retry. Informational notices also have a Dismiss control in the local view, and successful retained-agent dispatch clears only that agent's previous notice. Dismissal checks current session mutation authority and uses the coalesced sidecar save; it does not erase the retained terminal result or transcript. Blocking issues cannot be dismissed. Only a structured model-not-found signal (provider code or HTTP 404) moves a session to mevedel-model-fallback-provider or the host default, with a visible notice.

Standalone Plan metadata and its implementation retry record live in the sidecar; draft and accepted artifacts live under local/plans/. The retry record identifies completed preparation steps, cached Summary evidence, the Worktree fork point, reservation and published fork, the accepted implementation selection, and any reserved Goal construction. It allows retry without repeating an accepted rotation, summary, fork, or Goal. See Plan execution for the Here/Worktree, Fresh/Summary, and Direct/Goal workflows and their failure handling.

Direct implementation captures the accepted model, effort, and permission mode on one request; retry reuses that selection. Goal construction persists them on the Goal before clearing source recovery. Both Here and Worktree leave ordinary session policy untouched; a Worktree fork copies the planning session's defaults and preset. The accepted budget sets the Goal slot only, never mevedel-goal-token-budget. There is no baseline snapshot/restore scheme, so concurrent user setting changes survive settlement. Effective views, logs, and reminders do not become saved defaults.

The telemetry stream and diagnostics directory are observational artifacts, not resumable state. They are append-only within a run and are never consulted to restore a session. Because nothing restores from them, a session saved on a target keeps its profiler run directory on the client instead — in a per-run temporary directory, not diagnostics/ — so a multi-megabyte profile never crosses the connection. Diagnostics for such a run are consequently not portable. See telemetry.md for the event schema, redaction boundary, and profiler procedure.

The Goal remains in the session sidecar as a strict record: identity, objective, status/reason, token/time/turn accounting, optional budget, optional accepted-plan reference and implementation policy, and timestamps. The required :implementation-policy value is either nil or exactly (:model-provider STRING :reasoning-effort SYMBOL-or-nil :permission-mode MODE); the provider label is BACKEND:MODEL and MODE is ask, edits, or full-auto. mevedel-implementation-policy.el owns validation, exact model resolution, and current root-policy selection. Active ordinary root Goal turns and resume use that record; new ordinary chat with an inactive Goal uses the baseline. Already-admitted turns retain their snapshot through settlement. Child implementation turns inherit only the permission-mode override, not the root's full model policy; summary, control, directive, and unrelated child work is excluded.

There is no migration for this Goal shape. Older records without :implementation-policy and malformed policies load as no Goal; transcript history stays readable. This does not change the top-level session schema or introduce an older-Goal compatibility reader.

The Goal's :tokens-incomplete-p flag preserves missing native usage across save and reopen; the numeric count then records only a lower bound. Provider usage is authoritative when available. The request estimator supplies the fallback for gptel; native requests with missing counters follow the incomplete-usage budget policy.

Worktree sessions are ordinary sessions whose :working-directory is a Git linked worktree under the same workspace, created by /worktree create. Git availability, worktree-command support, and branch validity are checked on the session execution target before any worktree state is created. The linked checkout and its session remain on that same target, while setup-context paths use the target-native spelling. The old session remains live; the new session does not inherit active requests, permission queues, tasks, retained agents, or transcript history. Unless --clean is used, the new data buffer starts with a visible setup-context user turn explaining the source session, source directory, worktree directory, branch, purpose, and warnings. That turn is not sent automatically.

When a saved session's working directory no longer exists, it remains visible in the resume picker. Resume prompts for an existing replacement inside the workspace and persists that directory after the session opens successfully. For a Worktree Fork this does not recreate Git state: its original worktree path, branch, base commit, and fork type remain origin metadata while its current working directory changes. The picker labels the original path as missing or the session as retargeted.

A live session whose local working directory disappears, such as a worktree removed with git worktree remove outside mevedel, moves to its workspace root at the next durable mutation (save, publication, turn start, or execution arm) or publication retry. Every live buffer whose default-directory still names the missing directory moves with it, and one warning names both paths. Otherwise processes started from those buffers fail, leaving publication pending with no way to retry it. Remote directories are not probed on this path; they rely on the resume-time prompt above.

The prompt-index is rebuilt from mevedel-transcript-segments over the live segment. Only shared user spans whose real prompt text starts outside gptel-owned org tool/reasoning/summary scaffolding become rewind entries, so property drawers, compaction summaries, tool glue, and stale structural gaps are not offered as user turns. The rebuild is one forward pass: the block-depth prefix each span needs is carried along rather than recounted from the start of the buffer per turn, which matters because the rebuild runs on every settled save and a live segment is bounded only by the compaction threshold.

A turn that auto-compaction carries into a new segment keeps its prompt entry in the segment where it started, and that segment never sees the turn's final response. When such a turn settles in a segment with no prompt of its own, the save writes its fork point there and adds a :continuation plist (segment, resume position after the inherited summary, fork-point ID, and transcript cutoff) to the original entry. The entry itself is unchanged, so turn numbering, rewind, and fork behave as before; journal capture reads the turn from its continuation.

Delivered reminders and completed tool-response fragments also persist as trusted transcript records. The history adapter preserves reminder message boundaries and exact tool-call grouping across follow-up and restore. Compaction discards records with removed history; edited, filtered or incomplete fragments use normal backend serialization.

Tool IDs are established before persistence by the tool-result rendering adapter. It renders each completed call from its own authoritative request record; normalization and restoration preserve those IDs. No scan of result text guesses an original ID.

After mevedel restores persisted bounds, session restoration calls mevedel-transcript-normalize-properties. The transcript module reapplies properties from its canonical structural ranges; persistence does not parse transcript control forms itself.

Hook execution logs are append-only diagnostics. The in-memory hook-log slot is transient and capped, while hook-log.el keeps the session's persisted hook entries as sanitized plists. It is not read back into live session state on resume. Entries recorded before first materialization are backfilled when the session directory is created.

Permission diagnostics are also append-only. permission-log.el records permission queue lifecycle events so transient overlays can be diagnosed after a turn or agent is aborted. It is not read back into live session state on resume. Pre-materialization entries wait in a transient session queue and flush with the other diagnostic logs. Failed hook, repair, permission, and telemetry appends stay queued and retry after the next successful session save; they never block critical publication. Portable project diagnostic appends are collected per session batch and sent as their queued delta in one program after the batch body. On targets that support direct-async spawns the program runs in the background: its callback touches only in-memory state, and at most one runs per session. Appends collected meanwhile, and appends the program did not write, wait in an 8 MiB per-session backlog for the next batch; a failed append warns once per path. Other targets, and the flush at Emacs exit, run the program synchronously. A nested flush for another session proves that session's lease independently. A crash may tear the final line.

For mevedel chat buffers, save-time advice around gptel--save-state strips every gptel request-config Org property (GPTEL_BACKEND, GPTEL_MODEL, GPTEL_PRESET, GPTEL_SYSTEM, effort, tools, and the rest), keeping only GPTEL_BOUNDS. The sidecar is the sole durable source of session request configuration; a stale drawer copy would otherwise override live buffer-locals through gptel's send advice (gptel-org--send-with-props), silently undoing a mid-session model change. The same strip runs at chat-buffer initialization and agent hydration, preventing transcript properties from overriding sidecar request policy. Chat initialization preserves the incoming buffer-modified flag around this derived cleanup: stripping configuration must not turn a clean read-only follower into a locally edited buffer that cannot follow or request control. Existing local edits remain modified, and later setup hooks do not share this exemption. Restored sessions rebuild model, effort, and preset from the sidecar (mevedel-model-apply-session-policy, mevedel-preset-restore-session).

Resume contract

On-disk state normally reflects a completed turn boundary. Pending tool calls remain non-recoverable. Abort/error teardown is an explicit save boundary after prompts, agents, and the current request have been cleared, so resumed sessions do not resurrect aborted runtime state. Managed execution registries are likewise transient: resume never reattaches an operating-system process. After acquiring the session lock, resume queues a model-visible reconciliation reminder: prior commands may still run or have partial effects, so the next turn must inspect current state and prefer the newest user request. Aborting a live root request queues the same reminder before the explicit save boundary. ToolCall is the narrow exception to the non-recoverable tool rule: its guest machine remains transient, but the sidecar stores the envelope and bounded child audit. Resume converts a surviving checkpoint to an interrupted ToolCall row, never resumes the script, and commits the repaired segment together with clearing the checkpoint. Resume also atomically reconciles running Bash rows across the restored segment and its archived predecessors before rendering the view. The scan proceeds newest to oldest: a later execution-archive or execution-completion record marks an older copy as archived/superseded. Structured execution rows in later segments provide the same successor evidence, including rows retained in a compacted tail; a row with no successor becomes lost. Reconciliation can change transcript length; both fork staging and restored-buffer hydration reserialize GPTEL_BOUNDS before saving or publishing repaired text, even when no ToolCall checkpoint inserted anything. This keeps later tool and hidden-audit spans aligned after reload, including staging buffers without gptel save hooks. These repair saves rewrite only GPTEL_BOUNDS, not gptel's full save state: hydration precedes the chat setup that keeps gptel's request configuration out of the drawer.

After acquiring a PID lock, resume reconciles the sidecar segment counter with the files on disk and finalizes the predecessor when recovering a newer segment. Recovery persists the repaired counter and prompt index in the sidecar, so a second resume reads the same predecessor coordinates. Portable sessions instead restore the committed publication head; unpublished segment files never advance their counter. Read-only inspection repairs neither.

An active persisted Goal is restored paused, with an explicit session-resumed reason; opening a session never dispatches Goal work. /goal resume is required to continue. Rewind preserves session preset settings but clears Goal state.

Explicit session migration

The standalone migration script accepts a session directory or project root and upgrades sessions in place to the latest format. Close the selected session, or every session in the project, and keep them closed throughout migration. From the repository root:

# Migrate every session in a project:
npx @emacs-eask/cli emacs --batch -L . -l scripts/migrate-sessions.el \
  -f mevedel-migrate-session-main -- /path/to/project

# Or migrate one session:
npx @emacs-eask/cli emacs --batch -L . -l scripts/migrate-sessions.el \
  -f mevedel-migrate-session-main -- /path/to/project/.mevedel/sessions/SESSION-ID

There are no version selectors, prompts, separate artifact-migration commands, or manual directory swaps. --help prints usage. The command reports each session's outcome and the backup path when it changes a session. It exits zero when every selected session is migrated, already current, or closed before ever being saved; any failed session makes the exit status nonzero. A project run continues past individual conversion failures. Unsupported versions are failures, not successful skips. A live session or pending recovery aborts the whole run before any session is changed.

Supported source versions are v0.5.6, v0.5.9, v0.5.10, v0.5.11, v0.5.12, v0.5.13, v0.5.14 and v0.5.15; the current target is v0.5.15. Versions v0.5.7 and v0.5.8 are not supported. Each source follows an ordered chain of version-to-next-version steps. The script checks that the chain ends at the runtime codec's current format. To retire old support, remove the oldest prefix of steps and their unused conversion functions; the first remaining source becomes the minimum supported version. Retained steps do not need rewriting. Runtime readers and writers still accept only the current schema.

Safety and backups

Conversion validates a private staging copy, preserves session identity and transcript bytes, and updates retained publication sidecars and their checksums. Before installing the result, it rechecks that the source has not changed. It moves the original to a unique backup outside session discovery, then installs the converted copy at the original path. An ordinary installation failure restores the original path; backups are never overwritten. Already current sessions are not rewritten or backed up. A migration lock prevents two script invocations from operating on the same sessions directory concurrently. Backups are hidden siblings named .migration-backup-SESSION-ID-*/session; temporary staging parents are .migration-stage-*, and the lock is .migration-lock. If a process is killed during the rename window, inspect those locations and restore the original session path before retrying; do not open both copies as writable sessions. A lock left by a killed migration must be removed only after checking that the migration process has stopped.

Migration refuses PID locks, live leases, pending recovery, invalid metadata, checksum mismatches, symlinks and special files. An active lease past its expiry, as a crashed Emacs leaves it, counts as closed. Publication collection can leave an older retained manifest naming data files it removed, and older generations may hold unsupported sidecars. The copy drops such unreadable retained manifests, and a portable session's stale fixed sidecar, while keeping their data files. The published head itself must be complete and convertible. A released lease naming no head is a session closed before it ever saved and has nothing to migrate. A failed conversion leaves the original session in place and removes its staging copy. Backups are retained for manual inspection and removal, not opened or migrated automatically.

Preserved state and format defaults

Conversion preserves native-history references, titles, naming states, attachments and already-durable recovery state. A Goal predating incomplete-usage tracking gains an explicit complete-usage flag; an invalid Goal refuses conversion instead of being dropped silently by the runtime loader. Fields an older format never persisted start empty: queued input before v0.5.10, attachments before v0.5.11 unless imported below, naming input before v0.5.12, and chat boundaries before v0.5.13. Retained agents from before v0.5.14 gain an explicit :working-directory nil; migration does not guess a per-agent override from the root directory or transcript location. A legacy record already containing that field is refused instead of overwritten. v0.5.15 consolidates frozen mode-constraints, full-auto-mode, and full-auto-mode-exit reminder recipes into one argument-free mode-constraints recipe, preserving all other reminders and their order. Invalid legacy mode recipes refuse conversion. Runtime readers accept only the current recipe shape; close and explicitly migrate older sessions before reopening them. Unsaved queues and earlier chat titles cannot be reconstructed. Retrying an old attempted title asks for a description rather than guessing from history. Migration makes no model requests, creates no worktrees, and does not resume interrupted native turns.

The fork-first Worktree handoff changed within v0.5.13, so migration also checks its saved retry shape even when the top-level version is current. A summary retry without a recorded summary or target changes to fork-first preparation, preserving the accepted plan and selection. Here retries and current fork-first retries remain unchanged. A legacy retry with a cached summary or prepared target is refused because its effects cannot be inferred safely. Recover that handoff manually; migration neither deletes the target nor resets or replays implementation.

Legacy artifacts

The same command imports pre-v0.5.11 per-session artifacts into the project's artifact store, attaches their ids to the converted session, and removes its old artifacts/ folder and manifest entries. The session is not made their dedicated session. Project ownership is inferred from the session's location under PROJECT/.mevedel/sessions/; a detached session holding legacy artifacts is refused rather than writing to a guessed project. A store-aware session still holding legacy artifacts is also refused rather than losing existing attachments. Artifact imports require every session in that project to be closed, even when the command selects only one session, because they write the shared store. Metadata-only single-session migration does not require closing unrelated sessions.

Portable artifacts come from verified publications, never fixed caches. PID session artifact enumeration excludes hidden files and backups; links anywhere in a session are refused before importing. Files retain their payload names and bytes; whiteboards and documents retain their ids unless a fork's copy diverged. Identical fork copies reuse the same imported artifact. Each new artifact starts with one version, with bookkeeping in .state/ID/. Legacy artifact comments are not carried over: discussions replaced them without a migration. Unrecognized files in the reserved artifacts/shared-editing/ namespace refuse migration rather than being silently discarded.

Metadata validation precedes store imports. A failed import leaves the source session unchanged; completed imports may remain in the store and are reused on retry only while their payload or shared-item state still matches the legacy source. A changed source or imported artifact, or a missing imported payload, refuses the retry rather than overwriting either copy or attaching stale content. Resolve that difference before retrying. Interrupted individual imports remove their incomplete new item.

Incompatible session inspection

Session discovery reuses control observations only within its current enumeration. In particular, a pinned probe that found no portable lease is sufficient for that enumeration's PID profile check. Failed probes and contradictory controls still reject the entry; a later discovery probes again. An activated successor can prune lease generations between the batch's listing and record reads. If those observations yield no live record, discovery checks whether the generation names changed and reads the replacement set once. A nonempty replacement set that also yields no usable lease reports a discovery error rather than silently claiming the session has no publication. Unchanged records and manifest failures are not retried; lease, path and sidecar validation still apply to the refreshed observation.

Only a sidecar whose format is exactly the current mevedel-session-codec-format-version and whose complete current shape validates is resumable. The session chooser keeps surviving directories with missing, unreadable, unsupported, or obsolete sidecars visible as Inspect rows instead of silently omitting them. Each row reports the rejection reason, age, whole-directory disk usage, and absolute path; all incompatible directories are sized by one target-side program, and an independently failed measurement is shown as size unavailable. Compatible-only listing APIs and ordinary view redraws do not calculate disk usage. Restoring such a directory directly signals an error; no minimal session is synthesized from its name.

Selecting Inspect branches before resume authority calculation. It never deserializes or hydrates the sidecar, acquires a lock or lease, repairs or migrates state, materializes a publication, or initializes a session, request, composer, gptel mode, or Mevedel view. It opens an unbacked, unmodified, read-only bare Org buffer with saving, autosave, backups, and lockfiles disabled. Persisted presets and GPTEL_BOUNDS are not restored.

For a PID-lock file-workspace session, inspection enumerates the actual canonical segment-NNNN.chat.org regular files, preserves gaps, orders them by segment number, and reads the selected fixed file. For a portable project session, it enumerates only the canonical segment entries in the already validated immutable publication manifest and verifies the recorded SHA-256 before display. Mutable fixed portable caches are never fallback evidence. A single authoritative segment opens directly; several prompt in chronological order with the newest as default; zero authoritative segments fail closed. A directory whose discovery itself fails -- contradictory control artifacts or an unreadable publication -- is listed as undiscoverable: it remains visible with its diagnostic but is never inspected, because no authoritative transcript bytes can be resolved for it.

Archived segment inspection

mevedel-view-segments.el owns the ephemeral inspection buffer and switching state; mevedel-session-artifacts.el remains the sole owner of segment descriptors and bytes. Source-backed expand/collapse state within either projection remains owned by mevedel-view-disclosure.el.

The session cockpit's Navigate submenu projects persisted segments in the existing view: [ shows the previous segment, ] shows the next, and g chooses one directly. The picker lists the canonical range from segment 1 through the live segment, including each segment's latest prompt preview and readable, missing, or unreadable status. Adjacent navigation reports the exact broken path instead of skipping it; the picker lets the user bypass that segment. In a session that /clear divided, each row also names its chat, and c (M-x mevedel-view-go-to-chat) chooses a chat directly: an earlier chat opens at its last segment, the current chat returns to the live segment. The live view shows the earlier chats, newest first, on one line below its header; RET or a click on it opens the chat picker. The line is header chrome, so history begins after it.

Each archived projection renders exactly one segment and is read-only. It does not merge earlier segments, modify the authoritative data buffer, or become a resumable session state. The view remembers cursor, window, and fold state ephemerally while moving among segments; the cursor and window start return to the same rendered content by semantic render anchor, since a segment projected again need not match its earlier length. The live composer draft remains hidden and unchanged, and [Latest] returns to the live segment. Fresh resume always starts at latest.

Finalizing an archive restores its transcript properties before inserting the finalization timestamp, then stabilizes the saved absolute GPTEL_BOUNDS against the updated drawer. This applies both to local segment files and portable publication strings. Restoring the archive therefore preserves speaker roles and ignored internal notes even though the metadata has moved the body. The predecessor's prompt positions and completion cutoffs are refreshed against that exact finalized text before the transition sidecar is prepared. Existing continuation entries pointing into the predecessor are refreshed by fork-point identity, including when later prompts share the segment. A missing indexed completion fails preparation rather than silently removing completed evidence. Portable publication commits text and index together. File-workspace transitions publish the successor and frozen finalized predecessor before committing the sidecar; instruction saving follows that commit. An error or quit before commit restores the predecessor's original bytes and index. After commit, or while a portable publication remains pending, cleanup retains the new text/index pair. Keyboard quit is deferred through publication bookkeeping and rollback.

Live work may continue while an archived segment is displayed. Status, interaction, and request-progress chrome stays live, while streaming transcript updates wait until the user returns to latest. Live-tip actions such as Send, follow-up, Compact, Review, Verify, and slash commands are refused. Fork, Rewind, and conversation-variant switching use the historical assistant response at point. A successful Rewind returns to latest; cancellation and no-op Rewind remain on the archived projection.

Rewind

Rewind removes everything after a boundary, and each entry point places that boundary on the side that matches what it names. mevedel-view-rewind-at-point, also available as R in the session cockpit, names the assistant response at point: that turn is the last one kept. mevedel-rewind picks one of the user's own prompts across all segments and returns to just before it, so the prompt and its answer are discarded and it can be asked again -- which is the reason to select a prompt at all. The directive Rewind before this implementation... action is a before boundary for the same reason.

Coverage is complementary rather than duplicated: the point route reaches every boundary that follows a response but cannot empty a session, since no response precedes the first prompt, while the prompt picker reaches every boundary before a prompt including the first, which is the empty state. Both routes show the same impact, name the boundary and both sides of it, and require explicit confirmation.

The Navigate submenu's n/p actions move through rendered displays for inspection, while C-n/C-p move through user queries. These navigation actions change neither transcript nor session state; Rewind remains a separate explicit operation.

Rewind is an in-place logical transaction. It discards historical transcript and managed session state after the boundary, restores every captured working-tree file to the state that boundary owned, and keeps the same session identity, name, directory, working directory, and lineage. The candidate session state is the single place the boundary is decided; the transcript cutoff, instruction snapshot pruning, and remote staging pruning all derive their answers from its surviving turn count, and the file plan reads a turn's pre-turn checkpoint for a before boundary and its post-turn checkpoint for after. File-workspace sessions stage and swap a session directory with a rollback tree. Portable project sessions leave the directory and control state in place and commit one complete replacement manifest through the owned lease head. The impact lists the discarded prompt suffix in order, including ordinary chat and complete directive turns, alongside restored files and every known gap. External working-tree changes to captured files are overwritten. Git HEAD and the index are not changed, so the impact identifies staged files whose index content will diverge from the restored working tree. Failure before the commit point rolls back both session and file changes, including a live transcript already replaced during local publication. Portable project rollback first revalidates the same lease authority and never substitutes fixed session caches for the captured manifest. A failed rollback reports every inconsistent path and promotes its temporary repair directory to the target-side specialized recovery marker and bytes described above; a successful Rewind removes those rollback bytes and does not create a recovery tree. Every settled model turn, including the first, owns a durable pre-turn checkpoint. The impact marks coverage as complete or lists known gaps; gaps do not disable Rewind and are never presented as restored paths. Rewind creates no child session. A portable session's superseded head stays published, so mevedel-redo can restore its conversation as described below; a PID-lock session has no redo. Existing child sessions and worktrees are not removed; children forked after the target become detached from the Source's visible history.

Workspace-owned directive identity is not historical session state. Rewind retains each authored directive and its current request, removes only model activity at or after the target turn in that execution session, and recomputes the lifecycle from what survives. A surviving request edit remains Ready with a request-changed qualifier. Subdirectives consumed by discarded successful attempts return to their parent from immutable attempt snapshots; subdirectives authored later remain current, and neither receives independent activity. Historical instruction snapshots restore source presentation only; a source file restored by Rewind uses the normal safe anchor reattachment path. The directive turn or read-only inspector's Rewind action resolves an effectful attempt's exact execution session and pre-turn checkpoint, then uses this same Rewind transaction and impact confirmation.

Only a committed Rewind emits SessionStart(rewind); it does not emit SessionEnd. Any context produced by that event belongs to the next accepted prompt. Cancellation, rollback, and an empty impact emit no Rewind lifecycle event. Selecting the first prompt in the picker discards that prompt and its answer, which empties the session; selecting the latest response at point keeps everything and reports an empty impact instead.

Current session settings survive. Tasks, Goal, retained agents and mailboxes, pending Plan state, permission queues, and execution state are cleared because they do not have a trustworthy per-turn journal. Session-owned local/ is deliberately not per-turn state and remains unchanged.

Rewind refuses while the session has live executions and points the user to /ps and /stop; hiding a process behind older history would violate its session ownership boundary. It also requires aborting the current provider request, interrupting active agent turns, and pausing an active Goal before changing the source conversation.

Rewind, /clear and Save As also refuse while either pending-input category is nonempty, since a copied queue would be delivered from both sessions. The user must resolve the entries in the Pending Inputs cockpit or explicitly clear them with C-c C-q before a destructive transcript operation.

Redo

mevedel-redo, also available as D in the session cockpit beside Rewind's R, restores a portable session's conversation to one of its superseded published heads. The cockpit entry is inapt for a file session, which publishes no immutable heads and therefore has nothing to restore. Every committed head is immutable, so the state a Rewind moved away from is still on disk as a complete, hash-verified generation; redo is an exposure of that state rather than a new mechanism.

The picker lists turn states, not generations. A generation is written on every committed save, so mevedel-session-rewind-published-heads groups them by the turn count and fork point their published sidecar records, offers the newest generation of each state, drops the state the session is already in, and drops heads captured mid-turn -- a head whose latest prompt is one turn ahead of its turn count restores a half-arrived response, which is a recovery state rather than a choice. Rows are labelled with segment, turn, and that turn's prompt through mevedel-session-rewind--prompt-label, the same label the Rewind picker uses, so undo and redo read as one vocabulary over turns.

Restoring republishes the chosen generation as a new committed head under the session's reserved lease: the artifacts are materialized into a temporary staging root, the staged sidecar becomes the candidate logical state, and the whole set is published exactly as a Rewind publishes its replacement. Because the restore is an ordinary publication and not a rollback, the head it moves away from stays published in turn, so redo composes in both directions instead of consuming history. Where a session lives and what it talks to -- save path, execution target, working directory -- stays live and is never adopted from the snapshot.

Redo returns the transcript, sidecar state, instruction snapshots, retained agent transcripts, and persisted tool results, and it restores captured working-tree files in the same transaction. Captured bytes live in published file-history artifacts indexed by the sidecar's per-turn snapshots: a Rewind trims that index while republishing the bytes, so the head being restored still names its own captured file state. Coverage is Rewind's coverage -- uncaptured filesystem effects remain -- and workspace directive records a Rewind pruned do not come back.

Because files are involved, redo is a two-phase transaction and follows Rewind's ordering. Preparation reads only immutable published bytes, so materialization, the file plan, the modified-buffer prompt, and the confirmation all run with no lease held. The confirmation names the turns and captured files the restore returns, plus any captured file whose current contents changed externally and would be overwritten. One reserved-lease operation then rechecks that the current head is still the one the caller saw, rechecks the confirmed file plan, backs up current contents, and restores the files; the head CAS follows, and any failure before it rolls those files back. Declining the confirmation mutates nothing.

A PID-lock file session publishes no immutable heads and therefore has no redo, which is why the Rewind confirmation promises conversation and captured-file redo for a portable session and none for a file session. Redo refuses under the same stable-source conditions as Rewind, and a failed buffer install degrades to a warning naming its own frame rather than abandoning the refreshes that follow a committed head.

Fork

f in the session cockpit arms a Conversation Fork and F arms a Worktree Fork from the settled assistant response at point. Both focus the existing composer draft. The interaction row identifies the assistant turn and fork type, and the turns after that response are dimmed; [Cancel] or C-c C-k disarms it without changing the draft. An empty prompt, a local command, a failed syntax or mention preflight, or cancellation creates no child.

The next preflight-valid model-bound submission publishes an ordinary child session whose transcript ends at the selected response. Child then emits SessionStart(fork) and owns skill expansion, UserPromptSubmit, and the request. The draft and its referenced grants transfer from Source at publication. A Child hook rejection keeps them in the published child for revision without sending a request. The Source transcript, sidecar, lock, workflow state, and checkout remain live and unchanged. The child keeps the Source working directory and restores no files, so Conversation Fork also works outside Git. A sparse fork-provenance reminder regenerates the source session, shared working directory, and current-file caveat from durable child state; its complete injection record preserves the disclosure for both the user and subsequent model requests until compaction retires it.

Worktree Fork requires Git, its worktree command, and a supported checkout on the Source session's execution target. Dependency and repository preflight run before a branch or directory is created. It creates a linked worktree at the Source checkout's current HEAD, restores captured repository-local files from the selected turn before the first prompt, and retargets valid repository-local snapshot, permission, grant, and mention paths to the child checkout. An unavailable individual backup leaves that child file at HEAD and does not block the fork; a one-shot restore report on the child's pending reminder FIFO names every such file and every malformed copied path record that was dropped. Invalid restoration metadata, an unsafe target, or an unreadable required history store still aborts. Uncaptured files retain their HEAD contents, uncommitted Source changes are not copied, and captured external paths remain shared and non-isolated. Sparse fork provenance records the worktree path, branch, base commit, and uncaptured-file semantics; the one-shot report carries the restored-file count and restoration exceptions. The FIFO is transient, so a restart before the first child request can drop the report detail while the durable provenance facts remain available for regeneration. Failure outside Git never falls back to Conversation Fork.

mevedel-session-fork-create owns admission, staging, publication, and cleanup for both fork types. Worktree creation and restoration are conditional steps inside that transaction, with failures retaining the created Git artifacts and reporting their cleanup command.

The common fork projection copies current model, preset, effort, mode, permission, skill, reminder, and hook configuration into independent child-owned values. Prompt indexes, file snapshots, skill history, historical agent transcripts, and accepted-plan evidence stop at the fork point. Tasks, Goal, pending Plan/handoff state, addressable agents and mail, pending inputs, requests, interactions, queues, executions, callbacks, logs, caches, and one-shot prompt context start empty. Ordinary session-owned local content is copied into independent child state rather than shared, but managed local/plans/ state is projected separately: the mutable current plan and unrelated evidence are discarded, and only an accepted artifact that is valid at the fork point is preserved. Only dropped-file grants referenced by the transferred draft move to Child. A fork carries the Source's attached artifact ids, not artifact bytes: both sessions work on the same store artifacts. Duplicate in the artifacts cockpit or the browser's store list makes an independent copy.

Conversation and Worktree children receive independent session IDs and initially use those IDs as display names. Each child's first new authored prompt generates its own title; inherited history is not naming input. Their sidecars retain the Source ID, cumulative fork turn, stable fork-point ID, and fork type. Worktree branch and directory allocation continues to use the first unused Git/worktree suffix, independently of session display names.

Once a Fork exists, B switches variants for the exact assistant response at point. The shared assistant header also shows a text switch such as [⇆ Source · 2 variants], [⇆ Conversation · 2 variants], or [⇆ Worktree · 2 variants]. It remains available when the turn is folded. With one alternative, RET or mouse-1 opens it directly through the normal session restore path and positions its view at the same stable fork point. With several alternatives, the same action opens a stable chooser ordered Source first and then direct Forks oldest to newest. It marks the current session without moving it and shows each session identity, working directory, latest prompt, sharing status, and Worktree branch/recovery state. Switching redraws only source-backed history; each owning view keeps its composer draft and the sessions keep their working directories.

A fork-point group is a star discovered from persisted Source identity plus stable fork-point identity; Source stores no child list. A Child cannot fork its inherited response again and is directed back to Source, but a later Child response can become the Source of a separate group. Deleted, expired, or rewound-past variants disappear independently. If Source disappears or no longer contains the exact stable point, surviving direct Children remain a detached sibling group and Source is omitted. Reusing a descriptive turn number cannot reconnect that group.

Arming F reserves that exact branch and directory. Forking from an existing linked worktree creates a sibling under the workspace's .worktrees/ directory from that checkout's current HEAD. If a later staging or publication step fails, the created branch and worktree remain intact and the error reports their names plus an exact git worktree remove/git branch -D cleanup command. The armed composer keeps the same reservation, draft, and grants; retry reports the existing-artifact conflict instead of allocating another suffix. Creation rejects symbolic links at the .worktrees/ boundary and revalidates resolved workspace containment immediately before Git mutation.

Renaming a materialized session preserves live execution ownership and every artifact path. Existing session-relative work:// addresses and fork references remain valid because rename changes neither the ID nor the directory.

Agent transcripts

Retained-agent transcript files live under agents/. The sidecar's :agent-transcripts alist records presentation metadata for handles and terminal transcript inspection: status, counts, timing, reason, and verdict. Per-tool activity history is not persisted; it had no reader and made up a third of a large session's sidecar, and loading a sidecar drops the retired key. The separate :agent-registry is the addressability source of truth; it persists canonical and parent paths, role and frozen configuration, activity, unread mailbox, conversation location, and internal storage identity, plus each retained agent's latest settled payload and terminal outcome when present. Equal frozen configurations are one shared object, so the sidecar, printed with print-circle, carries each agent type's configuration once and references it from every other record; decoding copies each record's own. The RESULT mailbox remains a bounded preview; history addresses use the retained transcript identity.

On normal resume, a persisted active turn has no surviving provider request. Recovery settles it once as interrupted, releases its capacity slot, preserves the retained identity, conversation, and unread mail, and queues a canonical RESULT for its spawn parent. Read-only attach observes the on-disk state without rewriting it.

Live transcript views render directly from the running agent buffer. They do not restore or normalize saved GPTEL_BOUNDS while the agent is streaming, because partial reasoning/tool/system blocks may not have their closing marker yet. The session property normalizer treats such incomplete structural blocks as unclassified text until a complete block is present. When repairing persisted metadata, it only reclassifies tool-shaped org blocks that already carry a tool gptel property or overlapping non-empty GPTEL_BOUNDS tool id; pasted transcript text that happens to contain #+begin_tool stays ordinary user/ignored text.

Input history

The view input ring is persisted at <workspace-root>/.mevedel/input-history.el when the session is writable. Missing files are normal. Corrupt files are warned about once, renamed aside, and replaced with an empty in-memory ring. Accepting same-turn steering or a queued follow-up records its text in this ring immediately, independently of the transient queue state.

Generated state excludes

When mevedel writes generated workspace state, it best-effort appends exact entries to .git/info/exclude instead of ignoring the whole .mevedel/ tree. In a linked worktree, Git resolves that file to the repository's common exclude file. The generated entries are:

  • /.mevedel/sessions/
  • /.mevedel/tool-results/
  • /.mevedel/input-history.el
  • /.mevedel/leases/
  • /.mevedel/state/

Locking

File-workspace .lock files prevent concurrent edits. Same-host active lock → break / read-only / abort prompt; same-host stale lock → prompt to break; cross-host → break / read-only / abort prompt. Same-host locks are stale when their PID is dead or their recorded process identity differs from the live process's identity. On Linux that identity is the kernel boot UUID plus /proc/PID/stat's start tick count; time zones, daylight saving and wall-clock adjustments cannot turn the same process into a different holder. The lock's invocation timestamp is diagnostic only. Missing or malformed identity evidence, including on hosts without these /proc records, keeps a live PID active. Locks written without identity evidence can still be reclaimed when their PID is dead, but cannot establish PID reuse. Malformed PIDs and failed or unavailable process enumeration are not evidence of a dead holder. Process checks run on the Emacs host even when the current directory is remote. Lobby opens reclaim provably stale same-host locks without confirmation, using the same checked replacement; ordinary Emacs acquisition retains the prompt. This does not change portable leases.

Portable project sessions never use client PIDs as liveness authority. Their .lease/ directory contains portable generation records with an opaque client id, renewal and expiry times, and a diagnostic buffer name. A record is authority only while its timestamps are finite nonnegative numbers: these are file bytes another machine wrote, and an infinite expiry would never expire while a NaN fails every comparison it is put to, so either one is rejected rather than compared. A new owner exclusively creates the next generation in claiming, then activates it only if the immediately preceding live generation is still exactly the record it observed. When the claimant already observed that record's bytes as the newest generation, the fencing create, a verify of those bytes, the activation, and the prune run as one program. A failed verify stops the program before activation, and the candidate is aborted. Otherwise the create reads the older records back, and activation follows as a second program. A generation appearing beside a live owner's record without activating does not end that owner's lease, so its renewal heartbeat keeps running until another client's claim is actually active. Every complete acquire, renewal, and release transaction disables remote file caching so an external client's generation is observed consistently. Renew and release can update only their own generation and verify that no newer head won. A successful claim prunes older generations best-effort; aborted candidates are ignored and removed best-effort.

Each generation also carries :publication-head, initially nil and otherwise a validated .publications/.../manifest.el session-relative path. A new generation inherits its predecessor's head; renewal, status changes, and release preserve it. The publisher may replace it only through an exact-head generation check, so a stale client can update only its older record and cannot commit over a newer owner. The current head can be read uncached before a session object exists without acquiring or mutating the lease.

Each generation also carries the required boolean :unsettled-mutation latch. Managed mutation on a portable project target sets it before child launch. Renewal, publishing, release, and takeover preserve it. Proven settlement clears it by updating the exact owned generation only after no other armed mutating record remains; an unprovable result keeps it set. A restored owner therefore blocks mutation without reconstructing process records, and explicit acknowledgement must durably clear the owned generation before transient blocking state is removed.

The target-side .recovery/ marker is independent of lease liveness. Releasing or losing a client lease never deletes it, and a successor refreshes it after takeover before mutation admission. This preserves manual repair material until the successor explicitly confirms destructive abandonment.

The persisted states are claiming, active, publishing, released, and aborted. Unexpired claiming, active, and publishing records fence other clients. An interrupted claiming or publishing record remains bounded by its expiry, and its takeover still requires explicit confirmation. A non-owner may inspect the last published state read-only. Killing the owning data buffer cancels its renewal timer and marks only that client's active or inactive publishing generation released; it never interrupts live publication or deletes a newer owner's generation. Retry can normalize the same client's still-live publishing generation back to active before reserving a new window.

Auto-cleanup

Workspace journal expiry runs before session-cleanup eligibility checks, so disabling session expiry or using a TRAMP workspace does not disable it. mevedel-journal-max-age-days independently defaults to 14 days of ordinary recall from immutable entry creation (nil disables recall age filtering). Expired entries are hidden from ordinary recall even when their bytes must remain. A cleanup opportunity throttled to once an hour, offered by completed root turns and, on local Linux workspaces with an open root session, by the idle maintenance timer, plus idle work after decisions, retires eligible evidence through recoverable accepted manifests, preserving unreviewed digests, pending captures, evidence pins and unresolved proposal/write dependencies. A successful general no-action review counts as processing. Processed notes retire once references end, regardless of age. Resolved review/decision history has its own default 14-day window after the latest terminal decision and expires as a complete dependency group. Bounded coordination cleanup runs even when recall age filtering is disabled; capture coverage survives. See memory for the retention and human inspection contract.

Pending journal capture pins under a session's .journal-pins/ prevent that session from expiring. Publication-generation collection also retains each pinned head and every artifact file referenced by its manifest. Pins survive independently of a live buffer; publication or explicit discard must release them. An unreadable pin blocks generation collection, and even a malformed pin keeps its session from expiring. Completed root-turn autosave creates these pins before generation collection. Local portable root-turn settlement prepares evidence in a child and retains admission until the editor validates the unchanged source and publishes the pin; explicit checkpoints remain synchronous. A checkpoint containing all of an unsealed predecessor's turns replaces that predecessor's pin only after its own pin and ready marker are durable. Compaction success, /clear, a quiet period of mevedel-journal-seal-idle-minutes after the last completed root turn, root-buffer close, and Emacs exit seal completed checkpoints. Turn completion, idle sealing, successful root compaction, session close, session opening, and the idle maintenance timer queue background processing; Emacs exit cancels queued/active processing and timers and starts no inference. Accepted digest outcomes recover on the next processing opportunity before replacement inference. Completed root turns, session opening, and idle maintenance also queue abandoned-checkpoint recovery before processing, so work sealed at exit is processed when a session next opens. See memory. It repairs unready captures and seals frozen completed work under temporary source authority, without resuming a conversation or retaining authority during inference. Live or foreign PID holders, unreadable locks, live portable leases, publishing leases, unresolved mutation, and reserved transfers are left for normal session recovery. Expired ordinary portable owners are fenced through generation election; the committed session head is preserved. Recovery completes interrupted superseded pin release only after checking the successor's retained turn coverage.

mevedel-session-max-age-days (default 30) deletes expired sessions, including sessions whose sidecars are obsolete, unreadable, or missing. The mevedel session chooser sweeps synchronously before listing. Opening a session and ending a root turn offer the workspace a sweep that runs after the current command, once its transport is idle and no input is pending, so a long-running Emacs keeps expiring sessions. Exit cleanup scans every workspace registered during an interactive Emacs invocation before releasing live-session locks. A batch Emacs never sweeps at exit or from session activity: scripts and test subprocesses register workspaces incidentally, typically their working directory. Cleanup uses :updated-at when available, otherwise the sidecar or session directory modification time. The mevedel-session-keep-recent-count (default 3) most-recently-updated sessions are exempt regardless of age: the chooser sweeps before any lock is taken, so without this floor a long absence would delete the very session the user came back to resume. Cleanup skips sessions with journal pins. Each workspace is swept at most once a day per Emacs invocation: ages count in days, so a more frequent sweep finds nothing new, while the daily repeat keeps a long-running Emacs from retaining sessions indefinitely. Expired session cleanup removes its local/ scratch directory with the rest of the session. nil disables cleanup.

File-workspace cleanup skips active locks. Portable project cleanup skips a session open in this Emacs and otherwise deletes through mevedel-session-durability-delete-abandoned: the lease head must be absent, released without a control-transfer reservation, or an expired active or claiming record without unsettled mutation, and its last renewal must also be older than the cap. A head in another lease format (such as records written before control transfer added :transfer-generation) qualifies only when its numeric expiry is itself older than the cap; no owner of any format leaves its expiry that far behind. Unchanged saves keep the sidecar's :updated-at, so the renewal is the later proof of use. Cleanup claims the next lease generation without prompting, rechecks recovery markers and pins, and deletes the directory in one control program only after proving its claim is still the newest generation. A competing claim from another client keeps the session, and any kept session's claim is released.

Chooser cleanup still runs before incompatibility discovery. Consequently an expired session may be deleted before it can appear as an Inspect row. Inspection itself never archives, deletes, or modifies persisted session files.

This file-session retention pass does not run journal maintenance. Journal retention uses its own scheduled lifecycle opportunities; neither the chooser nor the exit retention pass drains it synchronously.

System sleep

When mevedel-sleep-inhibit is enabled (it is off by default), mevedel-sleep.el holds one operating-system sleep inhibitor while this Emacs has work that sleep would interrupt. The work that counts is:

  • a root or agent turn that is running or settling (mevedel-busy-p), including one waiting on a permission prompt (indefinitely, unless mevedel-permission-queue-timeout expires the unanswered queue; see permissions);
  • a live managed command other than Monitor, including a yielded Bash command that outlives its turn (mevedel-execution-working-p);
  • an open collaboration share or lobby within its idle allowance (see collaboration).

Monitors are excluded from the working-execution predicate: waiting for an event alone must not keep the machine awake. A request awakened by a Monitor event or Bash completion is still a running or settling turn and holds the inhibitor normally.

Only sleep is blocked. Screen blanking, locking and idle detection are unaffected. The inhibitor announces its reasons, such as request running, command running, so systemd-inhibit --list shows why the machine stays awake.

Work owners request an evaluation when work begins: request admission, a change in the live execution set, and collaboration start and guest arrival. These requests only matter while nothing is held. While any reason holds, a 30-second poll re-evaluates them, refreshes the announced reasons and releases the inhibitor once none remains. Turn and execution teardown therefore need no release path of their own, and a short command does not replace the inhibitor. When the reasons change, mevedel acquires a replacement before releasing the old inhibitor. The poll stops once no reason remains. A reason source that signals an error counts as no reason and warns once, so it cannot hold the lock indefinitely.

The inhibitor is a child process (systemd-inhibit ... cat by default). Its standard input is a pipe from Emacs, so the lock also goes away when Emacs exits or is killed. If the inhibitor cannot start or exits unexpectedly, for example when polkit refuses it, mevedel warns once. It retries only after all reasons have cleared and work begins again. Remote execution targets are not inhibited: the local turn already keeps this machine awake, and the default polkit policy does not let an SSH session block sleep. Toggling mevedel-sleep-inhibit through Customize applies at once: disabling it releases a held inhibitor, and enabling it evaluates the current work. A plain setq takes effect at the next evaluation. On platforms without systemd, set mevedel-sleep-inhibit-command to another command.

Defcustoms

Defined in mevedel-session-persistence.el unless another module is named:

  • mevedel-sessions-directory (default .mevedel/sessions/)
  • mevedel-session-max-age-days (default 30)
  • mevedel-session-keep-recent-count (default 3)
  • mevedel-file-history-max-snapshot-bytes (default 1 MiB)
  • mevedel-session-lease-seconds (in mevedel-session-durability.el, default 90; renewal runs from a timer that cannot fire inside blocking target I/O, so the owner reclaims its own expired lease)
  • mevedel-session-lease-renewal-seconds (in mevedel-session-durability.el, default 30)
  • mevedel-session-follow-published (in mevedel-session-control-transfer.el, default t)
  • mevedel-view-control-transfer-active-poll-seconds (in mevedel-view-control-transfer.el, default 2)
  • mevedel-session-publication-lease-seconds (in mevedel-session-durability.el, default 3600)
  • mevedel-view-input-history-size (in mevedel-view-history.el, default 500)
  • mevedel-sleep-inhibit (in mevedel-sleep.el, default nil)
  • mevedel-sleep-inhibit-command (in mevedel-sleep.el, default systemd-inhibit --what=sleep --who=mevedel --why=%s --mode=block cat)