A session is an artifact, not a service

Stella will hand you a session you can carry to another machine — and will not carry it for you. Why the line sits there, what Stella still owes you when the artifact lands somewhere new, and what it deliberately refuses to own.

Stella already survives a crash. Kill the process mid-turn and the next run picks up the resume point, with the no-clobber guarantee intact, because both live in a durable git store Stella keeps in its own directory.

What it does not survive is a machine. Your laptop dies, or you want to hand a half-finished session to a colleague, or start something on a desktop and finish it on a plane. None of that works today, and the reason is worth being precise about, because it points straight at where the line belongs.

This page describes a decided boundary, not shipped behaviour. The reasoning below is settled in ADR 0013; the export and replay APIs it describes are not built yet. Everything said about current behaviour — the local store, the staleness map, the reverse-RPC model — is shipping today.

Why local state is local on purpose

Stella's durable store is keyed on a machine-local workspace id. That id lives in .stella/private/, which is gitignored by construction, so a clone never carries it. Identity is decided by comparing absolute paths — a comparison with no meaning across machines. And when Stella sees the same id at a path where another copy is still live, it deliberately mints a fresh id, so two copies of a project can never silently write into each other's history.

Every one of those choices is right for the question that module answers: is this the same working copy I saw last time? Every one of them is fatal to resuming somewhere else.

The obvious fix is to give Stella a portable identity — an account, a project id, a login. That is the fix this project declines, and declining it is the whole decision.

The line

Stella provides a checkpoint API and a replay API. The checkpoint API returns a self-contained artifact; the replay API accepts one. Identifying a workspace, storing the artifact, moving it, authenticating it, and deciding who may read it are the caller's concerns.

That is not a division invented for this feature. It is the line Stella already draws everywhere else.

When Stella runs as a headless engine behind a host — the stella-serve crate — every governed side effect is remoted back to the host. Model calls and tool calls are requests the host answers. The engine never holds ambient authority, which means a served turn is structurally incapable of reading one of your files: it can only ask, and something else decides. The workspace was never Stella's in that model.

Durability is the same shape of problem, so it gets the same answer. Extending an existing line costs nothing to explain. Carving an exception into it costs you the ability to say what the system does.

Why not just build sync in

It is one integration instead of two, and for a single user with two machines that is a real advantage. It loses anyway, on three counts.

It requires an identity nobody can derive. Stella sees a directory. It has no way to know that ~/src/api on your laptop and /work/api on the build box are the same project — only something with a concept of project knows that. Every scheme that guesses eventually binds two unrelated codebases together, which is worse than an honest miss.

It contradicts what this project promises. Local-first state, BYOK credentials, and zero telemetry egress by default are load-bearing claims, with exactly two explicit opt-in exceptions. A first-party upload path makes them conditional — and the condition is precisely the thing people choose Stella to avoid. An artifact API adds no third path: producing an artifact writes bytes locally, and Stella still uploads nothing.

It builds the easy half twice. Storage, auth, tenancy, retention, audit — these are solved, and they do not get better for being reimplemented inside a CLI.

What Stella still owes you

"Identity is the caller's problem" is right. "Any artifact may be replayed into any tree" is not.

Replay an artifact into the wrong checkout and you get a session whose transcript describes files that were never there. The model reasons confidently from content that does not exist in the tree it is about to edit, and nothing reports it. That is a worse failure than the one the no-clobber guard exists to prevent, for the same reason it is worse to be quietly misinformed than loudly blocked.

So the artifact carries a fingerprint of the tree it came from, and replay verifies it and refuses by default. Stella is not naming your workspace. It is checking that this artifact belongs to this tree.

The fingerprint is not a new mechanism — it is the staleness map the no-clobber guard already keeps: every path this session touched, and the digest of what it saw there. Verifying it is re-hashing those paths in the target tree. A mismatch names the exact files, exactly as a refused write does, and nothing is consumed. Overriding it is possible, but it is a distinct argument you pass, never a fallback Stella takes on your behalf when the check fails.

That map covers the files the session actually touched and no others — so it cannot cry wolf over unrelated churn elsewhere in the repo, and it cannot detect a tree that differs only in files the session never read. Same precision, same reasoning as the guard it comes from: a check that fired on noise would get overridden by reflex, and then it would protect nothing.

Two ways to land, and you pick

Replaying against a tree you already have and building a tree from scratch are different operations with different failure modes, so they are different modes — never inferred from what happens to be on disk.

  • Apply (the default) replays against a checkout you already have, gated on the fingerprint. The same-laptop-next-morning case.
  • Materialize writes the session's files out first. The cross-machine case.

Materialize has a limit worth stating plainly: the artifact holds the files the agent touched, not your whole repository. Stella records the base commit it was captured against so a caller can name it, but it does not carry the base and will not — bundling your entire tree into a session artifact would move files the agent never read, and that is an egress decision Stella has no standing to make on your behalf.

Fetching the base is exactly the kind of thing a control plane is for. It has the repository, the credentials, and the policy about who may use them.

What is actually in the artifact

The conversation, verbatim. Which means the full content of every file the agent read — including whatever was in those files: the credentials in the .env you asked it to debug, the customer rows in a fixture, a key in a test.

That sentence belongs in the documentation of the type that holds it, where someone reads it before writing the code that moves the bytes — not in a release note and not in a banner.

Stella does not encrypt it, redact it, classify it, or expire it. Those need to know whose data it is, what jurisdiction it sits in, and who may read it, and Stella knows none of those. Its entire obligation here is to be accurate about the contents so your decision is an informed one. That is not a gap in the design; it is the boundary doing its job.

Forks stay visible

Two machines replay the same artifact. Which one wins is a policy question, and Stella does not answer it.

What it guarantees is that the divergence is honest: every replay starts its own session, with its own ref in the durable store, and the artifact's origin is kept as provenance. You get two histories with a common ancestor that can be named — not one history with two sessions interleaved into it, which is the outcome that cannot be untangled afterwards.

Where the line falls

Stella ownsYour control plane owns
what is in the artifactnaming a workspace or project
byte-exact round-tripstoring artifacts, and keeping them
refusing a tree the artifact did not come fromtransport, auth, tenancy
the version contract, and how long old artifacts stay readableencryption at rest and in flight
making a fork visibleretention, deletion, audit
telling you what the artifact containswho wins a conflict, and when

Oxagen is that control plane at enterprise scale — accounts and projects as the portable identity Stella refuses to invent, a store, access control, audit, and resuming a session from a browser.

That is a layering, not a dependency, and the distinction is the test of whether the line is in the right place. Stella has to stay fully usable and fully durable on its own: the work journal, crash resume, and the no-clobber guarantee are local mechanisms and stay local mechanisms. If a change ever made one of them conditional on a control plane existing, the change would be the thing that is wrong.

What this deliberately does not claim

  • It is not sync. There is no stella login, no Stella-hosted store, and no first-party upload path. Not deferred — declined.
  • Old artifacts do not read forever. A stored artifact stays replayable across a stated support window, not indefinitely. When a build genuinely cannot read one, it says so and names what can — it does not guess at a format it half-understands, which would resume your session subtly wrong.
  • Stella does not merge forks. It makes them visible. Resolving them needs to know which one matters, which is exactly what Stella does not know.
  • The artifact is not a backup of your repository. It is what the agent did, plus the identity of what it did it to.

Where this sits

The same rule as everywhere else in this codebase: where a mechanism can decide something, the mechanism decides it. Whether an artifact belongs to a tree is checkable, so it is checked, and the check refuses rather than warns. Everything genuinely not checkable from inside a directory — whose project this is, who may read it, which copy wins — is handed to the layer that can actually answer it, instead of being guessed at by the layer that cannot.