Skip to content

feat(desktop): move a Session between Maka installations from the app #5182

Closed

Description

@Joob1n

Problem

Moving a Session from one Maka installation to another — a release build to a dev build, one machine to another — has no path through the app.

maka session-export and maka session-import landed in #5113 and #5139, but they only work with Maka closed. A Runtime Host takes an exclusive lock on its Storage Root at startup and holds it for its lifetime, and both commands need that authority to read or write the context-offload store. Run either while the app is open and it refuses:

Context import requires an offline Storage Root; stop the Runtime Host first

So the one surface where a user would reach for this — a menu item in the app they already have open — is the one place it cannot run.

Desired outcome

Export a Session (and the subagent subtree under it) to a .maka-session file from the sidebar, and import one from Settings, without closing the app.

What a Session is on disk

Not a row. A Session spans four things in a workspace, and all four have to travel:

workspace/
  runtime.sqlite            Session rows, events, tool calls
  context-offload.sqlite    index of offloaded payloads
  context-offload-values/   the payload bytes, at sha256/<prefix>/<hash>
  artifacts/<sessionId>/    files the Session produced

exportSessionBundleState() / importSessionBundleState() already handle all four. What is missing is a way to call them from inside the process that holds the Storage Root.

Proposed flow

The Runtime Host is the single process holding the Storage Root, so it is the one that can do this. It does not need to take the lock again — it already has it, and can lend it.

Export

  1. Walk the subagent subtree under the named Session.
  2. runSessionQuiescentMutation([root, ...subtree]) — refuses while any of them has an active execution claim, and serialises against other Session mutations, so no Turn starts mid-export.
  3. runWithStorageRootLease(owner.lease, 'interactive', 'write', ...).
  4. exportSessionBundleState() under that lease: back up the operational database, check quiescence on the copy, filter out everything the subtree does not own, snapshot context, copy artifacts.
  5. Pack to .maka-session.

Steps 2 and 4 are not redundant: the kernel fence stops a new Turn from starting, the on-copy check catches state that was already mid-flight.

Import

  1. inspect() the bundle's manifest, then hydrate() into a private staging directory.
  2. runWithStorageRootLease(owner.lease, ...).
  3. importSessionBundleState() under that lease: artifacts and context land first, Session rows last, so a failure before the final step publishes nothing and leaves the ids free for a retry.

No Session fence on the import side — the Sessions being imported do not exist locally yet, so there is nothing to fence by id.

What had to be verified first

Two questions decided whether the Host can do this at all.

Can the Host use the lock it already holds? Not through the current entry point. The Storage Root lock is an election (tryLock, fail fast), not a mutex, and it is not reentrant — a second exclusive lock on the same file from the same process is refused:

first  exclusive: true
second exclusive (same process, different fd): false

So withOfflineContextSnapshot needs a lease-bound entry alongside the electing one. withLeaseBoundArtifactWriterLock is the same pattern, already in the same package.

Does an import racing the Host's own writes corrupt anything? For the databases, no:

  • Only one process can hold the interactive write authority (verified across processes: the second is refused), and the production store can only be opened through that lease.
  • Neither side holds a SQLite write transaction across an await. put() publishes its managed file before entering a synchronous #writeTransaction, and the import's merge runs BEGIN IMMEDIATE → COMMIT with no await in between.

So the usage recompute always sees a settled table.

Correction (2026-09-11, from review on #5186): that is not the whole question, and the conclusion drawn from it was wrong. The Store's operations read database state, await, and only then act on files — collection decides a payload is unreferenced, awaits, and unlinks it. An import that commits a reference inside that await leaves the reference pointing at a file about to be removed, and collection's re-check cannot see it because the check and the unlink straddle the await. A root-scoped fence is needed, shared by the import, the Store's publication, and collection.

For the payload files, yes — and this is a present-day defect, not a future one. The store publishes a managed blob atomically (staging file → fsync → link), while the import uses copyFile, which is observable at its final path in partial states. Copying a 64 MB file and stat-ing the destination every millisecond:

copyFile  partial sizes observed at the final path: 38   [2580480, 4177920, 5259264, ...]
link      partial sizes observed at the final path:  0

Payloads are content-addressed, so a bundle and its target routinely hold the same hash at the same path. Today that is harmless because the import only runs offline. It is not harmless once the Host runs it — and it already bites offline in one case: copyContextValueTree swallows EEXIST, so an import that crashes mid-copy leaves a truncated payload that the retry silently accepts as already present.

Proposed split

PR 1 — storage

  • copyContextValueTree: publish through a staging file and link, matching SqliteContextOffloadStore#publishManagedFile; on EEXIST, verify the existing bytes instead of skipping. Fixes the truncated-payload retry above.
  • withOfflineContextSnapshot: accept a caller-supplied lease. Without one it elects an owner exactly as today, so the CLI path is unchanged.

PR 2 — Runtime Host and desktop

  • Two Host operations, shaped after protocol/external-session.ts, running under the Host's lease, with the Session fence on the export side.
  • Desktop IPC and file dialogs, a sidebar entry for export, and a Settings entry for import.

Credentials stay out: a bundle names its connection by slug and model and carries no key, and the importing side resolves the slug against its own catalog.

I plan to implement this.

Activity

  1. Dante-dan commented on Sep 11, 2026

    @Dante-dan
    Contributor

    I propose splitting this into a storage-safety foundation followed by the Runtime Host/Desktop surface.

    The first change would make copyContextValueTree publish each context payload through staging plus link, and verify an existing content-addressed target on EEXIST rather than accepting a possibly interrupted copy. In the same slice, I would add a lease-bound withOfflineContextSnapshot entry point so the existing CLI path continues to elect an offline owner while the Host can use the lease it already owns.

    The follow-up would expose export and import operations from the Runtime Host. Export would fence the selected Session subtree with runSessionQuiescentMutation before creating its checked snapshot; import would inspect and hydrate privately, then publish context/artifacts before Session rows under the Host lease. The desktop UI would only invoke those host operations and keep credentials out of the bundle.

    I will start by adding targeted storage tests for interrupted-copy/retry behavior and the lease-bound versus offline paths. Then I will cover quiescent subtree export, failed-import atomicity, and the Host protocol boundary before adding the sidebar/settings actions. That preserves the existing portable-bundle contract from #5113 and #5139 while avoiding a second lock acquisition inside the running Host.

  2. Dante-dan commented on Sep 11, 2026

    @Dante-dan
    Contributor

    take

  3. Dante-dan commented on Sep 11, 2026

    @Dante-dan
    Contributor

    untake

  4. Joob1n commented on Sep 11, 2026

    @Joob1n
    MemberAuthor

    take

  5. Joob1n commented on Sep 13, 2026

    @Joob1n
    MemberAuthor

    Done, across three PRs.

    #5186 Publish an imported context payload by staging and link, and let a caller supply the Storage Root authority instead of electing it
    #5196 State the portable snapshot shape once, in validateContextSnapshot; publish by creating rather than replacing; publish only the payloads the bundle declares
    #5197 Two Host operations under the Host's own lease, and Settings › Import/export tasks

    The design in this issue survived review with two corrections worth recording here, since the body above asserts both:

    "No workspace-wide fence is needed" was wrong. The reasoning — only one process holds the write authority, and neither side holds a SQLite write transaction across an await — is correct as far as it goes, and it does not go far enough. The Context Store's operations read database state, await, and only then act on files: collection decides a payload is unreferenced, awaits, and unlinks it. An import committing a reference inside that await leaves the reference pointing at a file about to disappear, and collection's own re-check cannot see it because the check and the unlink straddle the await. #5186 added the root-scoped mutation fence that closes it. The issue body carries a correction to that paragraph.

    Local-only, not Host-scoped. The path a native picker returns is a path on the desktop machine, and the protocol reads it on the Host's filesystem. Those are the same filesystem only for the Local Host, so the feature is offered only beside that target. Carrying a bundle to or from a remote Host needs a byte-transfer contract rather than a path string; that is not here.

    Thanks to @M4n5ter and @me2seeks for the reviews — 18 findings across the three, including two that were only reachable because the tests I had written exercised paths production never takes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions