Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/FLOWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ sequenceDiagram
Hook->>N: startLegacyImport()
N->>I: importLegacyChats()
I->>DB: read legacy conversations/messages
I->>DB: write new_sessions / new_messages
I->>DB: write new_sessions / deepchat_messages
I->>DB: backfill normalized message/session hot-path tables
I-->>N: import status
```

Expand Down
17 changes: 16 additions & 1 deletion docs/architecture/agent-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,22 @@ agentRuntimePresenter/
| Stream loop | `src/main/presenter/agentRuntimePresenter/process.ts` | 调用 provider、累计 blocks、驱动 tool loop |
| Tool dispatch | `src/main/presenter/agentRuntimePresenter/dispatch.ts` | 调用 `ToolPresenter`、暂停交互、生成 tool 结果 |
| Context build | `src/main/presenter/agentRuntimePresenter/contextBuilder.ts` | 历史裁剪、resume context、token budget |
| Persistence | `src/main/presenter/agentRuntimePresenter/messageStore.ts` | 消息持久化与故障恢复 |
| Persistence | `src/main/presenter/agentRuntimePresenter/messageStore.ts` | 消息持久化、分页读取、结构化内容重组与故障恢复 |

## 持久化热路径

`DeepChatMessageStore` 现在采用“头表 + 结构化子表”的主链路模型:

- `deepchat_messages` 作为消息头表
- `deepchat_user_messages` / `files` / `links` 存 user 热字段
- `deepchat_assistant_blocks` 存 assistant blocks
- `deepchat_search_documents` / `_fts` 存历史搜索索引

关键语义:

- streaming 期间只增量更新 `deepchat_assistant_blocks`
- 最终进入 `sent/error` 时才写回稳定的 `deepchat_messages.content`
- 读路径优先从结构化表重组 `ChatMessageRecord.content`,缺行时再回退旧 JSON

## 兼容边界

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ export const AppErrorSchema = z.object({
| Route | Input summary | Output summary | Notes |
| --- | --- | --- | --- |
| `sessions.create` | title / provider / model / agent / projectDir | session summary | 会话创建主入口 |
| `sessions.restore` | `{ sessionId }` | session snapshot | 恢复主入口 |
| `sessions.restore` | `{ sessionId, limit? }` | `{ session, messages, nextCursor, hasMore }` | 恢复最新一页消息 |
| `sessions.listMessagesPage` | `{ sessionId, cursor?, limit? }` | `{ messages, nextCursor, hasMore }` | 向更老消息翻页 |
| `sessions.list` | optional filter | session summaries | 列表主入口 |
| `sessions.activate` | `{ sessionId }` | `{ activated: true }` | 当前窗口激活会话 |
| `sessions.deactivate` | none | `{ deactivated: true }` | 当前窗口关闭活跃会话 |
Expand Down
13 changes: 11 additions & 2 deletions docs/architecture/session-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ retirement 之后,会话管理被明确拆成两层:
| --- | --- | --- |
| `AgentSessionPresenter` | `src/main/presenter/agentSessionPresenter/index.ts` | renderer 唯一 session 入口 |
| `NewSessionManager` | `src/main/presenter/agentSessionPresenter/sessionManager.ts` | `new_sessions` 记录、窗口绑定、session CRUD |
| `NewMessageManager` | `src/main/presenter/agentSessionPresenter/messageManager.ts` | 新会话消息读取与 agent routing |
| `DeepChatSessionStore` | `src/main/presenter/agentRuntimePresenter/sessionStore.ts` | 活跃 runtime 状态 |
| `DeepChatMessageStore` | `src/main/presenter/agentRuntimePresenter/messageStore.ts` | 新消息持久化 |
| `DeepChatMessageStore` | `src/main/presenter/agentRuntimePresenter/messageStore.ts` | 新消息持久化、分页读取、结构化内容重组 |
| `SessionPresenter` | `src/main/presenter/sessionPresenter/index.ts` | legacy conversation/thread/export 兼容层 |
| `sessionPresenter/messageFormatter.ts` | `src/main/presenter/sessionPresenter/messageFormatter.ts` | 用户消息上下文格式化与 exporter 复用 |

Expand Down Expand Up @@ -71,3 +70,13 @@ sequenceDiagram

如果是当前聊天会话创建、发送消息、取消生成、tool interaction,请直接从
`agentSessionPresenter` 和 `agentRuntimePresenter` 开始读。

## 恢复与历史分页

新的聊天恢复链路已经不再假设“打开会话 = 一次性读取全量消息”:

- `sessions.restore` 只返回最近一页消息,默认 `100` 条
- `sessions.listMessagesPage` 负责继续向更老消息翻页
- renderer `messageStore` 首屏只加载第一页,`ChatPage` 在接近顶部时再拉旧历史

这样可以让大会话恢复保持稳定首屏时间,也把“历史很长”和“首屏可用”两个目标解耦开。
3 changes: 1 addition & 2 deletions docs/guides/code-navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@

| 功能 | 位置 | 备注 |
| --- | --- | --- |
| session route dispatch | `src/main/routes/index.ts` | `sessions.create` / `restore` / `activate` / `deactivate` / `getActive` |
| session route dispatch | `src/main/routes/index.ts` | `sessions.create` / `restore` / `listMessagesPage` / `activate` / `deactivate` / `getActive` |
| session orchestration | `src/main/routes/sessions/sessionService.ts` | `Scheduler` + session/message repositories |
| chat route dispatch | `src/main/routes/index.ts` | `chat.sendMessage` / `stopStream` / `respondToolInteraction` |
| chat orchestration | `src/main/routes/chat/chatService.ts` | send / stop / permission response owner |
Expand Down Expand Up @@ -132,4 +132,3 @@ rg "settingsChangedEvent|sessionsUpdatedEvent|chatStream" src/shared src/main sr
- `docs/archives/legacy-agentpresenter-architecture.md`
- `docs/archives/legacy-agentpresenter-flows.md`
- `docs/archives/thread-presenter-migration-plan.md`

29 changes: 29 additions & 0 deletions docs/issues/chat-history-pagination-stability/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# 会话历史分页稳定性 Plan

## 实现方向

- 以 [spec.md](./spec.md) 为准,优先修复共享 message store 的竞态和刷新语义。
- 在 renderer store 中区分“首屏恢复/切会话恢复”和“同会话刷新”:
- 首屏恢复仍以最近消息窗口为主。
- 同会话刷新根据当前已加载条数补齐相同规模的窗口,避免历史被截断。
- 为顶部翻页增加请求代次保护,确保异步返回只影响当前有效请求。
- 在主进程消息分页 helper 中允许多取 1 条记录,仅用于 `hasMore` 探测,不扩大公开分页上限。

## 兼容性

- 不调整 shared route schema,因此 IPC/client 调用方无需改协议。
- 刷新后的消息顺序仍保持 `orderSeq ASC`。
- `hasMoreHistory` / `nextCursor` 继续由 store 管理,聊天页滚动逻辑无需改交互。

## 测试策略

- renderer store:
- 覆盖 `loadOlderMessages()` 的跨会话失效保护。
- 覆盖同会话刷新时保留已加载历史窗口。
- main message store:
- 覆盖 `limit=500` 时依然正确返回 `hasMore`。

## 验证

- 运行聚焦 Vitest 用例验证回归场景。
- 完成后运行 `pnpm run format`、`pnpm run i18n`、`pnpm run lint`。
35 changes: 35 additions & 0 deletions docs/issues/chat-history-pagination-stability/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# 会话历史分页稳定性

## 背景

当前未提交改动为会话恢复增加了分页能力,但在 renderer store 和主进程分页实现里引入了三个回归:

1. `loadOlderMessages()` 在异步请求返回后没有校验当前活跃会话,切换线程时可能把旧线程历史写进新线程。
2. `loadMessages()` 每次刷新都只恢复最近 100 条消息,会把用户已经向上加载出来的历史重新截断。
3. 主进程分页通过 `limit + 1` 判断 `hasMore`,但底层 SQL helper 把请求再次限制到 500,导致 `limit=500` 时无法正确探测下一页。

## 目标

- 历史分页请求只能更新发起它的会话,不得污染当前活跃会话。
- 会话刷新应保留用户已经加载出来的历史窗口,避免流结束、重试、删除、工具交互后出现历史截断。
- 保持分页接口现有契约不变,同时修复 `limit=500` 时的 `hasMore` 误判。
- 修复不能让滚动加载、流式渲染或会话切换变卡顿。

## 非目标

- 不重做聊天页的滚动 UI 或消息渲染结构。
- 不修改 `sessions.restore` / `sessions.listMessagesPage` 的对外输入输出结构。
- 不改变消息排序、分页方向或现有默认页大小(100)。

## 约束

- 遵循 typed route / typed client / store 现有边界。
- renderer 继续使用 Vue 3 Composition API 和 Pinia store 模式。
- 保持已有流式事件刷新逻辑可用,避免破坏 `chat.stream.*` 的行为。

## 验收标准

- 当用户在顶部加载旧历史期间切换会话,旧请求返回后不会改写新会话的 `messages`、`nextCursor`、`hasMoreHistory`、`isLoadingHistory`。
- 对同一会话执行刷新时,若用户此前已加载超过 100 条历史,刷新后保留相同数量级的已加载窗口,不回退到最近 100 条。
- `limit=500` 的分页请求在仍有更旧消息时返回 `hasMore=true` 且提供可继续翻页的 `nextCursor`。
- 为上述场景补齐 renderer/main 单测。
8 changes: 8 additions & 0 deletions docs/issues/chat-history-pagination-stability/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# 会话历史分页稳定性 Tasks

- [ ] 确认 [spec.md](./spec.md) 没有遗留 `[NEEDS CLARIFICATION]`。
- [ ] 在 renderer message store 中补齐历史分页请求失效保护。
- [ ] 调整同会话刷新逻辑,保留已加载历史窗口。
- [ ] 修复主进程分页的 `limit=500` `hasMore` 探测边界。
- [ ] 更新 renderer/main 聚焦测试。
- [ ] 运行 `pnpm run format`、`pnpm run i18n`、`pnpm run lint`。
182 changes: 182 additions & 0 deletions docs/specs/sqlite-mainline-normalization/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# `agent.db` 主链路 SQLite 结构化收敛实施计划

## 1. 目标

把 `new_sessions` 与 `deepchat_*` 主链路中的热路径 JSON 依赖下沉到结构化 SQLite 表,同时保持:

1. renderer 侧消息类型与 exporter 兼容
2. legacy import / export 不回归
3. 升级过程不要求用户手动迁移

## 2. 关键设计决策

### 2.1 保留头表,拆出热字段

`deepchat_messages` 继续作为消息头表,保留:

- `id`
- `session_id`
- `order_seq`
- `role`
- `status`
- `is_context_edge`
- `created_at`
- `updated_at`

同时新增结构化表承载热字段:

- `deepchat_user_messages`
- `deepchat_user_message_files`
- `deepchat_user_message_links`
- `deepchat_assistant_blocks`
- `new_session_active_skills`
- `new_session_disabled_agent_tools`

`deepchat_messages.content` 与 `new_sessions.active_skills` / `disabled_agent_tools` 保留为回退源和兼容字段,但不再作为主链路读写来源。

### 2.2 repository 边界重组 JSON

renderer 与 exporter 仍消费 `ChatMessageRecord.content` 的 JSON 字符串形式,因此结构化内容只在
`DeepChatMessageStore` 边界按需重组。

这保证:

1. 新持久化模型不向 renderer 扩散
2. 旧视图与导出逻辑无需整体重写

### 2.3 恢复链路分页化

- `sessions.restore(sessionId, limit?)` 返回最新一页
- `sessions.listMessagesPage()` 继续向更老消息翻页
- renderer `messageStore.loadMessages()` 只请求第一页
- `ChatPage.vue` 在接近顶部时触发历史追加,并保持滚动锚点稳定

### 2.4 搜索索引与降级

搜索索引使用两层表:

1. `deepchat_search_documents`
2. `deepchat_search_documents_fts`

查询顺序:

1. 先执行 FTS5
2. FTS 不可用或执行失败时降级到 `LIKE`

索引同步点:

1. session create / rename / delete
2. message create / edit / delete / clear / fork / import
3. assistant message finalize / error

### 2.5 streaming 改成 block 级增量写

assistant streaming 期间:

1. `deepchat_assistant_blocks` 增量 replace
2. `deepchat_messages.status` 更新为 `pending`
3. 不再重复重写 `deepchat_messages.content`

最终态:

1. 进入 `sent/error`
2. 再把完整 blocks 序列化回 `deepchat_messages.content`
3. 同步搜索文档

### 2.6 migration + background backfill

schema migration 在启动时只做表创建与版本升级,不阻塞 UI。

后台 backfill 做三件事:

1. 从 `deepchat_messages.content` 拆 user / assistant 结构化数据
2. 从 `new_sessions` JSON 列拆 skills / disabled tools
3. 初始化搜索文档与 FTS 索引

backfill 需要:

1. 可重入
2. 记录级 fallback 安全
3. 与 legacy import 共用结构化写入 helper

## 3. 公开接口变化

### 3.1 Shared types

- `MessagePageCursor`
- `ChatMessagePageResult`
- `MessageStartResult`

### 3.2 Typed routes

- 扩展 `sessions.restore`
- 新增 `sessions.listMessagesPage`

### 3.3 Internal ports

- `MessageRepository.listPageBySession(...)`
- `IAgentImplementation.listMessagesPage(...)`
- `IAgentImplementation.processMessage(...) => { requestId, messageId }`

## 4. 兼容性策略

### 4.1 Legacy data

- 不删除 `conversations/messages`
- 不删除旧 `chat.db` import/export 兼容层
- `LegacyChatImportService` 导入后立即写结构化热路径

### 4.2 Read fallback

如果某条消息尚未完成结构化回填:

1. user 消息回退到 `deepchat_messages.content`
2. assistant blocks 回退到 `deepchat_messages.content`
3. session skills / disabled tools 回退到 `new_sessions` JSON 列

## 5. 测试策略

### 5.1 Main

- migration / backfill 幂等
- messageStore 结构化读写与 fallback
- session restore / page cursor / sendMessage 返回值
- searchHistory 的 FTS / LIKE 双路径

### 5.2 Renderer

- 首屏只恢复一页
- 向上翻页前插不跳动
- streaming + optimistic message + pagination 并存

### 5.3 Smoke

- 创建会话
- 重开大会话
- 顶部加载历史
- 历史搜索
- 编辑 / 重试 / 导出
- 升级后重启

## 6. 风险与缓解

### 风险 1:backfill 半途被中断

缓解:

1. 读路径保留记录级 fallback
2. 启动后 hook 可重复执行

### 风险 2:FTS5 在当前 SQLite 运行时不可用

缓解:

1. 表创建失败时保留普通表
2. 查询自动降级到 `LIKE`

### 风险 3:renderer 滚动位置抖动

缓解:

1. 顶部翻页前记录旧 scrollHeight 与 scrollTop
2. 消息前插后通过差值恢复锚点
Loading