| 1 | # Desktop Session v5 |
| 2 | |
| 3 | Desktop Session v5 makes `SessionRef{HostID: "local", SessionID}` the only |
| 4 | runtime identity for local conversations. Workspace membership is presentation |
| 5 | state, not a storage locator, and a tab is only a disposable view over that |
| 6 | identity. |
| 7 | |
| 8 | ## Authoritative state |
| 9 | |
| 10 | - `desktop-sessions-v5/by-id/<session-id>` owns immutable headers, manifests, |
| 11 | events, and content references. |
| 12 | - `desktop/workspace-state-v1.json` owns Workspace and Session ordering, |
| 13 | visibility, archives, and in-flight create reservations. |
| 14 | - Canonical title, model, and turn metadata remain events. Query indexes are |
| 15 | disposable projections and cannot add or remove Workspace members. |
| 16 | - Local Desktop uses one `SessionService` with host ID `local`. A Workspace |
| 17 | path is recorded in a new session header but is never needed to open it. |
| 18 | - Workspace attachment validates the candidate SessionID against its immutable |
| 19 | header `cwd`; the ordered registry is the membership authority, while the |
| 20 | header prevents one Session from being attached to the wrong Workspace. |
| 21 | |
| 22 | Creation and rotation reserve a SessionID in `pendingCreates`, seed and flush |
| 23 | the canonical session, attach the ID to its Workspace, and only then expose a |
| 24 | ready tab. Startup completes an interrupted attach when the session exists and |
| 25 | drops the reservation when it does not. Tab pruning is refused if snapshot, |
| 26 | metadata, or registry publication has not completed. |
| 27 | |
| 28 | ## Opening and recovery |
| 29 | |
| 30 | The Workspace browser lists registry IDs even while their projections are |
| 31 | being rebuilt. `OpenSession` first reads history without a controller and never |
| 32 | creates a replacement for a missing or damaged ID. A navigation sequence fences |
| 33 | late results so only the newest selection can replace the visible runtime. |
| 34 | Frontend transcript, draft, operation, paging, and hydration fences use the |
| 35 | complete SessionRef first; legacy paths are only a compatibility fallback. |
| 36 | |
| 37 | When a saved model no longer exists, Desktop retries the same SessionID with |
| 38 | the configured default model, appends the replacement `session/config` event, |
| 39 | and reports a non-blocking warning. History remains independent of projection |
| 40 | and controller startup. |
| 41 | |
| 42 | ## Upgrade boundary |
| 43 | |
| 44 | The v5 migrator reads, but never modifies, canonical v4 and legacy JSONL data. |
| 45 | Imports are staged, validated, atomically published, and attached to a Workspace |
| 46 | before their ledger record becomes complete. A verified unchanged source is |
| 47 | idempotent even after the destination advances. Different sources are never |
| 48 | merged merely because their IDs or content match; collisions receive a stable `migr-<hash>` ID. |
| 49 | Failures remain retryable and do not stop other sources from migrating. |
| 50 | |
| 51 | Canonical v4 migration enumerates durable session identities independently of |
| 52 | display metadata. A missing or stale catalog cache must never make a conversation |
| 53 | look empty and exclude it from migration; no open tab or legacy JSONL equivalent |
| 54 | is required. Source read failures are recorded in the migration ledger and exposed |
| 55 | by session architecture diagnostics. Startup retries them and finishes workspace |
| 56 | attachment for targets published before an interruption, without duplicating them. |
| 57 | |
| 58 | Older session inputs in global, known-project and saved-tab directories are |
| 59 | discovered during the same startup pass, including historical `sessions-v3` |
| 60 | directories alongside `sessions-v4`. Schema-1 |
| 61 | transcripts are imported even when their display sidecars are absent. Schema-2 |
| 62 | DAG transcripts enumerate every live head and import each head independently; |
| 63 | the first adopted head retains its source key across selection changes and |
| 64 | retirement. Older migration receipts are reconciled against source content so |
| 65 | upgrading from an older migrator does not duplicate continued targets. Retired |
| 66 | heads remain excluded. Prototype |
| 67 | v3 and linear v3/v3.1 stores use their explicit read-only adapters and are staged |
| 68 | into the v5 codec after validation. Paired transcript/event sources are compared |
| 69 | before publication; divergent histories remain readable but are refused rather |
| 70 | than silently choosing one. |
| 71 | |
| 72 | Automatic conflict-recovery artifacts are excluded from the ordinary v5 list. |
| 73 | Discovery recognizes `recovered`, `version_kind=recovery`, a recovery digest, |
| 74 | and the historical `-recovery-<16 hex>` filename convention. The exclusion |
| 75 | follows paired v3/v4 stores and conversion provenance, including archived |
| 76 | metadata when originals are absent. Original files remain untouched. An |
| 77 | ordinary user fork, a shared title, or an arbitrary filename containing |
| 78 | "recovery" does not qualify for exclusion. |
| 79 | |
| 80 | Persisted `manifest.source` provenance also links already-converted generations, |
| 81 | including v2 -> v3, v2 -> v3 -> v4, and native v3 -> v4. All inputs of a linked |
| 82 | head are staged separately so an ancestor's source digest cannot accidentally |
| 83 | read a continued descendant that has the same identity. Equal histories and |
| 84 | strict prefixes share one v5 target containing the complete history. Independent |
| 85 | continuations become separate targets; unrelated sessions are never coalesced |
| 86 | just because their text or title matches. An omitted legacy head is resolved from |
| 87 | the conversion's immutable original snapshot, not the old app's current selection. |
| 88 | Stored-only chains are resolved even after the original JSONL is removed. |
| 89 | If a native v3 source directory is removed after multiple conversions, its |
| 90 | persisted source path still identifies the shared origin across archived copies. |
| 91 | |
| 92 | Each source receives its own adoption receipt, including covered ancestors. |
| 93 | Already-completed receipts and v5 continuations remain authoritative; migration |
| 94 | does not delete existing v5 conversations or rewrite their content to force a |
| 95 | merge. Source changes with additional independent work are imported separately. |
| 96 | |
| 97 | Each startup still enumerates source directories, but completed migrations use a |
| 98 | lightweight file revision (file presence, mode, size and nanosecond modification |
| 99 | time). Unchanged sources do not replay history, construct temporary imports, |
| 100 | compare target history, reattach membership or rewrite the ledger. Legacy checks |
| 101 | include the same sidecar set as the importer's freeze operation. This revision is |
| 102 | a change detector for normal writes, not an integrity checksum for edits that |
| 103 | deliberately preserve both file size and modification time. |
| 104 | |
| 105 | The ledger records that a source was adopted. Continuing, archiving or deleting |
| 106 | the v5 target does not make that source eligible again. Missing revision markers |
| 107 | in older completed records are filled after a one-time comparison with the |
| 108 | recorded **source** content digest, never with the target's current history. |
| 109 | Changed source content is imported separately without overwriting continued v5 |
| 110 | work. Failed attempts retain the previous completion receipt; a source changed |
| 111 | during import is not certified as complete. Pending and failed imports remain |
| 112 | retryable on the next startup. |
| 113 | |
| 114 | | Ledger field | Older data | New reader/writer | Previous reader/writer | |
| 115 | | --- | --- | --- | --- | |
| 116 | | `sourceRevision` (optional) | Absent | Reconcile source digest once, then skip unchanged revisions | Ignores the field; may drop it on write, requiring reconciliation again | |
| 117 | | `previousCompletion` (optional) | Absent | Retains prior adoption proof across failed updates | Ignores the field; old releases retain their old migration behavior | |
| 118 | | `legacyHeads`, `legacyHeadsRevision` (optional) | Absent | Enumerates frozen heads once; skips replay while the source revision is unchanged | Ignores these fields; may drop them on write | |
| 119 | | `legacyPrimaryHead`, `legacySelectedHead` (optional) | Absent | Keeps per-head identity stable while routing the selected head to its paired store | Ignores these fields; old migration behavior remains | |
| 120 | | `legacyAdoption` (optional) | Absent | Preserves a pre-head-migration receipt when selection changes or migration is interrupted | Ignores this field; old migration behavior remains | |
| 121 | | `legacyConversions` (optional) | Absent | Caches resolved conversion ancestry/head IDs under the source revision | Ignores this field; discovery is repeated if it is dropped | |
| 122 | |
| 123 | | Source format | Old data | New reader | Previous reader | Compatibility | |
| 124 | | --- | --- | --- | --- | --- | |
| 125 | | JSONL / schema-1 | Preserved byte-for-byte | Imported without display metadata; event history takes precedence | Still reads original source | Read-only upgrade | |
| 126 | | schema-2 DAG | Preserved byte-for-byte | Every live head becomes a resumable v5 session; retired heads stay excluded | Original selection and branches remain intact | Read-only upgrade | |
| 127 | | Prototype v3 / linear v3 / v3.1 | Preserved byte-for-byte | Explicit adapter validates and stages into v5 | Still reads original source | Read-only upgrade | |
| 128 | | v5 output | Separate directory | Native v5 reader/writer | Not visible to older versions | Existing isolation boundary | |
| 129 | |
| 130 | The ledger stays at version 1. Writes preserve unknown root and record fields; |
| 131 | unreadable or future-version ledgers are refused rather than replaced. Session |
| 132 | formats and provider-visible message bytes are unchanged. |
| 133 | |
| 134 | Optional `submission/accepted` events added by #10392 survive canonical export |
| 135 | and migration unchanged. Receipts retain their original session scope: a |
| 136 | same-identity migration exposes them in history, while a conflict mapped to an |
| 137 | independent identity does not adopt the old session's admission keys. This |
| 138 | matches fork isolation and keeps host metadata out of provider messages. The |
| 139 | canonical title reader introduced by #10389 reads migrated history directly. |
| 140 | |
| 141 | Protocol 9 is a hard Desktop shell/host boundary. Older releases retain their |
| 142 | original data but do not see sessions created only in v5. Remote and Serve |
| 143 | session protocols are unchanged. |
| 144 | |
| 145 | ## 中文说明 |
| 146 | |
| 147 | Desktop Session v5 将 `SessionRef{HostID: "local", SessionID}` 设为本地会话 |
| 148 | 唯一运行时身份。Workspace 只管理展示归属与顺序,tab 只是可淘汰的视图, |
| 149 | Topic 和项目路径都不再参与打开 canonical 会话。 |
| 150 | |
| 151 | - `desktop-sessions-v5/by-id/<session-id>` 保存不可变 Header、Manifest、事件 |
| 152 | 和内容引用;canonical 事件仍是标题、模型及轮次 metadata 的唯一真相。 |
| 153 | - `desktop/workspace-state-v1.json` 保存 Workspace/Session 顺序、可见性、归档 |
| 154 | 状态和新建事务;查询索引损坏或重建时不得删除其中的 SessionID。 |
| 155 | - Registry attach 会用不可变 Header 的 `cwd` 校验 Workspace 归属;Registry |
| 156 | 仍是成员与顺序真相,但错误 Workspace 不能收录该 SessionID。 |
| 157 | - 新建与轮换必须依次完成 pending 预留、canonical seed/flush、Registry attach, |
| 158 | 然后才允许 tab 进入 Ready;淘汰 tab 前会重新验证持久化结果。 |
| 159 | - 打开会话先进行与 controller 无关的历史读取,缺失或损坏的 ID 不会生成 |
| 160 | 空白替代会话;navigation sequence 保证快速连续点击仅最后一次生效。 |
| 161 | - 前端 transcript、草稿、操作、分页与 hydration 防线优先比较完整 |
| 162 | SessionRef;legacy path 只作为旧会话兼容回退。 |
| 163 | - 原模型失效时,在同一 SessionID 上使用 Desktop 默认模型恢复,并追加新的 |
| 164 | `session/config` 事件;不改变已有历史和 provider-visible prompt/tool bytes。 |
| 165 | |
| 166 | v5 迁移器只读保留 canonical v4 与 legacy JSONL。每个导入都先在临时目录完成 |
| 167 | 校验和原子发布,再写入 Workspace;已验证且未变化的来源幂等复用,不因为 |
| 168 | ID 或正文相同合并不同来源,冲突稳定映射为 `migr-<hash>`。 |
| 169 | Protocol 10 是 Desktop shell/host 的硬边界;远端与 Serve |
| 170 | 协议保持不变,旧版只能看到升级前保留的数据。 |
| 171 | |
| 172 | canonical v4 迁移按持久化会话身份枚举,不依赖标题、轮次、预览等显示缓存。 |
| 173 | 缓存缺失或过期不能成为跳过会话的依据,也不要求会话有打开的 tab 或对应的 |
| 174 | legacy JSONL。源数据读取失败会记入迁移台账并通过会话架构诊断接口报告; |
| 175 | 下次启动会重试,并为中断前已发布的目标补齐 Workspace 归属,不重复创建。 |
| 176 | |
| 177 | 启动扫描同时覆盖全局、已登记项目和保存的 tab 所指向的旧目录,包括历史 |
| 178 | `sessions-v3` 与现有 `sessions-v4`。v1 JSONL / schema-1 日志不再依赖显示 |
| 179 | 元数据;通过统一文件分类排除事件、运行记录等辅助 JSONL,避免误导入为对话。 |
| 180 | v2 schema-2 DAG 从冻结副本枚举所有未删除分支,分别恢复为可续聊的 v5 会话; |
| 181 | 在旧版本中切换或删除主分支不会改变其他分支的迁移身份。已删除分支不会复活。 |
| 182 | v3 原型、线性 v3、v3.1 均使用显式只读转换器。旧快照与配对事件存储先比较 |
| 183 | 历史,选择包含完整较新历史的一方;存在真正分歧时保留源文件并报告失败。 |
| 184 | |
| 185 | 自动生成的冲突恢复副本不迁入 v5 普通会话列表。通过 `recovered`、 |
| 186 | `version_kind=recovery`、恢复摘要以及历史 `-recovery-<16位十六进制>` |
| 187 | 文件名识别,排除规则沿配对的 v3/v4 存储和转换来源链继承;原文件缺失时也检查 |
| 188 | 保留的归档元数据。原始文件保留不动。普通用户分支、相同标题,以及仅包含 |
| 189 | “recovery”文字的普通文件名不受影响。 |
| 190 | |
| 191 | 迁移还会根据 `manifest.source` 追溯已经转换的来源关系,覆盖 v2→v3、 |
| 192 | v2→v3→v4、原生 v3→v4 等多代目录同时残留的情况。每代源在独立临时目录中 |
| 193 | 校验,避免同 ID 的较新副本污染较早源的内容摘要。相同历史或只包含较早前缀 |
| 194 | 的源共用一份完整的 v5 会话;同源但各自续聊后产生不同内容的记录分别保留。 |
| 195 | 没有来源关系的对话,即使文本或标题相同,也不会被合并。 |
| 196 | |
| 197 | 旧转换记录未填写 head 时,从它保留的原始快照确认当时选中的分支,不拿旧软件 |
| 198 | 当前选择代替。即使原始 JSONL 已移除,保留的 v3/v4 转换链仍会去重。 |
| 199 | 原生 v3 目录在多次转换后被移除时,也按保存的原始来源路径识别各归档副本的关系。 |
| 200 | 被覆盖的较早源也记录各自的迁移完成证明,重启、v5 续聊或删除目标不会使它再次导入。 |
| 201 | 已完成的旧迁移记录和 v5 中的新工作仍受保护;不会为强行合并而删除已有会话或 |
| 202 | 覆盖其内容。旧源后来出现额外的独立工作时,按新分支保留。 |
| 203 | |
| 204 | | 来源格式 | 旧数据 | 新版读取行为 | 旧版读取行为 | 兼容结论 | |
| 205 | | --- | --- | --- | --- | --- | |
| 206 | | JSONL / schema-1 | 原字节保留 | 无元数据也迁移,事件历史优先 | 仍读取原文件 | 只读升级 | |
| 207 | | schema-2 DAG | 原字节保留 | 恢复全部未删除分支 | 原分支与默认选择不变 | 只读升级 | |
| 208 | | v3 原型 / 线性 v3 / v3.1 | 原字节保留 | 校验后转换并导入 v5 | 仍读取原文件 | 只读升级 | |
| 209 | | v5 输出 | 独立目录 | 原生读写 | 旧版不可见 | 沿用隔离边界 | |
| 210 | |
| 211 | 每次启动仍枚举旧目录,但已完成项只检查文件是否存在、类型、大小与修改时间。 |
| 212 | 源文件未变化时,不重读历史、不生成临时导入、不比较目标内容、不重新登记归属, |
| 213 | 也不重写台账。JSONL 检查包含导入器冻结的全部 sidecar。此标记用于识别正常文件 |
| 214 | 写入,不是用于检测刻意保持大小和修改时间不变的改写的完整性校验。 |
| 215 | |
| 216 | 台账确认的是旧源已经被接收;在 v5 续聊、归档或删除目标,都不会触发重新导入。 |
| 217 | 旧台账缺少 `sourceRevision` 时,会与台账保存的源内容摘要核对一次后补齐标记, |
| 218 | 不会拿旧源与已续聊的目标比较。旧源内容真正增加或变化时单独导入,保留 v5 中 |
| 219 | 的新工作。失败更新通过可选字段 `previousCompletion` 保留上次成功记录;迁移中 |
| 220 | 源数据发生变化则不标为完成,下次启动继续重试。 |
| 221 | |
| 222 | 新增可选字段 `legacyHeads`、`legacyHeadsRevision` 缓存已校验的分支列表, |
| 223 | 旧源不变时不重放 DAG。`legacyPrimaryHead` 固定首次迁移的主分支身份, |
| 224 | `legacySelectedHead` 标识应与配对存储比较的当前分支;`legacyAdoption` |
| 225 | 保留旧迁移器的成功记录,按源内容识别已经在 v5 续聊的目标。旧台账没有这些 |
| 226 | 字段时自动补齐,原有成功记录不会因升级或重试而丢失。 |
| 227 | `legacyConversions` 缓存已经核对的转换来源及分支身份,参与同一源版本校验, |
| 228 | 源数据未变化时无需重新回放原始分支快照。 |
| 229 | |
| 230 | 台账仍为 version 1,旧读取器忽略新增可选字段;旧写入器可能丢弃这些字段, |
| 231 | 重新升级后需要再次核对。旧版本本身仍按旧迁移策略运行。新写入保留未知根字段 |
| 232 | 和记录字段,拒绝覆盖损坏或未来版本的台账;会话格式与模型可见消息内容不变。 |
| 233 | |
| 234 | #10392 新增的可选 `submission/accepted` 事件通过 canonical 导出与迁移原样保留。 |
| 235 | 回执继续按原会话 ID 隔离:同 ID 迁移后的历史保留提交标识;因冲突映射为独立 ID |
| 236 | 的会话不会接收旧会话的提交幂等键,与 fork 隔离规则一致。回执不进入模型消息。 |
| 237 | #10389 新增的 canonical 标题读取接口可直接读取迁移后的历史。 |
| 238 |