返回 DeepSeek-Reasonix
SESSION_OWNERSHIP.md
根目录 / docs / SESSION_OWNERSHIP.md
1 # Session ownership, rewind, and worktree fallback
2
3 How Reasonix decides who may write a session, how conflicts are saved, and
4 how rewind and workspace isolation interact.
5
6 ## Session writers
7
8 A conversation lives in an append-only event log, `<id>.events.jsonl`
9 (session format 2). Every message entry carries its own id and the id of its
10 parent, so the log is a DAG: each path from a leaf back to the root is one
11 version of the conversation, called a *head*. A log starts with one head,
12 `main`; forks, rewinds, and concurrent writers add heads of kind `fork`,
13 `rewind`, and `concurrent`. The transcript file `<id>.jsonl` is a derived cache
14 of the selected head and is never the source of truth.
15
16 The selected head is the last `select` marker that still points at a live
17 head, otherwise the head with the newest activity. Opening a session by path
18 opens that head. `.event-index.json` mirrors the heads so the session catalog
19 can list them without replaying the log.
20
21 Writers append; they never rewrite or truncate the log. A save takes the
22 bounded `.jsonl.lock` flock, reads whatever another writer appended since its
23 own last save, and continues on its own head. The session lease
24 (`.lease.lock`, with a generation-bound `SessionWriter`) no longer gates
25 appending: it decides who writes the derived `.jsonl`, the indexes, and the
26 turn ledger, so a second window can join the same conversation without
27 waiting. A turn opens with a `turn_begin` marker and closes with `turn_end`;
28 a crash between them is noticed on the next open and the incomplete tail is
29 set aside with a `rewind` marker, never truncated. When the shutdown save
30 cannot take the save lock within its bounded wait, it appends the unsaved
31 tail without the lock on a fresh `concurrent` head and leaves the derived
32 files to the next locked save; a shutdown never writes a copy of the session.
33
34 Path changes (`new`, `clear`) still use the prepare-before-publish handoff:
35 the frontend acquires the target lease and binds the unpublished Session
36 before the controller swaps paths. `fork`, `branch`, `switch`, and
37 conversation rewind stay on the same path and move between heads instead.
38
39 Sessions saved before Reasonix 1.39.0 use format 1: a whole-file transcript
40 plus a position-based event log. A 1.39.0 or newer binary upgrades such a
41 session in place on its first save, once it can prove it is the only writer;
42 until then the session keeps the format-1 rules below. A binary older than
43 1.39.0 refuses to open a format-2 log and leaves the file untouched;
44 `reasonix doctor session <id> --export-v1 PATH.jsonl` writes the current head
45 back out as a format-1 session when a rollback needs it.
46
47 ## Conflicts
48
49 Two processes appending to one format-2 log cannot conflict; they interleave.
50 When a save finds that another writer extended the chain this session was on
51 and this session added nothing, it follows the disk. When both added content,
52 the save forks a `concurrent` head from the last shared message and continues
53 there; both sides receive a notice (`session_concurrent_writer`). Reloading
54 the log afterwards opens the newest head and lists the other one under *View
55 versions* (`session_head_switched`). Nothing is copied into a `-recovery-`
56 file, and a save never removes a head.
57
58 Format-1 sessions keep the previous rules until they are upgraded:
59
60 1. Event-log tail still matches this writer → normal save (no-op / append / replace).
61 2. Disk already covers the local prefix → adopt disk, no branch.
62 3. True divergence, replaced log, or deleted original → one stable recovery
63 file keyed by root branch ID + the live Session's first writer generation.
64 Lease rebinds keep that lane; later conflicts update the same path. There is
65 no recovery-on-recovery chain.
66
67 ## Heads as versions
68
69 Fork-from-here, `/branch`, and a conversation rewind append a `fork` marker
70 and a `select` marker: the new head starts at the chosen message and becomes
71 current, and the previous chain stays as another version of the same
72 conversation. The desktop switches the current tab to the new head in place;
73 the terminal replays the transcript. *View versions* lists the live heads with
74 their kind, makes another head current (`select`), renames a head, and
75 removes *covered* heads: heads whose whole chain is already part of the
76 current one. Removing appends a `retire` marker; a retired head leaves the
77 version list, and its bytes are reclaimed only when a single writer rotates
78 the log. Heads with unique content are never removed automatically, and a head
79 that was active within the last minute is reported busy instead of retired.
80
81 ## Rewind
82
83 - **Code**: restore file before-images. Already-restored files (current ==
84 before) are skipped. External changes refuse overwrite.
85 - **Conversation**: fork a `rewind` head at the turn boundary and make it
86 current. The previous chain is never truncated. A format-1 session forks a
87 new session file instead.
88 - **Both**: fork first, then restore files. A file conflict keeps the new head
89 and reports `partial=true`.
90 - **Undo**: restores the file after-images. If nothing was added on the rewind
91 head since, the controller returns to the parent head and retires the empty
92 rewind head; a continued rewind head stays as a version.
93
94 New checkpoints write `turns/<turn>/meta.json` plus raw `files/NNNN.before`
95 payloads (schema v3). The newest 100 turn directories are retained by default;
96 new checkpoint payloads are not duplicated into blobs. v1/v2 `turn-N.json`
97 files and their legacy blobs remain readable.
98
99 The v2 compatibility marker is also the v3 turn's liveness record. A previous
100 binary that truncates `turn-N.json` therefore tombstones the matching v3
101 directory; upgrading again cannot resurrect the removed future turns.
102
103 Structured writers (`write_file`, `edit_file`, `multi_edit`, notebook edit)
104 re-check existence, SHA-256, and mode before publish. A mismatch returns
105 `ErrFileChanged`.
106
107 ## Worktree fallback
108
109 Forking from a message offers two workspace policies. **Conversation only
110 (shared)** keeps the source workspace, including its current uncommitted files.
111 **Isolated worktree** creates a durable `reasonix/delivery-*` branch from the
112 repository's committed `HEAD`, opens the fork as a registered project, and
113 keeps the source checkout unchanged. Because Git worktrees do not copy local
114 changes, Reasonix requires a clean source checkout for this combined fork. A
115 dirty checkout is refused with guidance to commit/stash or use the shared fork.
116
117 If the folder is not a Git project or worktree prerequisites are unavailable,
118 Reasonix creates the conversation fork in the shared workspace and reports the
119 fallback. If conversation creation or tab attachment fails after a worktree was
120 created, automatic cleanup removes it only while its branch, `HEAD`, and status
121 still match the untouched creation result. Any detected change preserves the
122 worktree for recovery. A successfully attached worktree remains registered
123 across tab close/restart. New allocations also store a mode-0600 v1
124 `metadata.json` beside the checkout. It binds the original source checkout,
125 target branch, creation `HEAD`, managed worktree root, and temporary branch.
126 Older allocations without this metadata cannot use Merge-Back because Reasonix
127 will not guess a destination; the UI leaves them intact and shows manual merge
128 guidance. Unknown metadata versions also fail closed.
129
130 Merge-Back is a two-phase, failure-atomic operation. Preflight verifies the
131 managed path and repository identity, exact branches and `HEAD`s, clean source,
132 absence of an in-progress Git operation, all visible or detached Desktop work,
133 integrated terminals, workspace write leases, divergence, diff, and conflicts.
134 After the dual leases are held, Desktop briefly quiesces turn starts and
135 controller publication, then reserves both canonical source and worktree roots
136 through the Git mutation. Project-runtime owners, new turns, and terminal
137 create/write calls all use that admission gate; contained paths and symlink
138 aliases are covered without blocking prefix siblings or unrelated workspaces.
139 Uncommitted worktree changes block the merge unless the user explicitly opts
140 into an automatic commit; that option is off by default. Confirmation binds a
141 transient, NUL-safe token to the real index entries and status as well as every
142 dirty path's type, mode, bytes, or symlink target. Auto-commit seeds a private
143 `0600` temporary index from the confirmed `HEAD` and runs `git add -A` only
144 there. If the real index contains staged or index-only content that the full
145 working tree does not represent, Reasonix stops with the real index and both
146 versions untouched. Otherwise it creates a hook-free, single-parent
147 `commit-tree`, compare-and-swaps only the confirmed worktree branch, and then
148 installs the prepared index through Git's exclusive `index.lock` protocol only
149 if the real index bytes still match. Any failure after the branch CAS is marked
150 recovery-required; conflict preflight runs again on the exact new commit. A
151 target branch, `HEAD`, index, or content-token change refreshes the confirmation
152 instead of continuing. The source merge uses
153 `git merge --no-ff --no-commit --no-verify` with a Reasonix-scoped committer
154 identity, so it neither depends on user Git identity nor invokes commit hooks.
155 It binds the real index tree to a freshly computed merge tree. The worktree root, common repository,
156 symbolic branch, branch ref, `HEAD`, Git operation, and content token are
157 revalidated before preparation and before ref installation. Only while those
158 identities, the target branch, original `HEAD`, exact `MERGE_HEAD`, and prepared
159 tree still match does Reasonix create a hook-free `commit-tree` object with
160 fixed parents and tree. A short source mutation fence holds the real index,
161 `HEAD`, and `MERGE_HEAD` lockfiles and compares their exact snapshots. While
162 those checkout-local locks remain held, Git uses a detached administrative view
163 of the same common ref store to acquire only the branch ref locks. One
164 `update-ref --stdin` transaction verifies the
165 worktree branch ref and compare-and-swaps the target ref against its original
166 `HEAD`, so neither ref check can partially succeed. Post-commit verification
167 rechecks both checkouts plus the commit tree, real index tree, parents, refs,
168 clean state, and Git operations. After installation, `git merge --quit` removes
169 only Git's auxiliary merge state; Reasonix does not update the `MERGE_HEAD`
170 pseudoref directly or reset the prepared index. Owned preparation failures
171 before the CAS are aborted only when the prepared state can still be proved;
172 target-ref drift, post-CAS drift, or any state whose recovery cannot be proven
173 is marked recovery-required while every worktree resource and external state
174 is preserved.
175
176 A successful merge first navigates to the recorded source checkout. Every UI
177 navigation registers an opaque intent token with Desktop; the close request
178 must still own that exact token both before its snapshot and at the backend
179 removal linearization point. A newer intent therefore stops close and cleanup
180 while preserving resources. Otherwise Desktop closes only the exact idle
181 worktree tab while the exact source tab is still active. Cleanup is then a
182 separate, retryable step. It reserves the complete allocation containing both
183 the canonical worktree and its fixed recovery subtree while checking visible
184 and detached runtimes; every project-runtime creation, restoration,
185 delete/archive fallback, and redirect uses the same gate. Symlink and contained
186 paths are covered. Prefix siblings outside the allocation and other allocations
187 remain independent.
188
189 Finalization runs only when the temporary commit is contained by the target,
190 identities still match, and the full status including ignored files is empty.
191 Before moving anything, Reasonix writes a mode-0600 v2 `cleanup-state.json`
192 journal with the original root, an unguessable recovery root under the reserved
193 allocation, branch, `HEAD`, and a `planned` stage. It then uses ordinary
194 `git worktree move` and rechecks the common Git directory, symbolic branch,
195 branch ref, `HEAD`, operation state, full status, and registered path before
196 advancing the journal to `retained`. A crash in either stage is retried from the
197 Git worktree registry and the exact journal identity; multiple or unknown
198 candidates fail closed.
199
200 The recovery checkout deliberately stays registered and keeps its
201 `reasonix/delivery-*` branch checked out. Reasonix does not unregister the
202 worktree, delete its branch, unlink manifest entries, or recursively delete any
203 path. An already-open file descriptor therefore follows the moved checkout and
204 late writes remain recoverable; content recreated at the former public path is
205 also left untouched and reported. Desktop removes only the stale managed-project
206 registration after the recovery receipt is durable, keeps the source project
207 active, and does not add the hidden recovery path to the sidebar. Registration
208 failure is retryable while the recovery root and journal remain available.
209
210 New readers accept v1 cleanup journals only for preservation. A still-registered
211 legacy checkout can be converted to v2 after its exact identity and manifest are
212 proved; a detached or ambiguous legacy root is reported for manual recovery and
213 is never deleted or automatically re-registered. Unknown journal versions fail
214 closed. Metadata remains v1; older cleanup readers reject the unknown v2
215 journal and therefore preserve the recovery checkout.
216
217 Delivery worktrees stay optional. Non-isolated directories use the workspace
218 lease (`filelock`). Path-bound writes take shared ancestor compatibility locks,
219 shared hierarchy stripes through the concrete path, and an exclusive file
220 stripe for the duration of that tool. Whole-workspace writers take their exact
221 root and hierarchy stripe exclusively. Parent workspaces and directly opened
222 nested repositories therefore intersect, while two sessions can still write
223 different files (including in the same repo) at once. `bash`/MCP mutations take
224 the whole-workspace locks only for that command. Any tool call does the same
225 when a configured tool hook may write undeclared paths. File and hierarchy
226 identities map into bounded stripe sets; collisions may serialize unrelated
227 work but cannot weaken protection. Read-only bash does not take a write lease.
228 On macOS, folded domains coordinate case aliases while exact-case root locks
229 remain compatible with older binaries. An older process still recognizes only
230 the path spelling it opened; cross-spelling coexistence requires both processes
231 to use the folded protocol.
232 Conflict cards name the file or workspace being written. Git is never required.
233 A finished conversation does not keep the write lease; use a worktree when you
234 need a long-lived isolated tree.
235
235 lines MARKDOWN