返回 DeepSeek-Reasonix
SESSION_OWNERSHIP.zh-CN.md
根目录 / docs / SESSION_OWNERSHIP.zh-CN.md
1 # 会话所有权、回溯与 worktree 回退
2
3 <a href="./SESSION_OWNERSHIP.md">English</a>
4
5 Reasonix 如何决定谁可以写会话、冲突如何落盘,以及回溯和工作区隔离如何配合。
6
7 ## 会话写者
8
9 一个会话保存在只追加的事件日志 `<id>.events.jsonl` 中(会话格式 2)。每条
10 消息条目都带有自己的 id 和父消息 id,因此日志是一张 DAG:从任一叶子回溯到
11 根的路径就是该会话的一个版本,称为 *head*。日志初始只有一个 head `main`;
12 分叉、回溯和并发写入会新增 kind 为 `fork`、`rewind`、`concurrent` 的 head。
13 `<id>.jsonl` 只是选中 head 的派生缓存,永远不是权威来源。
14
15 选中 head 由最后一条仍指向存活 head 的 `select` 标记决定,否则取活动时间最新
16 的 head。按路径打开会话即打开该 head。`.event-index.json` 镜像全部 head,
17 会话目录据此列出版本,无需重放日志。
18
19 写者只追加,从不改写或截断日志。每次保存获取有界等待的 `.jsonl.lock` flock,
20 读入其他写者自上次保存以来追加的内容,然后继续写自己的 head。session lease
21 (`.lease.lock`,绑定带 generation 的 `SessionWriter`)不再限制追加:它只决定
22 谁写派生的 `.jsonl`、索引和回合账本,因此第二个窗口无需等待即可加入同一会话。
23 回合以 `turn_begin` 标记开始、以 `turn_end` 结束;两者之间崩溃会在下次打开时
24 被识别,未完成的尾部用 `rewind` 标记搁置,绝不截断字节。关机保存在有界等待内
25 拿不到保存锁时,会不加锁地把未保存的尾部追加到一个新的 `concurrent` head 上,
26 派生文件留给下一次加锁保存;关机永远不会复制出一份会话。
27
28 路径切换(`new`、`clear`)仍采用“先准备、后发布”的交接:前端先取得目标
29 lease 并给尚未发布的 Session 绑定写权限,Controller 才替换路径。`fork`、
30 `branch`、`switch` 和对话回溯则停留在同一路径上,只在 head 之间移动。
31
32 Reasonix 1.39.0 之前保存的会话使用格式 1:整文件 transcript 加基于位置的事件
33 日志。1.39.0 及更新版本在首次保存时、且能证明自己是唯一写者的前提下,就地把
34 它升级为格式 2;在此之前该会话沿用下文的格式 1 规则。早于 1.39.0 的版本会拒绝
35 打开格式 2 日志,且一字节不改;需要回滚时,
36 `reasonix doctor session <id> --export-v1 PATH.jsonl` 会把当前 head 导出为
37 格式 1 会话。
38
39 ## 冲突
40
41 两个进程向同一格式 2 日志追加不会冲突,只会交错。若保存时发现另一写者延长了
42 本会话所在的链而本地没有新增,就直接跟随磁盘;若双方都有新增,保存会从最后
43 一条共同消息分叉出 `concurrent` head 继续写入,双方各收到一次提示
44 (`session_concurrent_writer`)。之后重新加载会打开最新的 head,并在“查看
45 版本”里列出另一个(`session_head_switched`)。不会再复制出 `-recovery-`
46 文件,保存也绝不删除任何 head。
47
48 格式 1 会话在升级前沿用原规则:
49
50 1. 事件日志尾部仍匹配当前写者 → 正常保存。
51 2. 磁盘已经覆盖本地前缀 → 采用磁盘版本,不建分支。
52 3. 真正分歧、日志被替换或原会话被删除 → 写入一条由根 branch ID + 当前
53 Session 首次 writer generation 决定的稳定 recovery 文件。lease 重绑不会
54 改变该 lane;后续冲突更新同一路径,不再嵌套。
55
56 ## head 即版本
57
58 从消息分叉、`/branch` 和对话回溯都会追加一条 `fork` 标记和一条 `select`
59 标记:新 head 从所选消息开始并成为当前版本,原有链作为同一会话的另一个版本
60 保留。桌面端就地把当前标签页切到新 head;终端重放 transcript。“查看版本”
61 列出存活 head 及其类型,可以把另一个 head 设为当前(`select`)、给 head
62 改名,以及清理*已覆盖*的 head——即整条链已经包含在当前链中的 head。清理只
63 追加一条 `retire` 标记:退役 head 从版本列表消失,其字节只在单写者轮转日志
64 时回收。含独有内容的 head 永远不会被自动清理;最近一分钟内仍有活动的 head
65 会被报告为“正在使用”而不是退役。
66
67 ## 回溯
68
69 - **代码**:恢复 before-image。当前已等于 before 的文件跳过;外部修改拒绝覆盖。
70 - **对话**:在回合边界分叉出 `rewind` head 并设为当前。原有链永不截断。格式 1
71 会话则仍创建新的会话文件。
72 - **两者**:先分叉,再恢复文件。文件冲突时保留新 head 并返回 `partial=true`。
73 - **撤销**:恢复文件 after-image。若回溯 head 之后没有新增内容,Controller
74 回到父 head 并退役这个空的回溯 head;已经继续对话的回溯 head 作为版本保留。
75
76 新 checkpoint 写入 `turns/<turn>/meta.json` 和原始字节
77 `files/NNNN.before`(schema v3)。默认保留最近 100 个回合目录;新 checkpoint
78 不再把载荷重复写入 blob。旧的 v1/v2 `turn-N.json` 及其 blob 仍可读。
79
80 v2 兼容 marker 同时也是 v3 turn 的存活标记。旧版本截断 `turn-N.json` 后,
81 对应的 v3 目录会被视为 tombstone;再次升级不会让已经删除的未来 checkpoint 复活。
82
83 结构化写工具在发布前重新校验存在性、SHA-256 和 mode,不匹配则返回
84 `ErrFileChanged`。
85
86 ## Worktree 回退
87
88 从消息分叉时可以选择两种工作区策略。**仅分叉对话(共享工作区)**继续使用源工作区,
89 因此会保留并继续看到当前未提交文件。**隔离 worktree** 则从仓库已提交的 `HEAD`
90 创建持久的 `reasonix/delivery-*` 分支,把新分叉注册为独立项目,并保持源 checkout
91 不变。Git worktree 不会复制本地改动,所以组合分叉要求源 checkout 干净;检测到
92 未提交或未跟踪文件时,Reasonix 会拒绝创建,并提示先 commit/stash,或改用共享分叉。
93
94 如果当前目录不是 Git 项目,或环境不满足 worktree 前提,Reasonix 会在共享工作区中
95 完成会话分叉并明确提示已回退。如果 worktree 创建后,会话创建或标签页挂载失败,
96 自动清理只会删除分支、`HEAD` 和状态仍与创建结果完全一致的未使用 worktree;一旦
97 检测到任何变化,就会保留现场以便恢复。成功挂载的 worktree 会作为项目持久注册,
98 关闭标签页或重启后仍可发现。新建 allocation 还会在 checkout 旁以 `0600` 权限写入
99 v1 `metadata.json`,绑定原始 source checkout、目标分支、创建时 `HEAD`、受管
100 worktree 根和临时分支。旧版本创建且没有该元数据的 worktree 无法使用 Merge-Back,
101 因为 Reasonix 不会猜测目标分支;界面会保留现场并给出手动合并指引。未知元数据版本
102 同样按失败关闭处理。
103
104 Merge-Back 是“合并、清理分离”的失败原子流程。预检会验证受管路径和仓库身份、精确
105 分支与 `HEAD`、source 干净且没有进行中的 Git 操作、全部可见或 detached Desktop
106 活动任务、工作区写租约、integrated terminal、ahead/behind、diff 和冲突。取得双
107 workspace lease 后,Desktop 会
108 短暂封闭 turn start 和 controller publication,再为 canonical source/worktree 两个根登记
109 贯穿 Git 变更的 reservation。项目 runtime owner、新 turn 以及 terminal create/write 都
110 经过同一 admission;子目录和 symlink 别名受保护,prefix sibling 与无关项目不受影响。
111 worktree 有未提交改动时默认禁止合并;只有用户显式开启自动提交才会继续,并在精确新
112 提交上重新做冲突预检。确认 token 使用 NUL-safe 状态,同时绑定真实 index entries、
113 每个脏路径的类型、mode、文件内容或 symlink 目标。自动提交从确认的 `HEAD` 创建 `0600`
114 临时 index,`git add -A` 只作用于该副本。若真实 index 含有当前完整工作区未表示的
115 staged/index-only 内容,Reasonix 会停止,真实 index 和两个版本都保持原样。否则通过无
116 hook、单父提交的 `commit-tree` 创建精确提交,对确认的 worktree branch 做 compare-and-swap,
117 并仅在真实 index 字节仍一致时通过独占 `index.lock` 安装准备好的 index。branch CAS 后的
118 任何失败都返回 recovery-required;目标分支、`HEAD`、index 或内容发生漂移时不会继续。
119 source 合并使用带 Reasonix 命令级提交身份的
120 `git merge --no-ff --no-commit --no-verify`,不依赖用户 Git identity,也不运行 commit hook,
121 并把实际 index tree 与重新计算的 merge-tree 精确绑定;准备前及安装 ref 前都会重新验证
122 worktree root、Git common-dir、symbolic branch、branch ref、`HEAD`、Git operation 和内容
123 token。只有这些身份、目标分支、原始 `HEAD`、精确 `MERGE_HEAD` 和 prepared tree 都仍
124 一致时,才通过无 hook 的 `commit-tree` 创建固定 parents/tree 的提交。短生命周期 source
125 mutation fence 会持有真实 index、`HEAD` 和 `MERGE_HEAD` lockfile,并比较三者准确快照。
126 这些 checkout 局部锁保持期间,Git 通过指向同一 common ref store 的 detached 管理视图
127 只取得 branch ref 锁。单个
128 `update-ref --stdin` transaction 会同时验证 worktree branch ref,并用原目标 `HEAD` 对
129 target ref 做 compare-and-swap,避免任一 ref 检查部分生效。提交后还会复核两个 checkout、
130 commit tree、真实 index tree、parents、refs、干净状态和 Git operations。安装后使用
131 `git merge --quit` 只清理辅助 merge state,不直接更新 `MERGE_HEAD` pseudoref,也不 reset
132 prepared index。CAS 前只有仍能证明 prepared state 完整的失败才会 abort;target ref 漂移、
133 CAS 后漂移或无法证明恢复成功的状态返回 recovery-required,同时保留所有 worktree 资源和
134 外部状态。
135
136 合并成功后,Reasonix 先通过正常 Desktop 生命周期切换到记录的 source checkout。每次
137 前端导航都会向后端登记 opaque intent token;关闭请求在快照前和实际移除 Tab 的线性化
138 点都必须仍持有该 token。因此更新导航会停止关闭和清理并保留资源;稳定时后端也只会在
139 精确 source Tab 仍 active、精确 worktree Tab 仍 idle 时关闭页面和终端。
140
141 独立、可重试的 finalization 会 reservation 包含原 canonical worktree 与固定 recovery 子树的整个
142 allocation,并扫描可见及 detached runtime;项目 runtime 创建、恢复、删除/归档 fallback
143 和重定向都经过同一 admission gate。symlink 与子目录受保护,allocation 外的 prefix
144 sibling 和其他 allocation 不受影响。只有临时提交已包含在目标分支、身份一致且包含 ignored 文件的
145 完整 status 为空时,Reasonix 才会先以 `0600` 原子写入 v2 `cleanup-state.json`,记录原路径、
146 allocation 内随机 recovery 路径、branch、`HEAD` 和 `planned` 阶段。随后使用普通
147 `git worktree move`,再次验证 common-dir、symbolic branch、branch ref、`HEAD`、Git operation、
148 完整 status 和注册路径,再把 journal 推进到 `retained`。任一阶段崩溃都按 journal 与 Git
149 worktree 注册表的精确身份重试;多候选或未知状态失败关闭。
150
151 recovery checkout 会继续保持 registered,并继续检出其 `reasonix/delivery-*` 分支。Reasonix
152 不会注销 worktree、删除临时分支、逐文件 unlink 或递归删除任何路径。因此移动前已经打开的文件
153 描述符会跟随 checkout,晚到写入仍可恢复;原公开路径重新出现的内容也会原样保留并报告。恢复回执
154 持久化后,Desktop 只移除原 managed worktree 的陈旧项目注册,保持 source project active,且
155 不会把隐藏 recovery 路径加入侧栏;注册表写入失败可以借助 journal 重试。
156
157 新版本只以保留方式读取 v1 journal:仍注册的 legacy checkout 只有在精确身份和 manifest 均可
158 证明时才转换为 v2;已经 detached 或身份不明确的 legacy 路径只报告人工恢复,不删除也不自动
159 重新注册。未知 journal 版本失败关闭。metadata 继续使用 v1;旧 cleanup reader 会拒绝未知的
160 v2 journal,从而保留 recovery checkout。
161
162 Delivery worktree 仍是可选能力。非隔离目录使用 workspace lease(`filelock`)。
163 路径型写入对祖先兼容锁和目标路径层级分片加 shared 锁、对具体文件分片加
164 exclusive 锁,且只在该次 tool 期间持有。整区写入会独占精确根锁和对应层级分片,
165 因此父工作区与直接打开的嵌套仓库能够互斥,而两个会话仍可同时写不同文件(包括同一
166 仓库)。`bash`/MCP 的写操作只在该命令期间独占整区;若配置的 tool hook 可能写入
167 未声明路径,任何 tool 调用都会改用整区锁。文件和层级身份都映射到有界锁分片;哈希
168 碰撞最多让无关工作串行,不会削弱保护。只读 bash 不拿写锁。冲突卡片会说明正在写的
169 文件或整区。macOS 使用折叠身份协调大小写别名,同时保留原始大小写根锁兼容旧版;
170 旧版进程仍只认识它打开时的路径拼写,跨拼写共存需要双方都使用新协议。Git 不是运行
171 前提;对话结束后不继续占锁。需要长期隔离工作树时再用 worktree。
172
172 lines MARKDOWN