Skip to content

Commit e009864

Browse files
authored
Merge pull request #343 from Yumiue/refactor/sqlite-persistence
重构会话持久化:改为 SQLite 增量存储并移除 JSON 快照写入
2 parents 4d49d5e + a642a54 commit e009864

34 files changed

Lines changed: 3427 additions & 2340 deletions

docs/session-persistence-design.md

Lines changed: 107 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -2,106 +2,144 @@
22

33
## 模块职责与边界
44

5-
- `internal/session` 是会话领域模型、存储抽象与 JSON 持久化实现的唯一归属层
6-
- `internal/runtime` 负责决定保存时机、恢复会话状态和编排主循环,不承载文件存储细节
7-
- `internal/tui` 只消费 runtime 暴露的会话数据,不直接读写会话文件
5+
- `internal/session` 是会话领域模型、SQLite 存储实现和资产持久化的唯一归属层
6+
- `internal/runtime` 只决定何时创建会话、追加消息、更新会话头和替换 transcript,不关心底层表结构
7+
- `internal/tui` 只消费 runtime 暴露的会话数据,不直接读取数据库或资产文件
88

99
## 存储策略
1010

11-
NeoCode 当前使用本地 JSON 文件持久化会话,以保持实现简单、可调试且跨平台可移植
11+
NeoCode 当前使用工作区级 SQLite 数据库持久化会话,不再使用 `session.json` 文件
1212

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`
1726

1827
## 数据模型
1928

20-
`internal/session.Session` 持久化以下核心字段:
29+
### sessions
30+
31+
会话头保存摘要和 durable 状态:
2132

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`
2639
- `workdir`
27-
- `task_state`
28-
- `todos`
29-
- `messages`
40+
- `task_state_json`
41+
- `todos_json`
42+
- `activated_skills_json`
3043
- `token_input_total`
3144
- `token_output_total`
45+
- `last_seq`
46+
- `message_count`
3247

33-
其中:
48+
### messages
3449

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+
消息正文按行存储,一条消息对应一行:
4051

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`
4261

43-
`task_state` 固定包含以下字段:
62+
### session_assets
4463

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+
资产元数据入库,二进制内容落盘:
5465

55-
`todos` 固定包含以下要点:
5666
- `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+
该操作不写消息,只覆盖会话头字段。
63105

64-
其中 `status` 当前固定为:
65-
- `pending`
66-
- `in_progress`
67-
- `completed`
106+
### 替换 transcript
68107

69-
同时,当 session JSON 缺失 `todos` 字段时,`Load` 会按空 Todo 列表兼容加载。
108+
compact 成功后,runtime 调用 `ReplaceTranscript`,在单事务内:
70109

71-
## 读写行为
110+
- 删除该会话原有全部消息
111+
- 按新顺序写回 compact 后的消息
112+
- 同步更新 `task_state`、token 统计、provider/model/workdir 和消息计数
72113

73-
- `Save` 使用“临时文件 + 原子替换”写入完整会话 JSON
74-
- `Load` 在用户真正进入某个会话时读取完整历史,并严格要求 `schema_version``task_state` 字段存在
75-
- `ListSummaries` 只解析摘要字段,并按 `updated_at` 倒序返回;不合法的旧 session 文件会被直接跳过
114+
这是低频路径,允许重写整段 transcript。
76115

77-
## Token 计数持久化
116+
### 加载会话
78117

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`
83120

84-
## TaskState 与 compact
121+
## Token 持久化
85122

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 并持久化
91127

92-
## 保存时机
128+
## TaskState 与 Todo
93129

94-
- 用户消息提交后保存
95-
- assistant 完整回复后保存
96-
- 每个工具结果完成后保存
97-
- 避免在高频 UI 刷新路径中直接做磁盘 I/O
130+
- `TaskState` 是 compact 与多轮续航依赖的 durable summary
131+
- `Todo` 是结构化任务状态,独立持久化在 `sessions.todos_json`
132+
- 二者都属于会话头,不写入 `messages`
133+
- context 构建时优先读取 `TaskState``Todo`、最近消息和必要工具结果
98134

99135
## 并发约束
100136

101-
- `internal/session` 的存储实现自行保护共享访问
102-
- 保存时机统一由 runtime 决定,TUI 不直接触发磁盘写入
137+
- SQLite 负责单工作区数据库的一致性和事务边界
138+
- runtime 继续通过会话锁串行化同一 session 的关键写入路径
139+
- 不同 session 可以并行运行
103140

104141
## 演进约束
105142

106-
- 新增存储实现时,应优先在 `internal/session` 内扩展并通过接口注入
107-
- 不应把持久化逻辑重新分散到 `runtime``tui` 或其他上层模块
143+
- 新增持久化行为时,优先扩展 `internal/session.Store` 的意图型接口
144+
- 不要把 SQL、事务或表结构细节泄漏到 `runtime``tui` 或其他上层模块
145+
- 如需进一步优化读路径,应继续在 `internal/session` 内演进,而不是重新引入文件级快照保存

docs/session-todo-design.md

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,60 @@
11
# Session Todo 设计说明
22

3-
本文档补充说明 `internal/session` 中 Todo 的数据模型、持久化语义与边界约束
3+
本文档补充说明 `internal/session` 中 Todo 的数据模型、持久化语义和边界约束
44

55
## 设计目标
66

7-
- Todo 归属于 `Session`,不单独引入新的持久化子系统
8-
- Todo 只表示结构化待办状态,不替代现有 `TaskState`
9-
- Todo 的校验、规范化和基础增删改查统一收敛在 `internal/session`
7+
- Todo 归属于 `Session`,不单独引入新的持久化子系统
8+
- Todo 只表示结构化待办状态,不替代 `TaskState`
9+
- Todo 的校验、规范化和基础增删改查统一收敛在 `internal/session`
1010

1111
## 数据模型
1212

13-
`Session` 新增 `todos` 字段,对应 `[]TodoItem`
13+
`Session` 包含 `Todos []TodoItem` 字段
1414

15-
单个 `TodoItem` 目前包含
15+
单个 `TodoItem` 当前包含
1616

1717
- `id`
1818
- `content`
1919
- `status`
2020
- `dependencies`
21+
- `priority`
22+
- `owner_type`
23+
- `owner_id`
24+
- `artifacts`
25+
- `failure_reason`
26+
- `revision`
2127
- `created_at`
2228
- `updated_at`
23-
- 可选 `priority`
2429

25-
其中 `status` 固定为以下三个值
30+
其中 `status` 当前固定为
2631

2732
- `pending`
2833
- `in_progress`
2934
- `completed`
35+
- `failed`
3036

3137
## 持久化语义
3238

33-
- Todo 跟随 `Session` 一起通过现有 JSONStore 保存和加载。
34-
- `Save` 前会对 Todo 执行统一规范化与校验:
35-
- `id``content` 去空白
36-
- 空状态默认收敛为 `pending`
37-
- `dependencies` 去空白、去重、保持顺序
38-
- 拒绝重复 ID
39-
- 拒绝自依赖
40-
- 拒绝引用不存在的依赖项
41-
- `Load` 允许 session JSON 缺失 `todos` 字段,并按空 Todo 列表处理。
39+
- Todo 跟随会话头一起保存在 SQLite `sessions.todos_json`
40+
- runtime 修改 Todo 时只调用 `UpdateSessionState`,不会写入 `messages`
41+
- `LoadSession` 时会把 `todos_json` 还原为完整 `[]TodoItem`
42+
43+
## 规范化与校验
44+
45+
写入前会统一执行 Todo 校验和规范化,包括:
46+
47+
- `id``content` 去空白
48+
- 空状态收敛为 `pending`
49+
- `dependencies` 去空白、去重并保持顺序
50+
- 拒绝重复 ID
51+
- 拒绝自依赖
52+
- 拒绝引用不存在的依赖项
53+
- 使用 `revision` 保障更新时的乐观并发校验
4254

4355
## 与 TaskState 的关系
4456

45-
- `TaskState` 仍是 runtime/context 用于 compact 与续航的 durable summary。
46-
- `Todo` 是更细粒度的结构化状态,不直接注入 context,不写入消息历史。
47-
- 如果未来需要收敛两者关系,应通过单独演进,让 `TaskState``Todo` 派生摘要,而不是直接复用同一字段。
57+
- `TaskState` 仍是 runtime/context 用于 compact 和续航的 durable summary
58+
- `Todo` 是更细粒度的结构化执行状态
59+
- `Todo` 不直接拼入模型消息历史
60+
- 如需让 `TaskState` 汇总 Todo,应在 runtime/context 层显式投影,而不是复用同一个字段

go.mod

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,28 @@ module neo-code
33
go 1.25.0
44

55
require (
6+
github.com/Microsoft/go-winio v0.6.2
7+
github.com/atotto/clipboard v0.1.4
68
github.com/charmbracelet/bubbles v1.0.0
79
github.com/charmbracelet/bubbletea v1.3.10
810
github.com/charmbracelet/glamour v1.0.0
911
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
1012
github.com/creativeprojects/go-selfupdate v1.5.2
13+
github.com/prometheus/client_golang v1.23.2
1114
github.com/spf13/cobra v1.10.2
1215
github.com/spf13/viper v1.21.0
1316
golang.design/x/clipboard v0.7.1
1417
golang.org/x/net v0.52.0
1518
golang.org/x/sys v0.42.0
1619
gopkg.in/yaml.v3 v3.0.1
20+
modernc.org/sqlite v1.48.2
1721
)
1822

1923
require (
2024
code.gitea.io/sdk/gitea v0.22.1 // indirect
2125
github.com/42wim/httpsig v1.2.3 // indirect
2226
github.com/Masterminds/semver/v3 v3.4.0 // indirect
23-
github.com/Microsoft/go-winio v0.6.2 // indirect
2427
github.com/alecthomas/chroma/v2 v2.20.0 // indirect
25-
github.com/atotto/clipboard v0.1.4 // indirect
2628
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
2729
github.com/aymerick/douceur v0.2.0 // indirect
2830
github.com/beorn7/perks v1.0.1 // indirect
@@ -44,6 +46,7 @@ require (
4446
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
4547
github.com/google/go-github/v74 v74.0.0 // indirect
4648
github.com/google/go-querystring v1.1.0 // indirect
49+
github.com/google/uuid v1.6.0 // indirect
4750
github.com/gorilla/css v1.0.1 // indirect
4851
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
4952
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
@@ -59,11 +62,12 @@ require (
5962
github.com/muesli/reflow v0.3.0 // indirect
6063
github.com/muesli/termenv v0.16.0 // indirect
6164
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
65+
github.com/ncruces/go-strftime v1.0.0 // indirect
6266
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
63-
github.com/prometheus/client_golang v1.23.2 // indirect
6467
github.com/prometheus/client_model v0.6.2 // indirect
6568
github.com/prometheus/common v0.66.1 // indirect
6669
github.com/prometheus/procfs v0.16.1 // indirect
70+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
6771
github.com/rivo/uniseg v0.4.7 // indirect
6872
github.com/sagikazarmark/locafero v0.11.0 // indirect
6973
github.com/sahilm/fuzzy v0.1.1 // indirect
@@ -88,4 +92,7 @@ require (
8892
golang.org/x/text v0.35.0 // indirect
8993
golang.org/x/time v0.14.0 // indirect
9094
google.golang.org/protobuf v1.36.11 // indirect
95+
modernc.org/libc v1.70.0 // indirect
96+
modernc.org/mathutil v1.7.1 // indirect
97+
modernc.org/memory v1.11.0 // indirect
9198
)

0 commit comments

Comments
 (0)