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
PostToolBatchhook 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 SDKhook_responsefor 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, unlessmevedel-permission-queue-timeoutexpires 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(inmevedel-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(inmevedel-session-durability.el, default 30)mevedel-session-follow-published(inmevedel-session-control-transfer.el, default t)mevedel-view-control-transfer-active-poll-seconds(inmevedel-view-control-transfer.el, default 2)mevedel-session-publication-lease-seconds(inmevedel-session-durability.el, default 3600)mevedel-view-input-history-size(inmevedel-view-history.el, default 500)mevedel-sleep-inhibit(inmevedel-sleep.el, default nil)mevedel-sleep-inhibit-command(inmevedel-sleep.el, defaultsystemd-inhibit --what=sleep --who=mevedel --why=%s --mode=block cat)