|
2 | 2 |
|
3 | 3 | ## 模块职责与边界 |
4 | 4 |
|
5 | | -- `internal/session` 是会话领域模型、存储抽象与 JSON 持久化实现的唯一归属层。 |
6 | | -- `internal/runtime` 负责决定保存时机、恢复会话状态和编排主循环,不承载文件存储细节。 |
7 | | -- `internal/tui` 只消费 runtime 暴露的会话数据,不直接读写会话文件。 |
| 5 | +- `internal/session` 是会话领域模型、SQLite 存储实现和资产持久化的唯一归属层。 |
| 6 | +- `internal/runtime` 只决定何时创建会话、追加消息、更新会话头和替换 transcript,不关心底层表结构。 |
| 7 | +- `internal/tui` 只消费 runtime 暴露的会话数据,不直接读取数据库或资产文件。 |
8 | 8 |
|
9 | 9 | ## 存储策略 |
10 | 10 |
|
11 | | -NeoCode 当前使用本地 JSON 文件持久化会话,以保持实现简单、可调试且跨平台可移植。 |
| 11 | +NeoCode 当前使用工作区级 SQLite 数据库持久化会话,不再使用 `session.json` 文件。 |
12 | 12 |
|
13 | | -- 默认目录按工作区隔离:`~/.neocode/projects/<workspace-hash>/sessions/` |
14 | | -- 工作区哈希基于启动阶段确定的工作区根目录生成 |
15 | | -- `session.Workdir` 表示会话最近一次运行实际使用的目录,由启动 `workdir` 或请求级覆盖值写回,但不参与分桶 |
16 | | -- 旧的全局 `~/.neocode/sessions/` 开发期数据不迁移、不回读 |
| 13 | +- 数据库路径:`~/.neocode/projects/<workspace-hash>/session.db` |
| 14 | +- 资产目录:`~/.neocode/projects/<workspace-hash>/assets/<session-id>/<asset-id>.bin` |
| 15 | +- 工作区哈希基于启动时确定的工作区根目录生成 |
| 16 | +- `session.Workdir` 记录该会话最近一次运行实际使用的目录,但不参与分桶 |
| 17 | +- 开发阶段遗留的旧 `sessions/` JSON 目录不迁移、不回读、不兼容 |
| 18 | + |
| 19 | +SQLite 初始化固定使用以下 PRAGMA: |
| 20 | + |
| 21 | +- `journal_mode = WAL` |
| 22 | +- `synchronous = NORMAL` |
| 23 | +- `foreign_keys = ON` |
| 24 | +- `busy_timeout = 5000` |
| 25 | +- `user_version = 1` |
17 | 26 |
|
18 | 27 | ## 数据模型 |
19 | 28 |
|
20 | | -`internal/session.Session` 持久化以下核心字段: |
| 29 | +### sessions |
| 30 | + |
| 31 | +会话头保存摘要和 durable 状态: |
21 | 32 |
|
22 | | -- `schema_version` |
23 | | -- `id`、`title` |
24 | | -- `provider`、`model` |
25 | | -- `created_at`、`updated_at` |
| 33 | +- `id` |
| 34 | +- `title` |
| 35 | +- `created_at_ms` |
| 36 | +- `updated_at_ms` |
| 37 | +- `provider` |
| 38 | +- `model` |
26 | 39 | - `workdir` |
27 | | -- `task_state` |
28 | | -- `todos` |
29 | | -- `messages` |
| 40 | +- `task_state_json` |
| 41 | +- `todos_json` |
| 42 | +- `activated_skills_json` |
30 | 43 | - `token_input_total` |
31 | 44 | - `token_output_total` |
| 45 | +- `last_seq` |
| 46 | +- `message_count` |
32 | 47 |
|
33 | | -其中: |
| 48 | +### messages |
34 | 49 |
|
35 | | -- `schema_version` 为开发期强校验字段;当前实现只接受当前版本,不兼容旧 session 文件 |
36 | | -- `provider` / `model` 记录最近一次成功运行会话时使用的配置,供 compact 等流程优先复用 |
37 | | -- `task_state` 是会话级 durable task state,由 runtime 维护、session 持久化、context 只读投影 |
38 | | -- `token_input_total` / `token_output_total` 分别表示会话累计输入与输出 token |
39 | | -- token 字段仍使用 `omitempty`,但不再承担旧版 session JSON 兼容职责 |
| 50 | +消息正文按行存储,一条消息对应一行: |
40 | 51 |
|
41 | | -`internal/session.Summary` 只保留会话列表渲染所需的轻量字段,不加载完整消息历史。 |
| 52 | +- `session_id` |
| 53 | +- `seq` |
| 54 | +- `role` |
| 55 | +- `parts_json` |
| 56 | +- `tool_calls_json` |
| 57 | +- `tool_call_id` |
| 58 | +- `is_error` |
| 59 | +- `tool_metadata_json` |
| 60 | +- `created_at_ms` |
42 | 61 |
|
43 | | -`task_state` 固定包含以下字段: |
| 62 | +### session_assets |
44 | 63 |
|
45 | | -- `goal` |
46 | | -- `progress` |
47 | | -- `open_items` |
48 | | -- `next_step` |
49 | | -- `blockers` |
50 | | -- `key_artifacts` |
51 | | -- `decisions` |
52 | | -- `user_constraints` |
53 | | -- `last_updated_at` |
| 64 | +资产元数据入库,二进制内容落盘: |
54 | 65 |
|
55 | | -`todos` 固定包含以下要点: |
56 | 66 | - `id` |
57 | | -- `content` |
58 | | -- `status` |
59 | | -- `dependencies` |
60 | | -- `created_at` |
61 | | -- `updated_at` |
62 | | -- 可选 `priority` |
| 67 | +- `session_id` |
| 68 | +- `mime_type` |
| 69 | +- `size_bytes` |
| 70 | +- `relative_path` |
| 71 | +- `created_at_ms` |
| 72 | + |
| 73 | +## 运行时读写语义 |
| 74 | + |
| 75 | +### 创建会话 |
| 76 | + |
| 77 | +runtime 在新会话开始时调用 `CreateSession`,只写入一条空会话头,不写消息正文。 |
| 78 | + |
| 79 | +### 追加消息 |
| 80 | + |
| 81 | +runtime 在以下时机调用 `AppendMessages`: |
| 82 | + |
| 83 | +- 用户消息提交后 |
| 84 | +- assistant 完整回复后 |
| 85 | +- 每个 tool result 完成后 |
| 86 | + |
| 87 | +一次调用会在同一事务内完成两件事: |
| 88 | + |
| 89 | +- 追加 1..N 条消息 |
| 90 | +- 更新会话头上的 `updated_at`、`provider`、`model`、`workdir`、token 增量和消息计数 |
| 91 | + |
| 92 | +因此常规写入不再与历史消息总量线性耦合。 |
| 93 | + |
| 94 | +### 更新会话头 |
| 95 | + |
| 96 | +runtime 在以下场景调用 `UpdateSessionState`: |
| 97 | + |
| 98 | +- workdir 变更 |
| 99 | +- task_state 变更 |
| 100 | +- todo 列表变更 |
| 101 | +- skill 激活状态变更 |
| 102 | +- assistant 本轮没有正文,但 provider/model 或 token 统计发生变化 |
| 103 | + |
| 104 | +该操作不写消息,只覆盖会话头字段。 |
63 | 105 |
|
64 | | -其中 `status` 当前固定为: |
65 | | -- `pending` |
66 | | -- `in_progress` |
67 | | -- `completed` |
| 106 | +### 替换 transcript |
68 | 107 |
|
69 | | -同时,当 session JSON 缺失 `todos` 字段时,`Load` 会按空 Todo 列表兼容加载。 |
| 108 | +compact 成功后,runtime 调用 `ReplaceTranscript`,在单事务内: |
70 | 109 |
|
71 | | -## 读写行为 |
| 110 | +- 删除该会话原有全部消息 |
| 111 | +- 按新顺序写回 compact 后的消息 |
| 112 | +- 同步更新 `task_state`、token 统计、provider/model/workdir 和消息计数 |
72 | 113 |
|
73 | | -- `Save` 使用“临时文件 + 原子替换”写入完整会话 JSON |
74 | | -- `Load` 在用户真正进入某个会话时读取完整历史,并严格要求 `schema_version` 与 `task_state` 字段存在 |
75 | | -- `ListSummaries` 只解析摘要字段,并按 `updated_at` 倒序返回;不合法的旧 session 文件会被直接跳过 |
| 114 | +这是低频路径,允许重写整段 transcript。 |
76 | 115 |
|
77 | | -## Token 计数持久化 |
| 116 | +### 加载会话 |
78 | 117 |
|
79 | | -- runtime 在 provider 调用完成后更新 session 的累计 token 字段 |
80 | | -- 会话保存时,token 计数随 session 一起持久化 |
81 | | -- 会话重新加载时,runtime 从 session 恢复累计 token |
82 | | -- 自动 compact 成功后,runtime 会重置累计 token,并将重置后的值持久化 |
| 118 | +- `ListSummaries` 只查询 `sessions` 表,并按 `updated_at` 倒序返回摘要 |
| 119 | +- `LoadSession` 先读取会话头,再按 `seq` 顺序加载消息并组装完整 `Session` |
83 | 120 |
|
84 | | -## TaskState 与 compact |
| 121 | +## Token 持久化 |
85 | 122 |
|
86 | | -- `TaskState` 是继续执行多轮任务时的唯一 durable truth,不依赖聊天消息本身长期保存 |
87 | | -- compact 成功后,runtime 会同时回写 `session.TaskState` 和压缩后的 `session.Messages` |
88 | | -- `messages` 中的 `[compact_summary]` 只是展示层,不再是唯一续航载体 |
89 | | -- context 构建时会优先注入 `TaskState`,再注入 memo、最近消息和必要工具结果 |
90 | | -- 只有当 `TaskState` 已建立后,读时 micro compact 才允许清理旧的可重建 tool payload |
| 123 | +- runtime 在 assistant 调用完成后累计输入和输出 token |
| 124 | +- `AppendMessages` 可以原子地追加消息并累加 token |
| 125 | +- `UpdateSessionState` 和 `ReplaceTranscript` 可以直接覆盖 token 总量 |
| 126 | +- compact 成功后,runtime 会将 token 总量重置为 0 并持久化 |
91 | 127 |
|
92 | | -## 保存时机 |
| 128 | +## TaskState 与 Todo |
93 | 129 |
|
94 | | -- 用户消息提交后保存 |
95 | | -- assistant 完整回复后保存 |
96 | | -- 每个工具结果完成后保存 |
97 | | -- 避免在高频 UI 刷新路径中直接做磁盘 I/O |
| 130 | +- `TaskState` 是 compact 与多轮续航依赖的 durable summary |
| 131 | +- `Todo` 是结构化任务状态,独立持久化在 `sessions.todos_json` |
| 132 | +- 二者都属于会话头,不写入 `messages` 表 |
| 133 | +- context 构建时优先读取 `TaskState`、`Todo`、最近消息和必要工具结果 |
98 | 134 |
|
99 | 135 | ## 并发约束 |
100 | 136 |
|
101 | | -- `internal/session` 的存储实现自行保护共享访问 |
102 | | -- 保存时机统一由 runtime 决定,TUI 不直接触发磁盘写入 |
| 137 | +- SQLite 负责单工作区数据库的一致性和事务边界 |
| 138 | +- runtime 继续通过会话锁串行化同一 session 的关键写入路径 |
| 139 | +- 不同 session 可以并行运行 |
103 | 140 |
|
104 | 141 | ## 演进约束 |
105 | 142 |
|
106 | | -- 新增存储实现时,应优先在 `internal/session` 内扩展并通过接口注入 |
107 | | -- 不应把持久化逻辑重新分散到 `runtime`、`tui` 或其他上层模块 |
| 143 | +- 新增持久化行为时,优先扩展 `internal/session.Store` 的意图型接口 |
| 144 | +- 不要把 SQL、事务或表结构细节泄漏到 `runtime`、`tui` 或其他上层模块 |
| 145 | +- 如需进一步优化读路径,应继续在 `internal/session` 内演进,而不是重新引入文件级快照保存 |
0 commit comments