跳转至

session-new

raw source

The public POST /api/sessions call is the ownership boundary for creating a governed session. It must never leave a caller waiting behind invisible preparation with no session to inspect, and it must never keep creating after that caller has received a failure. One request therefore has exactly two terminal outcomes: one durable, recoverable session receipt, or one structured failure after all session-specific work is gone.

expanded spec

Creation is a bounded transaction whose commit point is the atomic session.json publication. Before that point, launcher resolution and prompt composition are read-only; Git may create exactly the candidate branch and worktree; and the global store may contain only the candidate's private preparation files. A deadline, client disconnect, cancellation, or phase failure before publication aborts active Git work, opens no launcher pane, removes the candidate store directory, worktree, and branch, releases its creation lock, and returns a structured non-2xx JSON body:

{ "error": "...", "code": "session_create_timeout|session_create_cancelled|session_create_failed", "phase": "..." }

Cleanup is part of the outcome, not a detached best-effort continuation. If exact cleanup cannot be proved, the request returns session_create_cleanup_failed and names the residue rather than claiming zero artifacts. No timeout path falls back to another launcher, an in-process create, or a background create.

Branch and worktree names are shorter than the session identity, so a different request may derive the same Git resources. The transaction therefore locks the exact {branch,path} pair with the existing lock primitive, then records whether the store, branch, and registered worktree were absent before preparation and which of them its successful operations created. Pre-existing resources fail without mutation. Rollback consumes that one ownership receipt and removes only resources this transaction proved it created; a failed add never turns the candidate name into authority to delete. A colliding request therefore cannot damage an already-published receipt, while a transaction that owns its candidate still removes exactly that candidate on abort.

Process-local ownership is not enough: the backend may die after creating Git or store resources but before publishing the record. Before the first Git mutation, while holding both the candidate id lock and exact {branch,path} lock, creation atomically publishes one private candidate receipt outside the session store. It binds the request digest and payload hash to the exact root, path, branch, all-absent resource pre-state, and the last durably reached preparation stage. A restart holding those same locks may inspect, clean, and restart the candidate only when that receipt is valid and matches every field of the retry. A different key, mismatched payload/resource identity, malformed receipt, or occupied orphan with no receipt conveys no ownership: all resources are preserved and creation fails loud. Normal rollback or record publication retires the private receipt; a matching retry also retires a publication-left receipt after recovering the public row.

Record publication itself fences any receipt that could not be retired: while that public record exists, create returns it and never enters candidate cleanup. Before terminal close may remove that fence, close holds the same session-id and exact {branch,path} resource locks, retires a valid matching candidate receipt, and proves it absent. If retirement cannot be proved, close fails before stopping or deleting the session and preserves its record, store, worktree, and branch. An irreversible record publication is never reported as a rollback, and a stale receipt can never outlive close with authority to consume a later colliding session.

The public request accepts the standard Idempotency-Key header. The backend deterministically maps a valid key to one candidate session id and binds it to the normalized {prompt,parent,launcher} payload. Creation for that id is serialized at the existing per-session lock. A same-key retry, including a concurrent retry or a retry after the response connection was lost, either joins the in-flight transaction and receives its one published receipt, or starts after a fully rolled-back failure. Reusing the key with another payload fails with session_create_key_reused and creates nothing. Callers that omit the header receive ordinary one-shot semantics with a backend-minted key; SpexCode's own CLI always sends a fresh key and retains it across its one bounded request attempt.

Before that attempt the CLI performs one bounded GET /api/instance authority probe. It is the small identity route, never /api/settings: creation authority must not enumerate session records or derive layout overlays. For an implicit target, each supplied root is resolved through the shared lightweight main-root resolver before comparison, so a linked worktree and an explicit configured main retain their canonical project identity; --api names the target and skips that comparison but still makes this availability decision: normally that explicit target owns the one POST. The only exception is an exact all-chain ECONNREFUSED, which proves that the selected target has no listener and permits the existing in-process fallback. The optional recorded-endpoint health read is only target discovery and has its own wall; it does not shorten the 1500ms instance probe. Every HTTP response, including 404 and 503, proves that a backend owns the target; a slow accepted connection, abort, reset, DNS failure, or unknown transport outcome is indeterminate. Those cases fail loud within the instance probe wall without local creation, because the remote owner may already have admitted the keyed request.

sessionCreateRequest is the only callable governed-session creation seam. It owns validation, maintenance admission, request identity, deadline/cancellation, and the private prepare/publish function's required context. The HTTP route, CLI no-listener fallback, and New Session composer call it. Preparation is not exported and cannot mint a never-aborted context for itself, so adding another caller cannot bypass the wall or transaction owner.

The record publication is the irreversible boundary. The transaction checks cancellation immediately before the synchronous atomic record replace and re-proves that the candidate path is the exact Git top-level, is checked out on the recorded branch, and that exact branch ref exists. JavaScript cannot interleave an abort between the final check and replace. A 201 may therefore never name a detached, switched, deleted, or rolled-back Git candidate. Once the record exists, a lost connection is a lost response, not a failed create: the same idempotency key recovers the receipt. The published record is already a normal durable queued session with its prompt and launch payload present, so backend restart and the ordinary queue supervisor can recover it. Queue draining is requested only after publication and is not awaited by the HTTP receipt; a slow, stopped, or broken launcher therefore cannot hold session creation open. This is the existing queue mechanism, not a second create worker. The successful HTTP route also nudges the graph's sessions projection after publication, so dashboard row delivery does not depend on the session-store watcher; any candidate worktree registry refresh remains its own asynchronous full convergence.

Target resolution on this write path reads the live filesystem-only spec projection. It needs only a node id and spec.md path; it must not build history, drift, graph, or eval projections before publication. The session's raw first **id** mention remains its only scope source, and prompt preset expansion remains the one shared launch/dispatch seam.

The existing creation owner reads portable-layout's main and branchPrefix settings through the shared config seam. The worktree lives under the resolved main checkout's supported .worktrees directory and the branch uses that prefix; creation does not rebuild the full board layout merely to read those settings.

Every transaction emits one-line structured phase diagnostics with an ISO timestamp, a non-secret request-id digest, candidate session id, phase, and event (start, finish, abort, or publish). The required phases are request, creation-lock, target-resolution, Git worktree, record write, and launcher queue. Diagnostics contain no prompt, raw idempotency key, auth material, environment, or process dump. They are operational evidence only; the record and Git tree remain source of truth.