OpenProgram / SessionStore / 实施交接
存储一致性
本页规范 meta.json、内存 _index 与 index.json 的时间和并发合同,以及 merged heads 和 store 锁范围。各部分保留独立的验收边界。
updated_at 表示最后一次会话内容追加时间,不表示任意元数据持久化时间。内存注册表的读、改、快照由独立锁保护;磁盘原子写串行化,但不在注册表锁内执行文件 I/O。延迟 flush 只能清除自己快照之前的 dirty 状态,不能覆盖并发更新。注册表时间与不变量
| 位置 | 禁止的行为 | 目标不变量 |
|---|---|---|
_persist_meta | 每次写 meta.json 都赋值 time.time()。 | 改名、置顶、已读、分支标记、HEAD 持久化不得改变会话内容排序时间;append 路径显式推进。 |
_index | 更新和 list_sessions() 复制不受同一把锁保护。 | 读者获得行字典的点时快照;并发创建、更新、删除不产生迭代异常或撕裂字段。 |
| deferred flush | timer 受锁保护,但快照序列化和其他直接保存路径未形成统一协议。 | 旧快照不得晚于新快照落盘;写入期间的新 mutation 必须保留 dirty 并由后续 flush 保存。 |
参考机制与取舍
现有 SessionMemoryIndex
节点和 session meta 已有自己的内部锁。采用其“锁内更新、锁外文件 I/O”边界,不增加事务框架。
stdlib threading
采用一把 registry state lock 加一把 write lock。前者保护 dict、dirty、timer;后者只保证磁盘快照按取得顺序写入。
atomic_write_text
继续负责单次文件替换的原子性;不新增数据库、文件锁或第三方依赖。
采用:显式内容时钟、行级浅拷贝快照、串行原子写。修改:_persist_meta 不再隐式更新时间。拒绝:对所有 SessionStore 操作加全局锁、每次 append 同步写 registry、把 updated_at 拆成新字段并迁移全部消费者。
目标状态流
updated_at,不自行推进。list_sessions 在同一锁内复制每行后再过滤和排序。失败、并发与兼容
- 磁盘写失败保留现有 warning;内存状态仍可服务当前进程,后续 mutation/flush 可再次保存。
list_sessions返回副本,调用方修改返回行不得反向修改 registry。- 创建时调用方提供的历史
updated_at保持原值;重建旧 index 不迁移 schema。 - 排序仍按
updated_at降序,缺失值仍按 0 处理;wire shape 和字段集合不变。 - 不在 registry state lock 内执行
atomic_write_text,避免列表读取被磁盘延迟阻塞。
明确排除
- 注册表合同不修改节点 DAG 的并发模型。
mark_merged和位置注册表分别由后文的原子更新与锁范围合同约束。 - 不处理 Windows、系统 credential store、孤儿文件、认证改动和其他审计项。
- 不引入 SQLite、跨进程 registry 锁或新的后台服务。
验收矩阵
| 公共入口 | 场景 | 可观察断言 |
|---|---|---|
update_session | 创建两会话后改名、置顶、标已读 | 目标会话的 meta/index updated_at 不变,列表顺序不变。 |
append_message | 追加一条内容 | meta/index 同时推进,列表按新时间排序。 |
list_sessions | 调用方修改返回行 | 后续列表结果不受影响。 |
| deferred flush | 旧快照写入期间并发更新 | 后续 flush 后 index.json 包含新字段,不被旧保存清除 dirty。 |
| 并发注册表访问 | 并发 update/list/flush | 无迭代异常,JSON 始终可解析,最终内存与磁盘一致。 |
缓存快照更新
缓存重建和首次加载在读取历史与元数据之前获取磁盘指纹,重建后的索引只记录这个已获取的指纹。读取期间的原子替换会改变磁盘指纹,下一次公共读取因此再次重建。旧快照不得使用较新的磁盘指纹标记为已同步。动态 DAG 上下文和终止状态沿用现有存储与锁。
验收使用两个真实 SessionStore 实例:读取旧快照之后、标记同步之前完成历史或元数据写入;随后 get_nodes 或 get_session 能看到新值。覆盖已有缓存和首次加载,取消重试保留原有时限并读取真实取消状态。
Windows 会话目录删除
显式删除会话、过期会话清理和容量清理共用平台删除适配器。普通 Git 对象因 Windows 只读属性导致删除失败时,清除该属性并仅重试该次删除一次。其他权限失败保持调用方原有的尽力处理行为;不修改 ACL,也不跟随链接改变目标权限。缺失历史的清理条件和归档保留规则保持不变。
验收通过公开 SessionStore reopen 和 delete_session 入口使用只读 Git 对象。原生 Windows 单元回归检查目录确实消失,而非仅从索引移除。实现遵循标准 shutil 只读文件删除示例,使用兼容 Python 3.11 的错误回调与只读属性检查。
适配器和公开回归已通过独立规格与质量审查。原生验收由 Windows 单元 CI 执行,本地模拟不能替代。
Merged heads 原子更新
mark_merged(session_id, head_ids) 是集合并集操作。并发 merge 可以来自子 agent 完成、task runner 和 Web 分支操作;两个调用必须保留双方写入,不能由后写者覆盖先写者。
状态合同
| 属性 | 要求 |
|---|---|
| 原子性 | 读取现有列表、去重追加、写回 idx.meta 在同一个 idx._lock 临界区完成。 |
| 顺序 | 保留既有顺序;每个调用按输入顺序追加此前不存在的非空、strip 后 ID。 |
| 幂等 | 重复 ID 不产生重复项;没有新增值时不写磁盘。 |
| 持久化 | 锁内只更新内存;_persist_meta 在锁外执行,避免文件 I/O 持有 index 状态锁。并发调用最终至少有一个持久化快照包含完整并集。 |
mark_merged 的 read-modify-write。它不扩展为通用 transaction API,不修改 branch naming、DAG 节点删除或 session index flush 协议。验收
- 两个线程在同一旧快照上并发追加不同 head,最终内存和重载后的磁盘均包含两者。
- 空白、重复输入保持当前清理与幂等语义。
- 持久化不在
idx._lock内发生;异常和现有调用方行为保持。
Store 锁范围
SessionStore._lock 仅保护会话位置映射和 LRU 缓存结构。_open、位置索引持久化和 delete_session 不得在该锁内执行目录扫描、原子写、fsync 或递归删除,避免一个会话的慢 I/O 阻塞其他会话。
并发合同
| 资源 | 锁 | 要求 |
|---|---|---|
_locations、_sessions 和 LRU 顺序 | _lock | 仅在读取、修改或复制这些内存结构时持有;不允许在锁内进行文件系统 I/O。 |
单个 session_id 的首次加载、重建、删除和缓存失效 | 每会话 RLock | 同一会话串行,不同会话可并行。不创建通用 transaction 层。 |
locations.json 发布 | 专用写锁 | 专用写锁串行化快照获取和发布;在全局状态锁内更新并复制快照,释放状态锁后落盘,防止旧快照后写覆盖新快照。 |
SessionMemoryIndex 持久化 | 现有 _persist_lock → idx._lock | 保持现有锁序和 stale rebuild 语义;不在 idx._lock 内进行 I/O。 |
验收
- 会话 A 的
read_meta/list_history被阻塞时,会话 B 的缓存读取在有界时间内完成。 - 会话 A 的递归删除被阻塞时,会话 B 的读取不等待 A。
- 同一会话的并发首次加载只发布一个缓存对象;删除与加载不交错发布已删除对象。
- 两个会话并发更新位置映射后,重载的
locations.json包含两项。
实现状态与证据
| 文件 | 职责 |
|---|---|
openprogram/store/session/session_store.py | 时间语义、registry state/write 锁、快照与延迟 flush。 |
openprogram/store/session/session_node_writer.py | node append 的单一 activity timestamp 与 meta/index 同步。 |
tests/component/store/test_session_index_consistency.py | 排序时间、返回快照和并发写入的公共回归。 |
注册表时间与快照合同已实现并完成独立审查。 规格审查与质量审查均通过;最终 gate 证据记录在任务台账。
原子 merged heads 与 store 锁范围在此集中维护设计合同和验收要求;文档合并本身不新增它们的实现或平台验收结论。