archive¶
Cold storage — archive proves and stops the exact session-owned runtime, preserves worktree/branch/conversation identity, and exposes only offline history.
raw source¶
The board had two ends and nothing between them. stop gives the process back but leaves the row sitting in
the list; close clears the row by destroying the work. Neither says the ordinary thing a human means about
most sessions: not this week, but don't throw it away. Without that verb the list is where every session
that was ever interesting goes to accumulate, and the cost is attention, not disk.
The instinct is to make shelving mean "delete the worktree, keep the branch" — the files are only ~13MB and a
branch rebuilds them in about a second. That instinct is wrong, and measuring says why: a branch ref carries
committed work only. What a retained worktree buys is the uncommitted state — the half-finished edit
the human will want exactly as they left it. stop already understood this ("step away, come back later" —
it keeps worktree, branch, transcript, and record). Archive is the cold-storage form of that promise: it first
reuses the exact stop/cleanup seam, then files the retained identity.
expanded spec¶
Archive is cold-storage filing with one hard invariant: archived => offline. A session keeps its
agent-authored lifecycle, worktree, branch, transcript, and conversation identity, but an archive may be written
only after the existing exact-instance stop guard has safely stopped that session-owned leaf, tmux, and adapter
transport. The shared project app-server/control plane is never touched; sibling references remain loaded. If
ownership or the stop proof is unprovable, archive fails loudly and leaves the record unarchived and visible.
Native thread ownership is unique whether the target is currently loaded or not. A native descendant collection
is runtime owned by its exact ancestor, not a separate Spex session: archive includes every uniquely-owned active
or already-archived descendant at any depth in the same cold transition, while preserving each native conversation
for later same-thread resume. For adapters whose archive RPC can race native child creation, the complete target
subtree census runs again after the mutation and before success. A new, missing, duplicated, or reassigned member
in that interval refuses and compensates instead of filing a false zero-runtime proof.
Archive eligibility reads the target's fresh native turn census, not its public lifecycle/status projection: an
inProgress turn refuses with zero mutation, while a loaded target whose complete turn census has no
inProgress turn may archive even if a hook-authored public status still reads working.
For a shared resident adapter, lifecycle mutation uses a target-scoped proof rather than the resource report's
full projection. It first proves the shared PID/start/detached-receipt/socket generation, obtains the paginated loaded-ID
set, and checks the target's active and archived descendant collections. Ordinary stop reads only the exact target
and still refuses a resident descendant subtree. Archive additionally reads each loaded member of the exact owned
subtree, then archives every member that was initially active under one generation fence; members already in the
archived collection are verified but not mutated. An absent target with no descendants may therefore stop or
archive even while an unrelated loaded or unowned sibling is slow or unresponsive; those sibling IDs still protect
the shared root, which the session mutation never signals. A loaded active subtree member, an unknown exact member read, a
member present in both or neither native collection, changed ancestry, duplicate ownership, or a shared-generation
identity change fails closed. Success requires a post-mutation recensus proving parent plus the identical descendant
closure uniquely archived, every subtree member unloaded, and every pre-existing unrelated loaded sibling retained.
The resource report remains the complete projection and may inspect every loaded reference; its latency or an
unrelated turn-read failure is not mutation authority.
Archive compensation is fenced to that same exact original generation at connection, request, and response
boundaries. If the shared generation changes after the archive request, commit state is reported unknown and no
thread/unarchive is sent to the replacement generation; recovery remains explicit rather than borrowing new
authority from a process that was never proved by the original preflight. On an unchanged generation, compensation
restores only members that were active before this attempt; an already-archived descendant is never unarchived.
The same opaque preflight receipt remains available until the session record and final offline proof are committed.
If native cold teardown succeeds but record lookup, final liveness, or filing then fails, outer compensation returns
that exact receipt to the adapter so the complete originally-active subtree is restored on the same generation.
Ordinary later resume carries no receipt and restores only the parent conversation.
It is the attention verb backed by the resource stop. This is the line that must not blur:
stopis the resource verb — give the process back. Reversible byresume.archiveis the cold-storage attention verb — first perform the same exact stop, then file the record.closeis terminal destruction, but it first completes that same exact cold-runtime transition for every launched session. It never deletes the worktree, record, branch, or Codex generation binding while the target native runtime is still live or unproven; a failed cold transition leaves the session visible.
After all cold/queued ownership proof and every refusing Codex generation-binding fence pass, close writes an
append-only close-authorized project runtime ledger entry before resource deletion. The entry names the exact
target record and a non-authoritative request source: user means no session claim was supplied, while
unverified-session-claim preserves a CLI-provided id without asserting that the id exists, owns the target, or
authenticated the request. The close route rejects the authoritative-looking session kind rather than silently
upgrading a forged value. The ledger outlives the target session directory, so the request context remains
inspectable after a successful terminal close but never grants deletion authority. A refused close writes no
entry and never changes a Codex generation binding into its record-removing phase. Archive remains reversible
only through resume, which unarchives before recreating the runtime. Close is not reversible.
Close has three ownership-proof entries into that one terminal result. A live row first uses the ordinary exact
stop proof, then removes its record, worktree, and branch. A proven-cold archived row must not pretend to be
live again just to retire: it verifies that the record's cold proof still binds the target adapter/thread and
that every target-owned PID, tmux window, rendezvous transport, and loaded thread remains absent, then removes
the record, worktree, and branch directly. That cold retirement path sends no signal and neither probes nor
requires ownership of unrelated references on the shared project app-server; archive already returned the
target's runtime. Any target runtime that has reappeared, stale/swapped target identity, unreadable cold proof,
or ambiguous target ownership fails loudly before deletion and leaves the shelf row intact. A live PID is not
ownership by number alone: the shared leaf-identity seam requires a readable process-start token and harness
owner argv. A live PID whose argv proves a different process is a stale artifact and does not block cold
retirement; malformed or unreadable PID/start/argv evidence remains unknown and fails closed, while an argv
owner match remains live-owned and fails. Continuing-cold proof may list loaded IDs and the target's own native
collection/descendants, but it never thread/reads or waits on an unrelated loaded sibling.
A prepared queued row that has never launched takes the other target-only retirement path. Close serializes
with the drainer on the same session transition/record lock; if close wins while the record is still queued, it
also holds the exact recorded branch/path create-resource lock and proves any matching private creation receipt
retired before stop or deletion. An unretirable receipt leaves the public row and every resource intact, because
the record is the fence that prevents old create authority from reaching a later collision. Once that proof lands, it
verifies that no harness thread identity, tmux window, live/recycled leaf PID, rendezvous transport, ahead
commit, or dirty work exists, then removes the prepared prompt, record, worktree, and branch before releasing
capacity. It sends no signal and asks nothing of unrelated shared references because no target runtime was ever
created. If the drainer wins first, status is no longer queued and ordinary live close owns teardown. Any
target/runtime/work ambiguity fails loudly with the queued row intact. Close reports success and releases
capacity only after worktree, branch, prompt, and record removal have each been proven complete.
Archiving never removes or moves the worktree/branch and writes no timeline row. Success means the exact leaf is stopped and the record is archived; a failed stop means no archive field change. An archived record is therefore always offline and consumes no active slot or loaded-thread reference of its own.
For adapters with a shared resident control plane, the read projection consumes one project-wide exact loaded-ID
census, not one RPC per row. A cold proof is current only when that census is healthy and the exact thread is absent;
an externally reloaded or ambiguous thread projects visible with archiveHazard even if its historical proof still
matches. A deliberately absent shared root is a healthy empty census only when its registered PID is dead and its
socket has no live listener; stale files alone never prove absence. Explicit close resolves the all-record store,
including cold rows. For a proven-cold row it judges only the target's continuing cold ownership facts, never
unrelated shared-root references, and succeeds/nonzero only according to whether the record is actually removed.
The copy states the cold result plainly: the retained worktree/branch/conversation can be resumed, while the session-owned runtime is gone. A failed ownership proof is a visible hazard, never a successful archive.
The record stays a projection, never a log. archived is a declared field in session.json's closed key
set, written like every other. That key set is rebuilt from the typed record on each write and never merged
over what was read, so the file self-cleans: a field retired from the code leaves disk the next time anything
touches that record, and no migration verb or GC pass is needed. Cold filing carries three deliberate projection
fields: archived is the visible filing bit, cold_proof is the versioned witness bound to the resolved adapter
and exact session/thread identity and is written only after the full leaf/adapter cold proof, and
adapter_recovery records a partial/unknown adapter mutation that resume must reconcile before relaunch. Resume
clears the recovery token only after the active collection is confirmed and clears the cold proof before launch;
a failed reconciliation leaves both visible and retryable. These fields are not redundant metadata or a second
archive store: they are the durable visibility and recovery contract that prevents a live runtime being hidden
by a boolean.
Enumeration is existence; filtering is a view. The board keeps enumerating shelved records — the store is the existence truth (state) and a view preference must never decide what exists. Consumers filter:
- the spec-delta is skipped for a shelved row. That per-worktree git-history probe is the board's dominant per-row cost, and shelving is precisely the human saying to stop spending it here — so a retained archive costs one enumerated record and no git walk. Its cached delta is evicted on the next board read, so the cache stays bounded by the working set rather than by everything ever shelved.
spex session lshides shelved rows;--allincludes them, and naming one explicitly always shows it — an explicit selector is the human already saying which row they mean. The default API/graph/session projections likewise exclude archived rows; an explicit archive/history read is the only way to request them.- the console's list shows one population at a time.
The console gives the archive a door, not a zone. Archived history is a flat cold-storage list: it has no
needs-you/running/offline status partitions, no lifecycle triage, and no active subtree counts. The door is a star,
the third of three equal pills in
the list header beside New and Search, and it is permanent: a control that appears only when it has
contents cannot be found when you want it, and its absence would be the only thing telling you the archive
exists at all. The star carries no numeric count — the archive is a destination, not a backlog meter — and stays live:
a permanently-visible control that silently does nothing reads as broken, so pressing it at zero opens the
archive and says nothing archived rather than swallowing the press. Reaching an archived session from outside the list
(URL, search, an originator chip) lands the view on the side that holds it, the same promise the
ancestor-unfold makes within a list.
The trap was never the empty room — it was a dead exit. Everything that can open this view reads a board snapshot, and a snapshot can be stale: the board serves the pre-write value for close to a second after a record flips, and a session can be restored from the CLI or another tab while you are looking at it. So you CAN end up looking at an archive that just emptied. That is harmless as long as the star still works; it was a trap only while the star was inert, because then the one way out did not respond. Guarding the view instead of the exit fixed the symptom and produced a worse bug — a visible control that does nothing. The human's toggle is therefore authoritative, and an emptied archive is simply somewhere you can leave. Replacing the archive/history rows during a refresh must not override that toggle while the selected session id and that session's archived state are unchanged. Automatic side selection still follows a real selection change or a change in the selected session's archived state, so deep links and external archive/resume transitions land on the side that actually owns the selected row without turning ordinary refreshes into navigation.
Selecting an archived session shows the archive card, which is an offline cold-storage card with restore as
its only lifecycle exit. There is no relaunch action while archived; resume first clears archived, then
follows the normal starting -> online state machine and preserves the conversation.
One vocabulary everywhere. /archive is the filing command and /resume is the sole restore action. The
CLI spex session archive <SEL> and POST /api/sessions/:id/archive perform cold filing; the legacy
unarchive spelling is only a loud signpost to resume and never a record-only mutation. The dashboard card
uses the same resume endpoint, so every restore observes starting -> online and preserves the conversation.
Filing is a row decision as much as a console one, so it is also on the session row's right-click menu —
one item that names the move OUT of the row's current state rather than a pair where one is always inert. It
acts immediately, with no confirm: close earns its prompt by destroying work, while a prompt guarding a
reversible act is friction pretending to be care. It sits with the row's other non-destructive actions, never
grouped with close, so a mis-aimed right-click can cost you a moment's confusion but never the work.