| 1 | # 列表与搜索不可变读取实施说明 |
| 2 | |
| 3 | [English](READ_SNAPSHOT_PAGINATION.md) |
| 4 | |
| 5 | ## 问题与修复边界 |
| 6 | |
| 7 | 本地会话持续写入标题、结果时,Desktop 可能提示 `session_operation:stale_cursor:The session list changed. Reload it.`。原先的元数据双重检查要求写入在整个读取期间保持静止,导致首屏也可能失败。只删除双重检查仍不够:续页重新读取、排序实时数据,会让旧游标失效;前端首屏超过 200 行时,还可能漏掉内部第二次 RPC 的过期错误。这条错误本身不能证明 SSH、Git、Docker 或远程 Reasonix 发生故障。 |
| 8 | |
| 9 | 修复由 `desktop/read_snapshot_store.go` 统一持有读取结果。续页按固定序号读取冻结结果,不再重新排序,也不比较全局索引版本。快照只提供读视图;写命令继续校验当前真实身份、归属和生命周期。 |
| 10 | |
| 11 | ## 实施范围与代码落点 |
| 12 | |
| 13 | | 入口 | 改动 | |
| 14 | | --- | --- | |
| 15 | | 本地项目、全局、分组、筛选列表 | `session_workspace_sidebar.go` 生成冻结结果;移除 `session_topic_index.go` 的第二套排序缓存 | |
| 16 | | 置顶栏 | 使用相同快照分页,收集完成后释放;错误不提交部分置顶结果 | |
| 17 | | 旧版项目树入口 | `ListProjectTree` 返回标准 RPC 错误,不把部分子列表当成成功 | |
| 18 | | 历史会话元数据 | `history_read_snapshots.go` 配合一个 `sessioncatalog.WithReadView` | |
| 19 | | 历史全文搜索、单会话搜索 | 共用冻结候选与摘要构建器;`historycatalog.CaptureSearch` 流式捕获一次 SQL 排序结果 | |
| 20 | | 前端项目列表 | `projectTreeWindow.ts` 整窗暂存,`ProjectTree.tsx` 提交和调度 | |
| 21 | | 前端历史列表与搜索 | `useHistoryCatalog.ts` 统一请求代次、恢复和提交 | |
| 22 | |
| 23 | 不修改 canonical 正文 history-window、持久化会话格式或发送给 provider 的数据。 |
| 24 | |
| 25 | ## 读取协议和生命周期 |
| 26 | |
| 27 | 1. 空游标创建或加入同查询的构建。查询身份包含入口类型、目标、筛选和排序,不包含页大小。 |
| 28 | 2. 成员、展示、分组和生命周期来自同一份 registry 投影;每个 canonical 会话只读取一次元数据。旧目录的内部多页查询使用一个 SQLite 读事务。 |
| 29 | 3. 固定结果集合和排序。全文搜索先捕获候选,再释放源库读事务、读取原文生成摘要;摘要与内容指纹来自同一次读取。指纹不匹配时请求重建索引,并标记覆盖不完整;真实 I/O 错误使构建失败。 |
| 30 | 4. 只有完整构建才能发布。同查询并发读可以共用构建数据,但每位调用者获得独立、可幂等释放的句柄。 |
| 31 | 5. v1 游标包含随机的进程内句柄、查询绑定和下一行序号。重复读取相同游标得到同一页。旧格式、重启、过期或回收后的游标明确失败,不尝试解释为实时偏移。 |
| 32 | 6. 消息、标题、结果和分组顺序变化不打断旧快照;下一次刷新读取新状态。相关归档、删除、移动、接管和源替换使依赖它的结果失效。 |
| 33 | 7. 响应增加可选 `snapshotId`、`snapshotExpiresAt`;历史入口保留 `staleCursor`,增加 `readError`。RPC 保留 `stale_cursor`,增加结构化 `readReason`。`ReleaseReadSnapshot` 可重复调用。 |
| 34 | |
| 35 | 元数据列表通过 catalog/registry 校验生命周期,不为此打开正文。搜索另外验证源文件身份;携带 `contentDigest` 的上下文请求会在使用消息下标前拒绝变化后的内容。 |
| 36 | |
| 37 | ## 资源治理 |
| 38 | |
| 39 | | 项目 | 默认值 | |
| 40 | | --- | --- | |
| 41 | | App 内句柄总数,含共享构建缓存租约 | 64 | |
| 42 | | 已计量的内存行数据与保留的依赖元数据 | 32 MiB | |
| 43 | | 同时构建数 | 2 | |
| 44 | | 单结果转临时 SQLite 的阈值 | 512 KiB | |
| 45 | | 临时 SQLite 总预算 / 单文件预留 | 256 MiB / 64 MiB | |
| 46 | | 单份捕获结果 | 64 MiB | |
| 47 | | 空闲有效期 / 最长有效期 | 10 / 30 分钟 | |
| 48 | | 单次公开分页上限 | 200 行 | |
| 49 | |
| 50 | 未发布构建、搜索中间候选也计入存储预算。全局管理锁只处理账本,不持锁执行数据库、文件读取或生命周期回调。读取租约保护正在使用的结果;一个等待者取消不会取消其他等待者。关闭 App 时取消构建并清理临时文件,不扫描其他进程的目录。 |
| 51 | |
| 52 | 访问时校验过期,后续构建或关闭时回收过期条目。这是快照存储预算,不是整个进程的 RSS 上限:SQLite 缓存、registry 投影和单份原文重放仍有临时工作内存。磁盘采用保守预留,因此四个已转磁盘的结果或候选文件就可能用完配额。预算不足必须明确报错,不能静默截断结果。 |
| 53 | |
| 54 | ## 前端恢复与自动刷新 |
| 55 | |
| 56 | - 一个逻辑窗口跨多次 RPC 时,所有分页先暂存,成功后一次提交。首屏内部第二页过期也能恢复。 |
| 57 | - 每次逻辑操作最多自动重建一次。追加失败后重建整个已展开窗口,并整体替换;第二次失败保留当前内容并提供重试。 |
| 58 | - 拒绝混用快照 ID、不前进的游标,以及空结果仍返回下一页的异常。 |
| 59 | - 后台事件在固定 200 ms 窗口内合并;读取中只记录一次待刷新,不不断取消;同一列表后台刷新启动间隔至少 500 ms。显式改变查询或排序立即淘汰旧请求代次。 |
| 60 | - 保留展开规模、稳定行身份和选中状态;项目列表恢复可见行位置,并避免覆盖用户在期间主动滚动的位置。结果替换、组件卸载和过时响应都会释放对应句柄。 |
| 61 | - 历史元数据列表与正文命中共用一个 UI 代次,一起恢复和提交;命中身份包含 `partIndex`,避免同消息内多个工具参数相互覆盖。 |
| 62 | - 项目、当前会话、查询或筛选切换时立即隔离旧结果和游标,旧按钮回调、预览和搜索上下文不能写入新身份。后台刷新失败仍保留同一身份的已有内容。 |
| 63 | - 项目移除后重新加入不复用请求序号;旧请求完成时不能消费新代次的待刷新任务。列表失效、折叠窗口重置、项目移除及归档均释放退役快照。 |
| 64 | - 历史列表与普通搜索的查询绑定包含捕获的当前会话身份,避免切换期间共用错误构建;明确指定目标会话的搜索仍以目标为准,不因前台焦点变化失效。 |
| 65 | |
| 66 | ## 验收与交付 |
| 67 | |
| 68 | 确定性测试覆盖 205/405/1000 行、持续元数据写入、分组变化、归档边界、跨查询游标、无关索引写入期间排序稳定、源删除、磁盘转存、过期、预算失败、独立释放和关闭。前端测试覆盖两路历史结果恢复、保留错误前内容、待刷新合并、切换查询/排序和异常游标。Chromium `independent-sessions` 增加了三种大窗口规模。 |
| 69 | |
| 70 | 验收分别执行根模块的 catalog/history 测试、Desktop 独立 Go 模块测试、定向 `-race`、生成的 host 契约检查、前端类型检查/lint 和浏览器场景。此前 `TestLargeTranscriptSwitchPhaseMeasurement` 在原始 HEAD 上也已复现卡住;排除它后的探索性广泛回归仍超过 10 分钟总时限,未取得完整测试集通过的证据。生成契约与检查必须顺序执行,避免测试期间修改生成产物造成无效的过期判断。 |
| 71 | |
| 72 | 发布前还要用真正的 macOS/Windows 测试包验证:本地持续对话、SSH 已连接、展开超过 200 行、旧历史全文搜索、归档/恢复和关闭清理。本地 Go 与浏览器证据不能替代原生安装包验收。本次本地实施不包含 Git 推送、版本发布或生产部署。 |
| 73 |