diff --git a/.agents/skills/add-provider/SKILL.md b/.agents/skills/add-provider/SKILL.md index 88430431c4..3e742ddb9a 100644 --- a/.agents/skills/add-provider/SKILL.md +++ b/.agents/skills/add-provider/SKILL.md @@ -34,9 +34,9 @@ Use this when the provider supports OpenAI Chat Completions or Responses-compati Typical files: -- `src/main/presenter/configPresenter/providers.ts` -- `src/main/presenter/configPresenter/providerId.ts` -- `src/main/presenter/llmProviderPresenter/providerRegistry.ts` +- `src/main/provider/defaults.ts` +- `src/main/provider/providerId.ts` +- `src/main/provider/providerRegistry.ts` - `src/shared/providerDbCatalog.ts` when models come from the public provider database - `test/main/**` provider registry or creation tests @@ -47,8 +47,8 @@ Azure, Bedrock, Ollama, or ACP. Typical files: -- `src/main/presenter/configPresenter/providers.ts` -- `src/main/presenter/llmProviderPresenter/providerRegistry.ts` +- `src/main/provider/defaults.ts` +- `src/main/provider/providerRegistry.ts` - Settings components only when the existing generic form lacks required fields - Focused tests for provider creation and connection checks @@ -59,9 +59,9 @@ transports. Typical files: -- `src/main/presenter/llmProviderPresenter/providers/Provider.ts` -- `src/main/presenter/llmProviderPresenter/Adapter.ts` -- `src/main/presenter/llmProviderPresenter/managers/providerInstanceManager.ts` +- `src/main/provider/providers/Provider.ts` +- `src/main/provider/Adapter.ts` +- `src/main/provider/managers/providerInstanceManager.ts` - `src/shared/contracts/routes/*` and `src/renderer/api/*Client.ts` for interactive auth - `src/renderer/settings/components/*` for provider-specific settings UI - Main and renderer tests covering the new behavior @@ -81,11 +81,11 @@ Typical files: 1. Read `docs/features/provider-runtime/spec.md` when the provider work touches the provider runtime scope. Also read `plan.md` and `tasks.md` if they exist for an active provider-runtime goal. 2. Inspect the current provider files before editing: - - `src/main/presenter/configPresenter/providers.ts` - - `src/main/presenter/configPresenter/providerId.ts` - - `src/main/presenter/llmProviderPresenter/providerRegistry.ts` - - `src/main/presenter/llmProviderPresenter/aiSdk/providerFactory.ts` - - `src/main/presenter/llmProviderPresenter/managers/providerInstanceManager.ts` + - `src/main/provider/defaults.ts` + - `src/main/provider/providerId.ts` + - `src/main/provider/providerRegistry.ts` + - `src/main/provider/aiSdk/providerFactory.ts` + - `src/main/provider/managers/providerInstanceManager.ts` - `src/renderer/settings/components/ProviderApiConfig.vue` 3. Classify the request into one supported path. 4. Add the smallest explicit source changes for that path. diff --git a/AGENTS.md b/AGENTS.md index 6516fc312c..391710c8e6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,6 +30,12 @@ - OxLint for JS/TS; the tracked `commit-msg` hook runs `commitlint`. - Names: Vue components PascalCase (`ChatInput.vue`); variables/functions `camelCase`; types/classes `PascalCase`; constants `SCREAMING_SNAKE_CASE`. +## UI primitives: shadcn-vue + VueUse first +- For renderer UI changes, when product/interaction/performance needs are met, **prefer shadcn-vue** under `src/shadcn/components/ui` via `@shadcn/components/ui/*` (Button, Dialog, Select, Switch, Skeleton, Spinner, Empty, Alert, Badge, Field, etc.). Do not hand-roll equivalent spinners, pulse skeletons, empty states, or form controls. +- Prefer **VueUse** (`@vueuse/core`) for mechanical browser utilities: `useEventListener`, `useDebounceFn` / `refDebounced`, `useWindowSize`, `useResizeObserver`, `useIntervalFn`, `useStorage`, etc. +- Missing shadcn pieces: `pnpm dlx shadcn-vue@latest docs ` then `add` (see `components.json` and `pnpm run update-shadcn`). Never invent registry flags; do not `--overwrite` without explicit approval. +- Custom UI is allowed only when shadcn cannot cover the semantic (virtual-list measurement, spotlight/onboarding overlays, domain message/artifact chrome, native truncate `title=`). Document the exception in the PR. + ## Testing Guidelines - Framework: Vitest (+ jsdom) and Vue Test Utils. - Location mirrors source under `test/main/**` and `test/renderer/**`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e81858aa6..43a9990109 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v1.1.0-beta.3 (2026-07-17) +- Reworked the overall architecture and improved reliability +- 重构整体架构,提升可靠性 + ## v1.1.0-beta.2 (2026-07-16) - Centralized Subagent capability policy ownership across the agent runtime and sessions - Added explicit subagent tape lineage with linked tape views and cross-tape recall diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 9339fbacb5..a43eabde62 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,150 +1,137 @@ # DeepChat 当前架构概览 -本文档描述 `2026-07-13` 的当前主架构。renderer-main boundary 使用 typed routes/events;agent -执行层使用显式 kind router、两个 typed backend,以及 DeepChat 专属 loop。 +本文档描述 `2026-07-16` 的 main 进程实际结构。旧的全局 `Presenter`、 +`LifecycleManager`、全局 `EventBus` 和业务模块查找入口已经删除。 -## 主链路 +## 总体结构 ```mermaid -flowchart LR - Renderer["Renderer / Stores / Views"] --> Client["renderer/api clients"] - Client --> Bridge["window.deepchat / preload bridge"] - Bridge --> Contracts["shared/contracts routes + events"] - Contracts --> Routes["src/main/routes dispatcher"] - Routes --> SessionOwners["explicit session owners
search / translation / export / usage / catalog"] - Routes --> Services["SessionService / ChatService"] - Services --> Coordinators["Lifecycle / Turn / Assignment / Projection"] - Coordinators --> Manager["AgentManager
descriptor.kind router"] - Manager --> DeepBackend["typed DeepChat backend"] - Manager --> AcpBackend["direct ACP backend"] - DeepBackend --> DeepRuntime["DeepChatAgentRuntime"] - DeepRuntime --> DeepInstance["DeepChatAgentInstance"] - DeepInstance --> Loop["DeepChatLoopEngine + LoopRun"] - AcpBackend --> AcpRuntime["AcpAgentRuntime"] - AcpRuntime --> AcpInstance["AcpAgentInstance"] - Loop --> Tools["ToolPresenter ports"] - Loop --> Tape["TapeRecorder / message projection"] - Loop --> Memory["MemoryRuntimeCoordinator"] - AcpInstance --> Projection["ACP message/Tape/event/trace adapters"] +flowchart TD + Renderer["Renderer"] --> Preload["Preload bridge"] + Preload --> Contracts["shared typed routes / events"] + Contracts --> RouteMaps["各模块 route map"] + RouteMaps --> Modules["负责该行为的模块"] + + App["App composition"] --> Platform["Platform / Settings / Data"] + App --> Capabilities["Provider / Tool / MCP / Skill / Plugin / Memory / Knowledge / Workspace"] + App --> Agent["Agent: DeepChat / ACP"] + App --> Session["Session"] + App --> Entries["Desktop / Remote / Scheduler / Deeplink"] + + Entries --> Session + Session --> Agent + Agent --> Capabilities + Capabilities --> Platform ``` -关键边界: - -- `AgentManager` 只解析 executable descriptor 和 app session 的 `agentId`,再按 canonical - `descriptor.kind` 选择 backend;它不拥有 prompt、tool、Tape 或 Memory。 -- `kind=acp` 使用 direct ACP backend 和外部 ACP protocol loop,不进入 `DeepChatLoopEngine`。 -- `kind=deepchat + providerId=acp` 仍是受支持的兼容组合:session 走 DeepChat backend/loop,provider - 选择才进入 `AcpProvider` adapter。 -- 四个 `sessionApplication` coordinator 拥有 core session lifecycle、turn、assignment 和 projection; - Session/Chat routes、Remote 和 Cron 通过 consumer-owned narrow ports 使用同一组实例。 -- `AgentSessionPresenter` 与 `IAgentSessionPresenter` 已退休;main routes、Remote、Cron、Tool、MCP 与 - Floating 直接依赖 consumer-owned coordinator ports。 -- history、translation、export、usage、RTK、catalog 和 startup migrations 由 typed routes/lifecycle - hooks 直接组合各自 owner。 -- `AgentRuntimePresenter` 仍初始化 `DeepChatAgentRuntime`,并保留 DeepChat state/delegate、message、Tape、 - prompt/tool/provider adapter wiring;它不再实现 unified agent interface,也不负责 ACP runtime 构造。 +`src/main/app/composition.ts` 是唯一的组合入口。它创建模块、传入明确依赖、注册 route、 +排定启动与停止顺序,但不导出模块列表,也不提供按名称查找模块的方法。 -## 模块职责 - -| 模块 | 位置 | 职责 | -| --- | --- | --- | -| renderer clients | `src/renderer/api/` | typed renderer clients,吸收 bridge/channel 细节 | -| shared contracts | `src/shared/contracts/` | route registry、schema、typed event catalog | -| main routes | `src/main/routes/` | typed route dispatch、services、handlers,以及 session history/translation owners | -| `AgentManager` | `src/main/agent/manager/agentManager.ts` | executable descriptor lookup、app-session lookup、explicit kind routing | -| backend contracts | `src/main/agent/manager/` | required common/kind facets、typed DeepChat backend、direct ACP backend | -| shared agent data | `src/main/agent/shared/` | descriptor/codec、legacy DTO boundary、app-session shell、shared data ports | -| DeepChat runtime | `src/main/agent/deepchat/instance/` | lazy per-session instance cache与 session-owned state | -| DeepChat loop | `src/main/agent/deepchat/loop/` | `LoopRun`、provider/tool round state machine、fixed awaited commits与窄 ports | -| DeepChat Memory adapter | `src/main/agent/deepchat/memory/` | sole runtime coordinator、prompt contributor、background ingestion observer | -| ACP runtime | `src/main/agent/acp/` | catalog、launch、client/process/session/protocol、direct instance/runtime | -| session application | `src/main/presenter/sessionApplication/` | Lifecycle、Turn、AgentAssignment、Projection coordinators 与窄 dependency ports | -| session boundary owners | `src/main/routes/sessions/`, `src/main/presenter/exporter/agentSessionExporter.ts`, `src/main/presenter/usageStatsService.ts` | history、translation、current export、usage dashboard/backfill | -| startup maintenance | `src/main/presenter/startupMigrations/` | default legacy import and stateless session-data migrations | -| shared session policies | `src/main/agent/shared/` | available-agent catalog and assistant-model selection | -| `AgentRuntimePresenter` | `src/main/presenter/agentRuntimePresenter/` | retained DeepChat state/delegate façade及现有 message/Tape/provider/tool adapters | -| `ToolPresenter` | `src/main/presenter/toolPresenter/` | MCP/local tool 聚合、collision policy、权限预检查、调用路由 | -| `MemoryPresenter` | `src/main/presenter/memoryPresenter/` | Memory rows、retrieval、write、vector、maintenance kernel | -| `LLMProviderPresenter` | `src/main/presenter/llmProviderPresenter/` | provider/model runtime和 DeepChat ACP-provider compatibility adapter | -| `RemoteControlPresenter` | `src/main/presenter/remoteControlPresenter/` | remote channel control;session 操作走四个 narrow ports,generation control 走 manager port | -| `CronJobsService` | `src/main/presenter/cronJobs/` | detached session run、composition-owned starter、cron 调度和 Remote 投递 | - -## Agent runtime 分层 - -### Control plane - -`AgentDescriptor` 是 `DeepChatAgentDescriptor | AcpAgentDescriptor`。catalog list 对 legacy/malformed row -保持兼容读取,backend open 使用 capability-strict decode;unknown、disabled、malformed 或 kind mismatch -失败关闭,不 fallback 到 DeepChat。 - -`AgentSessionHandle` 只保留双方真实共有的 lifecycle、send/cancel/snapshot/close、pending、settings 与 -tool-interaction facet。transfer、subagent、generation control 和 ACP controls 使用 required -kind-specific facet;agent handle/backend 不再有 `legacy/direct runtimeKind` 分支。 - -### DeepChat instance 与 loop - -一个 active/hydrated app session 对应一个 `DeepChatAgentInstance`。instance 持有 identity/config、status、 -pre-stream cancellation、active run、pending/steer、ordered interactions、skill/tool cache 和 compaction -projection。每个 turn 使用独立 `LoopRun` 保存 abort、request sequence、provider-round count、round -messages 与 stream state。 - -`DeepChatLoopEngine` 的固定核心顺序是: +依赖方向是: ```text -enterProviderRound - -> consumeProviderRound - -> updateOutput - -> executeToolBatch - -> afterRoundPersisted - -> settleTurn +App composition + -> Desktop / Remote / Scheduler / Deeplink + -> Session + -> Agent runtime + -> Provider / Tool / MCP / Skill / Plugin / Memory / Knowledge / Workspace + -> Platform / Settings / Data ``` -input/context preparation、prompt contributors、ViewManifest/rate gate 和 tool adapters 在固定入口接线; -内部 commit 可 await。只有 typed ordered tool-interaction outcome 会形成持久 pause,最后一项解决后创建 -fresh resume run。外部 hook notifications 仍是 non-blocking observer。 - -### Tape、Memory 与 observability - -- Tape 是现有 append-only semantic ledger;message store 是 mutable renderer projection;trace store 保存 - 可选的 request diagnostics。 -- tool round 在 message projection commit 后通过 `TapeRecorder.appendToolFact` 按 call→result 顺序写入, - 保持 provenance、idempotency、pending exclusion 和 fail-open。 -- causal observation 是 Tape/ViewManifest + message terminal status + trace 的 pure-read join;历史 renderer - event 没有 durable store,明确返回 `not_persisted`。 -- `MemoryRuntimeCoordinator` 是 extraction chains/epochs/cooldown/access dedupe/cursor orchestration 的唯一 - runtime owner,同时实现 `MemoryPromptContributor` 与 `MemoryIngestionObserver`。`MemoryPresenter` 继续拥有 - Memory kernel/schema/vector/maintenance;instance 只持有 stable session handle。 - -## Renderer-main 与兼容边界 - -- migrated renderer 业务代码使用 `renderer/api/*Client`、`window.deepchat` 和 shared contracts。 -- `SessionPresenter` 仍是旧 conversations/messages、thread/export 与窗口清理的 compatibility/data façade, - 不是当前 agent runtime。 -- `startupMigrations/LegacyChatImportService` 和旧数据表继续服务 import compatibility;current - agent-session export 由 `AgentSessionExportService` 负责,旧 conversations/messages export 仍由 - `SessionPresenter` compatibility path 负责。 -- `AcpProvider` 只为 DeepChat descriptor 选择 ACP provider 的兼容路径保留;direct `kind=acp` 不调用它来 - 执行主 turn。 - -## 防回归规则 - -- `scripts/architecture-guard.mjs` 阻止 retired agent backend/path/symbol、agent handle legacy/direct - `runtimeKind`、internal `agentType ?? type` fallback、DeepChat loop 到 presenter/routes/Electron/SQLite/ACP - 的 import,以及 direct ACP instance 到 DeepChat loop、`MemoryPresenter`、presenter root entry 或 - `SQLitePresenter` 的依赖。 -- 同一 guard 保持 Memory unique owner/structure、causal observation read-only 和 renderer typed boundary。 -- 同一 guard 阻止 removed session-boundary methods/interface declarations、foreign owner imports,以及五个 - startup hook 中的 presenter dependency、unsafe cast 和 optional task probe 回流。 -- 同一 guard 阻止 Session/Chat、Remote、Cron 等 migrated consumer 重新依赖 session presenter、重复构造 - coordinator、coordinator 反向导入 session-boundary owner,以及引入 combined session application façade。 -- `scripts/agent-cleanup-guard.mjs` 覆盖 `src/main/agent/**` 与 retained presenter/tool/skill hot paths,防止旧 - agent/session presenter import 回流。 - -## 推荐阅读顺序 - -1. [FLOWS.md](./FLOWS.md) -2. [architecture/agent-system.md](./architecture/agent-system.md) -3. [architecture/tool-system.md](./architecture/tool-system.md) -4. [architecture/session-management.md](./architecture/session-management.md) -5. [architecture/agent-memory-system/spec.md](./architecture/agent-memory-system/spec.md) -6. [architecture/agent-system-layered-runtime/README.md](./architecture/agent-system-layered-runtime/README.md) +下层模块不能反向读取 App、Desktop、Remote 或 Scheduler。需要通知 renderer 时,App 在创建模块时 +传入有类型的发送函数。 + +## 生命周期 + +### App + +`src/main/appMain.ts` 负责 Electron 进程入口、single-instance、deeplink 缓存和退出请求。 +`src/main/app/mainProcess.ts` 负责数据库解锁、连接、迁移和启动失败清理。 +`src/main/app/composition.ts` 负责创建、连接、启动和停止业务模块。 + +`startMainProcess()` 只返回 `MainProcessControl`。它只能聚焦主窗口、处理 deeplink、清理权限、 +确认退出、查询主窗口和停止 main 进程,不能读取业务模块。 + +### Session + +`src/main/session/` 负责可长期保存的 Session 规则: + +- `lifecycle.ts`:创建、草稿、关闭和基础生命周期; +- `turn.ts`:发送、排队、停止和交互回复; +- `assignment.ts`:Agent、model、project、fork 和 subagent 结果处理; +- `query.ts`:不会偷偷载入 Agent 的查询; +- `deletion.ts`:删除顺序和两类 backend 清理; +- `data/`:transcript、Tape、pending input、settings、search 和 trace。 + +窗口与 Session 的绑定不在 Session 数据中,由 `DesktopSessionBinding` 负责。窗口关闭不会默认删除 +Session,也不会默认停止仍由其他入口使用的任务。 + +### Agent + +`AgentManager` 根据 `AgentDescriptor.kind` 选择两套独立实现: + +- `DeepChat`:`DeepChatAgentRuntime`、`DeepChatAgentInstance` 和 `DeepChatLoopEngine`; +- `ACP`:`AcpAgentRuntime`、`AcpAgentInstance` 和 ACP protocol runtime。 + +一个已载入的 Session 只有一个对应 instance。每次 Turn 使用独立 Run 保存取消信号、provider round、 +request sequence 和临时输出状态。Session 拥有长期数据,Agent runtime 只通过窄接口读写这些数据。 + +## 模块职责 + +| 模块 | 位置 | 负责内容 | +| --- | --- | --- | +| App | `src/main/app/` | 进程启动、退出、维护状态、组合依赖 | +| Desktop | `src/main/desktop/` | window、tab、tray、shortcut、floating、browser、renderer binding | +| Session | `src/main/session/` | Session 生命周期、Turn、查询、长期数据和删除规则 | +| Agent | `src/main/agent/` | Agent catalog、backend 选择、DeepChat/ACP instance 和执行 | +| Provider | `src/main/provider/` | Provider/model 配置、实例、请求和认证 | +| Tool | `src/main/tool/` | Tool catalog、执行、权限和本地 Agent tools | +| MCP | `src/main/mcp/` | MCP 配置、server/client 生命周期和 MCP 调用 | +| Skill | `src/main/skill/` | Skill 文件、扫描、同步、选择和贡献 | +| Plugin | `src/main/plugin/` | Plugin package、安装状态和能力登记 | +| Memory | `src/main/memory/` | 长期记忆、检索、写入、索引和后台维护 | +| Knowledge | `src/main/knowledge/` | 内置知识库、切片、索引和检索 | +| Workspace / File | `src/main/workspace/`、`src/main/file/` | Workspace 授权、文件树、搜索、转换和临时文件 | +| Remote | `src/main/remote/` | channel runtime、endpoint binding、远程命令和结果发送 | +| Scheduler | `src/main/scheduler/` | Cron job、run、delivery 和 detached Session | +| Settings | `src/main/settings/`、各模块 `settings.ts` | 底层设置存储和各模块自己的配置解释 | +| Data | `src/main/data/`、各模块 `data/` | SQLite 连接、schema,以及各模块自己的 table 访问 | + +Desktop 内仍有 `WindowPresenter`、`TabPresenter` 等历史类名。它们只是 Desktop 模块内部的具体实现, +不是全局入口,也不能被业务模块用来查找其他能力。 + +Desktop 的平台合同: + +- app-scoped 命令使用 application menu accelerator;`globalShortcut` 只用于真正的全局窗口显示/隐藏; +- primary app chrome 和列表行使用桌面 cursor 语义,内容 hyperlink 保留 link affordance; +- chat search、message jump 和 app chrome 默认使用 immediate/native scroll,不启用全局 smooth scroll; +- macOS window material 按 main/settings/window state 设置,Windows/Linux 保持各自平台选项; +- 修改 shortcut settings 后重新注册 menu accelerator,不创建第二套 renderer shortcut owner。 + +## 数据边界 + +`MainDatabase` 只负责连接、事务、schema、诊断、修复、备份和 reopen。业务 table 由各模块自己的 +database 对象取得。长期运行对象不能缓存一次打开数据库时创建的旧 table;数据库维护完成后, +它们通过稳定的 database owner 读取当前连接。 + +通用 `SettingsStore` 和 `SecretStore` 只提供底层存储。Provider、MCP、Agent、Desktop、Sync、 +Knowledge、Hook、Skill、Project 和 Upgrade 分别解释自己的配置,不通过一个通用 Config 业务入口。 + +## 通信边界 + +- Renderer 调用使用 `src/shared/contracts/` 中的 typed route。 +- 各模块在自己的 `routes.ts` 创建 route map;App 统一注册并拒绝重名。 +- 发给 renderer 的通知使用 typed event envelope。 +- main 内部业务操作使用直接调用,不通过全局 event bus。 +- route 只做通信适配;event 只表示已经发生的事实。 + +## 验证 + +模块行为由 typecheck、lint 和对应的 unit/integration tests 验证。Agent legacy boundary 仍由 +`scripts/agent-cleanup-guard.mjs` 做窄范围检查;其余依赖方向在模块测试和 code review 中维护,不再运行 +全仓库启发式扫描器。 + +详细合同见 [Agent 系统](./architecture/agent-system.md)、 +[Session 管理](./architecture/session-management.md)、[Tool 系统](./architecture/tool-system.md)、 +[Memory 系统](./architecture/memory-system.md)、[Tape 系统](./architecture/tape-system.md) 和 +[事件系统](./architecture/event-system.md)。已完成的 main-process realignment 实施记录由 Git 历史保存。 diff --git a/docs/FLOWS.md b/docs/FLOWS.md index 902161c8e0..7552be6279 100644 --- a/docs/FLOWS.md +++ b/docs/FLOWS.md @@ -1,287 +1,237 @@ # DeepChat 当前核心流程 -本文档只描述当前代码仍在使用的流程。旧 `AgentPresenter` / `startStreamCompletion` -等历史流程不再作为仓库内长期文档保留,需要追溯时用 `git log` / `git show` 查看历史提交。 +本文档描述 `2026-07-16` 的实际流程。旧 `Presenter`、通用 lifecycle hook 和全局 `EventBus` +不再属于当前流程。 -## 1. 创建会话并发送消息 +## 1. main 进程启动 ```mermaid sequenceDiagram - participant R as Renderer - participant C as SessionClient/ChatClient - participant Route as src/main/routes - participant App as Lifecycle / Turn / Assignment / Projection - participant S as AppSessionService - participant M as AgentManager - participant B as Typed Backend - - R->>C: create/send/restore - C->>Route: window.deepchat.invoke(route) - Route->>App: narrow lifecycle/turn/projection port - App->>S: create/bind/read app-session shell - App->>M: resolve executable descriptor/session handle - M->>B: switch descriptor.kind and open handle - App->>B: initialize/send/snapshot - B-->>R: existing message projection + chat.stream.* events + participant Entry as appMain.ts + participant Main as app/mainProcess.ts + participant DB as Database + participant App as app/composition.ts + participant Desktop as Desktop + + Entry->>Entry: single-instance / deeplink cache + Entry->>Main: startMainProcess() + Main->>Main: create splash and settings stores + Main->>DB: unlock, open, migrate + Main->>Main: migrate config storage and register protocols + Main->>App: createMainProcessControl(dependencies) + App->>App: create modules and connect narrow dependencies + App->>App: register module route maps + App->>Desktop: create first main window + App->>App: start shortcut, tray, Scheduler and Memory maintenance + App->>App: schedule deferred/background work + App-->>Main: MainProcessControl + Main->>Main: close splash ``` -关键文件: +首个窗口之前必须完成数据库、配置迁移、route 注册和 ACP registry migration。Skill 扫描、MCP、 +Remote、Provider warmup、legacy import 和统计回填在窗口可用后调度。 -- `src/renderer/api/SessionClient.ts` -- `src/renderer/api/ChatClient.ts` -- `src/main/routes/sessions/sessionService.ts` -- `src/main/routes/chat/chatService.ts` -- `src/main/agent/shared/appSessionService.ts` -- `src/main/agent/manager/agentManager.ts` -- `src/main/agent/manager/deepChatAgentBackend.ts` -- `src/main/agent/manager/directAcpAgentBackend.ts` -- `src/main/presenter/sessionApplication/` +## 2. 创建 Session 并发送消息 -`SessionService` / `ChatService` 直接使用 consumer-owned coordinator ports;原 aggregate session -presenter 已退休。history、translation、export、usage、RTK、catalog 与 startup maintenance 直接进入 -各自 owner。agent kind resolution 和 executable backend selection 只发生在 `AgentManager`。 -`new_sessions.session_kind` 仍表示 `regular | subagent`,不决定 DeepChat/ACP backend。 +```mermaid +sequenceDiagram + participant R as Renderer / Remote / Scheduler + participant Route as Module route or entry service + participant Session as Session Lifecycle / Turn + participant Manager as AgentManager + participant Backend as DeepChat or ACP backend + + R->>Route: create or send + Route->>Session: narrow operation + Session->>Manager: resolve descriptor and session handle + Manager->>Backend: choose by descriptor.kind + Session->>Backend: initialize / send / cancel / snapshot + Backend-->>R: persisted result and typed renderer event +``` -## 2. DeepChat 消息处理主循环 +Desktop、Remote 和 Scheduler 共用同一套 Session 生命周期。Scheduler 每次运行创建新的 detached +Session;Remote 保存自己的 endpoint binding;Desktop 只保存 renderer binding。 + +## 3. DeepChat 执行 ```mermaid flowchart TD - Start["DeepChat backend.send"] --> Instance["DeepChatAgentInstance"] - Instance --> Prepare["input preparation
Tape/user fact/compaction"] - Prepare --> Prompt["base + post-compaction context contributors
including MemoryPromptContributor"] - Prompt --> Run["create LoopRun / register active generation"] - Run --> Engine["DeepChatLoopEngine"] - Engine --> Attempt["request sequence
ViewManifest -> rate gate -> provider stream"] - Attempt --> Acc["accumulator + throttled output projection"] - Acc --> ToolCheck{"typed tool batch?"} - ToolCheck -->|no| Settle["settleTurn"] - ToolCheck -->|yes| Tools["ToolCatalog/Execution/Result ports"] - Tools --> Outcome{"ordered interaction outcome?"} - Outcome -->|yes| Pause["persist batch, settle run, wait for UI"] - Outcome -->|no| Tape["message commit -> TapeRecorder tool facts"] - Tape --> Engine - Pause --> Fresh["final item -> fresh resume LoopRun"] - Fresh --> Engine - Settle --> Observe["terminal projection + pending drain
background MemoryIngestionObserver"] + Send["SessionTurn.send"] --> Instance["DeepChatAgentInstance"] + Instance --> Prepare["读取 Session 数据并准备 prompt"] + Prepare --> Run["创建独立 LoopRun"] + Run --> Provider["ProviderRuntime.streamChat"] + Provider --> Output["更新 message projection"] + Output --> Tool{"有 Tool 调用?"} + Tool -->|否| Settle["提交结果并结束 Turn"] + Tool -->|是| Execute["ToolService 执行"] + Execute --> Interaction{"需要用户交互?"} + Interaction -->|是| Pause["保存交互并暂停"] + Interaction -->|否| Tape["写入 Tape tool fact"] + Tape --> Provider + Pause --> Resume["最后一项完成后创建新的 resume Run"] + Resume --> Provider ``` -关键语义: - -- `generationSettings` 在 session 创建、草稿和 active session 中统一传递,覆盖 system prompt、 - temperature、topP、max tokens、reasoning effort、verbosity 等运行时设置。 -- `sessions.compact` 触发手动上下文压缩;自动压缩设置保存在 agent/session 配置中。 -- message trace 独立落库,renderer 通过 `sessions.listMessageTraces` 查询。 -- `providerRoundCount` 按 outer round 递增,`requestSeq` 按实际 provider attempt 递增;strict retry 不会 - 伪造新的 outer round。 -- `afterRoundPersisted` 可以 await,并在 message projection 后通过稳定的 - `TapeRecorder.appendToolFact` 写 terminal tool call/result;失败保持 fail-open。 -- Memory prompt contribution 是 awaited、sanitized、hard-budgeted、fail-open;terminal extraction 是 - background observer,并保持 epoch/cursor/fence 合同。 -- 失败消息会保留恢复上下文,tool output guard 会限制过大的工具输出进入后续上下文。 -- `agent-core/update_plan` 工具只更新实时 plan snapshot 和 `chat.plan.updated` event;plan 是 - 生成中的临时浮窗 UI,不写入 assistant 正文 block。renderer 按 session 保存当前 app - 运行内的 live plan snapshot;切换会话时只显示当前 session 的 plan,且不从历史消息 - rehydrate。reload 后不恢复旧 plan float。内部 tool call 仍隐藏,不暴露成普通消息块。 - -## 3. 工具调用、权限和 Subagents +Provider、Tool、Skill、Memory 和 Session data 都通过创建时传入的必需接口使用。DeepChat runtime +不能从 App、Routes、Desktop、Remote 或 Scheduler 查找依赖。 + +- `generationSettings` 在 Session 创建、草稿和 active Session 中统一传递,包括 system prompt、 + temperature、topP、max tokens、reasoning effort 和 verbosity。 +- `providerRoundCount` 按 outer round 递增,`requestSeq` 按实际 Provider attempt 递增;strict retry + 不会伪造新的 outer round。 +- Memory prompt contribution 必须等待结果、清理内容、限制大小并允许失败;terminal extraction 在后台 + 执行,并保持 epoch、cursor 和 fence 约束。 +- `TapeRecorder.appendToolFact` 在 message projection 完成后写 terminal tool call/result;写入失败不影响 + 当前回复完成。 + +## 4. ACP 执行 ```mermaid -sequenceDiagram - participant L as DeepChatLoopEngine - participant PA as Presenter Tool Adapters - participant T as ToolPresenter - participant M as MCP Presenter - participant AT as AgentToolManager - participant P as Permission Services - participant R as Renderer - - L->>PA: ToolCatalogPort / ToolExecutionPort - PA->>T: getAllToolDefinitions / preCheck / callTool - - alt MCP tool - T->>M: callTool(request) - M-->>T: result - else local agent tool - T->>AT: callTool(name, args, conversationId) - AT->>P: check/consume approvals - AT-->>T: result - end - - alt requires interaction - L-->>R: persisted ordered interaction batch - R->>PA: respondToolInteraction() - PA-->>L: stay paused or create fresh resume run - end +flowchart LR + Session["Session"] --> Manager["AgentManager"] + Manager --> Kind{"descriptor.kind"} + Kind -->|acp| Direct["Direct ACP backend"] + Direct --> Runtime["AcpAgentRuntime"] + Runtime --> Process["ACP process / protocol session"] + Process --> Projection["Session message / Tape projection"] + Kind -->|deepchat| Deep["DeepChat backend"] ``` -当前本地 agent tools 包括文件系统、命令执行、chat settings、subagent orchestration 等能力。 -Subagent 会话以 `sessionKind='subagent'` 存储;父会话写入同时冻结 child Tape incarnation 与 head 的 -Tape link,并通过显式 linked Tape view 回查子会话内容,不复制 child entries。reset/rebuild 后的 child -必须重新 link,不能用复用的 entry ID 冒充旧快照。 -MCP/Skill/ToolPresenter 继续拥有资源和执行策略;LoopEngine 只依赖窄 port,不 import presenter。 +Direct ACP 不进入 `DeepChatLoopEngine`。`kind=deepchat + providerId=acp` 仍是独立的兼容组合, +它使用 DeepChat loop,并把 ACP 当作 Provider。 -## 4. 会话恢复、分页和搜索 +## 5. Tool、MCP、Skill 和 Plugin ```mermaid -sequenceDiagram - participant R as Renderer messageStore - participant S as SessionClient - participant Route as SessionService - participant P as SessionProjectionCoordinator - participant DB as DeepChatMessageStore - - R->>S: restore(sessionId, limit=100) - S->>Route: sessions.restore - Route->>P: getSession + listMessagesPage - P->>DB: listPageBySession - DB-->>R: latest page + nextCursor - R->>S: listMessagesPage(cursor) - S->>Route: sessions.listMessagesPage +flowchart LR + Agent["DeepChat runtime"] --> Tool["ToolService"] + Tool --> Local["Local Agent tools"] + Tool --> MCP["McpService"] + Tool --> Permission["Permission services"] + Skill["SkillService"] --> Tool + Plugin["PluginService"] --> Skill + Plugin --> MCP ``` -结构化持久化当前模型: - -- `deepchat_messages` 存消息头和稳定 JSON fallback。 -- `deepchat_user_messages`、`deepchat_user_message_files`、`deepchat_user_message_links` - 存 user message 热字段。 -- `deepchat_assistant_blocks` 存 assistant block 增量。 -- `deepchat_search_documents` / `_fts` 存历史搜索索引。 +Tool 负责 catalog、权限预检查和执行路由。MCP 负责 server/client 生命周期。Skill 负责 Skill 文件和 +选择。Plugin 只登记 package 提供的能力,不接管 MCP、Skill 或 Tool 的运行状态。 -`sessions.searchHistory` 由 typed route 直接调用 -`src/main/routes/sessions/sessionHistorySearch.ts`,由该 owner 保持 FTS、LIKE 与 legacy SQL fallback。 +模型只能看到 `tape_search` 和 `tape_context`。Subagent 完成后,父 Session 保存指向 child Tape +固定 head 的 link;查询时通过明确的 linked Tape view 读取,不把 child entries 复制到父 Tape。 +`subagent_orchestrator` 只在当前 Agent policy 开启且存在有效 slot 时提供,Subagent child 不能继续递归 +创建 Subagent。 -## 5. ACP direct backend 与 provider compatibility +## 6. renderer binding ```mermaid -flowchart TD - Entry["Routes / Remote / Cron"] --> App["Session application coordinators"] - App --> Manager["AgentManager"] - Manager --> Kind{"descriptor.kind"} - Kind -->|acp| Direct["DirectAcpSessionBackend"] - Direct --> AcpRuntime["AcpAgentRuntime"] - AcpRuntime --> AcpInstance["AcpAgentInstance"] - AcpInstance --> Protocol["ACP session/prompt/permission loop"] - AcpInstance --> Projection["message/Tape/event/trace adapters"] - Kind -->|deepchat| Deep["DeepChat backend + LoopEngine"] - Deep --> Provider{"providerId"} - Provider -->|acp| Compat["AcpProvider compatibility adapter"] - Provider -->|other| LLM["ordinary LLM provider"] +sequenceDiagram + participant Window as Desktop window/tab + participant Binding as DesktopSessionBinding + participant Query as SessionQuery + participant Runtime as Agent runtime + + Window->>Binding: activate(webContentsId, sessionId) + Binding->>Query: read Session and messages + Query-->>Window: projection + Window->>Binding: deactivate or destroy + Binding->>Binding: remove renderer binding only + Note over Runtime: Session and running task are not deleted by window close ``` -ACP 配置选项走 `sessions.getAcpSessionConfigOptions` / -`sessions.setAcpSessionConfigOption`;远程控制创建 ACP session 时会使用 channel -`defaultWorkdir` 或全局默认项目路径,并拒绝没有 workdir 的 ACP 默认 agent。 +普通列表、历史和 binding 查询不会载入 Agent instance。只有执行、完整 restore 或明确的 backend +设置操作可以 hydrate runtime。 -direct `kind=acp` 的 workdir、mode/config/commands、cancel 和 protocol permission 由 ACP instance/runtime -拥有;它不进入 DeepChat LoopEngine。现有 `AcpProvider` 仅保留给 -`kind=deepchat + providerId=acp`,该组合仍使用 DeepChat prompt/tool/Tape outer lifecycle。 - -## 6. Spotlight Search +## 7. 数据库维护 ```mermaid sequenceDiagram - participant UI as Spotlight overlay - participant Store as spotlight store - participant Route as typed sessions route - participant Search as SessionHistorySearch - participant Settings as settings navigation registry - - UI->>Store: open/query/select - Store->>Route: sessions.searchHistory(query) - Route->>Search: search(query, limit) - Search-->>Store: sessions/messages hits - Store->>Settings: merge settings/actions/agents - Store-->>UI: mixed results + participant Route as Sync / Database Security route + participant App as App maintenance + participant Entry as Remote / Scheduler + participant Runtime as Session / Memory + participant DB as MainDatabase + + Route->>App: runDatabaseMaintenance(operation) + App->>Entry: stop new remote requests and scheduled runs + App->>Runtime: fence Memory and suspend Session runtimes + App->>DB: checkpoint and close + App->>DB: import or encryption operation + App->>DB: reopen + App->>Runtime: resume Memory maintenance + App->>Entry: restart Hook, Scheduler and Remote ``` -Spotlight 默认由 `CommandOrControl+P` 打开,混排 recent sessions、agents、settings、actions -和历史消息。agent results 使用 shared available-agent catalog policy;消息命中会写入 pending jump, -`ChatPage` 在目标消息加载完成后滚动并高亮。 - -## 7. Startup Maintenance - -五个 lifecycle startup hooks 只负责调度:legacy import 调用 -`LegacyChatImportService`,usage backfill 调用 `UsageStatsService`,两类 session-data cleanup 调用 stateless -startup migration functions,RTK health 调用 RTK runtime service。task id、priority、resource 与持久状态 key -保持稳定。 +维护期间新的 `chat.*`、`sessions.*`、`remoteControl.*` 和 `cronJobs.*` route 会被拒绝。 +恢复失败时 App 进入 failed 并停止,不在关闭了一半的数据库上继续运行。 -## 8. Provider Import And Deeplinks +## 8. Remote ```mermaid sequenceDiagram - participant OS as deepchat:// URL - participant D as DeeplinkPresenter - participant W as Settings window - participant P as ProviderImportService - participant C as ConfigPresenter - - OS->>D: deepchat://provider/install?v=1&data=... - D->>W: provider install preview event - W->>P: validate/apply preview - P->>C: update builtin provider or create custom provider + participant C as Remote channel + participant R as RemoteService + participant S as Session ports + participant A as AgentManager + participant D as DeliveryService + + C->>R: authenticated command / message + R->>R: resolve endpoint binding and command + R->>S: create, restore, send, cancel or interact + S->>A: resolve typed backend + A-->>S: stream / terminal projection + S-->>R: typed result + R->>D: render and deliver to channel ``` -当前支持: +Remote 负责 channel runtime、endpoint binding、授权、命令解析和结果发送;它不拥有 Session 或 Agent +状态。`/agent` 只通过 Session assignment 选择可用 Agent。Feishu/Lark scan auth 的 begin/poll/cancel、 +host 选择和 `open_id` pairing 保持在 Remote channel adapter 内,token 和 pairing secret 不进入 renderer +或聊天文本。Window 关闭不终止 Remote-bound Session。 -- `deepchat://start` -- `deepchat://mcp/install` -- `deepchat://provider/install` -- provider config import scan/apply,包括 Codex、Claude Code、Cherry Studio、CC Switch 等来源 -- model config import/export,以及 built-in/custom provider 的 credential-only import +## 9. Scheduler -## 9. Scheduled Tasks +Scheduler 查询到期 job 后,为每次 run 创建新的 detached regular Session: -```mermaid -sequenceDiagram - participant UI as Settings Scheduled - participant Client as CronJobsClient - participant Service as CronJobsService - participant Utility as Scheduler utility - participant Starter as Cron session starter - participant App as Lifecycle / Turn - participant Runtime as Agent runtime updates - participant Remote as RemoteControlPresenter - - UI->>Client: list/upsert/toggle/runNow - Client->>Service: cronJobs.* route - Service->>Utility: reconcile enabled jobs - Utility->>Service: RUN_DUE - Service->>Starter: start run - Starter->>App: create detached session + send task prompt - Runtime-->>Service: DeepChatInternalSessionUpdate status/output/completion - Service->>Remote: optional notification-only delivery +```text +find due job + -> acquire run identity and timeout + -> create detached Session with saved Agent/settings/project + -> SessionTurn.send + -> wait for terminal result or cancel + -> persist run status + -> optional Remote delivery + -> compute next run ``` -Triggers 使用 cron 表达式。每次触发创建独立 detached session;Remote 投递只发送通知,不进入普通 -Remote 会话上下文。starter 在 composition root 接线,不依赖 route runtime 初始化。 +Job、run、retry、timeout 和 delivery 由 `src/main/scheduler/` 负责。Scheduler 使用与 Desktop/Remote 相同 +的 Session lifecycle,不直接构造 Agent runtime,也不复用上次 run 的 Session。Database maintenance 和 +shutdown 会先停止接受新 run,再等待或取消已接收 run。 -## 10. Remote Control +## 10. Sync 和导入 -```mermaid -flowchart LR - Telegram["Telegram"] --> Remote["RemoteControlPresenter"] - Feishu["Feishu/Lark"] --> Remote - QQ["QQBot"] --> Remote - Discord["Discord"] --> Remote - WeChat["WeChat iLink"] --> Remote - Remote --> Auth["channel auth / binding store"] - Remote --> Runner["remote conversation runner"] - Runner --> Ports["Lifecycle / Turn / Assignment / Projection ports"] - Runner --> Generation["AgentManager generation port"] -``` +本地备份、数据库导入和 S3-compatible cloud sync 由 `src/main/sync/` 发起,并统一包在 App database +maintenance 中。Cloud flow 为:读取 SyncSettings 中的 endpoint/bucket/path/credential,生成或读取加密 +数据库备份,上传/下载对象,校验完成后再替换本地数据。Secret 只保存在 main process 的 secret store, +renderer 只接收脱敏状态。 -统一远程控制支持绑定、默认 agent、默认 workdir、`/sessions`、`/model`、状态输出、媒体/Markdown -渲染和工具交互提示。各 channel 的协议差异留在 `remoteControlPresenter//` -和 `remoteControlPresenter/services/*CommandRouter.ts`。 +导入成功后重新打开数据库,各模块通过稳定 database owner 读取新 table;长期运行对象不能继续缓存 +旧连接产生的 table。任何 close/import/reopen 失败都会让 App 进入 failed 并停止,不执行半恢复。 -## 11. Local Data Security +## 11. 退出 -SQLite 数据库加密由 `DatabaseSecurityPresenter` 管理: +`before-quit` 先询问 Knowledge 是否允许退出。确认后,`MainProcessControl.stop()` 只运行一次, +并按明确顺序停止: -- `databaseSecurity.getStatus` -- `databaseSecurity.enable` -- `databaseSecurity.changePassword` -- `databaseSecurity.disable` +```text +cancel startup work + -> stop Scheduler / Remote / Hook + -> suspend Session runtimes + -> stop Plugin / MCP / browser / Desktop resources + -> stop Workspace / Skill / watcher / exec host + -> fence and drain Memory + -> stop Knowledge / Provider / ACP + -> close SQLite + -> destroy shortcut / notification / tray +``` -启用后使用 SQLCipher 迁移 `agent.db`,密码优先通过 Electron `safeStorage` 包装保存; -safeStorage 不可用或解包失败时进入 manual unlock。 +更新安装复用同一条停止路径。数据重置和 App restart 也先完成停止,再由 Device 执行最终操作。 diff --git a/docs/README.md b/docs/README.md index b5dd0b1ee2..59a2f4789d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,101 +1,64 @@ # DeepChat 文档索引 -本文档反映 `2026-07-13` 的当前代码结构。SDD 已按目标类型拆分:feature 和 -architecture 使用三件套,small bug 使用单个 issue `spec.md`。文档清理只在开发者明确触发 -`deepchat-sdd-cleanup` 时执行。 - -当前 renderer-main 默认路径是 typed client / typed event: - -```text -Renderer - -> renderer/api clients - -> window.deepchat - -> shared/contracts/routes + shared/contracts/events - -> src/main/routes dispatcher - -> route services / consumer-owned narrow ports - -> sessionApplication coordinators / typed runtimes / retained resource presenters -``` - -`useLegacyPresenter()`、`presenter:call`、`remoteControlPresenter:call` 和 -`src/renderer/api/legacy/**` 已经退休。业务模块的新能力应从 `renderer/api/*Client` 和 -shared contracts 进入;少数仍需要 raw IPC 的能力只能封装在明确 allowlist 的 preload/API 边界内。 - -## 已实现架构决策 - -| 文档 | 状态 | 用途 | -| --- | --- | --- | -| [architecture/agent-system-layered-runtime/](./architecture/agent-system-layered-runtime/) | 已实现 | Agent control plane、ACP 独立 runtime、DeepChat instance/loop/Tape/Memory 的分层决策、兼容合同与验证记录 | - -阅读当前实现仍以“当前必读”中的文档为准;分层改造的决策背景、不可回归合同与最终验证证据 -保留在该目标的 `README.md`、`spec.md`、`migration-and-validation.md` 与 `modules/` 中。 +本文档反映 `2026-07-16` 的当前代码。历史实施过程、已完成 issue 和一次性 SDD 通过 Git +历史查询,不再长期留在 `docs/`。 ## 当前必读 | 文档 | 用途 | | --- | --- | -| [ARCHITECTURE.md](./ARCHITECTURE.md) | 当前主架构、能力 owner、typed boundary 规则 | -| [FLOWS.md](./FLOWS.md) | 当前消息、工具、ACP、导入、定时任务、远程控制流程 | -| [architecture/agent-system.md](./architecture/agent-system.md) | session application coordinators / `agentRuntimePresenter` 细节 | -| [architecture/tool-system.md](./architecture/tool-system.md) | `ToolPresenter`、agent tools、ACP helper 分层 | -| [architecture/session-management.md](./architecture/session-management.md) | 新会话管理、分页恢复、legacy 数据平面边界 | -| [architecture/event-system.md](./architecture/event-system.md) | EventBus 与 typed events 的当前分工 | -| [guides/code-navigation.md](./guides/code-navigation.md) | 当前代码导航入口 | -| [guides/getting-started.md](./guides/getting-started.md) | 新开发者快速上手 | +| [ARCHITECTURE.md](./ARCHITECTURE.md) | main 进程模块、所有权、生命周期和依赖方向 | +| [FLOWS.md](./FLOWS.md) | 启动、Session、Agent、Tool、Remote、Scheduler、Sync 和退出流程 | +| [architecture/agent-system.md](./architecture/agent-system.md) | DeepChat / ACP backend、Run、权限和 Subagent 合同 | +| [architecture/session-management.md](./architecture/session-management.md) | Session 数据、binding、恢复、删除和 transfer | +| [architecture/tool-system.md](./architecture/tool-system.md) | Tool、MCP、Skill、Plugin 和权限边界 | +| [architecture/memory-system.md](./architecture/memory-system.md) | Memory 存储、检索、写入、隔离和维护 | +| [architecture/tape-system.md](./architecture/tape-system.md) | Tape、ViewManifest、回放和 Subagent lineage | +| [architecture/event-system.md](./architecture/event-system.md) | typed route、typed event 和 main 内部调用规则 | +| [guides/getting-started.md](./guides/getting-started.md) | 当前代码入口和本地开发命令 | | [guides/plugin-packaging.md](./guides/plugin-packaging.md) | `.dcplugin` 打包、内置分发和 release 规则 | -| [spec-driven-dev.md](./spec-driven-dev.md) | SDD 目录规则、GitHub 同步与清理入口 | +| [release-flow.md](./release-flow.md) | 版本、分支、tag 和平台构建流程 | +| [spec-driven-dev.md](./spec-driven-dev.md) | SDD 分类、产物和清理规则 | -## 仍有运行时用途的基线 +## 进行中的目标 -| 文档 | 用途 | +只有尚未完成或仍需外部验证的工作保留 `plan.md` / `tasks.md`。 + +| 文档 | 状态 | | --- | --- | -| [architecture/baselines/main-kernel-bridge-register.json](./architecture/baselines/main-kernel-bridge-register.json) | `architecture-guard` 读取的 legacy bridge 机器登记表 | +| [features/acp-v1-reliability/](./features/acp-v1-reliability/) | ACP capability、auth、session lifecycle 与 diagnostics 待实施 | +| [features/cua-cross-platform-computer-use/](./features/cua-cross-platform-computer-use/) | 已实现主体,等待 CI platform matrix 验证 | +| [features/mcp-oauth-authentication/](./features/mcp-oauth-authentication/) | 已实现主体,等待真实 OAuth smoke | +| [architecture/chat-scroll-ownership/](./architecture/chat-scroll-ownership/) | chat viewport ownership、windowing 与真实 Chromium 验证进行中 | +| [architecture/remove-mcp-permission-system/](./architecture/remove-mcp-permission-system/) | MCP permission removal 尚未实施 | +| [architecture/memory-quality-gates-and-observability/](./architecture/memory-quality-gates-and-observability/) | retrieval artifact upload 待完成 | +| [architecture/memory-vector-store-v2/](./architecture/memory-vector-store-v2/) | v2 已落地,保留 migration window 后的 VSS removal follow-up | +| [issues/chat-history-search-scroll-coordinates/](./issues/chat-history-search-scroll-coordinates/) | 等待 Electron/macOS 物理滚动验证 | -其它 dependency、scoreboard、test failure、zero-inbound 报表属于按需生成的审计快照。当前代码 -需要重新审计时,运行 `pnpm run architecture:baseline` 生成临时报表并按需提交。 +## 保留的产品合同 -## 当前代码地图 +以下 feature spec 仍承担跨模块产品或扩展合同,不是实施历史: -```text -docs/ -├── README.md -├── ARCHITECTURE.md -├── FLOWS.md -├── architecture/ -│ ├── agent-system.md -│ ├── agent-system-layered-runtime/ -│ ├── event-system.md -│ ├── session-management.md -│ ├── tool-system.md -│ └── baselines/ -├── features/ -│ └── / -├── issues/ -│ └── / -├── guides/ -│ ├── getting-started.md -│ ├── code-navigation.md -│ └── plugin-packaging.md -└── spec-driven-dev.md -``` +- [Provider Runtime](./features/provider-runtime/spec.md) +- [DeepChat Skills Management](./features/deepchat-skills-management/spec.md) +- [Plugins Hub](./features/plugins-hub/spec.md) +- [Complete Directory Management](./features/complete-directory-management/spec.md) -## SDD 保留规则 +## 机器读取基线 + +| 文件 | 使用方 | +| --- | --- | +| [agent-system-layered-runtime-baseline.json](./architecture/baselines/agent-system-layered-runtime-baseline.json) | architecture baseline generator test 的 canonical fixture | +| [main-kernel-bridge-register.json](./architecture/baselines/main-kernel-bridge-register.json) | architecture baseline generator 的 retired boundary register | -- `docs/features/**` 和 `docs/architecture/**` 下的 active goal folder 保留 `spec.md`、 - `plan.md`、`tasks.md`。 -- `docs/issues/**` 下的小 bug goal 只保留一个 `spec.md`,内容包含 issue 描述、定位、 - 修复计划、任务清单、验证方式和 GitHub issue 链接(如有)。 -- feature / architecture 的已实现能力只保留仍有维护价值的 `spec.md`;删除对应 - `plan.md` / `tasks.md`。 -- 已实现能力的当前维护事实也要并入 `README.md`、`ARCHITECTURE.md`、`FLOWS.md` 或对应 guide。 -- 已修复 issue,尤其是关联 GitHub issue 且已关闭的,可以在手动 SDD cleanup 时删除。 -- 过期、未开工、只描述旧实现或旧分支的 SDD,在手动 SDD cleanup 时删除。 +其它 dependency、scoreboard、zero-inbound 报表由 `pnpm run architecture:baseline` 按需生成, +不作为长期文档。 -## 阅读建议 +## 文档保留规则 -1. 先读 [ARCHITECTURE.md](./ARCHITECTURE.md) 建立当前主链路心智模型。 -2. 再读 [FLOWS.md](./FLOWS.md) 看发送消息、工具调用、导入和远程控制时序。 -3. 深入实现时,按模块进入: - - 聊天执行链路:[architecture/agent-system.md](./architecture/agent-system.md) - - 工具与权限:[architecture/tool-system.md](./architecture/tool-system.md) - - 会话与兼容边界:[architecture/session-management.md](./architecture/session-management.md) -4. 如果需要理解已退休设计,优先用 `git log` / `git show` 追历史提交,不再依赖仓库内长期归档文档。 +- 当前事实写入核心 architecture、flow 或 guide,不再保留重复的 implemented SDD。 +- 完成的 feature / architecture 删除 `plan.md` 和 `tasks.md`;只有维护合同需要时保留压缩后的 + `spec.md`。 +- 已修复 issue 直接删除;历史和验证证据由 Git 与测试保存。 +- active goal、未完成 task 和 `[NEEDS CLARIFICATION]` 不得在 cleanup 中删除。 +- 文档引用的路径必须存在;旧实现只通过 `git log` / `git show` 查询。 diff --git a/docs/architecture/agent-memory-correctness-privacy-hardening/spec.md b/docs/architecture/agent-memory-correctness-privacy-hardening/spec.md deleted file mode 100644 index 60a5632a27..0000000000 --- a/docs/architecture/agent-memory-correctness-privacy-hardening/spec.md +++ /dev/null @@ -1,233 +0,0 @@ -# Agent Memory Correctness and Privacy Hardening — Specification - -> Status: **implemented** -> Classification: **architecture** -> Post-commit gate: **`memory-native-validation` passed** - -## Purpose - -DeepChat Agent Memory derives durable memories from the effective conversation tape, stores semantic -state in SQLite, and uses a per-agent DuckDB sidecar for vector retrieval. The system performs model-backed -extraction, semantic write decisions, embedding, reflection, persona evolution, and maintenance outside the -foreground response path. - -This hardening work closes correctness and privacy gaps at the boundaries between those asynchronous -operations and authoritative state. It defines how extraction is checkpointed, how stale reads and writes are -rejected, how destructive operations cancel old work, how conflict aggregates remain valid, and how native -resources are drained safely. - -## Problems Addressed - -| Area | Previous risk | Required outcome | -| --- | --- | --- | -| Extraction | Prompt-side tail truncation could consume unseen messages and attach imprecise lineage | Every consumed message is presented in full through bounded, message-aligned chunks with exact lineage | -| Injection | Provider and vector awaits could return snapshots invalidated by forget, clear, edit, persona, or working-memory changes | Injection is authoritatively revalidated and fails closed on a concurrent semantic mutation | -| Destructive operations | Clear or agent deletion could race with old provider work and recreate deleted state | Old work is fenced and aborted per agent before destructive mutation begins | -| Semantic decisions | Concurrent sessions or maintenance could apply an LLM decision based on stale content | Semantic transitions use revision-checked atomic writes with bounded retry | -| Conflicts | A challenger and its target could be edited or removed independently, leaving an invalid aggregate | Conflict participants are guarded, transitions are transactional, and historical damage is repairable | -| Vector lifecycle | Query, upsert, reset, close, and dispose could overlap on the same native store | Every operation runs under a manager-owned generation lease | -| Provider lifecycle | Requests could bypass admission, outlive deadlines, or accumulate without settling | All Memory provider work uses bounded admission, deadlines, cancellation, and unsettled-request caps | -| Provenance | A lower-cased 32-bit hash could collide and suppress valid memories | New writes use a case-preserving SHA-256 key with verified legacy compatibility | -| Native validation | Native bindings, FTS, and migrations could silently skip in ordinary tests | A fresh Node ABI CI job treats missing capabilities and skipped migrations as failures | - -## Architectural Invariants - -1. SQLite `agent_memory` remains authoritative. DuckDB remains a rebuildable vector sidecar. -2. The raw tape remains the evidence source of truth; Memory writes only non-reconstruction anchors. -3. Extraction failure never advances the cursor past unfinished content. -4. Triage and extraction receive exactly the same complete chunk. -5. A semantic commit advances the per-agent read epoch before another await may begin. -6. Clear, agent deletion, and dispose invalidate a destructive operation generation before asynchronous - cleanup. -7. A stale read fails the whole injection closed; the foreground path does not retry it. -8. A stale write never overwrites a newer semantic state and never falls through to an implicit `ADD`. -9. Unresolved conflict participants cannot be mutated independently. -10. Supported vector operations keep native store access inside a manager-owned scoped callback. -11. Provider deadlines include time spent waiting for rate-limit admission. -12. Public IPC, renderer DTOs, tool contracts, event payloads, and the legacy status vocabulary remain - compatible. - -## Requirements - -### Extraction Chunks, Cursor, and Lineage - -- Extraction input is packed on message boundaries with a CJK-aware token estimate. -- A chunk is limited to approximately 4,000 estimated tokens and 12,000 Unicode code points. -- An oversized single message is split by an iterator-based grapheme/code-point-safe linear scan. -- The cursor for an oversized message advances only after its final fragment succeeds. -- One session queue task processes at most four chunks and enqueues the immutable remainder on the same - session chain. -- Any extraction failure, disable, dispose, or extraction-epoch change stops the chain without consuming - unfinished content. -- `source_entry_ids` and the `memory/extract` anchor contain only the entries and fragment metadata for the - current chunk. -- The shared lineage codec accepts non-negative safe integers, filters invalid elements, preserves first-seen - order, removes duplicates, and returns `null` when no valid IDs remain. - -### Authoritative Injection Finalization - -- Every injection captures the current per-agent read epoch before asynchronous retrieval. -- After all provider and vector awaits, retrieval performs one agent-scoped `listByIds` read for the union of - candidate IDs and replaces snapshots with the latest rows. -- Persona and working memory are read only after the final retrieval await. -- Before reading persona and working memory, injection verifies the original epoch and synchronously flushes - correctness-only working-memory dirty state. -- Injection captures the post-flush epoch, assembles the payload, performs one final - enabled/managed/disposed/epoch gate, and returns without another await. -- Runtime rechecks memory enablement before prompt append, selected-memory access accounting, and the - `memory/view_assembled` anchor. - -### Destructive Operation Fencing - -`MemoryOperationFence` contains an `agentId` and a monotonically increasing destructive generation. - -- `clearMemories` invalidates the generation before deleting SQLite rows, including when no rows exist. -- Agent deletion invalidates the generation before its first await. -- Dispose invalidates all active work and stops new provider, task, and vector-lease admission. -- Generic edit, archive, forget, delete, and restore do not invalidate the destructive generation; they rely - on revision CAS, authoritative reads, and conflict guards. -- Extraction, decision, maintenance, reflection, persona, and embedding continuations recheck the fence after - every external await and before side effects. -- A stale continuation cannot write SQLite rows, cursors, audits, events, working memory, or vectors. - -### Conflict Aggregate Integrity - -- Generic edit, archive, forget, delete, and restore reject both an unresolved challenger and a target - referenced by an unresolved challenger. -- Whole-agent clear remains permitted because it removes the entire aggregate. -- `CHALLENGE` commits challenger insertion and the target transition in one SQLite transaction guarded by - target revision and liveness. -- Maintenance archival and lifecycle preview exclude rows with non-null `conflict_state`. -- A challenged target remains lifecycle-active and recallable while being archive-ineligible. -- Integrity repair is idempotent: - - a valid live target missing `challenged` state is repaired; - - a challenger with a missing, cross-agent, or invalid target is archived and unlinked; - - a challenged target with no valid challenger is cleared; - - a non-conflicted row with a residual `conflict_with` link is unlinked. -- Repair emits only content-free aggregate audit metadata. -- Participant lookup uses the `(agent_id, conflict_with, status, superseded_by)` index. - -### Revisioned Semantic Writes - -Schema migration v41 adds: - -```sql -decision_revision INTEGER NOT NULL DEFAULT 1 -``` - -- Content, category, importance, lifecycle, supersede, conflict, and persona semantic changes increment the - revision. -- Access accounting, decay, embedding metadata, audit writes, consolidation stamps, and provenance re-key do - not increment it. -- UPDATE atomically checks agent, ID, expected revision, liveness, and conflict state while replacing content, - category, provenance, and last-access time; it also resets embedding metadata and queues re-embedding. -- UPDATE and its confidence change run in one transaction. -- SUPERSEDE and CHALLENGE combine insertion with a conditional target transition in one transaction. -- A first revision conflict performs one complete fresh provenance lookup, recall, prompt, and model decision. -- During retry, only an explicit valid `ADD` may create a row. Another conflict, provider failure, or parse - failure returns `concurrent-update`. -- Maintenance merge records both participant revisions before the model await and applies survivor content, - embedding reset, importance/confidence, and retired-row supersede in one transaction. -- If either participant changed, maintenance rolls back and skips the stale result without another model call. -- A provenance owner equal to the secondary participant may become the survivor. An unrelated third owner - causes a safe skip and consolidation stamp. - -### Vector Store Lifecycle - -- Query, query-by-ID, upsert, delete, and reconcile run through `withStoreLease`. -- A lease carries the per-agent store generation. The manager does not return the raw store, and callers must - not retain the callback argument; TypeScript cannot prevent deliberate closure caching. -- Close, reset, delete, and dispose stop new admission and wait for active leases before touching the native - store. -- Reset failure marks `requiresReset`; later leases retry reset under the per-agent lock and fail open to FTS - for the current request when recovery still fails. -- Permanent retirement and dispose keep admission closed. -- Embedding-ready status, warm readiness, and reconcile watermarks are accepted only for the generation that - produced them. -- Store drains run per agent in parallel. One stuck native operation does not prevent other idle stores from - closing. -- After the shared five-second dispose deadline, only stores with an active native operation may remain open. - -### Provider Gateway and Bounded Dispose - -All Memory text, embedding, and dimension requests pass through one agent-aware provider gateway. - -| Purpose | Deadline | -| --- | ---: | -| Query embedding | 800 ms | -| Dimension lookup | 15 s | -| Embedding batch or warmup | 30 s | -| Extraction, decision, or maintenance text | 60 s | -| Dispose drain | 5 s | - -- Rate-limit admission is required and precedes the provider call. -- The gateway always passes an optional `AbortSignal`; it does not infer provider support from function arity. -- Requests are grouped by agent, provider, model, and purpose. -- At most two underlying unsettled requests are allowed per group and 64 globally. -- Capacity is released only when the underlying promise settles, not when an outer deadline wins the race. -- Agent clear or deletion aborts that agent's active controllers. Dispose aborts all controllers. -- Providers that ignore cancellation may finish later, but their results and rejections are absorbed without - side effects or unhandled rejections. - -### Provenance Compatibility - -New provenance keys use: - -```text -v2:: -``` - -The digest input is: - -```text -agentId + NUL + kind + NUL + NFC(trim(collapseWhitespace(content))) -``` - -- Case is preserved. -- New writes use only v2. -- Lookup order is v2, then legacy v1. -- A legacy hit must belong to the same agent and kind and must match v2-normalized content. -- A matching legacy row is lazily re-keyed in a short transaction. -- A mismatched legacy hit is treated as a collision and does not suppress a new v2 row. -- Working memory supports its historical fixed-seed legacy key and removes a redundant legacy internal row - when the v2 row already exists. -- Lazy compatibility cleanup does not increment `decision_revision`; no eager backfill runs at startup. - -### Native SQLite Validation - -The `memory-native-validation` GitHub Actions job uses Node 24 and a fresh dependency installation. - -- It explicitly runs the native dependency's install lifecycle for the Node ABI. -- It executes an open/read/write/reopen/close smoke test. -- It sets `DEEPCHAT_REQUIRE_NATIVE_SQLITE=1` for the focused suite. -- Missing bindings, unavailable FTS, fresh-schema failure, skipped migration, or incomplete migration fail the - job. -- The suite covers fresh schema, legacy schema migration to v41, reopen, catalog repair coexistence, conflict - indexing, and real FTS behavior. -- Local Electron development does not switch the shared `node_modules` tree to the Node ABI. - -## Compatibility and Failure Semantics - -- Migration v41 is additive; existing rows receive `decision_revision = 1`. -- The existing cursor column remains authoritative; fragment persistence is not introduced. -- Legacy provenance remains readable and is migrated lazily. -- Public Memory routes, renderer DTOs, tools, events, and status values are unchanged. -- Recall remains fail-open to keyword retrieval when provider or vector infrastructure fails. -- Injection privacy validation, stale semantic writes, conflict integrity, and destructive cancellation fail - closed. -- User-visible semantic audit data is preserved; new repair audit data remains content-free. - -## Non-Goals - -- Replacing the current FTS index or keyword-selection algorithm. -- Adding a tape ingestion projection or batch decision protocol. -- Adding working-memory debounce, vector-store LRU, management pagination, or audit retention. -- Splitting the legacy status field into separate lifecycle and embedding state columns. -- Changing renderer behavior or public Memory contracts. -- Automatically switching a developer's shared native dependencies between Node and Electron ABIs. - -## Acceptance - -The implementation is accepted when all requirements above are represented in code and regression tests, -the architecture documentation is self-contained, non-native local validation passes without changing the -Electron ABI, and the post-commit `memory-native-validation` job passes in GitHub Actions. diff --git a/docs/architecture/agent-memory-performance-and-scale/spec.md b/docs/architecture/agent-memory-performance-and-scale/spec.md deleted file mode 100644 index cfc57a6067..0000000000 --- a/docs/architecture/agent-memory-performance-and-scale/spec.md +++ /dev/null @@ -1,366 +0,0 @@ -# Agent Memory Performance and Scale — Specification - -> Status: **implemented** -> -> Classification: **architecture** -> -> Local performance gate: **passed** -> -> Post-commit native CI gate: **passed** in PR -> [#1952](https://github.com/ThinkInAIXYZ/deepchat/pull/1952), including Native SQLite storage, -> retrieval evaluation, and the complete performance matrix - -This document defines the maintained requirements and acceptance criteria for predictable Agent Memory work -at large per-agent and multi-agent scales. - -## Purpose - -Agent Memory stores authoritative semantic state in SQLite, derives its extraction input from the -conversation Tape, and uses per-agent DuckDB sidecars for vector search. The implementation already -protects semantic operations with authoritative revalidation, revision compare-and-swap, read epochs, -destructive operation generations, vector-store leases, provider deadlines, and fail-closed privacy -gates. Those correctness mechanisms are baseline invariants, not optional performance costs. - -The remaining scale risks were algorithmic amplification and unbounded resource growth in the Electron -main process: non-indexed keyword scans, repeated stale-vector scans, full-session effective Tape views, -per-candidate provider calls, per-row embedding persistence, unbounded maintenance work, and missing -store, list, content, and audit caps. - -The target operating envelope is predictable work with 10,000–50,000 memories per agent, 100,000 Tape -entries per session, and 100 configured agents. The goal is not constant latency on every machine; it is -bounded local work, provider work, materialization, and native resource use. - -## Goals - -- Keep pre-first-token SQLite work index-backed and bounded by candidate limits. -- Make extraction cost proportional to the unconsumed Tape range instead of complete history. -- Bound an eight-candidate extraction to five steady-state provider calls and six under contention. -- Persist embedding batches with batch-level DuckDB and SQLite statements rather than per-row loops. -- Give working memory and maintenance explicit debounce, row, call, token, and concurrency budgets. -- Bound startup warmup, open DuckDB stores, management pages, submitted content, and operational audit - growth. -- Maintain a deterministic scale suite that enforces complexity, cross-size recall growth, and stable relative - work reductions without using shared-runner absolute latency as a hard gate. - -## Non-Goals - -- Changing retrieval scoring, reciprocal-rank fusion, default weights, top-K, or similarity thresholds. -- Introducing an external search or vector service, or adding a runtime dependency. -- Combining per-agent DuckDB files or changing the sidecar file format. -- Weakening authoritative final revalidation, semantic revision checks, destructive cancellation, - vector leases, provider deadlines, or fail-closed privacy behavior. -- Splitting lifecycle state from embedding state. -- Replacing the Memory Settings information architecture; pagination only bounds browsing while - `memory.search` remains the full-corpus search path. -- Implementing memory export. A future export path needs independent pagination that includes historical, - superseded, conflicted, persona, and archived rows; it must not reuse the management-page contract. -- Optimizing real provider network latency. This architecture limits request count, concurrency, queueing, - and local work. - -## Architectural Invariants - -1. SQLite `agent_memory` remains authoritative. FTS, Tape projection, and DuckDB vectors are rebuildable - derived state. -2. Tape remains the evidence source of truth. Projection failure may reduce performance but cannot corrupt - or replace Tape. -3. Every provider continuation rechecks its operation fence after admission and completion. Destructive - clear, agent deletion, configuration replacement, and disposal prevent older work from committing. -4. Semantic writes use revision-aware conditional transitions. A stale decision cannot overwrite newer - content or silently fall through to `ADD`. -5. Recall performs one final agent-scoped authoritative materialization before returning rows. -6. Vector operations run under manager-owned generation leases. Derived-data generation and resource lease - epoch remain separate concepts. -7. Provider deadlines include rate-limit admission, and every model or embedding request uses the shared - provider gateway. -8. Performance optimizations fail open only where correctness permits it: FTS may fall back to bounded LIKE, - projection may fall back to the authoritative Tape view without advancing a cursor, and vector failure - may fall back to lexical recall. -9. Privacy and stale-write checks remain fail closed. -10. No scale optimization may change the DuckDB format or consume a global SQLite schema migration version - for rebuildable derived state. - -## Requirements - -### Conditional Keyword Recall and FTS v4 - -- FTS metadata uses schema version 4 and records a separate policy version. A predicate, scope encoding, or - tokenizer change invalidates and rebuilds the derived index. -- FTS contains only recallable rows: `superseded_by IS NULL`, not archived or conflicted, and not persona or - working memory. -- Authoritative DML and the FTS dirty generation share one outer transaction. Explicit mirror maintenance - runs inside a nested savepoint; mirror failure rolls back only the savepoint, commits the authoritative - mutation, and leaves the derived generation dirty. -- Filtered rebuild indexes only recallable rows. It never indexes the complete table and relies on query-time - filtering afterward. -- Keyword selection is deterministic: code/path terms precede CJK terms, which precede ASCII terms; within a - class, longer Unicode-code-point terms precede shorter terms, with first occurrence as the final tie-break. - At most eight terms are retained, then restored to original query order. -- Corpus term-statistics SQL and unbounded keyword-stat caches are absent. BM25 supplies frequency weighting. -- When trigram FTS is available and every selected term is at least three Unicode code points, recall executes - only a bounded BM25 branch and a bounded importance/created-at supplement using the same MATCH expression. -- When FTS is unavailable, unicode61 is active, or all useful terms are short, recall executes at most one - agent-scoped bounded LIKE query. -- A recall uses exactly one keyword strategy: `fts-only` or `like-fallback`; it never combines FTS and LIKE. -- MATCH includes a deterministic agent-scope token and content. Scope has zero BM25 weight. Scope policy v2 - uses a fixed 24-bit token to reduce trigram posting intersections, while final `agent_id` revalidation - prevents cross-agent results even if tokens collide. -- Runtime structural failure marks the mirror dirty and enables a cooled lazy rebuild. Transient busy/locked - errors affect only the current request. unicode61 stays LIKE-only and does not maintain an unused mirror. -- Incremental import excludes derived FTS metadata and invalidates the target metadata after importing - authoritative rows. Reopen performs a filtered rebuild. -- `clearByAgent` removes only that agent's mirror rows and cannot disable FTS for other agents. -- Both lexical and importance branches cap their candidate windows before materialization. A common term - cannot materialize every match for an agent. - -### Vector Readiness Certificate - -- Ordinary recall never calls `hasStaleEmbeddings`. -- A ready certificate binds agent, provider/model, dimension, configuration generation, and logical - derived-data generation. Closing and reopening an LRU resource changes only the lease epoch and does not - invalidate the certificate. -- Startup warm, destructive rebuild, reset, and embedding identity change perform complete verification. -- Verification runs under a per-agent vector-mutation barrier. It pages through current embedded SQLite IDs, - compares them bidirectionally with DuckDB IDs, removes sidecar extras, and triggers destructive reindex if - SQLite IDs are missing vectors. -- A certificate is installed only when read epoch, embedding identity, and logical store generation remain - stable across the complete scan. -- Embedding drain, reconciliation, sidecar mutation, SQLite-ready transition, and certificate verification - share the mutation barrier. Verification cannot delete a vector written by an in-flight drain before its - SQLite transition completes. -- A normal embedding drain does not create a missing certificate. A valid certificate remains valid after a - successful revision-aware dual write. -- Authoritative vector materialization additionally requires an embedded row with the current model - fingerprint and dimension. Pending rows remain lexically recallable. - -### Tape Ingestion Projection - -- `deepchat_memory_ingestion_projection` and its metadata are rebuildable derived tables with projection - version 1 and idempotent creation. -- Each effective message stores session ID, message ID, order sequence, current lineage entry ID, role, - content, final status, and effective tool-use state. -- The reducer is injected into the lowest-level Tape table. Every append, including anchors, events, tool - facts, and unrelated rows, advances or invalidates session metadata. -- Tape append and projection update share one SQLite transaction. Reducer failure rolls back its savepoint, - invalidates the session metadata, and still permits the authoritative Tape append. If invalidation also - fails, the Tape append rolls back. -- Metadata compares against the previous maximum entry ID for the same session, never a global - `entry_id - 1` assumption. -- Message revisions use the shared effective-view rank and entry-ID tie-break. Only sent/error messages are - projected. -- A final tool fact before the sent/error message is a valid runtime order. Only explicit `success` and - `error` tool statuses are terminal; missing, pending, loading, or unknown statuses cannot set final tool - use. The final message reconstructs equivalent `had_tool_use`. Retraction and mutations whose equivalence - cannot be proven mark the projection stale. -- Session delete, clear, fork, truncate, and rewind clean or invalidate projection state transactionally. -- Stale metadata triggers one full authoritative Tape read and one `buildEffectiveTapeView` rebuild, followed - by transactional projection replacement. The already-built view is reused for the current extraction. -- Rebuild failure uses the authoritative full view for that extraction but sets every cursor commit boundary - to null. -- A read or rebuild failure places the session in a 30-second passive retry cooldown. During cooldown, - extraction skips before another full Tape read or provider call. The failure cache holds at most 256 - sessions, clears on success and session lifecycle reset, and never schedules an independent timer. -- Normal extraction reads the requested `(session_id, order_seq)` range in one SQLite statement and does not - call `getBySession()`. -- Range ordering matches SQLite BINARY semantics: `orderSeq ASC, messageId ASC`. Equal-order fragments form - one cursor commit group, and no chunk commits that sequence before its final fragment succeeds. -- Reading the last 20 messages of a 100,000-entry Tape materializes only the bounded range. - -### Candidate and Decision Batching - -- Parsed candidates are normalized and stably deduplicated by `(kind, normalized content)` before recall or - provider work. First occurrence wins. -- Candidate content is limited to 2,000 Unicode code points. Oversized automatic candidates are rejected with - a content-free aggregate audit reason and are never silently truncated. -- Decision retrieval performs one bounded keyword query per candidate, one batch query embedding for all - unresolved candidates, all vector queries inside one lease, and one authoritative `listByIds` for the - union. Each candidate retains at most three neighbors. -- A decision partition contains at most four candidates, three 400-code-point neighbor excerpts per - candidate, and 12,000 estimated input tokens. At most two partitions are admitted. -- Budget reduction drops the lowest-priority neighbor excerpt first and never truncates the candidate. - Candidates that still cannot fit use the initial safe `ADD` fallback without adding a third decision call. -- Batch results align by candidate index. The first duplicate index wins. A missing or invalid initial item - falls back only that candidate to `ADD`; a batch provider failure falls back only that batch. -- Model-generated merged content above 2,000 Unicode code points is invalid. -- Candidate application is serial and preserves original order. -- Every provider admission and response checks the operation fence. A stale fence stops remaining partitions - and prevents later external disclosure or writes. -- Final application re-resolves provenance, permanent-forget audit state, owner revision, liveness, pinned - neighbors, and status inside the transaction boundary. -- The query-vector snapshot binds provider/model, dimension, and configuration generation. Retry never uses a - vector produced by a different embedding identity. -- Only the first four revision conflicts retry. Retry refreshes provenance, lexical/vector neighbors, and - authoritative rows while reusing only an identity-compatible candidate embedding, and performs at most one - additional decision provider call. -- During retry, only an explicit valid `ADD` may create a row. Missing, invalid, stale, or failed retry results - return `concurrent-update`. -- Provider calls are bounded to five in steady state and six under contention. - -### Bulk Embedding Persistence - -- A drain processes at most 50 pending rows per batch and continues through the same per-agent supervisor - until the backlog is empty. -- Pending snapshots include ID and expected revision. -- Each batch performs one provider embedding call and one authoritative `listByIds` revalidation. -- One vector-mutation scope and lease contain one DuckDB transaction with one bulk delete and one - parameterized multi-values insert. -- SQLite performs at most one revision-aware success update and one revision-aware error update per batch. - Ready transitions require agent, ID, expected revision, pending state, and liveness. -- CAS misses cannot mark an old vector ready; their sidecar orphans are deleted in the same mutation scope or - left to bounded reconciliation. -- A provider-wide failure leaves retryable rows pending and does not emit per-row pending-to-pending writes. - Only malformed individual vectors enter the error batch. -- Concurrent callers share one supervisor promise covering merged follow-up cycles. The implementation does - not create an unbounded promise chain or fire-and-forget a hidden final cycle. - -### Working Memory and Bounded Maintenance - -- A domain mutation marks working memory dirty and starts a 100 ms trailing debounce. Twenty synchronous - mutations for one agent trigger at most one asynchronous refresh. -- A read or injection encountering dirty working memory cancels the timer and synchronously flushes before - persona/working assembly, while preserving the initial epoch gate and final no-await gate. -- Archive work uses a set-based `UPDATE ... RETURNING` and processes at most 256 rows per pass. -- Eligibility uses current-time age algebra over `COALESCE(last_accessed, created_at)` and importance; it does - not depend on `pow()`, stale materialized decay, or a lifetime `access_count === 0` veto. -- Reflection and persona use one aggregate plus indexed top-N repository query rather than full-agent - materialization and JavaScript sorting. -- A maintenance budget permits eight calls and 24,000 estimated input tokens per pass. Step quotas are - challenge 4, merge 2, reflection 1, and persona 1; quotas are not borrowed. -- Reserving a call consumes it even if gateway admission or the provider fails. -- Heavy steps run challenge, merge, reflection, then persona. Heavy work uses a process-wide fair semaphore - of two agents and per-agent whole-pass singleflight. Cheap maintenance does not occupy the semaphore. -- Challenge selection uses bounded indexed fairness ordering by - `COALESCE(last_consolidated_at, 0), created_at, id`. Every selected challenger is stamped after an attempt, - including normalization, size, budget, gateway, and provider failures. -- Conflict integrity repair selects only actual anomalies, scans at most 256 rows, and uses a constant number - of set-based statements. Sibling-group size does not increase statement count. -- Maintenance prompt construction performs incremental token preflight before concatenating strings. -- No-change passes do not perform whole-agent decay refresh, consolidation stamps, or other unbounded writes. - -### Startup, Native Stores, and Resource Bounds - -- Startup obtains enabled, managed, memory-enabled agents first, then performs candidate-scoped indexed latest - activity lookup and collects at most eight agents. -- Embedding connection warmup is keyed by `provider:model`, with a process-lifetime success set, in-flight - singleflight map, and five-minute failure cooldown. -- Per-agent vector stores have a soft cap of eight and an idle TTL of 15 minutes. An unref'ed 60-second sweep - enforces both the cap and TTL. -- Eviction selects a candidate outside locks and rechecks lease count, open-in-flight state, and identity - inside the agent lock. It never closes normal admission merely to test evictability. -- Busy stores may temporarily exceed the soft cap; lease release triggers immediate convergence. -- Expected lease competition waits or retries and does not clear the logical certificate or mark embeddings - as failed. -- With 100 agents sharing one embedding model, startup performs at most one provider warmup and opens at most - eight sidecars. - -### Pagination, Content, and Audit Bounds - -- `memory.page` is an additive typed route with default and maximum limit 100. -- Management visibility matches the legacy list: superseded, conflicted, persona, and working rows are - excluded; archived rows remain visible. -- Ordering is `created_at DESC, id DESC`. The repository reads `limit + 1` and emits a cursor only when another - page exists. -- The opaque cursor is canonical base64url JSON `{v:1,createdAt,id}`. Invalid encoding, version, shape, unsafe - timestamp, or empty ID produces route validation failure rather than page-one fallback. -- After cursor validation, a non-DeepChat Agent receives an empty page and never reaches the memory - presenter. -- `memory.list` remains wire-compatible for one compatibility window, is deprecated, and has an architecture - guard against new production callers. -- Renderer pagination supports replace, append, load-more, ID deduplication, and request-generation rejection - of stale responses. Agent changes reset pages. Update bursts are coalesced, and refresh atomically reloads - the previously loaded page depth. -- Dirty editors preserve their local draft even when the refreshed window omits the row. Clean editors close - only after the complete refreshed loaded window omits the row. -- Non-empty active-memory search continues to use server-side `memory.search` and hides the unrelated - management-page Load more action. Archived search remains local to loaded management pages, so Load more - stays available when archived rows are included. -- User add, manual edit, and memory-tool content is limited to 12,000 Unicode code points. Automatic extraction - and model-generated merged memory are limited to 2,000. Validation exists at route/tool and domain layers. -- Existing oversized rows are not migrated or truncated and remain readable and recallable. -- Operational audit cleanup applies only to `memory/maintenance_llm`, `memory/reflect`, `memory/repair`, - `memory/conflict_repair`, and `memory/extract`. -- Per agent, cleanup retains the newest 10,000 allowlisted events and deletes at most 500 older events per - cheap pass. -- `memory/add`, `memory/manual_edit`, `memory/archive`, `memory/forget`, `memory/restore`, - `memory/challenge_resolved`, every `persona/*` event, and unknown, malformed, or legacy causal rows are - retained permanently. -- Cleanup classification never uses a blanket `memory_ref_id IS NULL` rule and cannot change - `hasForgetEvent` results. - -### Performance Evidence and CI Gate - -- `test:main:memory-perf` uses an independent Vitest configuration and does not run in ordinary `test:main`. -- The deterministic matrix covers memory sizes 1,000, 10,000, and 50,000; Tape sizes 10,000 and 100,000; 100 - agents sharing one model; a 101-row embedding drain proving 50/50/1 batching; and eight candidates with - three neighbors each. -- Production-path observers record SQLite statements, repository calls, materialized rows, provider calls, - DuckDB statements, open stores, active leases, and queue/cache high-water marks. Production defaults to no - observer and has no global observer state. -- CI hard-asserts statement, materialization, provider, store, lease, queue, and cache bounds. -- The recall scale gate uses 11 paired samples with alternating execution order. It requires the FTS median - growth factor from 10,000 to 50,000 rows to be no more than 65% of the legacy LIKE growth factor and asserts - that every indexed sample remains on the `fts-only` strategy. -- The 50,000-row FTS/LIKE point ratio is reported but is not a shared-runner gate. The 100,000-entry Tape tail - median remains required to be no more than 20% of the full-view baseline. -- Absolute targets of 50 ms p95 for 50,000-row recall and 25 ms p95 for a 100,000-entry Tape tail are reported, - not enforced on shared runners. -- The native CI job rebuilds the Node ABI binding, sets `DEEPCHAT_REQUIRE_NATIVE_SQLITE=1`, and treats a missing - native binding or skipped performance suite as failure. - -## Compatibility and Failure Semantics - -- No third-party dependency or DuckDB format change is introduced. -- FTS and projection can be dropped and rebuilt. Their failure falls back to bounded LIKE or the authoritative - Tape path without corrupting authoritative state. -- Bulk vector persistence does not change the sidecar schema. -- `memory.page` is additive, and `memory.list` remains temporarily wire-compatible. -- Operational audit deletion is intentionally irreversible but applies only to the exact allowlist and is - protected by retention and forget-event parity tests. -- Existing recall scoring and quality semantics remain unchanged. -- Local package commands use `mise exec -- pnpm`; CI continues to use its configured pnpm toolchain directly. -- No GitHub issue is created or linked. -- Open design questions: none. - -## As-Built Performance Evidence - -The reference run used Apple M4 Pro/arm64, macOS 26.5.2, Node 24.14.1, and -`DEEPCHAT_REQUIRE_NATIVE_SQLITE=1`. Recall wall-clock values and point ratios are reference data. Complexity -bounds, the recall cross-size growth advantage, and the Tape relative ratio are the portable gates. - -| Scenario | Size | New median/p95 | Legacy median/p95 | New/legacy | -| --- | ---: | ---: | ---: | ---: | -| Safe-trigram FTS / bounded LIKE | 1k | 0.699 / 0.731 ms | 0.165 / 0.181 ms | 422.8% | -| Safe-trigram FTS / bounded LIKE | 10k | 1.075 / 1.123 ms | 1.313 / 1.489 ms | 81.9% | -| Safe-trigram FTS / bounded LIKE | 50k | 3.546 / 3.645 ms | 8.428 / 8.567 ms | **42.1%** | -| Tape current-range / full effective view | 10k | 0.058 / 0.060 ms | 12.888 / 14.853 ms | 0.45% | -| Tape current-range / full effective view | 100k | 0.052 / 0.068 ms | 192.222 / 195.931 ms | **0.03%** | - -An Ubuntu 22.04 shared runner observed a 95.0% recall point ratio at 50,000 rows while the FTS and LIKE -10,000-to-50,000-row growth factors were 2.67 and 5.45 respectively. The architecture therefore gates the -portable scaling advantage rather than an architecture-sensitive point ratio. - -The 1,000-row FTS fixed cost and the 50,000-row FTS/LIKE point ratio are intentionally not shared-runner gates. -The portable recall gate compares the 10,000-to-50,000-row growth factors, while the Tape gate compares the -100,000-entry range and full-view medians. The reference p95 values are below both report-only targets. - -Production-path complexity evidence also confirms: - -- Safe-trigram recall uses one SQLite query and materializes at most 40 results. -- A current Tape range uses one query and materializes 20 rows. -- A 101-row embedding backlog drains in 50/50/1 provider and persistence batches. -- Eight candidates with three neighbors use triage, extraction, and two decision calls when vector embedding - is not required. -- One hundred shared-model agents prewarm at most eight stores and one provider connection. -- Common-term recall remains agent-scoped across 100 agents. -- Maintenance uses the intended indexes at 50,000 rows, and a 1,000-sibling conflict transition uses one - set-based statement. - -Targeted main/native tests, the complete main and renderer suites, Memory renderer tests, the reference -performance suite, and type checking pass. PR #1952 also passed the required post-change Native SQLite, -retrieval quality, recall-growth, Tape-scale, and complete performance CI gates without weakening or skipping -their assertions. - -## Acceptance - -The architecture is accepted when all requirements above remain represented in production code and -regression tests, the growth, relative Tape, and complexity performance gates pass, authoritative data remains -safe under derived-state failure, and the post-commit native CI gate completes successfully. diff --git a/docs/architecture/agent-memory-system/spec.md b/docs/architecture/agent-memory-system/spec.md deleted file mode 100644 index f3f633fd86..0000000000 --- a/docs/architecture/agent-memory-system/spec.md +++ /dev/null @@ -1,1067 +0,0 @@ -# Agent Memory System — Architecture & Design - -## 1. Overview - -DeepChat's current memory implementation is a per-agent long-term memory layer built around the tape. It -derives durable memory from the effective tape, keeps lineage back to source entries, and treats the raw tape -as the evidence source of truth. `agent_memory` is the synthesized cache used for recall, update, forgetting, -and audit. - -Each agent owns its own memory rows, audit rows, config, and DuckDB vector sidecar. The implementation adds no -new infrastructure and reuses SQLite, DuckDB, the existing embedding manager, the tape effective view, and the -scoring helpers. `Crystal` and `Episodic` session-summary layers remain reserved placeholders, not active read -or write paths. - ---- - -## 2. Design principles - -These hold across every module and are the system's core invariants. - -1. **Sidecar, one-directional.** Memory consumes the tape effective view and writes only `memory/*` and - `persona/*` tape anchors that are **non-reconstruction**: they never move the summary cursor, never - participate in context rebuild, and never write back the original facts. -2. **Never block the turn.** Recall is fail-open: any error returns the original system prompt unchanged. - Extraction is fail-safe: a failure leaves the extraction cursor unadvanced so the span is retried, and - never throws into the conversation. -3. **Fail-open where losing data is the risk; fail-closed where writing wrong/stale data is the risk.** - - Fail-open: triage failure → extract anyway; decision-model failure → degrade to `ADD`; neighbor - recall failure → proceed with no neighbors; transient embedding-service failure → re-mark - `pending_embedding` for retry; vector-write/dimension failures may mark rows `error`, but the - embedding drain periodically requeues a bounded batch after cooldown and the Health panel exposes a - per-agent reindex entry. - - Fail-closed: a vector sidecar whose embedding identity (provider/model/dim) cannot be verified is - disabled and recall serves FTS only — it never silently returns vectors from the wrong model. -4. **Never hard-delete on the durable path.** Contradiction and staleness use supersede chains and - soft archival. Only an explicit user UI delete/clear or agent-deletion cleanup hard-deletes. -5. **Hot path adds zero synchronous LLM calls.** Extraction, embedding, reflection, persona evolution, - merging, and decay all run off the hot path; the only model call near a turn is the optional triage - gate, and even that runs inside the background extraction chain, not in the send path. -6. **Expensive work is offline.** Consolidation/merge/reflection/persona run on a self-scheduled - sleep-time pass gated by a restart-durable cooldown and an LLM budget. -7. **Auditable, content-free observability.** Maintenance and user audit rows record *provenance - metadata only* (ids/action/model, with an optional `session_id` — never raw text) in an audit table; - injection manifests are persisted as tape anchors. -8. **Message-boundary extraction.** CJK-aware chunks cap estimated tokens and Unicode code points; - triage and extraction see the same chunk, and the cursor advances only after the last fragment of a - message succeeds. -9. **Authoritative, epoch-gated reads.** Retrieval re-reads the FTS/vector union after provider awaits; - a concurrent domain mutation invalidates the entire injection before prompt append or accounting. -10. **Revisioned aggregate writes.** `decision_revision` protects UPDATE/SUPERSEDE/CHALLENGE with one - bounded fresh retry; unresolved conflict participants cannot be independently mutated. -11. **Bounded external lifecycles.** Provider work passes RateLimit admission and purpose deadlines; - vector operations use manager-owned leases, and disposal has one five-second drain deadline. -12. **Execution epochs are separate from read epochs.** Ordinary semantic mutations advance the per-Agent - read epoch. Clear, Agent deletion, dispose, and effective `memoryEnabled` / `memoryEmbedding` transitions - advance one per-Agent execution epoch and abort stale provider continuations before they can write rows, - cursors, audits, events, vectors, prompt sections, or access accounting. -13. **Scale is contractually bounded.** Recall chooses either indexed FTS or one LIKE fallback; extraction - batches candidate recall/decisions; embedding persists in bulk; maintenance has row/call/token/concurrency - budgets; startup, vector-store residency, management pages, content, and operational audit history all - have explicit caps. - ---- - -## 3. System overview - -```mermaid -flowchart TD - subgraph Renderer - UI["Settings: Memory page
(MemorySettings / Config / Manage)"] - MC["MemoryClient / ConfigClient"] - end - subgraph Main["Electron main process"] - ARP["AgentRuntimePresenter
(construction + thin delegates)"] - RC["MemoryRuntimeCoordinator
(runtime seam owner)"] - MEM["MemoryPresenter
(the memory kernel)"] - TAPE["DeepChatTapeService
(tape projection / search / context)"] - TOOLS["Agent tools
(memory_* · tape_*)"] - subgraph Stores - SQL["SQLite
agent_memory · agent_memory_fts
agent_memory_audit · tape entries + projection"] - DUCK["DuckDB sidecars
(one .duckdb per agent)"] - end - end - UI --> MC --> MEM - ARP --> RC - RC -->|MemoryPromptContributor
buildInjection| MEM - RC -->|afterTurnSettled / afterCompactionApplyReturned
then extractAndStore when admitted| MEM - RC -->|view/extract anchors + cursor/projection| SQL - TOOLS -->|remember/recall/forget| MEM - TOOLS -->|tape_search/tape_context| TAPE - MEM --> SQL - MEM --> DUCK - TAPE --> SQL - MEM -->|memory.updated event| MC -``` - -**Ownership rules** - -- The renderer settings UI talks to the kernel only through typed IPC (`MemoryClient` for management, - `ConfigClient` for config). -- `MemoryRuntimeCoordinator` is the sole runtime seam owner. It owns per-session extraction chains/queue, - epochs, projection-retry cooldown, injection-access dedupe and cursor/window orchestration; it directly - implements `MemoryPromptContributor` and `MemoryIngestionObserver`. -- `AgentRuntimePresenter` retains only coordinator construction, dependency wiring and thin lifecycle delegates. - A `DeepChatAgentInstance` keeps one stable Memory session handle; neither presenter nor instance duplicates - coordinator state. -- `MemoryPresenter` is the single public memory facade. Internal services own write decisions, - recall, scheduling, lifecycle, persona, conflicts, embeddings, and DuckDB orchestration; external - callers still reach those capabilities only through the facade. -- `DeepChatTapeService` owns the searchable tape projection (the log-as-memory read model). -- `DeepChatTapeService.readCausalObservationSlice()` is a pure-read Tape/message/trace join. Architecture - guards prevent it from calling Memory or storage mutation APIs, and non-interference tests prove it does not - change ingestion projection/cursor state. - ---- - -## 4. Module layout - -| Layer | File | Responsibility | -| --- | --- | --- | -| Kernel | `src/main/presenter/memoryPresenter/index.ts` | `MemoryPresenter` facade — public method compatibility, service wiring, narrow port binding, `dispose()` and deleted-agent cleanup orchestration | -| Kernel | `memoryPresenter/context.ts` | `MemoryRuntimeContext` — read epochs, execution epochs and effective execution-config identity, disposed state, validation/guard helpers, private provider abort coordination, audit/events, and model resolution; it exposes no repository/provider/vector escape hatch | -| Kernel | `memoryPresenter/runtimeConstants.ts` | Internal runtime scheduling, lifecycle, working-memory, and warmup constants | -| Domain | `memoryPresenter/domain/types.ts` | Persistence-shaped memory rows plus lifecycle, recall, write, management, and maintenance domain types; no runtime or storage-concrete dependencies | -| Domain | `memoryPresenter/domain/audit.ts` | Content-free audit rows, actors, statuses, inputs, and query result types | -| Kernel | `memoryPresenter/types.ts` | `MemoryPresenterDeps`, tunable constants, and compatibility re-exports for existing import paths | -| Kernel | `memoryPresenter/ports.ts` | Root-owned narrow repository, audit, policy, provider, vector, change-sink, provenance, and collaborator capabilities; composite ports are composition-only | -| Kernel | `memoryPresenter/injection.ts` | Lightweight public sub-entry for injection helpers/types without loading the facade composition root | -| Services | `memoryPresenter/services/rowMutations.ts` | Shared stateful row mutation implementation for insert/update/supersede/conflict/provenance/confidence primitives; consumers depend on root-owned collaborator ports, not this concrete class | -| Services | `memoryPresenter/services/retrievalService.ts` | Hybrid recall/search/injection orchestration, keyword query, query embedding gate, soft timeout, RRF fusion, access recording | -| Services | `memoryPresenter/services/writeCoordinator.ts` | Sync writes, extraction writes, user/tool writes, decision ring application, audit, and background trigger ports | -| Services | `memoryPresenter/services/maintenanceService.ts` | Startup/prewarm/consolidation timers, merge/challenge/reflection/persona maintenance, decay/archive passes | -| Services | `memoryPresenter/services/workingMemoryService.ts` | Working-memory blob read/build/refresh/delete, correctness-only dirty/flush finalization, cold-start refresh coalescing, and legacy working-provenance resolution | -| Services | `memoryPresenter/services/reflectionService.ts` | Reflection thresholding, insight insertion, watermarks, and embedding trigger port | -| Services | `memoryPresenter/services/personaService.ts` | Persona draft/evolution/approval/rejection/rollback/anchor flows and per-agent persona lock | -| Services | `memoryPresenter/services/conflictService.ts` | Conflict aggregate guard, listing/resolution, integrity repair, and maintenance scheduling through narrow ports | -| Services | `memoryPresenter/services/managementService.ts` | List/get/lifecycle/health/delete/clear/status delegation and management-facing row projection | -| Infra | `memoryPresenter/infra/providerGateway.ts` | Agent-aware RateLimit admission, purpose deadlines, destructive abort, and bounded unsettled provider work | -| Infra | `memoryPresenter/infra/vectorStoreManager.ts` | DuckDB sidecar infrastructure: scoped generation leases, readiness certificates, recoverable reset, lease-safe LRU/TTL eviction, and parallel close/drain orchestration | -| Infra | `src/main/lib/asyncSemaphore.ts` | Fair process-wide admission for heavy per-agent maintenance | -| Infra | `memoryPresenter/infra/embeddingPipeline.ts` | Pending embedding drain, reindex/backfill, embedding/vector warmups, dimension cooldowns, and `isReindexing` | -| Infra | `memoryPresenter/infra/memoryVectorStore.ts` | `MemoryVectorStore` — per-agent DuckDB sidecar (HNSW/cosine, identity gate, transactional upsert, disk reclaim) | -| Core | `memoryPresenter/core/candidates.ts` | Pure memory candidate normalization | -| Core | `memoryPresenter/core/decision.ts` | The Mem0-style decision ring prompt + tolerant parser (`ADD/UPDATE/SUPERSEDE/NOOP/CHALLENGE`) | -| Core | `memoryPresenter/core/batchDecision.ts` | Pure 4-candidate/12k-token decision partitioner and indexed batch-result parser | -| Core | `memoryPresenter/core/maintenanceBudget.ts` | Shared per-pass call/token accounting with fixed challenge/merge/reflection/persona quotas | -| Core | `memoryPresenter/core/extraction.ts` | Triage + extraction prompts and parsers; reflection/persona prompts; persona small-step (Levenshtein) guard | -| Core | `memoryPresenter/core/injectionPort.ts` | `sanitizeForInjection`, the token-budgeted Context Assembler, the injection manifest, and the sole legacy/canonical injection input normalizer | -| Core | `memoryPresenter/core/lifecycle.ts` | Lifecycle diagnostics and archive/freshness thresholds | -| Core | `memoryPresenter/core/recallKeyword.ts` | Pure keyword candidate extraction and selection for recall | -| Core | `memoryPresenter/core/scoring.ts` | Recall `retrievalScore`, `decayScore`, RRF `fuse()`, provenance keys | -| Shared | `shared/types/agent-memory.ts` | Memory category/audit constants, deterministic importance floors, and the authoritative agent-id pattern/predicate | -| Storage | `sqlitePresenter/tables/agentMemory.ts` | `agent_memory` table + `agent_memory_fts` FTS5 + keyword search | -| Storage | `sqlitePresenter/tables/agentMemoryAudit.ts` | `agent_memory_audit` content-free maintenance ledger | -| Storage | `sqlitePresenter/tables/deepchatTapeSearchProjection.ts` | `deepchat_tape_search_projection` (+ meta + FTS) evidence projection | -| Storage | `sqlitePresenter/tables/deepchatMemoryIngestionProjection.ts` | Versioned effective-message range projection used only by memory extraction | -| Runtime seam | `agent/deepchat/memory/memoryRuntimeCoordinator.ts` | sole extraction queue/epoch/cooldown/access/cursor owner; direct prompt contributor + ingestion observer | -| Runtime contracts | `agent/deepchat/memory/memoryPromptContributor.ts`, `memoryIngestionObserver.ts` | stable session handle, awaited prompt contribution, discriminated terminal/compaction ingestion outcomes, bounded drain result | -| Runtime wiring | `agentRuntimePresenter/index.ts` | coordinator construction/dependencies and thin instance/lifecycle delegates; no duplicate orchestration maps | -| Runtime | `agentRuntimePresenter/tapeService.ts` | `search()` / `getContext()` / `ensureSearchProjection()` | -| Tools | `toolPresenter/agentTools/agentMemoryTools.ts` | `memory_remember` / `memory_recall` / `memory_forget` | -| Tools | `toolPresenter/agentTools/agentTapeTools.ts` | Atomic model recall pair: `tape_search` / `tape_context`; info, anchors, and handoff remain below the model-tool boundary | -| Skills | `resources/skills/memory-management/SKILL.md` | Discoverable guidance for recall/remember discipline and Memory vs Skill vs Scheduled Task routing | -| Contracts | `shared/contracts/routes/memory.routes.ts` | All `memory.*` IPC routes + DTO schemas | -| Contracts | `shared/contracts/events/memory.events.ts` | `memory.updated` event plus the schema-derived authoritative update-reason type | -| Renderer | `renderer/settings/components/Memory*.vue`, `renderer/api/MemoryClient.ts` | The settings IA (page, config tab, manage tab) | - -Directory boundaries enforce dependency direction and are checked by `scripts/architecture-guard.mjs`. -`domain/` depends only on shared contracts; `core/` must not import `context`, `services`, `infra`, -`runtimeConstants`, or facade entrypoints. `infra/` and `services/` may consume domain/core types and -root runtime contracts (`types`, `ports`, `context`, `runtimeConstants`), but may not import each -other's concrete implementations. Services and infra receive only their declared narrow capabilities: -they cannot inject composition-only repository/audit/provider composites, import SQLite table concrete -types, or recover the former `ctx.deps`/`ctx.provider` service-locator path. Business calls between -services stay facade-wired through root-owned collaborator ports. `index.ts` is the only composition -root that may import every internal layer; it applies the performance observer once to the complete -repository and projects every capability from that same object. Other root files are lightweight -entrypoints/contracts and may not import `services`, `infra`, or the facade entrypoint. The shared -lineage codec remains the only parser/serializer for `source_entry_ids`. -The memory guard fails closed when its TypeScript config/program cannot be created, resolves composite -ports by symbol with file-specific allowlists, locks the runtime-context public surface and exact root -compatibility exports, and follows local lineage JSON helper data flow. Architecture tests use an -in-memory virtual source overlay, so parallel or interrupted runs cannot leave files under `src/`. - ---- - -## 5. Data model and storage responsibilities - -Memory is split across six stores. Each has one job; none is authoritative for more than its job. - -| Store | Holds | Rebuildable? | -| --- | --- | --- | -| SQLite `agent_memory` | Authoritative memory rows: content, kind, optional agentic category, canonical lifecycle/embedding state, legacy status shadow, importance, confidence, decay, lineage (`source_entry_ids`), supersede chain, persona state, conflict link | No — source of truth for synthesized memory | -| SQLite `agent_memory_fts` | Keyword recall (BM25), external-content mirror of `agent_memory` | Yes — rebuilt idempotently; degrades to `LIKE` | -| SQLite `agent_memory_audit` | Maintenance/user provenance ledger (ids/action/model; `scheduler`/`user` actors + optional `session_id`; drives the cooldown) | No — but content-free | -| SQLite `deepchat_tape_search_projection` | Searchable evidence projection of the effective tape (summary + refs + FTS) | Yes — rebuilt from raw tape entries | -| SQLite `deepchat_memory_ingestion_projection` | Effective final messages keyed by session/message with order, lineage entry, and tool-use bit for bounded extraction ranges | Yes — rebuilt from raw tape entries | -| DuckDB sidecar (one `.duckdb` per agent) | `memory_vector` (HNSW/cosine) + `embedding_meta` (provider/model/dim identity) | Yes — re-embedded from `agent_memory` | - -The raw tape (`deepchat_tape_entries`) remains the ultimate evidence source of truth and also stores the -`memory/extract` and `memory/view_assembled` audit anchors (both non-reconstruction). - -### 6.1 `agent_memory` columns - -`id`, `agent_id`, `user_scope`, `kind`, `category`, `content`, `importance`, `status`, `embedding_id`, -`embedding_dim`, `embedding_model`, `source_session`, `provenance_key`, `is_anchor`, `superseded_by`, -`created_at`, `last_accessed`, `access_count`, `decay_score`, `source_entry_ids`, `confidence`, -`last_consolidated_at`, `conflict_state`, `conflict_with`, `persona_state`, `decision_revision`, -`lifecycle_state`, `embedding_state`. - -A unique partial index on `(agent_id, provenance_key)` enforces idempotent dedup. -New provenance keys are `v2::` over -`agentId + NUL + kind + NUL + NFC(trim/collapseWhitespace(content))`; case is preserved. Legacy v1 -lookups require a normalized-content equality check before transactional lazy re-keying. - -### 6.2 Enums - -| Enum | Members | Notes | -| --- | --- | --- | -| `AgentMemoryKind` | `episodic`, `semantic`, `reflection`, `persona`, `working` | `working` is an internal single-blob session-open cache (never recalled/embedded/archived). `crystal` is reserved (no read/write path). | -| `AgentMemoryCategory` | `user_preference`, `project_fact`, `task_outcome`, `heuristic`, `anti_pattern` | Optional agentic write contract. `task_outcome` normalizes to `episodic`; the other categories normalize to `semantic`. `reflection`/`persona`/`working` rows always carry `NULL`. | -| `AgentMemoryLifecycleState` | `active`, `archived`, `conflicted` | Internal source of truth for business lifecycle. Supersede and challenged-target state remain separate axes. | -| `AgentMemoryEmbeddingState` | `pending`, `ready`, `error`, `fts_only`, `not_applicable` | Internal source of truth for vector processing. Persona/working rows are `not_applicable`. | -| Legacy `AgentMemoryStatus` | `pending_embedding`, `embedded`, `error`, `fts_only`, `archived`, `conflicted` | Synchronous compatibility shadow and public DTO vocabulary. It is projected from the two canonical axes; services/core never use it for decisions. | -| `AgentMemoryPersonaState` | `draft`, `active`, `superseded`, `rejected` | Only meaningful for `kind='persona'`; `NULL` for everything else. Legacy persona rows are read as active while not superseded. | -| `AgentMemoryConflictState` | `challenged` | Marks the *target* of an open challenge. | - -### 6.3 Canonical state transitions and compatibility projection - -```mermaid -stateDiagram-v2 - [*] --> ActivePending - ActivePending --> ActiveReady: embedding succeeds - ActivePending --> ActiveError: embedding fails - ActivePending --> ActiveFtsOnly: vector unavailable - ActiveReady --> ArchivedReady: archive - ActiveError --> ArchivedError: archive - ArchivedReady --> ActivePending: restore and clear refs - ArchivedError --> ActivePending: restore and clear refs - ActivePending --> ConflictedPending: open challenge - ConflictedPending --> ActivePending: keep challenger / keep both - ConflictedPending --> ArchivedPending: keep target -``` - -Lifecycle has projection precedence: `archived` and `conflicted` shadow the embedding state. For active -rows, `pending → pending_embedding`, `ready → embedded`, `error → error`, and -`fts_only | not_applicable → fts_only`. Archive preserves embedding state and refs; restore always returns -to active/pending, clears refs, and increments `decision_revision`. Every transition writes canonical state -and the legacy shadow in one SQL statement or transaction. - -Canonical indexes include `idx_agent_memory_active_recall`, `idx_agent_memory_recall_importance_v5`, -the archive/cognitive/conflict/recent v3 ordered indexes, the agent/global pending v2 indexes, and -`idx_agent_memory_conflict_target_v2` plus `idx_agent_memory_management_page_v3`. The v42 steady state -drops retired status-partial indexes; a downgraded v41 binary may recreate them, and the next v42 startup -cleans them again. - -### 6.4 Lineage contract - -`source_entry_ids` stores **tape `entry_id` integers** (a JSON array), scoped by `source_session`. It is -dropped when there is no source session, and never stores message ids. It is collected in the same pass -that builds the extraction span (a message contributing no text appears in neither the span nor the -lineage). - ---- - -## 6. The read path - -Recall, FTS liveness, embedding drain, maintenance, conflict, management, and Health queries use -`lifecycle_state`/`embedding_state`. Public route/tool/export responses call the shared pure projection and -continue exposing the legacy vocabulary. Startup does not scan or repair the full memory table. Migration -installs the temporary v41 status-only compatibility triggers in the v42 version transaction before recording -the schema version; catalog repair, schema-aware import, and the same trigger definitions keep canonical state -and shadow synchronized. Normal startup only verifies trigger DDL. A missing or stale trigger with mismatches -first checkpoints and creates a `*.memory-state-repair.bak`, preserves canonical lifecycle for the known old -internal-trigger mismatch, repairs other mismatches legacy-first, installs the current DDL, and asserts zero -remaining mismatches in one recovery transaction. Catalog repair backfills only columns added in that repair -transaction. Legacy-only malformed status is normalized tolerantly; importer rows with malformed canonical axes -or unknown kinds are skipped individually and reported without rolling back unrelated valid rows. - -Recall runs before each send and adds a read-only memory section to the system prompt. All three runtime -entry points — normal send, context-pressure compaction recovery, and resume — funnel through one -`MemoryPromptContributor.contribute()` seam so injection is identical across paths; a disabled agent short-circuits and -the system prompt is returned byte-for-byte unchanged. - -```mermaid -flowchart TD - Q[user query] --> AP["MemoryPromptContributor.contribute
(normal · compaction-recovery · resume)"] - AP --> EN{memory enabled?} - EN -- no --> SP0[system prompt unchanged] - EN -- yes --> BI[buildInjection] - BI --> PE["active persona
(drafts/rejected excluded)"] - BI --> WM["working L1 blob
(no access bump)"] - BI --> RC["recall(query)"] - RC --> FTS["FTS5 / BM25
(agent_memory_fts)"] - RC --> VEC["DuckDB vector
(memory_vector)"] - FTS --> RRF["RRF fusion
combined = retrievalScore + rrf"] - VEC --> RRF - PE --> CA[Context Assembler] - WM --> CA - RRF --> CA - CA --> SANI["sanitizeForInjection + CJK-aware hard token budget
persona > working > units > episodic"] - SANI --> INJ["<context-data kind=memory> appended to system prompt"] - INJ --> MAN["persist memory/view_assembled manifest anchor
(auditable · non-reconstruction)"] - INJ --> LLM[model replies] -``` - -**`buildInjection` details** - -- Captures the per-Agent read epoch and execution fence, then resolves recalled units with access recording - disabled. After all provider/vector awaits, retrieval performs one Agent-scoped `listByIds` authoritative re-read of the - FTS/vector union and replaces stale snapshots with the latest rows. -- Re-checks the original epoch, synchronously flushes any correctness-only working dirty state, captures the - post-flush epoch, and only then reads the **active** persona and working blob. Draft/rejected persona is - never injected and working reads do not bump `access_count`. -- Performs one final enabled/managed/disposed/read-epoch/execution-fence gate and returns immediately without - another await. Any concurrent semantic mutation or enabled/embedding ABA transition fails the whole - injection closed. The coordinator binds its own execution token before the await and revalidates it before - prompt append, access accounting, and the `memory/view_assembled` anchor. -- Produces a `MemoryInjectionPayload` (selfModel + working + memories + `tokenBudget`) and a manifest - (selected/dropped/queryHash). The coordinator appends the section and persists a `memory/view_assembled` - anchor. - -**Sanitization (`sanitizeForInjection`).** Persona and recalled bodies are neutralized before injection by -inserting a zero-width character into instruction-like markers: line-leading `#`, code fences (```` ``` ````), -role prefixes (`system:`/`assistant:`/`user:`), and literal `` container preceded by a `READONLY_NOTICE` telling the model to -treat it strictly as data. This is the persistent prompt-injection mitigation. - -**Context Assembler (token budget).** Admission priority is `persona > working > recalled units > episodic`, -but priority only decides order — the resolved budget (default **1200** tokens; configured values are clamped -down to a max of **8000**, and a value below **64** or malformed falls back to the 1200 default) is a hard -ceiling every section counts against. `estimateTokens` is CJK-aware (CJK/Kana/Hangul characters counted -at density **1.5**, other characters at `1/4`) so Chinese sections are not over-admitted. Persona and working -are placed first with a floor reservation so neither starves the other; recalled memories are admitted -whole-line (never half a sentence); episodic summaries sit last so they are cut first. The assembler is the -final boundary and never trusts an upstream size cap. - ---- - -## 7. The write path - -`MemoryIngestionObserver.afterTurnSettled()` receives discriminated initial/resume turn outcomes; -`afterCompactionApplyReturned()` receives every normally returned initial/context-pressure compaction apply -that has a compaction intent, including `succeeded: false`; a thrown apply or a cycle with no intent does not -trigger the observer. -Session close uses coordinator `beginSessionDestroy()` / `finishSessionDestroy()` fencing; app shutdown calls -`MemoryIngestionObserver.drainAndFence()` to close global ingestion admission and await the bounded outstanding -session chains. The coordinator applies the frozen trigger matrix, serializes admitted work per session, and -never runs extraction on the hot path. It builds its span from the **effective** tape view (after -retract/replace/tool-dedup), -not from raw messages. The span carries only user-visible text: a user entry contributes its message text and -an assistant entry contributes only its `content` blocks — assistant reasoning (`reasoning_content` / -`reasoning` blocks) is deliberately excluded so internal chain-of-thought never becomes a durable memory. The -raw tape still records reasoning in full; only the extraction input drops it (a message contributing no -visible text appears in neither the span nor the lineage). - -Fallback extraction is task-aware. The span builder computes `hadToolUse` and `visibleTextChars` in the same -effective-view pass that collects text and lineage. `hadToolUse` is derived by matching effective `tool_call` -rows' `payload.messageId` against the selected message window; raw tape rows are not filtered by `orderSeq`. -A fallback span is admitted only when it has visible text and either used a tool, reaches the long-span -backstop (`delta >= 6`), or is a short but substantive text span (`delta >= 2` and at least 160 visible -characters). Empty visible-text spans return before extraction and do **not** advance the memory cursor. -Compaction-triggered extraction keeps its old behavior. - -```mermaid -flowchart TD - T["initial / resume turn outcome"] --> TS["MemoryIngestionObserver.afterTurnSettled"] - C["initial / context-pressure
compaction apply returned"] --> CS["MemoryIngestionObserver.afterCompactionApplyReturned"] - TS --> EQ["MemoryRuntimeCoordinator
(per-session serial chain, epoch-guarded)"] - CS --> EQ - D["app shutdown"] --> DF["MemoryIngestionObserver.drainAndFence
(stop admission + bounded drain)"] - EQ --> CUR["read cursor + validate/rebuild ingestion projection
range-read effective messages (from,to]"] - CUR --> TRI{"triage gate
(cheap KEEP/SKIP, fail-open)"} - TRI -- SKIP --> ADV0["advance cursor, no write"] - TRI -- KEEP --> EX["extraction → JSON candidates (≤8)"] - EX --> CW["stable dedupe + batched neighbor recall/decisions"] - CW --> DUP{provenance hit?} - DUP -- hit --> REV["handleProvenanceHit
(restore / suppress / decision)"] - DUP -- miss --> NB["retrieveForDecision top-10 neighbors
(keyword any + no inline prune)"] - NB --> DEC{decision ring} - DEC --> ADD[ADD] - DEC --> UPD["UPDATE (+confidence bump)"] - DEC --> SUP["SUPERSEDE (+markSuperseded)"] - DEC --> NOOP[NOOP] - DEC --> CH["CHALLENGE → conflicted row + conflict_with"] - ADD --> PEND["agent_memory pending_embedding
+ source_session + source_entry_ids"] - UPD --> PEND - SUP --> PEND - REV --> PEND - PEND --> ADV["cursor advances MAX + memory/extract anchor (when created>0)"] - PEND --> EMB["single per-agent embedding drain
(50 rows · bulk DuckDB/SQLite)"] - EMB --> DUCK["DuckDB memory_vector + status=embedded"] -``` - -**Chunking and triage.** The effective window is packed on message boundaries into CJK-aware chunks capped -at approximately **4,000 estimated tokens** and **12,000 Unicode code points**. An oversized message is split -with an iterator-based grapheme/code-point-safe linear scan; its cursor boundary is committed only after the -last fragment succeeds. One queue task processes at most four chunks and enqueues the immutable remainder on -the same session chain. Triage and extraction receive the exact same complete chunk—there is no prompt-side -tail slice. `parseTriageDecision` skips only on an explicit `SKIP` without `KEEP`; a thrown triage call remains -fail-open. A successful SKIP consumes only that chunk's committable message boundary. - -Queue admission also binds the resolved Agent identity, session epoch, and current execution token. The epoch -and token are checked when the task starts and after each asynchronous extraction response. Cursor and -Tape-anchor commits form the immediately following synchronous segment, so no configuration callback can -interleave between validation and those writes. Continuation tasks inherit the original token and admission -epoch; a configuration transition or session Agent change makes queued work stale without advancing its cursor, -writing an anchor, or automatically re-enqueuing it. Before a session publishes a reassigned Agent identity, new -ingestion is paused, the session epoch advances, and the prior extraction chain drains. A later normal admission -captures fresh identities and scans from the retained cursor to the then-current tail. - -**Extraction.** For each complete chunk the model returns at most **8** raw candidates (enforced both in the -prompt and as a hard parse cap), each `{category, content, importance}`. Parsing is tolerant -**per entry** — a malformed individual entry or empty content is skipped, raw `category`/legacy `kind` are -preserved, malformed importance is left for normalization, and each span keeps at most one `task_outcome`. -A **top-level** parse failure (empty response, no JSON array, invalid JSON, or a non-array), however, is -reported as a discriminated `MemoryCandidateParseResult` (`{ ok: false, reason }`) rather than silently -degraded to `[]`, so the caller can retry the span instead of consuming it; a successful parse returns -`{ ok: true, candidates }` (an empty array is a valid success). - -Before recall or another provider call, normalized candidates are stably deduplicated by -`(kind, provenance-v2-normalized content)` and automatic content is capped at **2,000 characters**. An -oversized item becomes a content-free `candidate-too-large` audit outcome rather than being truncated. -Candidate neighbor retrieval performs one bounded keyword query per item, one batch query-embedding call, -one vector lease for all vector queries, and one authoritative `listByIds`; each item keeps at most three -neighbors. - -**Candidate normalization.** Every write entry point (`coordinateWrite`, `directAddMemory`, and -`writeMemoriesSync`) normalizes candidates before provenance-key generation or storage. A valid category -takes precedence over legacy `kind`: `task_outcome` becomes `episodic`, all other valid categories become -`semantic`; a missing category may keep a valid legacy `episodic`/`semantic` kind with `category=NULL`; -an invalid category becomes `semantic` + `NULL`. Importance is clamped/defaulted and then raised to -`CATEGORY_IMPORTANCE_FLOOR[category]` when a category is present. `reflection`, `persona`, and `working` rows -are never allowed to carry a category. - -**The extraction model is not a hardcoded "small" model.** Two resolvers exist. `resolveExtractionModel` uses -the agent's configured `memoryExtractionModel` when set; the extraction/triage/decision path falls back to the -**active chat model**. The offline passes use a separate `resolveConsolidationModel` whose fallback is the -agent's **default model** (`resolveAgentDefaultModel`), not the active chat model — consolidation, reflection, -and persona run on the scheduler with no active chat session, so the active chat model is structurally -unavailable to them. The cost saving comes from the triage gate avoiding the larger extraction call, plus the -*option* to point extraction at a cheaper model. - -**Decision ring (`coordinateWrite`).** Candidates are applied serially in original order, but model work is -batched: at most four candidates per prompt, three 400-character neighbor excerpts each, 12,000 estimated -input tokens, and two initial decision calls. The partitioner drops lowest-priority excerpts before using a -safe initial `ADD` budget fallback; it never truncates candidate content or opens a third initial batch. -Each candidate then follows this state machine: - -1. Trim; empty → no-op. Teardown guard → no-op. -2. Compute the provenance key and check for an existing row. A pure scheduler-archived row is restored to - `pending_embedding`; a user/runtime-forgotten row is suppressed; an archived superseded conflict loser is - suppressed; an active duplicate returns `noop(duplicate)`. A non-archived superseded row does not revive - by provenance alone when a model path is available — its live chain head is fed into the decision ring. -3. Otherwise use its three-item batch-retrieved neighbor set. This keeps FTS-only agents capable of semantic - write decisions without requiring vectors. On the first attempt, a missing/invalid result or provider - failure degrades only that candidate to `ADD`. Revision/liveness conflicts are collected in original - order; only the first four receive one fresh provenance/recall pass and one shared retry decision call, - reusing their first query embeddings. During retry only an explicit valid `ADD` may insert; another - conflict, provider failure, missing result, or parse failure returns `noop(concurrent-update)`. -4. Apply: `ADD` inserts with the candidate category. `UPDATE` uses one conditional SQL statement plus the - confidence update in one transaction, atomically checking agent/id/revision/liveness/conflict while - replacing content/category/provenance, resetting embedding metadata, and incrementing - `decision_revision`. `SUPERSEDE` and `CHALLENGE` atomically combine successor/challenger insert with their - conditional target transition. `NOOP` does nothing. - -Each successful candidate semantic commit immediately advances the read epoch and marks working memory dirty -before the next await. Batch success, failure, disable, and dispose share one finalization path for working -flush, embedding scheduling, and events. A partial batch keeps its cursor unadvanced; provenance/revision CAS -makes replay converge without losing the side effects of already committed rows. - -The write outcome is a discriminated union `MemoryWriteOutcome` (`created` / `updated` / `superseded` / -`noop` / `challenged`). The same coordinator backs both extraction and the agent-facing `memory_remember` -tool and the user-facing `memory.add` route (when an extraction model is configured; otherwise the user-add -path is a pure dedupe-add). - -**Two-phase persistence.** Phase 1 is a synchronous SQLite insert as `pending_embedding`. Phase 2 is one -per-agent `{dirty,running}` drain loop over at most 50 rows: one batched `getEmbeddings`, one authoritative -`listByIds`, one DuckDB transaction containing a bulk delete and multi-values insert, and at most one -revision-aware SQLite success update plus one error update. Content/config/clear races therefore cannot mark -an old vector ready. A whole-provider failure leaves rows pending without per-row no-op writes; malformed -individual vectors enter the error batch. Cooldown-bounded retry and manual per-agent reindex remain available. - -The effective-message ingestion projection is maintained at the lowest Tape append boundary, so every -message/tool/anchor/event append advances or invalidates the session watermark. Reads compare projection -version and session max entry ID; a mismatch rebuilds transactionally through the single -`buildEffectiveTapeView` implementation. Rebuild failure uses that full view for the current extraction and -does not falsely advance the cursor. The steady path materializes only the requested `(orderSeq, messageId)` -range while preserving the final message entry ID as lineage. - -**Cursor.** `memory_cursor_order_seq` (on `deepchat_sessions`) is written `SET = MAX(existing, floor(x), 0)`, -so a late/stale extraction can never roll it back. It advances only when `extractAndStore` returns `ok: true` -— i.e. the model output parsed into a (possibly empty) candidate array; a transient LLM error or a top-level -parse failure returns `ok: false` and leaves the cursor for retry, so a span is never consumed by an output -the model could not understand. A `memory/extract` anchor is written only when at least one row was created. - ---- - -## 8. Retrieval and scoring - -`retrieve()` resolves the per-agent retrieval config, fetches `topK*2` candidates from each path, fuses them, -and trims to `topK`. `searchMemories()` is the only caller allowed to pass a search-only `topKOverride` -(default 50, route max 100); agent-facing recall and prompt injection continue to use the configured -retrieval `topK` and still record access only for recall/injection hits. - -- **Keyword path.** `agent_memory_fts` (FTS5, BM25-ranked) with the tokenizer chosen at runtime — `trigram` - for CJK/substring matching, else `unicode61`. Query terms are selected deterministically by kind - (code/path → CJK → ASCII), Unicode code-point length, and original position, with a cap of eight. When the - tokenizer is trigram and every selected term is at least three code points, the path runs BM25 and one - importance/recency supplement using the exact same MATCH. Otherwise it runs exactly one bounded per-agent - LIKE query. FTS and LIKE never run together, and no corpus-frequency stats/cache is consulted. FTS v3 - contains only recallable rows. Authoritative writes mark the derived generation dirty, then maintain the - mirror inside a nested savepoint; failure rolls back only the savepoint and forces one bounded LIKE until - filtered rebuild. Persona, working, archived, conflicted, and superseded rows remain outside the index. -- **Vector path.** Only when an embedding model is configured. The query is embedded, DuckDB returns nearest - neighbors by cosine distance, distances are converted to similarity and filtered by `similarityThreshold`, - and the same row-class exclusions as the keyword path (persona, working, archived, conflicted, superseded) - are applied to the matches. A readiness certificate binds agent, provider/model, dimension, config - generation, and logical-store - generation. A missing certificate makes that turn **FTS-only** and schedules non-destructive warmup; ordinary - recall never performs a stale-existence scan. SQLite authoritative revalidation additionally requires vector - rows to remain `embedded` with the current fingerprint/dimension, so edited pending rows cannot surface via - old sidecar vectors. Rows are never deleted merely because readiness is absent. - Query embeddings are tracked per `agentId::embeddingFingerprint` group and then by full normalized query: - identical concurrent queries share one provider call, two distinct fresh queries may run concurrently, and - a third distinct query degrades that turn to FTS-only. The 800 ms soft timeout and 30 s stale replacement - window still apply per caller. Vector matches rejected by the SQLite liveness filter are queued for - fire-and-forget deletion from the current vector store so dead vectors stop occupying candidate slots. - -**RRF fusion (`fuse`).** Each path contributes `1/(rrfK + rank + 1)` per item, accumulated when a memory -appears in both lists. The final order is: - -```text -combined = retrievalScore + RRF -``` - -`retrievalScore` (which carries the real vector similarity, vs. an FTS baseline of `0.3` for keyword-only -hits) is the dominant term, so a strong vector hit is never overtaken by a weak keyword-only hit. RRF is only -an additive boost that lifts dual-path evidence above equally-scored single-path hits. Sorting is by -`combined`, tie-broken by `retrievalScore`. - -**`retrievalScore` (recall ranking).** - -```text -recency = 0.5 ^ (age / halfLifeForKind) # semantic 14d · episodic 30d · reflection 60d -base = w.sim·similarity + w.rec·recency + w.imp·importance -confidenceFactor = max(0, 1 + 0.5·(confidence − 0.7)) # default confidence 0.7 -floor = 0.15 · importance -retrievalScore = max(base · confidenceFactor, floor) # important-but-decayed memories keep a floor -``` - -Defaults: `topK=6`, `rrfK=60`, `similarityThreshold=0.2`, weights `{similarity 0.6, recency 0.25, -importance 0.15}`. Malformed config clamps to defaults (`topK ≤ 100`, `rrfK ≤ 1000`). - -**`decayScore` (forgetting, separate from recall).** - -```text -anchor = last_accessed ?? created_at -decayScore = 0.5 ^ ( (now − anchor) / (30d · (1 + clamp01(importance))) ) -``` - -Important memories stretch their half-life, so they survive longer before becoming archive-eligible. Recall -and forgetting are deliberately two different scores: a memory can rank low for recall yet still be retained. -`category` is not a second scoring axis: it is ignored by retrieval, decay, RRF, and rerank logic. Its only -ranking/retention effect is indirect, through the deterministic importance floor applied at write time. -Archive eligibility uses the same half-life formula but converts the threshold into an age boundary inside -the bounded archive query. Ordinary maintenance therefore does not rewrite materialized decay scores across -the full Agent corpus. - ---- - -## 9. Consolidation, forgetting, and offline scheduling - -Memory schedules its **own** maintenance — it does not rely on the repo-wide scheduled-tasks service. - -```mermaid -flowchart TD - TM["event-driven arms:
60s after start one-shot batch
5min idle debounce after each write
config-change arm on enable / model-available"] --> CP["runConsolidationPass(agent)"] - CP --> CD{"6h LLM cooldown
seeded from agent_memory_audit"} - CD -- within --> CHEAP["cheap bounded maintenance:
archive ≤256 + repair + audit prune ≤500"] - CD -- past --> LLMJOB["global semaphore ≤2
challenge → merge → reflection → persona
shared 8-call / 24k-token budget"] - LLMJOB --> POST["bounded archive + working flush"] - POST --> AUD["agent_memory_audit
(provenance-only) + status=completed"] - AUD --> EV["memory.updated → UI refresh
(post-audit emit gated on touched;
archiveStale emits its own when archived>0)"] -``` - -**Event-driven arms, one pass.** A per-agent **5-minute idle debounce** is armed on every mutating write. -After app start, a one-shot 60-second startup timer lists agents with active memories, filters them through -`shouldArmMaintenance` (safe id · managed agent · memory enabled), sorts them, and schedules deterministic -5-second-staggered idle timers. Maintenance-relevant DeepChat config writes also arm eligible agents: -`memoryEnabled`, `memoryExtractionModel`, `assistantModel`, and `defaultModelPreset` trigger the arm because -they affect enablement or consolidation model resolution. Custom-agent changes arm that agent, while builtin -changes fan out to active inheritors with the same deterministic stagger. Renderer update saves send diff-only -config patches, so unchanged model keys do not reach this field-presence gate. Every arm funnels into -`runConsolidationPass`, which then enforces the cooldown. - -Execution invalidation is intentionally separate from that maintenance fan-out. A custom config update -synchronizes the Agent's resolved `memoryEnabled + memoryEmbedding` identity. A builtin update synchronizes the -builtin first and skips execution fan-out when that identity is unchanged. A real builtin transition bulk -resolves all managed DeepChat Agents—including disabled Agents—in one repository read, unions them with observed -execution states, and advances only Agents whose resolved identity actually changed; explicit child overrides -therefore remain valid. Enumeration and per-Agent synchronization failures are isolated, and maintenance arming -still runs. The builtin baseline and every first Agent observation seed identity without advancing the epoch. -Extraction-model, assistant-model, default-preset, and persona-policy changes may arm future maintenance but do -not invalidate already-admitted execution. - -**Restart-durable cooldown.** The LLM-backed work runs at most once per **6 hours** per agent. The watermark -is seeded from the audit table (`getLatestCompletedEventAt('memory/maintenance_llm')`) when the in-memory -value is absent, so the cooldown survives restarts. Within the cooldown only cheap local upkeep runs (no LLM, -no audit row). A missing-model pass is recorded `skipped` and does **not** advance the cooldown (it retries -next trigger). - -**Heavy maintenance budget.** At most two Agents execute heavy maintenance concurrently through a fair -process-wide semaphore; the complete same-Agent pass remains singleflight. Each admitted pass runs -challenge → merge → reflection → persona under one shared budget of **8 calls / 24k estimated input tokens**, -with non-borrowable per-step call quotas **4 / 2 / 1 / 1**. Reservation happens before gateway admission, so -provider failures cannot create an unbounded retry loop. Cheap repair/archive/audit cleanup does not occupy a -heavy slot. - -**`mergeNearDuplicates` (budgeted).** Scans only active `embedded` non-persona/non-working rows that match -the current embedding fingerprint/dimension. It uses the row's stored DuckDB vector via -`queryByMemoryId()` rather than re-embedding row content through the provider; the store method reads the -source vector by id and reuses the existing parameterized vector query path, then filters the source id out -of the results. The pass advances an in-memory `{ createdAt, id }` compound cursor so large same-timestamp -windows are not skipped. Each pass is bounded by **64 stored-vector neighbor scans** and its two-call share -of the pass budget (remainder deferred). For each -row it picks the first current, live neighbor with similarity ≥ **0.85** and runs the same decision prompt; -only `UPDATE`/`SUPERSEDE` may fold the pair. The pass snapshots both revisions before the LLM await and applies -the survivor content/embedding reset plus the retired row's supersede transition in one transaction guarded -by both revisions and liveness. A stale participant rolls the whole merge back without an LLM retry. If the -merged provenance owner is the secondary participant, the fold may converge into that row; an unrelated -third owner causes a safe skip plus consolidation stamp instead of a stale three-row fold. Importance and -confidence only ever rise. - -**Non-destructive archival (`archiveStale`).** A row is soft-deleted only when **all** of: `decayScore < -0.05`, age `> 90 days`, still active, and not an unresolved conflict participant. Access count remains a -diagnostic but is not an eligibility veto. One set-based pass archives at most **256** rows; decay eligibility -is expressed from the access/creation age rather than requiring an Agent-wide score refresh. Anchors, persona, -and working rows are exempt. A challenged target remains lifecycle-active and recallable; -`conflict_state` protects it from archive and generic single-row mutation until aggregate resolution/repair. -`restoreMemory` reverses it for normal memories. There is no hard delete on this path. Maintenance also runs -a cheap repair that normalizes any legacy persona/working row back to `status='fts_only'` while preserving -embedding refs until a bounded dead-vector sweep deletes the corresponding DuckDB vectors from a successfully -opened current sidecar. The sweep runs only when the current vector store is already warm/ready, filters refs -by both current embedding fingerprint and current ready dimension before applying its batch limit, re-checks -row lifecycle plus dim/model before vector delete, and clears SQLite refs only if the row is still prunable -with the same dim/model after deletion. Old-fingerprint or old-dimension refs remain as traceable metadata -residue; maintenance does not cold-open those sidecars or let them occupy the current cleanup batch. -Only rows actually inspected by bounded maintenance are stamped. Reflection/persona consume one aggregate -plus indexed top-N query instead of loading the full Agent corpus. Working-memory mutations use a 100 ms -trailing debounce; injection synchronously flushes a dirty blob between the initial and final read-epoch -gates. Status counts remain a single aggregate query. - ---- - -## 10. Cognitive layers - -The store is layered rather than a flat bag of facts. - -- **Units** — `episodic` and `semantic` atomic memories from extraction. -- **Reflection** — `maybeReflect` distills high-level insights (`kind='reflection'`) from recent atomic - memories. It is the load-bearing change that removes silent persona drift: reflection now runs **only from - the offline pass**, gated on accumulated importance (≥ `5.0`) since the last reflection, with ≥ 3 source - memories and up to 20 inputs. Reflections decay slowest (60-day half-life). -- **Working memory (L1)** — a single `kind='working'` blob per agent (≤ **400 tokens**), refreshed off the - hot path, read at session open without an access bump, and excluded from recall / FTS / embedding / - consolidation. It is injected as its own section ahead of recalled memories. -- **Context Assembler** — see §6; priority `persona > working > units > episodic` under a hard CJK-aware - budget, emitting a selected/dropped manifest. - -`Episodic` session-summary and `Crystal` layers are intentionally deferred (placeholders only). - ---- - -## 11. Guarded persona evolution - -Automatic self-model evolution is an **opt-in, default-off, approvable** experiment — it must never drift -silently or inject unapproved text. - -- **Two gates.** Requires both `memoryEnabled` and `personaEvolutionEnabled` (independent, default off). When - off, reflection still runs but no persona draft is produced. -- **Draft → approval.** `maybeEvolvePersona` (offline only, under a per-agent persona lock) produces at most - one outstanding `persona_state='draft'`, gated on ≥ 3 memories and accumulated importance ≥ `5.0`. A draft - is **never injected**. The user approves / rejects / rolls back / anchors via IPC routes. -- **Small-step constraint.** A draft whose normalized Levenshtein change ratio vs. the active self-model - exceeds `0.6` is flagged `needsReview` and kept out of any auto-approval path. -- **Real `is_anchor` guard.** Once a version is anchored, `rollbackPersona` refuses to overwrite it; rollback - only re-activates historical (superseded) versions. This settles the previously-inert `is_anchor` field. - ---- - -## 12. Execution-memory query - -`agent_memory` is a synthesis cache; the **raw tape is the evidence source of truth**. The execution log is -made directly queryable to the agent via a searchable projection plus two tools. - -```mermaid -flowchart TD - AG["agent calls tape_search(query)"] --> TS["DeepChatTapeService.search"] - TS --> CURP{"projection isCurrent?
(version 2 + maxEntryId)"} - CURP -- no --> RB["ensureSearchProjection rebuild
(append tail · or full replaceSession)"] - CURP -- yes --> PJ["deepchat_tape_search_projection"] - RB --> PJ - PJ --> BM["FTS5 / BM25
(LIKE fallback / supplement)"] - BM --> OV["overview only:
{entryId, kind, name, createdAt, summary, refs, score}
— no raw payload"] - OV --> TC["agent calls tape_context(entryIds)"] - TC --> EXP["getContext bounded expansion:
before/after window + per-entry / total byte budget"] -``` - -- **Projection.** `deepchat_tape_search_projection` denormalizes the effective tape into searchable rows: - `search_text`, a per-entry `summary` (≤ 1200 chars), and structured `refs` (filePaths, commands, - errorCodes, exitCode, ids). It carries its own content version (`PROJECTION_VERSION = 2`) and a - per-session watermark; `isCurrent` short-circuits when the version and `maxEntryId` match, otherwise it - rebuilds — incrementally (append the new tail) when prior entries are an exact prefix, else a full replace. - An independent FTS watermark lets the FTS index lag and self-heal lazily at query time. -- **`tape_search`** returns an **overview with no payload** (forcing `tape_context` for actual content), - BM25-ranked with a `LIKE` fallback/supplement. -- **`tape_context`** expands specific entry ids with a bounded window (before/after default 2, clamp ≤ 20), - an entry cap (default 50), and byte budgets (per-entry default 2048 / max 8192; total default 16384 / max - 65536), UTF-8-safe truncated. The recall pair is DeepChat-agent-only and is advertised atomically only when - both `searchTape` and `getTapeContext` runtime ports are present for a persisted DeepChat session. These - ports are wired directly to `SessionProjectionCoordinator`, not the memory kernel. -- Memory extraction and injection do not invoke either model tool. They consume the effective Tape and exact - lineage through internal runtime contracts, so model-facing Tape exposure does not gate Memory behavior. -- The whole projection/search layer is fail-open: any error degrades to a coarser search over the effective - tape rather than throwing (the one exception is an unparseable time boundary, which is reported). - ---- - -## 13. Lifecycle and correctness - -This is where the "stabilization" and "kernel hardening" work concentrates. - -- **Scoped vector leases.** Query/query-by-id/upsert/delete/reconcile run only inside manager-owned callbacks - carrying a per-agent store generation. Close/reset/delete/dispose stop new admission and drain active - leases before touching the native store. A failed reset marks `requiresReset`; later leases retry reset - under the per-agent lock and fail open to FTS for that request if recovery still fails. Permanent retire and - dispose remain closed. Persona operations use a separate per-agent lock. Open stores have a soft cap of - **8** and an idle TTL of **15 minutes**; a 60-second unref sweep and post-release convergence evict only - lease-free LRU entries. All-busy stores may temporarily exceed the cap without violating lease safety. -- **Vector identity gate.** On opening an existing sidecar, `embedding_meta` (provider/model/dim) is compared - against the current request. A mismatch or a legacy sidecar with no identity → the store is marked unusable - and recall serves FTS only, with an explicit warning. A model/dimension switch closes the old instance and - recreates the file. The dimension is baked into the DuckDB column type, so a dimension change requires a - full file reset (`destroyFile` + recreate), not an in-place migration. -- **Warmup bounds.** Startup prewarms only the eight most recently active Agents. Embedding connection warmup - is deduplicated by provider/model for the process lifetime; a failure is retried only after five minutes. -- **Transactional vector upsert.** `upsert` wraps delete-then-insert in `BEGIN/COMMIT` with `ROLLBACK` on - error, so there is no "deleted-but-not-inserted" hole. -- **Archive / forget / restore lifecycle.** Agent-facing `forgetMemory` is a soft archive: it marks normal - rows `archived`, writes a content-free `memory/forget` runtime audit event even when the row was already - archived, and leaves recall correctness to status filters plus SQLite re-checks while dead-vector pruning - removes obsolete sidecar entries over time. It only requires a managed agent, so users can forget while - memory is disabled. `restoreMemory` re-marks a normal archived row `pending_embedding` and re-embeds it, - and remains gated by `canWriteAgentMemory` (managed agent · memory enabled · not disposed). Generic - archive/forget/restore refuse `persona` and `working` rows; persona lifecycle is controlled by - persona-specific routes and working rows are kernel-owned. Permanent UI delete (`deleteMemory`) hard-deletes - the row and best-effort deletes its vector; if the sidecar is not already open, the manager may open the - current embedding store inside the per-agent vector lock using the known warm/current dimension. -- **Conflict aggregate integrity.** Generic edit/archive/forget/delete/restore reject both challengers and - targets referenced by unresolved challengers. `CHALLENGE` is a single SQLite transaction, and an idempotent - startup/maintenance repair restores missing target state, archives invalid challengers, clears orphan - challenged state, and removes stray `conflict_with` links. The participant lookup uses the - `(agent_id, conflict_with, status, superseded_by)` index. -- **Read epoch vs. execution epoch.** Every semantic SQLite commit advances the Agent read epoch; - operational access/decay/embedding/audit writes do not. One per-Agent execution epoch covers both - destructive invalidation and resolved `memoryEnabled + memoryEmbedding` changes. Clear advances it before - row deletion even for an empty store; Agent deletion does so before its first await. Configuration - synchronization occurs at Presenter admission and config notifications, updating runtime and vector identity - together; fence capture itself is an O(1) epoch read. Observation seeds without advancing, then each effective - transition—including A-to-B-to-A—advances once. All such paths abort the Agent's provider work, so late - completions cannot recreate cleared data or cross a configuration boundary. Agent cleanup removes the observed - configuration snapshot but retains the advanced epoch, preventing a previously captured fence from becoming - valid after state recreation. Execution identity encodes provider/model as a collision-free tuple; the - colon-delimited persisted `embedding_model` fingerprint remains unchanged for compatibility. -- **Provider control-flow classification.** Gateway cancellation, deadline, and capacity rejection retain their - existing `AbortError` surface but carry distinct internal codes. Retrieval suppresses only an explicitly tagged - cancellation after its execution fence becomes stale; deadlines, capacity rejection, and unrelated errors keep - their normal propagation or degradation behavior. -- **Embedding-drain config guard.** A background embedding drain captures the embedding identity it started - with; before writing vectors, and before a reindex reset, it re-checks the agent's current `memoryEmbedding` - fingerprint and discards the batch if the config changed mid-flight, so a stale drain can never write - vectors from a superseded model into a freshly reset sidecar. -- **Provenance and revival.** New keys are `v2::` over agent/kind and NFC-normalized content - while preserving case. Lookup is v2 then legacy v1; a v1 hit must pass normalized content equality before - a transactional lazy re-key, otherwise it is treated as a hash collision. Working memory additionally uses - its historical fixed-seed v1 resolver and removes a redundant legacy internal row when v2 already exists, - without bumping `decision_revision`. Provenance hits are split into classification and execution. Pure - scheduler-archived rows may be restored; rows with a user archive or runtime forget audit are suppressed - with `suppressed-user-forget`; archived rows that are also superseded are treated as conflict losers and - suppressed with `suppressed-conflict-loser`; active duplicates stay no-op. Non-archived superseded hits are - conservative without a model path and otherwise go through the decision ring. Only a decision-backed - `SUPERSEDE` collision can revive that old row and retire its former head. The A→B→A manual-edit path treats a - revival-returned retired head equal to the edited row as already atomically retired and does not issue a stale - second supersede. `updateUserContentAndInvalidateEmbedding` keeps a row's `provenance_key` aligned with its new - content; if the new key is owned by a suppressed row, the - update keeps the original row unchanged rather than reviving the suppressed owner. -- **Vector cleanup and generation writeback.** Vector deletion has three layers: direct delete opens the - current sidecar when possible; inline prune treats missing SQLite rows as prunable while retaining - lifecycle/model/dimension guards; and warmup runs a one-shot keyset reconciliation. Embedding-ready status, - warm readiness, and reconcile watermarks are accepted only for the same lease generation, so late native - completions become no-ops. -- **Close-safe teardown (`dispose`).** A `disposed` flag is set first so any already-fired timer's pass - becomes a no-op; `canWriteAgentMemory` / `canReadAgentMemory` both include `!disposed` and are re-checked - after every `await`. Dispose globally invalidates/aborts provider work, stops task and lease admission, and - gives all provider/tasks/leases one absolute five-second drain deadline. Store drains run per agent in - parallel, so one stuck native operation does not prevent other idle stores from closing; after the deadline - only stores with an active native operation may remain open. Fire-and-forget extraction is fenced before - every side effect and cannot outlive disposal semantically. -- **Agent-deletion cleanup.** Deleting a DeepChat agent atomically clears `agent_memory` + - `agent_memory_audit` in one SQLite transaction, then best-effort destroys that agent's DuckDB sidecar file. -- **Disk reclaim.** A per-row hard delete does not shrink the DuckDB file; only a whole-store reset - (`clearMemories` / agent delete) calls `destroyFile` to actually reclaim (there is no `VACUUM`), so the file - grows between resets. - ---- - -## 14. Contracts - -### 15.1 Agent-facing tools - -| Server | Tool | Behavior | -| --- | --- | --- | -| `agent-memory` | `memory_remember` | Persist a durable fact/event with optional category; routes through the decision ring (`coordinateWrite`). | -| `agent-memory` | `memory_recall` | Recall relevant memories for a query (ranking/limit are kernel-side). | -| `agent-memory` | `memory_forget` | **Archive** (soft delete) a memory by id so it is no longer recalled. | -| `agent-tape` | `tape_search` | Overview-only search of the tape projection (no payload); exposed only as part of the recall pair. | -| `agent-tape` | `tape_context` | Bounded evidence expansion of specific entry ids; exposed only as part of the recall pair. | - -`tape_info` and `tape_anchors` remain lower-level diagnostic APIs. `tape_handoff` remains a runtime-only -transition API and requires a non-empty durable summary. All three names stay reserved but are absent from -model definitions, prompts, configuration UI, and Agent tool dispatch. - -Tool results are success-enveloped even for "soft" outcomes (memory disabled, `noop`, not-found) — callers -inspect `result.ok`, not `isError`. Hard infra failures throw. - -### 15.2 IPC routes (`memory.*`) - -`memory.page`, deprecated `memory.list`, `memory.getStatus`, `memory.search`, `memory.add`, `memory.delete`, `memory.clear`, -`memory.restore`, `memory.getSourceSpan`, `memory.listConflicts`, `memory.resolveConflict`, -`memory.listPersonaVersions`, `memory.rollbackPersona`, `memory.listPersonaDrafts`, -`memory.approvePersonaDraft`, `memory.rejectPersonaDraft`, `memory.setPersonaAnchor`, -`memory.listAuditEvents`, `memory.listViewManifests`, `memory.reindex`. - -- `memory.search` is read-only: it uses a search-only depth override (default 50, route max 100) so the - Memory Manager can return more than the agent's recall `topK`, excludes persona/working rows at the SQL - search layer before applying result limits, and it does not bump `access_count`. -- `memory.page` is the management list contract. It uses `(created_at DESC, id DESC)` keyset pagination, - defaults/caps at 100 rows, and returns an opaque base64url v1 cursor only when another page exists. Invalid - cursors are route errors, never implicit first-page requests. After cursor validation, non-DeepChat Agents - receive an empty page without reaching the memory presenter. `memory.list` remains wire-compatible for one - deprecation window and has no production renderer caller. -- `memory.add` accepts optional `category`, runs the decision ring, and writes a `memory/add` user audit row. -- `memory.reindex` is a fire-and-forget per-agent rebuild entry for managed, memory-enabled DeepChat agents. - It returns `{ started }`, where `started=false` means the guard rejected the request or a reindex was already - in flight. -- `memory.getSourceSpan` resolves a memory's `source_entry_ids` to readable role/content via the effective - tape view (powers the lineage UI). -- `MemoryItemSchema` carries `category`, `sourceEntryIds`, `conflictWith`, `personaState`, `isAnchor`, - `needsReview`; the status enum includes `conflicted`/`archived`/`fts_only`. - -### 15.3 Events - -`memory.updated` is the only event. Its `reason` is one of `extract`, `delete`, `clear`, `persona-evolve`, -`persona-draft`, `persona-approve`, `persona-reject`, `persona-rollback`, `reindex`. The payload carries -`{agentId, reason, version}` and **never** any memory content. - -### 15.4 Audit ledger (`agent_memory_audit`) - -Background maintenance and writes record `memory/maintenance_llm`, `memory/reflect`, `persona/evolve`, -`memory/challenge_resolved`, `memory/add`, and runtime `memory/forget` — with `actorType` -(`scheduler`, `user`, or `runtime`), an optional `session_id`, a terminal -`status` (`completed`/`skipped`/`failed`), and `inputRefs`/`outputRefs` that contain only ids, action -strings, counts, ratios, and booleans. No raw memory text or persona content is ever stored. -Operational event types (`memory/maintenance_llm`, `memory/reflect`, `memory/repair`, -`memory/conflict_repair`, `memory/extract`) are retained to the newest 10,000 rows per Agent and pruned at -most 500 per cheap pass. User lifecycle events, every `persona/*` event, unknown types, and malformed/legacy -causal rows are permanent and never selected by this cleanup. - -Manual/user-authored content is capped at **12,000 characters** and automatic/model-merged memory at -**2,000 characters**, at both route/tool and domain boundaries. Existing oversized rows remain readable and -recallable; the limit applies only when submitting new content. - ---- - -## 15. Settings surface - -Memory is a first-class, top-level settings section, configured strictly per-agent. - -- **IA.** A top-level `settings-memory` page (`/memory`, group `knowledge`) with an in-page agent picker - (default builtin `deepchat`) and bidirectional `?agentId=` URL sync. Two tabs: **Config** and **Manage**. -- **Config tab** exposes config the kernel already supported but the UI didn't: `memoryEmbedding`, - `memoryExtractionModel`, `memoryInjectionTokenBudget`, `memoryRetrieval` (`topK`/`rrfK`/ - `similarityThreshold`/`weights`), and `personaEvolutionEnabled`. Its DEFAULTS and LIMITS mirror the kernel - constants exactly (topK 6 / rrfK 60 / threshold 0.2 / budget 1200; ranges topK 1–100, rrfK 1–1000, budget - 64–8000). -- **Manage tab** reuses `MemoryManagerPanel` (Memories / Persona / Activity) and is the only surface that - uses `MemoryClient`. The Memories view loads keyset pages of at most 100 rows, appends with ID dedupe, and - resets to page one on Agent/event generation changes; its category/text filters intentionally cover only - loaded rows. Memory rows show a category badge and the Memories list has a local category filter; - `NULL` / missing categories are displayed and filtered as `uncategorized`. The manual add form exposes - `kind` and importance but not category. -- **Inheritance.** Per-agent config inherits the builtin `deepchat` root then applies its own overrides - (`override ?? base ?? default`). Clearing an override writes an **explicit `null`** (so an inherited value - is never ossified onto a child agent); untouched booleans are omitted from the patch. The agent editor - keeps only an "enable memory" toggle + a deep-link to this page. Builtin execution-identity changes enumerate - all managed DeepChat Agents without post-update enabled filtering and invalidate only inheritors whose - effective enabled/embedding identity changed. - ---- - -## 16. Schema and migrations - -A single global schema version is shared across all SQLite tables (the migration runner takes the max of -every table's latest version). The current global maximum is **42**. - -| Table | Change | Migration | -| --- | --- | --- | -| `agent_memory` | v32 backfills `embedding_model` + `source_entry_ids`; v33 adds `confidence` + `last_consolidated_at` + `conflict_state`; v34 adds `persona_state`; v35 adds `conflict_with`; v37 adds nullable `category`; v41 adds `decision_revision`; v42 adds CHECK-constrained `lifecycle_state`/`embedding_state`, uses marker-driven tolerant combined/partial backfill, reconciles the shadow, retires obsolete status indexes, installs compatibility triggers in the version transaction, and promotes clean FTS policy metadata without rebuild. Catalog repair receives the newly added column set and performs targeted backfill without overwriting a valid sibling canonical column. **Columns remain additive.** | Yes (`getMigrationSQL`) | -| `agent_memory_fts` | FTS5 v3 external-content virtual table containing recallable rows only; deterministic Agent scope token and savepoint-isolated explicit mirror maintenance; tokenizer probed at runtime | No — built idempotently | -| `agent_memory_audit` | New table at v36: maintenance/user provenance ledger (`scheduler`/`user` actors + optional `session_id`), ids/metadata only | Yes (whole table) | -| `deepchat_tape_search_projection` (+ meta + FTS meta) | Searchable projection of the effective tape + FTS5 (content `PROJECTION_VERSION=2`) | No — version-exempt, rebuilt idempotently from raw tape | -| `deepchat_memory_ingestion_projection` (+ meta) | Effective final-message range projection for extraction (`PROJECTION_VERSION=1`) | No — version-exempt, rebuilt idempotently from raw tape | -| `deepchat_sessions` | `memory_cursor_order_seq` now written monotonically (`MAX(...)`) | — | -| DuckDB (per agent) | `memory_vector` (HNSW/cosine, `M=16`, `ef_construction=200`) + `embedding_meta` (identity; mismatch → fail-closed to FTS) | No — built at runtime | - -Migration statement errors for additive idempotent ops (duplicate `ADD COLUMN`, index already exists) are -selectively ignored, so the v32 backfill is safe against DBs that already have the columns. - ---- - -## 17. End-to-end flow - -```text -enable memory (top-level Memory page / agent toggle) -→ MemoryPromptContributor.contribute (unified across normal / compaction-recovery / resume) -→ buildInjection: active persona (drafts excluded) + working L1 (no bump) + recall -→ recall = FTS5/BM25 ∪ DuckDB vector → RRF fusion (retrievalScore dominant; FTS-only + bg reindex on dim change) -→ Context Assembler: sanitize + CJK-aware hard token budget (persona > working > units > episodic) -→ appended; persist memory/view_assembled manifest anchor -→ model replies -→ initial/resume terminal outcome → MemoryIngestionObserver.afterTurnSettled -→ initial/context-pressure compaction apply normally returns with intent (`succeeded` true or false; - thrown apply / no intent excluded) → MemoryIngestionObserver.afterCompactionApplyReturned -→ session close → MemoryRuntimeCoordinator.beginSessionDestroy / finishSessionDestroy fencing -→ app shutdown → MemoryIngestionObserver.drainAndFence (stop admission + bounded drain) -→ admitted extraction stays per-session serial and epoch-guarded -→ cursor + validated/rebuilt ingestion projection range + message-aligned CJK-aware chunks + exact lineage -→ fallback admission (visible text + tool/backstop/substantive text) or compaction -→ same complete chunk through triage → extraction → stable dedupe/2k cap → batched neighbor recall/decision - (≤4 candidates/call, ≤3 neighbors, ≤2 initial calls, revision CAS + one ≤4-candidate retry) -→ SQLite pending_embedding → cursor advances MAX + memory/extract anchor -→ background single per-agent embedding drain (50 rows · bulk DuckDB/SQLite) → DuckDB -→ [offline] self-scheduled sleep-time pass (6h cooldown, global concurrency 2): challenge → merge - → reflection → persona under shared 8-call/24k budget + bounded archive/repair/audit/working upkeep -→ agent_memory_audit (provenance-only) + memory.updated event -``` - ---- - -## 18. Testing - -Memory quality uses one versioned scope manifest for portable, Native, eval, and performance suites. -`test:memory:eval` uses a test-local `deterministic-lexicon-v1` embedder with real SQLite FTS and production -fusion; it never reads credentials or calls a network provider. Runtime diagnostics are bounded to 64 Agents, -256 samples per distribution, and a 24-hour Agent TTL. Process-wide queue/vector/provider gauges are retained -outside Agent eviction and surfaced through the required `MemoryHealthDto.runtime` field. The complete metric -ownership, units, cleanup, and privacy classification are documented in the -[Memory Runtime Metric Dictionary](../memory-quality-gates-and-observability/metrics.md). - -Coverage mirrors source under `test/main/**` (and `test/renderer/**` for UI), pinning each invariant: - -- Injection sanitization; per-session serial extraction lock; monotonic cursor; insert error - classification. -- Message-aligned ASCII/CJK/emoji chunking, oversized-message replay, exact lineage, partial-batch - finalization, read-epoch fail-closed behavior, and clear/delete destructive generations. -- Vector upsert transaction + identity guard (fail-closed to FTS); reindex on dimension change; bounded - `error` retry; scoped lease/reset recovery; generation-checked ready/reconcile writeback. -- Decision ring (five branches + bounded CAS retry); provenance v2/v1 compatibility; stale maintenance merge - rollback; conflict aggregate guard/repair; category propagation and reflection/persona/working guards. -- Dual-score forgetting / four-condition archival; offline consolidation (cooldown / budget / - restart-durable / idle debounce); reflection recall; working blob; guarded persona (default-off / draft / - anchor / eval gate). -- Lineage DTO + source span; tape projection FTS/BM25 + `tape_context`; atomic agent-deletion cleanup. -- Settings surface (override clear / inheritance / clamp; category badge/filter); retrieval eval (hit@3 / MRR / - nDCG). -- The independent `test:main:memory-perf` suite covers 1k/10k/50k recall, 10k/100k Tape, 100 shared-model - Agents, a 101-row embedding drain in 50-row chunks, and eight decision candidates. CI hard-gates statement/materialization/ - provider/resource caps and same-process relative medians; median/p95 absolute wall-clock values are reports. - -The independent `memory-native-validation` CI job installs the Node 24 ABI artifact through the native dependency's -own install lifecycle, runs an open/read/write/reopen/close smoke, and sets -`DEEPCHAT_REQUIRE_NATIVE_SQLITE=1`. Missing bindings, FTS initialization, fresh schema, v37/v38/v40→v41 -migration, reopen, migration validation, or the memory performance suite fail rather than skip. The focused Native suite runs only in this -disposable CI dependency tree; local development keeps the Electron ABI binding installed. - ---- - -## 19. Known limitations and risks - -- **Triage SKIP is permanent per successful chunk.** A wrongly-SKIPped durable chunk is consumed and not - re-extracted; mitigated by conservative fail-open triage (KEEP unless an explicit SKIP). Unprocessed later - chunks and incomplete oversized-message fragments remain behind the cursor. -- **Category prose can drift.** The category enum/floors have one shared source of truth, but the automatic - extraction prompt and the `memory-management` skill intentionally carry separate prose for different - audiences. -- **Manual add category is hidden.** `memory.add` supports category, but the Manage-tab manual add form only - exposes `kind` and importance; user-added rows default to uncategorized unless another caller supplies - category. -- **Memory-management skill is opt-in.** The bundled skill is discoverable and has no `allowedTools`, but it - is not auto-pinned into every conversation to avoid permanent prompt cost. -- **DuckDB disk reclaim.** Per-memory hard delete and dead-vector pruning remove vector rows but do not shrink - the DuckDB file; only a whole-store reset reclaims disk space (no `VACUUM`), so the file can still grow - between resets. Archived/superseded vectors are correctness-safe because recall re-checks SQLite, but they - are not quality-neutral: if left in the HNSW result window they crowd out live candidates. Direct delete, - inline prune, and one-shot warm-store orphan reconciliation remove current-sidecar dead vectors over time. - Management search disables inline prune so read-only search does not write DuckDB. -- **FTS5 native dependency.** Ordinary non-native test runs may still use their configured fallback, but the - dedicated native CI gate requires a working binding and treats FTS/migration skips as failures. -- **Vector query threshold.** `MemoryVectorStore.query` does not apply a distance cutoff itself; the - `similarityThreshold` is enforced presenter-side after distance→similarity conversion. - ---- - -## 20. Appendix — tunable constants - -| Constant | Value | Gates | -| --- | --- | --- | -| `DEFAULT_RETRIEVAL.topK` | 6 | recalled candidates returned | -| `DEFAULT_RRF_K` | 60 | RRF rank constant | -| `DEFAULT_SIMILARITY_THRESHOLD` | 0.2 | vector recall cutoff | -| retrieval weights | `{sim 0.6, rec 0.25, imp 0.15}` | `retrievalScore` base | -| `DEFAULT_INJECTION_TOKEN_BUDGET` | 1200 (max-clamp 8000; <64 or invalid → 1200) | Context Assembler hard ceiling | -| CJK token density | 1.5 (others `1/4`) | `estimateTokens` | -| recall half-lives | semantic 14d · episodic 30d · reflection 60d | `recencyScore` | -| `DEFAULT_CONFIDENCE` / boost | 0.7 / 0.5 | recall confidence factor | -| `IMPORTANCE_FLOOR_COEF` | 0.15 | recall floor | -| `FTS_SIMILARITY_BASELINE` | 0.3 | keyword-only hit similarity | -| `FORGET_HALF_LIFE_MS` | 30d (× `1 + importance`) | `decayScore` | -| `CATEGORY_IMPORTANCE_FLOOR` | user_preference 0.5 · project_fact 0.6 · task_outcome 0.55 · heuristic 0.5 · anti_pattern 0.6 | write-time category floor | -| archive thresholds / batch | decay < 0.05 · age > 90d / 256 | `archiveStale` | -| decision batches | 4 candidates · 3 neighbors · 12k tokens · 2 initial calls · ≤4 retries in one call | extraction decision work | -| `MAX_CANDIDATES` | 8 | extraction candidates per chunk | -| extraction chunk | ~4000 estimated tokens / 12000 Unicode code points / 4 chunks per queue task | message-aligned input shared by triage and extraction | -| `MEMORY_FALLBACK_MIN_DELTA` | 6 | min orderSeq delta before fallback extraction | -| `MEMORY_MIN_AGENTIC_TEXT_CHARS` | 160 | short non-tool fallback text threshold | -| `CONSOLIDATION_IDLE_MS` | 5min | idle debounce after a write | -| `MAINTENANCE_START_DELAY_MS` / `STARTUP_ARM_STAGGER_MS` | 60s / 5s | one-shot startup batch arm for active enabled agents | -| `CONSOLIDATION_COOLDOWN_MS` | 6h | LLM-backed pass cooldown (restart-durable) | -| maintenance calls / tokens / concurrency | 8 / 24000 / 2 Agents | shared pass budget; step quotas challenge 4 · merge 2 · reflection 1 · persona 1 | -| `CONSOLIDATION_MERGE_SIMILARITY` | 0.85 | near-duplicate merge threshold | -| `CONSOLIDATION_MAX_NEIGHBOR_SCANS` | 64 | stored-vector neighbor scans per consolidation pass | -| `VECTOR_PRUNE_BATCH_LIMIT` | 256 | prunable archived/superseded/internal-kind vector refs per maintenance pass | -| `ERROR_RETRY_COOLDOWN_MS` / batch | 10min / 50 | bounded automatic retry for rows stuck in embedding `error` | -| `ORPHAN_RECONCILE_BATCH` | 512 | keyset page size for warm vector orphan reconciliation | -| vector stores / idle TTL / sweep | soft cap 8 / 15min / 60s | lease-safe LRU convergence | -| startup prewarm / embedding warm retry | 8 Agents / 5min | provider:model success is process-lifetime deduplicated | -| management page / audit prune | 100 rows / keep 10000, delete 500 | bounded management and operational history | -| memory content | manual 12000 chars / automatic 2000 chars | submission-time limits; existing rows unchanged | -| `RECALL_QUERY_EMBEDDING_TIMEOUT_MS` / stale / max concurrent | 800ms / 30s / 2 | foreground query-embedding soft timeout and per-agent+model cap | -| provider deadlines | query 800ms · dimension 15s · embedding 30s · text 60s | RateLimit admission included in the absolute deadline | -| unsettled provider cap | 2 per agent/provider/model/purpose · 64 global | released only when the underlying promise settles | -| `MEMORY_SEARCH_DEFAULT_LIMIT` | 50 | default management search depth | -| `WORKING_BLOB_TOKEN_LIMIT` | 400 | working-memory blob size | -| persona thresholds | ≥ 3 memories · importance ≥ 5.0 · changeRatio > 0.6 → needsReview | guarded persona evolution | -| reflection thresholds | ≥ 3 memories · importance ≥ 5.0 · reflection importance 0.8 | offline reflection | -| HNSW | `M=16`, `ef_construction=200`, cosine | DuckDB vector index | diff --git a/docs/architecture/agent-plan-task-refactor/spec.md b/docs/architecture/agent-plan-task-refactor/spec.md deleted file mode 100644 index 8d7abb9162..0000000000 --- a/docs/architecture/agent-plan-task-refactor/spec.md +++ /dev/null @@ -1,66 +0,0 @@ -# Agent Plan / `update_plan` Float-Only Spec - -> Status: **implemented direction** — DeepChat agent plan follows Codex-like transient progress UI. - -## Problem - -DeepChat 之前同时存在两套 plan 展示路径: - -- 实时路径:`update_plan` / stream `plan` update → `chat.plan.updated` → renderer float。 -- 正文路径:runtime 或 ACP mapper 把 plan snapshot 转成 assistant `type:'plan'` block,再由旧正文 renderer 渲染。 - -这会造成同一轮生成同时出现右下角计划浮窗和正文计划表。对 agent 执行进度来说,plan 更像运行时进度而不是最终回答内容;持久化到正文还会引入 reload rehydrate、终态同步、旧计划闪回等额外状态问题。 - -## Decision - -采用 Codex-like **float-only** 行为: - -- `update_plan` 工具保留,参数结构不变。 -- `chat.plan.updated` 保留,仍是 plan UI 的唯一实时输入。 -- agent runtime 不再把 `update_plan` snapshot upsert 成 assistant `type:'plan'` block。 -- stream `plan` event 和 ACP `plan` update 也只产生实时 plan event,不新增正文 block。 -- renderer 不再从历史 assistant `type:'plan'` block rehydrate plan float。 -- renderer 按 `sessionId` 保存当前 app 运行内的 live plan snapshot;A/B/C 多个 session - 可同时拥有各自 snapshot,但当前 ChatPage 只显示当前 session 的一个 float。 -- 旧正文 plan block renderer 和 plan-block construction helper 删除;旧历史 block 可保留在数据中但默认隐藏。 -- 不新增数据库表,不做 migration。 - -## Lifecycle - -1. 新一轮 turn 开始时,renderer 调用 `agentPlanStore.beginTurn(sessionId)` 清理上一轮 live snapshot。 -2. 生成中收到任意 session 的 `chat.plan.updated` 后,renderer 更新对应 session 的 live snapshot。 -3. plan float 只在当前 session 的 turn active、存在 pending interaction 或终态 linger 时可见; - 其他 session 的 snapshot 保存在内存中,切换过去时才显示。 -4. 正常结束、停止、错误、`max_steps` 后,runtime 如仍有 `in_progress` step,会发送带 - `terminalReason` 的最终 `chat.plan.updated`,随后 float 随 turn 结束消失。 -5. session switch 只加载消息历史,不从旧 `type:'plan'` block 恢复;但不清除当前 app - 运行内已有的 session-scoped live snapshot。reload 后内存 snapshot 为空,因此不恢复旧 plan float。 - -## Scope - -- 后端:`agentRuntimePresenter`、ACP content mapping、相关 main tests。 -- 前端:`ChatPage.vue`、assistant message rendering、相关 renderer tests。 -- 兼容:保留 shared message 类型里的 `type:'plan'` / `plan_entries` 字段以读取旧数据;不保留新建或渲染正文 plan block 的 helper/component。 - -## Acceptance Criteria - -- 生成中收到 `chat.plan.updated`:浮窗显示并更新步骤。 -- A/B/C 多个 session 同时运行时,store 保留各自 live plan;当前页面只显示当前 session 的 plan。 -- 正常完成:正文没有新增 plan block;float 随 turn inactive 隐藏。 -- stop/error/max steps:若存在开放步骤,发布终态 `chat.plan.updated`,不会残留 spinning UI。 -- pending question/permission:float 保持可见,直到用户响应后 turn 继续或结束。 -- session switch:不 rehydrate 历史 plan,也不丢当前 app 运行内的 live plan。 -- reload:不恢复旧 plan float。 -- 内部 `update_plan` tool call 仍标记为 internal,不出现在正文活动列表。 -- 旧历史 plan-only assistant message 不渲染空行、空 toolbar 或正文 plan 表格。 - -## Validation - -- main tests 覆盖 runtime 不插入 plan block、terminal event、ACP plan event 不产 content block。 -- renderer tests 覆盖 ChatPage 不 rehydrate 历史 plan、session-scoped live plan 切换不丢、 - 当前页面只显示当前 session plan、assistant 正文不渲染 legacy plan block。 -- 完成后运行: - - `mise exec -- pnpm run format` - - `mise exec -- pnpm run i18n` - - `mise exec -- pnpm run lint` - - 按改动范围运行相关 Vitest。 diff --git a/docs/architecture/agent-runtime-presenter-thinning/spec.md b/docs/architecture/agent-runtime-presenter-thinning/spec.md deleted file mode 100644 index e32e856fd2..0000000000 --- a/docs/architecture/agent-runtime-presenter-thinning/spec.md +++ /dev/null @@ -1,68 +0,0 @@ -# Agent Runtime Presenter Thinning — Spec - -> Status: implemented -> Baseline: `dev@868241322`, 2026-07-14 - -## Problem - -The layered runtime migration moved session-owned mutable state into -`DeepChatAgentInstance` and split direct ACP from the DeepChat backend, but -`src/main/presenter/agentRuntimePresenter/index.ts` is still an implementation owner rather than a -thin presenter boundary: - -- 8,167 lines; -- 211 class methods; -- 29 class fields; -- prompt assembly, generation-setting normalization, permission review, tool-result adaptation, - interaction projection, compaction projection, and turn orchestration remain in one file. - -The latest reliability work increased the file by 557 net lines. The backend split was real, but it -did not finish the presenter split. - -## Goal - -Reduce `AgentRuntimePresenter` to the remaining turn/session façade and wiring by moving cohesive, -independently testable policies to their existing owners or focused modules. This goal must remove -responsibilities from the class, not move the entire class behind a new name. - -## Acceptance Criteria - -- `agentRuntimePresenter/index.ts` is at most 3,200 lines. -- The class has at most 130 methods. -- Public presenter and session-application contracts remain unchanged. -- Extracted modules do not receive the presenter instance or a generic service locator. -- Generation settings, auto-approve review, system prompt/resources, and tool-result normalization - have focused tests that do not construct the full presenter graph. -- Existing provider round, Tape, compaction, pending input, permission, tool, Memory, and terminal - ordering remains unchanged. -- A repository check prevents the presenter from silently growing past the accepted ceiling. - -## Constraints - -- No new runtime dependency. -- No IPC, event payload, database schema, renderer, or user-facing behavior change. -- Preserve cancellation and stale-instance checks at their current boundaries. -- Keep `DeepChatAgentInstance` as the owner of per-session mutable runtime state. -- Keep `ToolPresenter`, `SkillPresenter`, `McpPresenter`, Memory, message, and Tape ownership intact. - -## Non-goals - -- Rewriting the provider/tool loop. -- Moving the 8,000-line implementation unchanged into a `TurnRunner` class. -- Introducing a DI container, plugin lifecycle, base presenter, mixin, or inheritance hierarchy. -- Forcing `index.ts` below 1,000 lines in one high-risk change. The remaining turn runner can be - extracted separately once these collaborators are stable. -- Fixing unrelated behavior found during extraction. - -## Outcome - -- The initial policy-extraction checkpoint reduced `agentRuntimePresenter/index.ts` from 8,167 to - 4,905 lines and `AgentRuntimePresenter` from 211 to 135 methods. That checkpoint did not yet meet - the final 3,200-line / 130-method criteria. -- Generation policy, prompt/resource assembly, permission review, tool normalization, interaction - projection, session settings, tool resolution, deferred execution, ACP compatibility, compaction - projection, and provider permission settlement now have focused owners with explicit dependencies. -- Follow-up [runtime lifecycle ownership](../deepchat-runtime-lifecycle-owners/spec.md) moved the - initial/resume turn, provider/tool loop, and paused-interaction control flows behind three explicit - owners, reduced the presenter boundary to 2,604 lines / 122 methods, and adopted the 3,200-line - architecture guard, completing the acceptance criteria. diff --git a/docs/architecture/agent-scoped-extensions/spec.md b/docs/architecture/agent-scoped-extensions/spec.md deleted file mode 100644 index 7ef29d442d..0000000000 --- a/docs/architecture/agent-scoped-extensions/spec.md +++ /dev/null @@ -1,60 +0,0 @@ -# Agent-scoped Skills and MCP - -## User Need - -Skills and normal MCP servers are global resources, but different DeepChat agents need independent -choices about which of those resources they can use. The Plugins pages must preserve the existing -management experience while interpreting enable/disable actions as edits to the current DeepChat -agent. - -Official plugins are different: their installation, enablement, process lifecycle, contributed -resources, and status are global. ACP agents use an external runtime and cannot use the Plugins Hub. - -## Goal - -Give each DeepChat agent an independent Skills and MCP policy while keeping resource installation -global. Keep plugin availability global for DeepChat agents and unavailable for ACP agents. - -## Acceptance Criteria - -1. Each DeepChat agent can independently configure its available skill set. -2. Each DeepChat agent can independently configure its available normal MCP server set. -3. Switching DeepChat agents updates Skills and MCP enablement from that agent's policy. -4. Runtime prompt, skill tools, MCP definitions, and MCP calls use the session agent's policy. - DeepChat runtime passes resolved `enabledMcpServerIds` into tool catalog, fingerprint, and MCP - call/pre-check paths (plugin-owned MCP remains globally available when the plugin is enabled). -5. Existing global skill installation/import and MCP server definitions remain global resources. -6. Existing users retain compatible behavior: the built-in `deepchat` agent defaults to globally - available Skills and MCP servers; other DeepChat agents inherit its policy. -7. `/plugins/skills` keeps the full Skills management view and writes `enabledSkillNames` instead of - globally disabling a skill. -8. `/plugins/mcp` keeps the full MCP management view and writes `enabledMcpServerIds` instead of - globally disabling a server when used in agent scope. -9. Skill discovery, `skill_list`, and `skill_view` enforce `enabledSkillNames`. -10. Plugin-owned MCP servers and skills follow global plugin enablement and are not separately - filtered by a DeepChat agent plugin policy. - -## Constraints - -- Do not copy skill files or MCP definitions per agent; store only allow-lists in agent config. -- Keep session-level active skills bounded by the agent's available skill set. -- Keep plugin installation, trust, enablement, lifecycle, and status global. -- Keep ACP shared MCP selections separate from DeepChat agent MCP policy. -- Preserve the Presenter, typed route, renderer client, and Vue ownership boundaries. - -## Non-goals - -- Per-agent plugin process instances or plugin allow-lists. -- Changing external tool agents' skill directory formats. -- Removing the global Skills library or MCP server catalog. -- Adapting DeepChat plugins for ACP agents. -- Replacing Skills or MCP management pages with a generic policy panel. - -## Decisions - -- `DeepChatAgentConfig.enabledSkillNames` and `enabledMcpServerIds` are nullable allow-lists. -- `null` / `undefined` inherits the built-in `deepchat` policy; `[]` disables the category. -- Plugin-owned MCP servers follow the owning plugin and are excluded from normal MCP selection. -- Plugins are globally enabled for every DeepChat agent; `enabledPluginIds` is not part of agent - config. -- ACP selection replaces the Plugins Hub with an unavailable state. diff --git a/docs/architecture/agent-system-layered-runtime/README.md b/docs/architecture/agent-system-layered-runtime/README.md deleted file mode 100644 index 5d9a14a68f..0000000000 --- a/docs/architecture/agent-system-layered-runtime/README.md +++ /dev/null @@ -1,471 +0,0 @@ -# Agent System Layered Runtime — 总体设计 - -> 状态:implementation complete;`ASLR-000..092` 已完成,最终全量验证与受控 baseline -> regeneration 记录见 [migration-and-validation.md](./migration-and-validation.md#aslr-092-final-close-out-record)。 -> 迁移前基线:`dev@1a57d15b99a6`(2026-07-11) -> 本文是已实现架构的决策入口。`spec.md` 定义验收合同,`migration-and-validation.md` -> 保留兼容与验证证据,`modules/` 描述各模块的本地合同。 - -## 结论 - -迁移前的问题不是“Presenter 文件太长”这么简单,而是三种不同层次被压进了同一组类型和 -singleton。以下问题描述是迁移前的历史快照: - -1. agent catalog / session application control plane; -2. 某个具体 agent session 的运行时实例; -3. DeepChat 自己拥有的 LLM/tool loop。 - -迁移前,ACP 又被包装成 LLM provider,穿过 DeepChat loop 后再进入 ACP 自己的外部 loop。这个双层执行 -模型让统一抽象看似成立,实际只能靠 optional method、`providerId === 'acp'` 和 fallback 分支维持。 - -本次迁移目标不是把 ACP 与 DeepChat 变成两个毫无关系的孤岛,也不是创建一个万能 hook/plugin -框架。已实现目标是: - -- 顶层只共享 catalog、app session shell、transcript/event projection 与资源选择引用; -- ACP 与 DeepChat 使用两个明确的 session backend; -- DeepChat 每个 session 对应一个 `DeepChatAgentInstance`; -- DeepChat 专属 `LoopEngine` 负责固定、typed、可 await 的生命周期; -- MCP、skills、memory 等继续由原模块拥有,通过窄 adapter 参与 loop; -- 现有 Tape 继续作为 append-only semantic ledger,不另建第二套 Tape,也不把 raw token stream - 伪装成可重放事实。 - -当前实施边界:`AgentManager` 已按 strict `AgentDescriptor.kind` 路由 typed DeepChat backend 与 direct ACP -backend;fake registry、unified optional implementation、reflection legacy backend 和 agent handle -legacy/direct runtime-kind 分支均已退休。DeepChat 使用 lazy `DeepChatAgentRuntime`、per-session -`DeepChatAgentInstance`、per-turn `LoopRun` 与 fixed awaited `DeepChatLoopEngine` lifecycle;`kind=acp` 使用 -`AcpAgentRuntime`/`AcpAgentInstance` 和 external protocol loop。`kind=deepchat + providerId=acp` 仍明确走 -DeepChat loop + `AcpProvider` compatibility。 - -Memory runtime orchestration 已收敛到唯一 `MemoryRuntimeCoordinator`,通过 awaited -`MemoryPromptContributor` 与 background `MemoryIngestionObserver` 接入;Tape tool facts 已迁到 stable -per-fact `TapeRecorder` path,causal observation 只读联结现有 Tape/message/trace。core lifecycle、turn、 -assignment 与 projection 已由四个 composition-owned session application coordinators 承担; -`AgentSessionPresenter` 与 main-process `IAgentSessionPresenter` 已退休。typed routes、Tool、MCP、Floating -与 hooks 直接使用分离的 coordinator ports;history、translation、export、usage、RTK 与 catalog owner -继续独立。startup hooks 直接调用 migration/maintenance owner。`AgentRuntimePresenter` 保留 DeepChat -state/delegate 与 adapter wiring,不再构成 generic agent runtime。 -后续的 [Agent Runtime Presenter Thinning](../agent-runtime-presenter-thinning/spec.md) 又把 generation、 -prompt/resource、permission review、tool adaptation、interaction projection、session settings、ACP -compatibility 与 compaction/provider-permission coordination 移到 focused owner;presenter boundary -经 [runtime lifecycle ownership](../deepchat-runtime-lifecycle-owners/spec.md) 继续拆分后现为 2,604 行 / -122 methods,并由 3,200 行 architecture guard 约束。 -current docs、architecture guards 与 baseline generator -已在 `ASLR-091` 收敛;`ASLR-092` 已完成 canonical baseline write、全量 -main/renderer/Memory/native/build/E2E gates 与最终契约 diff。 - -## 文档地图 - -| 文档 | 单一职责 | -| --- | --- | -| [spec.md](./spec.md) | 目标、约束、决策、验收标准 | -| [migration-and-validation.md](./migration-and-validation.md) | 兼容矩阵、回滚边界、测试门禁 | -| [modules/agent-manager.md](./modules/agent-manager.md) | 顶层 control plane 与 kind router | -| [modules/shared-data-and-io.md](./modules/shared-data-and-io.md) | shared table、typed repository、transcript/output ports | -| [modules/acp-runtime.md](./modules/acp-runtime.md) | 独立 ACP process/session/protocol backend | -| [modules/deepchat-agent-instance.md](./modules/deepchat-agent-instance.md) | per-session 实例与状态所有权 | -| [modules/loop-engine-and-lifecycle.md](./modules/loop-engine-and-lifecycle.md) | DeepChat round loop、typed stages、await/pause | -| [modules/tape-and-observability.md](./modules/tape-and-observability.md) | Tape facts、manifest、trace、projection | -| [modules/prompt-context-and-compaction.md](./modules/prompt-context-and-compaction.md) | prompt 顺序、Tape view、budget、compaction | -| [modules/tools-skills-and-mcp.md](./modules/tools-skills-and-mcp.md) | tool/resource resolution 与独立 owner | -| [modules/permission-and-interactions.md](./modules/permission-and-interactions.md) | ordered tool interactions、ACP permission、pause/fresh resume | -| [modules/memory-integration.md](./modules/memory-integration.md) | Memory 的两个 loop seam 与不可回归合同 | - -## BEFORE:迁移前历史快照 - -### 迁移前规模与职责 - -| 位置 | 迁移前规模 | 迁移前职责 | -| --- | ---: | --- | -| `agentSessionPresenter/index.ts` | 4210 行 / 158 methods | route facade、session CRUD、DeepChat/ACP dispatch、draft/subagent/transfer、title、import、search、export、dashboard | -| `agentRuntimePresenter/index.ts` | 7636 行 / 229 methods / 41 readonly fields | 所有 session 的内存态、turn orchestration、prompt、provider、Tape、compaction、memory、permission、tool、events | -| `agentRuntimePresenter/process.ts` | 626 行 | 真正的 provider round + tool loop | -| `agentRuntimePresenter/dispatch.ts` | 1873 行 | tool execution、permission、pause、normalization、persistence、renderer projection | -| `agentRuntimePresenter/tapeService.ts` | 1838 行 | Tape bootstrap/effective view/search/manifest/replay/fork | -| `llmProviderPresenter/providers/acpProvider.ts` | 2035 行 | ACP process/session/prompt/permission/event 到 LLM provider 的兼容包装 | -| `agentRepository/index.ts` | 597 行 | DeepChat config 与 ACP manual/registry/install state 的混合 repository | - -更早的 presenter-split 提案只把问题定义成 façade service extraction,没有覆盖随后进入 runtime 的 -Memory、projection、agent-scoped extensions 和 ACP 分离,因此已由本设计取代;历史内容通过 Git -记录查询,不再作为仓库内并行架构入口。 - -### 迁移前调用关系 - -```text -Renderer / typed routes - │ - ▼ -AgentSessionPresenter (4210 lines) - │ - ├─ AgentRegistry - │ └─ production only registers "deepchat" - │ - └─ resolves BOTH deepchat/acp to one AgentRuntimePresenter - │ - ├─ all sessions stored in session-keyed Maps - ├─ prompt/context/compaction/memory/tools/permission - ├─ processStream + dispatch - └─ provider.coreStream - │ - ┌─────────────┴─────────────┐ - ▼ ▼ - normal LLM provider AcpProvider - │ - ▼ - external ACP process loop -``` - -迁移前的 `AgentRegistry` 不是实际 manager:生产只注册一个 `deepchat` implementation,遇到任意已知 -`deepchat` 或 `acp` agent id 都返回这个 implementation。UI catalog 反而来自 -`ConfigPresenter -> AgentRepository`。 - -### 迁移前 DeepChat initial turn 顺序 - -下列迁移前顺序是迁移期间必须保持的兼容合同: - -```text -accept input / claim pending item - -> status=generating + register pre-stream AbortController - -> resolve session skills, final ToolPresenter tool set, extension policy and base prompt - -> ensure Tape ready and snapshot pre-turn history - -> prepare optional compaction intent - -> if intent: - create compaction projection - -> append user message fact - -> apply compaction - -> trigger the then-current compaction-to-Memory path only after a normal initial-path return - else: - append user message fact - -> emit user refresh and dispatch UserPromptSubmit notification (fire-and-forget) - -> append summary/reconstruction/memory sections - -> assemble Tape effective view and context - -> create mutable assistant placeholder - -> consume the claimed pending item / emit initial assistant refresh when required - -> enter runStreamForMessage and register the active generation - -> for each outer provider round - -> increment providerRoundCount and enforce max - -> for each provider attempt in that round (strict retry may add one) - refresh prompt/tools when required - -> context preflight / pressure recovery - -> increment requestSeq - -> synchronously attempt request ViewManifest (failure logs and remains fail-open) - -> rate-limit gate - -> provider.coreStream + accumulate blocks + throttled echo - -> on strict overflow retry, repeat the attempt without incrementing providerRoundCount - -> execute/intercept/normalize/persist each tool call under the current policy - -> collect ordered interactions and execution state from pre-check, question, - post-call permission or skill draft - -> continue next provider round - -> finalize assistant fact and terminal events - -> drain pending input when allowed - -> schedule non-blocking Memory extraction when the then-current rules admit it -``` - -### 迁移前根因 - -1. `IAgentImplementation` 是 optional capability soup,而不是两个对等 runtime 的公共合同。 -2. `Agent` 同时携带 `type`、`agentType?`、DeepChat-only `config` 与 ACP-only `installState`。 -3. 一个 singleton 用几十个 `Map` 模拟实例所有权。 -4. `processMessage`、`runStreamForMessage`、`processStream`、`dispatch` 分别持有一段 lifecycle。 -5. `AgentRepository` 的物理表复用扩散成了 domain repository 与通知语义也复用;DeepChat CRUD - 甚至调用 `notifyAcpAgentsChanged()`。 -6. `src/main/lib/agentRuntime` 实际包含 process/shell/search/path/question 等不同 owner 的工具, - 名称让它看起来像第二套 agent runtime。 -7. 迁移前 hooks notification 是 `queueMicrotask` fire-and-forget observer,不是可以 await 的 loop - lifecycle。 - -## AFTER:已实现的当前架构 - -```text -typed routes -> SessionService / ChatService -- narrow ports ─┐ -remote -> RemoteConversationRunner -- four narrow ports ─────┤ -cron -> Cron session starter -- Lifecycle / Turn ports ──────┤ -Tool / MCP / Floating / hooks -- owner-specific ports ────────┘ - ▼ -Lifecycle / Turn / AgentAssignment / Projection -├─ AppSessionService ─ new_sessions / window binding / shared CRUD -└─ AgentManager (control plane) - ├─ AgentCatalog - │ ├─ DeepChatAgentRepository ─┐ - │ └─ AcpAgentRepository ├─ shared agents table + typed codecs - └─ explicit switch(agent.kind) - │ - ├─ kind=deepchat - │ ▼ - │ DeepChatAgentRuntime - │ └─ DeepChatAgentInstance(sessionId) - │ ├─ identity + effective session config - │ ├─ generation/pending/interaction state - │ └─ DeepChat LoopEngine - │ ├─ fixed typed lifecycle - │ ├─ resource contributors/adapters - │ ├─ ProviderPort / ToolPort - │ ├─ TapeRecorder / OutputSink - │ └─ per-turn LoopRun state - │ - └─ kind=acp - ▼ - AcpAgentRuntime - └─ AcpAgentInstance(sessionId) - ├─ process / remote session / workdir - ├─ ACP protocol prompt + permission - ├─ ACP MCP delivery adapter - └─ existing message/Tape/event compatibility adapter - -Independent owners (not children of AgentManager): - McpPresenter | SkillPresenter | MemoryPresenter | PluginPresenter | LLMProviderPresenter - └─ expose narrow ports/adapters to the relevant backend - -Session boundary owners (composed by typed routes and lifecycle hooks): - SessionHistorySearch | SessionTranslation | AgentSessionExportService | UsageStatsService - LegacyChatImportService | session-data migrations | available-agent catalog | RTK runtime service -``` - -DeepChat backend 内的 provider selection 独立于 agent kind: - -```text -DeepChat LoopEngine -> generic ProviderPort - ├─ ordinary LLM provider - └─ AcpAsLlmProviderAdapter (compatibility; providerId=acp) -``` - -### 完成后会发生什么变化 - -| 关注点 | BEFORE | AFTER | -| --- | --- | --- | -| agent dispatch | fake registry 按 id 解析,两个 kind 都回到同一 runtime | `AgentDescriptor.kind` 显式 `switch` 到两个 agent-session backend;provider selection 仍是 DeepChat session 内的正交维度 | -| shared types | optional 字段袋与 mega-interface | discriminated descriptor/backend;kind-specific required capabilities | -| session runtime | singleton + session-keyed maps | 一个 active/hydrated session 一个 `DeepChatAgentInstance` 或 `AcpAgentInstance` | -| DeepChat loop | 跨 4 个大文件的隐式顺序 | 一个 DeepChat-only `LoopEngine`,固定 typed stages | -| async lifecycle | ad hoc callbacks;外部 hooks 不可 await | 内部 lifecycle 可 await;只有 typed tool interaction outcome 可产生 persistent pause;外部通知仍 non-blocking | -| ACP | ACP agent 与 DeepChat agent 选择 ACP provider 都经过外层 DeepChat loop | `kind=acp` 使用独立 backend;`kind=deepchat + providerId=acp` 保留兼容 provider adapter | -| MCP/skills | Presenter 调用散落在 runtime | 原 Presenter 继续拥有资源,DeepChat/ACP 各用不同 delivery adapter | -| permission | DeepChat tool permission 与 ACP protocol permission 在同一 runtime 汇合 | 共用 UI decision/output port,但两个 backend 各自拥有 continuation | -| Tape | message/tool facts、anchors、manifest、trace 分散调用 | 同一现有 Tape,由 `TapeRecorder` 明确写入顺序与 provenance | -| Memory | orchestration 约 600 行嵌在 runtime | 保持 `MemoryPresenter` 不动;一个 awaited prompt contributor + 一个 background ingestion observer | -| `lib/agentRuntime` | 无明确 owner 的混合目录 | 文件迁到 process runtime、tool、workspace 或 DeepChat prompt 等真实 owner | -| public API/data | route/event/schema/table 已上线 | 在本重构中保持兼容;旧 DTO 只存在于 boundary adapter | - -### 不会发生什么变化 - -- 不改变 renderer route name、input/output schema、typed event name 或 payload。 -- 不改变 `agents`、`new_sessions`、`deepchat_*`、`acp_sessions`、`acp_turns` 的 schema。 -- 不把 Tape 改成 raw token/event log,也不增加第二个 Tape store。 -- 不改变 prompt section 顺序、tool collision policy、permission policy、tool 并发条件或 - tool-output fitting。 -- 不改变 ACP regular session 与 ACP-backed subagent 当前已被测试锁定的差异。 -- 不改变旧数据/API 允许的 `kind=deepchat + providerId=acp` 组合;它仍是 DeepChat session,并继续走 - DeepChat loop 与 ACP provider compatibility adapter。 -- ACP direct backend 继续写现有 structured message/search/export projection 与当前 Tape facts; - `acp_turns` 仍只保存 ACP turn metadata,不能替代 transcript;现有 request trace 在 - `connection.prompt` 前以相同 correlation/redaction/fail-open 语义写入。 -- 不修复在审计中发现的现存行为不对称;若要修,另开行为变更目标。 -- 不改写 Memory service/schema/projection/retrieval/maintenance。 -- 不把 MCP、skills、memory 的全局数据 owner 搬进 `AgentManager`。 -- 不引入 DI container、generic event bus、priority-based plugin pipeline 或新依赖。 - -## 目标目录模型 - -这是 ownership 模型,不要求一次提交创建全部文件;每个实施 slice 只创建当前需要的实体。 - -```text -src/main/agent/ -├── manager/ # control plane and kind routing -├── shared/ # app-session/transcript/output ports and boundary codecs -├── acp/ # ACP catalog/process/session/protocol/persistence/mapping -└── deepchat/ - ├── instance/ # per-session DeepChat state/lifecycle - ├── loop/ # provider round + tool loop state machine - ├── resources/ # explicit adapters/contributors - ├── memory/ # runtime coordinator + prompt/ingestion ports; MemoryPresenter stays owner - └── pending/ # durable pending input coordination - -src/main/presenter/ -├── sessionApplication/ # lifecycle/turn/assignment/projection coordinators -└── agentRuntimePresenter/ # retained DeepChat state/delegate + message/Tape/resource adapters -``` - -最终路径名可以在每个机械 move PR 中按仓库约定微调,但以下依赖方向不可改变: - -```text -manager -> shared contracts -> concrete backend -deepchat instance -> loop -> narrow ports -acp instance -> ACP protocol modules -> narrow shared IO ports -resource adapter -> independent presenter/service owner - -forbidden: -loop -> presenter root singleton -loop -> Electron window/routes -kind=acp backend -> DeepChat LoopEngine -MemoryPresenter -> DeepChat runtime implementation -MCP/SkillPresenter -> AgentManager state -``` - -## 最薄共同合同 - -内部 canonical descriptor 使用 `kind`,不再传播 `type + agentType?` 双字段: - -```ts -type AgentDescriptor = DeepChatAgentDescriptor | AcpAgentDescriptor - -interface AgentDescriptorBase { - id: string - kind: 'deepchat' | 'acp' - name: string - enabled: boolean -} - -type AgentSessionBackend = - | { kind: 'deepchat'; session: DeepChatAgentInstance } - | { kind: 'acp'; session: AcpAgentInstance } -``` - -`kind` 只决定 agent-session backend,不限制 DeepChat 的 provider。ACP descriptor 内再用 -`source: 'manual' | 'registry'` 区分 required launch fields,不能假设每个 ACP agent 都有 registry 或 -install state。 - -`AgentManager` 默认显式 `switch (kind)`。只有双方真实共享的操作才有共同入口: -`open`、`send`、`cancel`、`close`、`snapshot`。其余能力必须 kind-specific,不得重新变成 optional -method: - -- DeepChat:Tape、compaction、pending/steer、generation settings、local tools、skills、memory。 -- ACP:remote session、workdir、mode/config options、commands、process、protocol permission。 - -## DeepChat lifecycle - -生命周期是固定 pipeline,不是开放式插件排序器: - -```text -prepareTurn - -> prepareInput - -> assembleRequest - -> startProviderRun - -> enterProviderRound (increments/checks providerRoundCount) - -> beforeProviderRequest (increments requestSeq per attempt) - -> consumeProviderRound - -> executeToolBatch (0..n) - -> afterRoundPersisted - -> settleTurn -``` - -允许 await 的 seam: - -| Seam | 类型 | 能否持久 pause | 主要参与者 | -| --- | --- | --- | --- | -| `assembleBasePrompt` | ordered awaited contribution | 否 | base/runtime/env/skills/tooling/permission;发生在 compaction intent 之前 | -| `prepareInput` | awaited coordinator | 否,只等待/取消 | Tape/history、compaction intent、user fact、apply compaction | -| `contributeContextPrompt` | ordered awaited contribution | 否 | summary/reconstruction/memory;发生在 compaction 之后 | -| `beforeProviderRequest` | awaited gate/transform | 否,只等待/取消 | budget/recovery/manifest/rate limit | -| `executeToolBatch` | awaited operation + typed outcome | 是,只有合法 tool interaction origin 可返回 ordered pause outcome | pre-check permission、question、post-call permission、skill draft | -| `afterRoundPersisted` | awaited commit callback | 否 | Tape/output state | -| `afterTurnSettled` | commit callback + observers | 只有内部 commit awaited | pending drain、Memory ingestion、notifications | -| `afterCompactionApplyReturned` | normal-return callback | 否 | only initial/context-pressure current Memory ingestion trigger;throw/no-intent 以及 resume/manual 不调用 | - -`pause` 会结束当前 provider run;用户逐项处理 ordered interactions,中间项不会创建 run,只有最后一项 -解决后才按现状从持久 projection/context 创建一个新的 resume run,不是恢复旧 call stack。raw -`onProviderEvent` 不开放为 await hook。它是流式 hot path,继续使用纯 accumulator 与节流 echo。 - -外部 `HooksNotificationsService` 继续是 fire-and-forget observer。让用户 shell command 阻塞 agent -loop 会改变现有性能、超时和取消语义,因此明确禁止。 - -ASLR-057 已把这条边界落实为 typed notification observer。loop 只交付 detached snapshot,且不 await -observer Promise/thenable;同步异常、异步拒绝与悬挂均不改变 terminal outcome。auto-grant、review、 -streaming permission、skill activation/cache 是独立 control collaborators,interleaved-reasoning trace -属于 internal diagnostics。现有 hook payload、调用顺序、agentId fallback、route/config、`queueMicrotask` -与 command timeout 不变。 - -## Tape 语义 - -目标可观测链由现有 Tape、trace 与 message terminal projection 共同组成,不逐 token 记日志: - -```text -user message fact - -> context/view manifest - -> assistant/tool call/tool result facts - -> compaction/handoff/memory audit anchors - -> current terminal message status + optional current runtime status -``` - -`ASLR-080` 已在现有 replay reader 上增加 pure-read causal observation slice:request 通过 -`sessionId/messageId/requestSeq` 精确联结 ViewManifest 与 trace,assistant/tool output 只能声明 -`message_only` correlation。renderer terminal event history 当前没有持久化事实,read model 明确返回 -`eventHistory=not_persisted`,不能从 pending/terminal message 反推历史事件;runtime status 也只接受调用方 -从已 hydrate instance 非物化 peek 得到的当前值,否则为 unavailable。 - -`ASLR-081` 已用 injected storage seams 与可用时的 native SQLite tables 固化 non-interference:所有 read -模式前后的 Tape order/ViewManifest、message/trace、effective view、replay hash、Memory ingestion -projection/cursor 与完整 user schema 相同,现有 table/projection/cursor write seams 为零调用;AST guard -禁止 reader 引入 Memory runtime value import/call。cooldown 不属于 `DeepChatTapeService`;ASLR-059 已将其 -迁移到唯一 `MemoryRuntimeCoordinator` 并用 public coordinator contract 覆盖。该证明没有填补历史 -renderer event 缺口; -`eventHistory=not_persisted` 仍是 read model 的真实边界。 - -- `deepchat_tape_entries.entry_id` 继续提供 per-session monotonic order。 -- mutable streaming block 继续保存在 message projection;final/replacement/retraction 进入 Tape。 -- request body/headers 继续只在 trace storage;现有 messageId/requestSeq correlation 保持,不向 Tape - 新增 trace reference payload。 -- 本目标不新增 interaction/terminal Tape entry;若现有 facts + projection 无法满足新的审计需求,另立 - data/behavior SDD,不能在结构重构中顺手改变 effective view。 -- `memory/*` 与 `persona/*` anchors 继续是 non-reconstruction。 -- session clear/delete 仍可按当前合同删除 Tape;“append-only”描述的是存续 session 内的修改模型, - 不是永久不可删除。 -- live replay 不能重放外部 provider/tool side effect;本目标只保证 audit/replay slice 可解释。 -- old session 即使没有 Tape/ViewManifest 也只返回 partial/unavailable read model,不触发 bootstrap/backfill。 - -## 状态所有权 - -| 状态 | 目标 owner | -| --- | --- | -| agent identity、kind、display summary | `AgentCatalog` | -| app session title/project/pin/draft/window binding | shared `AppSessionService` / `new_sessions` | -| lifecycle transaction / turn commands / assignment policy / renderer projection | four `sessionApplication` coordinators over narrow owner ports | -| DeepChat effective config、status、pending inputs、ordered interactions | `DeepChatAgentInstance` | -| pre-stream cancellation before active generation registration | `DeepChatAgentInstance` preparation state | -| active run、abort signal、per-attempt requestSeq、outer providerRoundCount、round messages、overflow retry flags | per-turn `LoopRun` | -| ACP process handle、remote session id、mode/config/commands | `AcpAgentInstance` / ACP runtime | -| message projection、Tape facts、trace | narrow adapters over existing stores/types | -| MCP server definitions/runtime | `McpPresenter` | -| skill catalog/content/runtime | `SkillPresenter` | -| memory rows/vector/maintenance | `MemoryPresenter` | -| Memory extraction chains/epochs/retry cooldown/injection-access dedupe | DeepChat `MemoryRuntimeCoordinator` adapter(现有 runtime orchestration 的唯一新 owner) | -| session history search / translation / current export | typed session route owners / `AgentSessionExportService` | -| usage backfill and dashboard | `UsageStatsService` | -| legacy default import and session-data startup migrations | `LegacyChatImportService` / stateless startup migration functions | -| available-agent filtering / RTK health | available-agent catalog policy / RTK runtime service | - -## 实施原则 - -1. Strangler migration:旧 façade 始终可调用,内部逐步 delegation;每个 slice 可单独回滚。 -2. 先 characterization,再移动 ownership;发现 bug 时单独修复,不夹在 refactor commit。 -3. 先拆 control plane 与 instance,再抽 loop/ACP/Tape adapter;Memory 最后接线。 -4. ACP catalog/control plane 可以早拆,ACP direct execution 必须晚拆并有 parity tests。 -5. 共享物理表不等于共享 domain repository;本目标不做 DB rename。 -6. 任何 prompt、request、event 或 Tape order 差异都视为 blocking regression,除非另有批准的行为 spec。 - -## 完成标准 - -当且仅当下列条件同时满足,目标才算完成: - -- fake `AgentRegistry` 与 `IAgentImplementation` optional mega-interface 已删除。 -- internal agent descriptor 只用 discriminated `kind`;legacy DTO alias 只在 route adapter。 -- `kind=acp` session 不再进入 DeepChat `LoopEngine`;DeepChat agent 选择 ACP provider 的兼容路径仍可 - 通过 generic `ProviderPort` 进入 ACP provider adapter。 -- DeepChat session state 由实例持有,LoopEngine 不保存跨 session mutable maps。 -- LoopEngine 不 import presenter root、Electron window/routes 或 concrete SQLite presenter。 -- prompt/tool/permission/compaction/memory 调用顺序与基线一致。 -- Tape/trace/ViewManifest/replay 与 Memory 全部不可回归合同通过。 -- renderer routes/events 和持久化 schema 无未批准 diff。 -- `src/main/lib/agentRuntime` 被清空/删除,文件归属真实 owner。 -- 当前 architecture docs 与 guard 已更新,旧 split proposal 不再显示为并行实施计划。 - -## 关联现有合同 - -本设计不复制以下事实,实施时直接以它们为维护合同: - -- [Agent System current state](../agent-system.md) -- [DeepChat vs ACP current comparison](../deepchat-vs-acp-agents/spec.md) -- [Tape baseline](../deepchat-tape-baseline/spec.md) -- [Tape view assembler](../deepchat-tape-view-assembler/spec.md) -- [Tape view policy](../deepchat-tape-view-policy/spec.md) -- [Tape replay contract](../deepchat-tape-replay-contract/spec.md) -- [Agent-scoped extensions](../agent-scoped-extensions/spec.md) -- [Agent Memory architecture](../agent-memory-system/spec.md) diff --git a/docs/architecture/agent-system-layered-runtime/migration-and-validation.md b/docs/architecture/agent-system-layered-runtime/migration-and-validation.md deleted file mode 100644 index 1e7ffe4fc9..0000000000 --- a/docs/architecture/agent-system-layered-runtime/migration-and-validation.md +++ /dev/null @@ -1,552 +0,0 @@ -# Agent System Layered Runtime — Migration and Validation - -## 1. Compatibility policy - -This is a structural migration. Existing behavior is the baseline contract, including awkward behavior that -may deserve a later fix. A refactor PR is blocked when it changes observable ordering, fallback, persistence, -permission settlement or resource selection without a separate approved behavior spec. - -No phase requires a DB migration. Every phase writes the same persisted shapes and can roll back by restoring -the previous delegate. - -## 2. Wire and storage freeze - -### Wire contracts - -The following remain stable: - -- all `sessions.*`, `chat.*`, `config.*`, `memory.*`, MCP, skills and provider route names; -- route schemas and renderer client return shapes; -- `chat.stream.*`, `sessions.*`, plan, memory, ACP mode/config/command and catalog event payloads; -- RemoteControl and CronJobs ports and status/output semantics; -- preload/typed bridge boundaries. -- the supported `kind=deepchat + providerId=acp` route/storage combination and its - `MessageStartResult`/request projection. - -Internal discriminated types are converted to current wire DTOs by boundary codecs. A deprecated alias may -remain in the DTO during this goal, but it cannot leak back into domain/backend contracts. - -Legacy agent rows use two read policies: catalog listing preserves current tolerant parse/null/default/filter -behavior per row, while backend open requires a valid executable descriptor and may return typed unavailable. -Malformed `config_json`/`state_json`, missing manual command, invalid source×kind and source/id collision cannot -fail the whole catalog or fallback to another kind. - -### Storage contracts - -Do not rename, merge or recreate these stores in this goal: - -| Store | Contract to preserve | -| --- | --- | -| `agents` | common row plus current JSON columns; typed codecs split above it | -| `new_sessions` | app identity/title/project/pin/draft/skill/subagent shell;`session_kind` remains `regular | subagent`, not backend kind | -| `deepchat_sessions` | provider/model/settings/summary/memory cursor | -| structured message/search tables | hot read/write projection and fallback JSON behavior | -| pending input tables | state/claim/order/recovery semantics | -| `deepchat_tape_entries` | per-session monotonic semantic facts/anchors/manifests | -| `deepchat_message_traces` | current redacted provider request trace behavior | -| `acp_sessions` / `acp_turns` | app conversation to remote session/turn mapping | -| `agent_memory` / audit / projection tables | current Memory schema/version/transaction semantics | -| per-agent DuckDB sidecars | current embedding identity and lifecycle behavior | - -## 3. Baseline behavior matrix - -### Session and turn - -- input/config/default precedence remains input > agent config > global defaults; -- explicit project-dir null/normalization stays intact; -- session list hydration remains lightweight and lazy; -- status and the pre-stream AbortController are registered before long preparation awaits, while the active - generation remains registered only after context assembly and assistant placeholder creation; -- stale run completion cannot overwrite a newer run's state; -- claimed pending inputs recover after crash and are not exposed twice; -- queue/steer ordering, max count and single-flight drain stay unchanged; -- partial assistant output and terminal cancellation are persisted/emitted once; -- first-turn readiness/title generation remains non-blocking and stale-run safe. - -### Provider and context - -- every provider attempt passes rate admission and cancellation checks; -- `providerRoundCount` increments/checks max at each outer round entry;`requestSeq` increments per provider - attempt before ViewManifest, including strict retry within the same outer round; -- base resource prompt is assembled before compaction intent preparation; user fact precedes compaction apply - on the intent path; summary/reconstruction/Memory and context follow compaction; -- context preflight, pressure compaction/recovery and strict retry retain current order; -- context overflow is not confused with quota/rate-limit errors; -- interleaved reasoning preservation uses current model portrait rules; -- image/video endpoint budget bypass stays unchanged. - -### Tool, skill and permission - -- MCP name collision precedence and tool source routing remain unchanged; -- only the currently allowed readonly agent-tool batch may execute in parallel; -- mutating/side-effect tools are not replayed or retried for output fitting; -- output guard/offload/downgrade and screenshot normalization remain unchanged; -- agent-scoped MCP/skill/plugin policy and disabled tools apply to the bound `agentId`; -- `skill_view` activation refreshes tools/prompt only under the current rules; -- question, pre-check permission, post-call `requiresPermission` and post-success skill-draft interactions - preserve their current ordered batch and durable continuation; -- a paused run settles; intermediate interaction responses stay paused, and only the final response creates the - current fresh resume run rather than resuming the old provider call stack; -- `default`, `auto_approve`, `full_access` and auto-review fallback behavior remain unchanged. - -### ACP - -- global enablement and manual/registry/install/env configuration remain compatible; -- alias normalization remains in effect; -- workdir is required/synchronized before send, with current reset/rollback behavior; -- system prompt is marked sent only after a successful ACP prompt; -- session load/resume/new fallback, modes, config options and commands remain compatible; -- advertised commands do not imply a direct `executeCommand` route/SDK facet; -- protocol permission promises settle on decision, timeout, cancel, clear and shutdown; -- transfer into ACP remains rejected before mutation; supported ACP -> DeepChat transfer closes the direct ACP - runtime at the current post-ownership commit point (legacy DeepChat + ACP-provider keeps compatibility - binding cleanup there); -- regular ACP keeps its current compatibility prompt/local-resource behavior; -- ACP-backed subagent keeps current isolation/bypass/retry behavior; -- remote ACP sync's legacy conversation behavior is not silently unified with `new_sessions`. -- direct `kind=acp` uses the existing structured message/Tape/event writers so restart/search/export remain - compatible; `acp_turns` remains metadata-only. -- direct `kind=acp` writes the same fail-open `acp://session/prompt` request trace with current - message/request correlation, redaction/truncation and trace-before-prompt order; -- trace persistence failure is characterized at the real fail-open emitter/adapter boundary, not by a - provider-private mock that injects a rejecting persistence implementation; -- DeepChat descriptors selecting `providerId=acp` keep the DeepChat outer loop, regular compatibility - system-prompt/resource descriptions and ACP-as-provider adapter. - -### Tape - -- user/final/tool facts remain monotonic and idempotent by provenance; -- edit/delete are replacement/retraction facts, not in-place Tape mutation; -- effective view, bootstrap/backfill, anchors, handoff, true-fork merge/discard, and production - subagent Tape links remain compatible; -- one ViewManifest write is synchronously attempted before each actual provider request at the current point; - write failure logs and remains fail-open, so a request may legally have no manifest; -- trace/replay default remains metadata-only; raw payload inclusion stays opt-in; -- session clear/delete retains current Tape deletion semantics. - -## 4. Memory no-regression contract - -Memory is the highest-risk cross-cutting participant and migrates last. These IDs are the single authoritative -wording; module and task documents reference them without redefining them. - -| ID | Frozen invariant | -| --- | --- | -| `MEM-01` | Disabled Memory or any injection failure returns the original prompt unchanged. | -| `MEM-02` | Injection keeps sanitization, untrusted/read-only framing and hard token budget. | -| `MEM-03` | Only active persona is injected; working memory is separate; unapproved persona draft is excluded. | -| `MEM-04` | Injection access is recorded only for final selected manifest IDs. With a non-null messageId it is deduped by session/message under the current TTL/cap; null-messageId pressure-recovery calls keep current non-deduped accounting. This is not extraction dedupe. | -| `MEM-05` | `memory/view_assembled` failure does not remove an already assembled prompt. | -| `MEM-06` | Extraction input comes from the effective Tape/projection with the exact lineage of the window built inside the serialized task. | -| `MEM-07` | Extraction stays background and per-session serial; sibling sessions may progress independently. Enqueue keeps the trigger path and existing compaction upper bound only. The serialized task ensures the current epoch and reads the latest cursor/tail when it starts; only same-job chunk continuations carry `expectedEpoch`. | -| `MEM-08` | Cursor advances only after `ok: true`; failed/disabled work cannot consume the range. | -| `MEM-09` | Projection validation/rebuild failure falls back to authoritative Tape without committing cursor and keeps the retry cooldown. | -| `MEM-10` | Edit/delete/retry/pending rollback/clear/destroy invalidate stale epochs and rewind/rebuild at the current boundary. | -| `MEM-11` | Agent delete clears Memory rows/audit transactionally before best-effort vector sidecar cleanup. | -| `MEM-12` | App shutdown aborts and drains Memory before SQLite closes; late writes remain fenced. | -| `MEM-13` | Initial and resume terminal triggers preserve the outcome matrix below; returned abort and thrown AbortError are distinct. | -| `MEM-14` | At the existing extraction-enabled compaction entry points only—initial input and context-pressure recovery—a non-null intent triggers extraction after any normal `applyCompactionIntent` return, including `succeeded=false`; any throw, including AbortError, triggers nothing, and no intent triggers nothing. Resume and manual compaction do not enqueue compaction extraction; resume terminal fallback remains governed by `MEM-13`. | - -Terminal trigger matrix: - -| Origin/outcome | Enqueue fallback extraction | -| --- | --- | -| initial turn returns `completed` | yes | -| initial turn returns `aborted` | no | -| initial turn returns `paused` or `error` | no | -| initial turn throws AbortError or another error | no | -| resume returns `completed` | yes | -| resume returns `aborted` | yes | -| resume returns `paused` or `error` | no | -| resume throws AbortError or another error | no | - -Compaction trigger matrix: - -| Entry point | Intent/apply outcome | Enqueue compaction extraction | Upper bound | -| --- | --- | --- | --- | -| initial input or context-pressure recovery | no intent | no | none | -| initial input or context-pressure recovery | `applyCompactionIntent` returns with `succeeded=true` | yes | intent target cursor | -| initial input or context-pressure recovery | `applyCompactionIntent` returns with `succeeded=false` | yes | intent target cursor | -| initial input or context-pressure recovery | apply throws AbortError or another error | no | none | -| resume or manual compaction | any intent/apply outcome | no | none | - -Memory service internals are out of scope. ASLR-059 moved the current queue/counter, chains/epochs, -cooldown/access-dedupe and cursor orchestration into one runtime-scoped `MemoryRuntimeCoordinator`; instances -only keep a session handle. ASLR-060 made that coordinator the direct `MemoryPromptContributor` implementation -and preserved the fixed PostCompaction slot. ASLR-061 made it the direct discriminated -`MemoryIngestionObserver`, preserved the complete `MEM-13/14` matrices, and added the composition-root -admission-fence -> Memory dispose -> bounded chain-wait with typed pending outcome -> SQLite-close shutdown -order. A timeout is not reported as settled; permanent coordinator and Memory operation fences reject late -writes. The existing start-time epoch leaves session-id reuse behavior intentionally unchanged; any change -requires a separate behavior spec. - -## 5. Golden causal fixtures - -Add deterministic fixtures using fake provider/tool ports; never invoke real side effects twice. - -### Multi-round success - -```text -session status generating + pre-stream AbortController -base prompt + Tape/history + compaction intent -user message projection + Tape fact -compaction apply when intent exists -summary/reconstruction/Memory + context -assistant placeholder + active generation -request ViewManifest attempt seq=1 (write may fail-open) -provider text/tool events -tool call/result projection + Tape facts -request ViewManifest attempt seq=2 -provider final response -assistant final projection + Tape fact -terminal event/status idle -eligible background Memory scheduling -``` - -### Permission pause/resume - -```text -provider round -> tool batch -pre-check | question | post-call permission | post-success skill draft -> ordered interactions -persist all pending actions + execution state -user decision -resolve the first matching continuation -execute/deny/answer/confirm under the current origin-specific rule -persist result and remove that interaction -if interactions remain: stay pending and return without a run -otherwise: rebuild context and start one fresh resume run -terminal settlement -``` - -### Context recovery - -```text -assemble initial Tape view -preflight pressure -compaction/recovery attempt -rebuild system prompt including current Memory rule -attempt request manifest with recovered policy/cursor (fail-open) -rate gate -single provider attempt or documented strict retry -``` - -### ACP permission - -```text -ACP prompt active -protocol permission request registered with timeout -renderer action projection -decision | cancel | timeout | close -ACP promise settles once -action/turn terminal state persists -``` - -## 6. Characterization coverage policy and map - -Phase 0 does not translate every behavior row into a new presenter-level test. It first maps existing durable -proof, adds only high-value gaps that can be asserted through a stable public/port seam, and assigns contracts -that require a new typed seam to the phase that introduces that seam. This scheduling does not weaken any -behavior matrix in this document; the assigned phase cannot pass until its narrow contract fixture exists. - -Permanent tests assert behavior, ordering, failure policy, persistence or compatibility. Do not retain tests that -only inspect presenter-private maps, mirror the complete internal mock call graph, prove a file was temporarily -moved, assert each proposed stage was called, or dual-run provider/tool side effects. Migration-only parity, -import-path and private-shape tests must be removed after their comparison; final import ownership belongs in the -architecture guard. - -| Compatibility row | Phase 0 proof | Owning narrow-seam gate | -| --- | --- | --- | -| malformed agent config/state, missing manual command, invalid source×kind, source/id collision, legacy DTO | repository characterization over current public reads/writes | typed tolerant/strict codec contracts in `ASLR-010` and repository ownership contracts in `ASLR-011` | -| initial/tool/multi-round loop, max rounds, skill refresh, rate wait, cancel and stale run | existing `agentRuntimePresenter/process` contract suites plus focused strict-retry outer-round/request-sequence characterization | complete typed lifecycle causal order in `ASLR-052..056` / Phase 6 | -| prompt order, compaction, overflow recovery and ViewManifest success/failure | existing runtime, compaction and Tape/ViewManifest suites | typed input/context/request lifecycle order in `ASLR-052..054` / Phase 6 | -| tool catalog order/collision/policy/cache/revision, exact execution options/order, normalization/output fitting and abort | existing ToolPresenter, process, dispatch and output-guard suites | typed adapter contracts plus real ToolPresenter collision boundary in `ASLR-055` / Phase 6 | -| pre-check permission, question, skill draft, multiple ordered interactions and fresh-run resume | existing dispatch/runtime interaction suites plus ASLR-056 four-origin order/execution-state and explicit execute-count fixtures | typed batch outcome, post-call no-replay and final-item-only resume completed in `ASLR-056` / Phase 6 | -| external hook payload/order, snapshot isolation and non-blocking failure policy | existing hooks/runtime suites plus ASLR-057 observer order, snapshot, sync-throw, rejected/never-settling contracts | typed notification observer separated from control collaborators and internal diagnostics in `ASLR-057` / Phase 6 | -| ACP regular/subagent behavior and DeepChat + ACP-provider compatibility versus direct `kind=acp` | existing ACP provider/session and DeepChat compatibility suites; Phase 0 had no direct backend | direct instance/composition parity completed in `ASLR-070..071`; route/title/subagent-retry/pending/remote/cron/no-fallback switch completed in `ASLR-072`; generic provider contract and legacy ACP backend retirement completed in `ASLR-073` / Phase 7 | -| Tape facts, effective view, manifest, replay/privacy | existing Tape fact/view/replay suites | recorder/output causal order in `ASLR-052`; pure-read causal join, partial/unavailable states and event-history gap in `ASLR-080`; injected-seam plus native-table non-interference, replay privacy and AST-enforced Memory non-access proof completed in `ASLR-081`; runtime cooldown remains assigned to `ASLR-059`, and event history remains `not_persisted` | -| Memory injection, extraction, cursor, lineage, fencing and current trigger asymmetries | existing Memory and runtime integration suites plus the authoritative `MEM-01..14` matrices below | `MemoryPromptContributor` and `MEM-01..05` parity completed in `ASLR-060`; complete `MEM-13` returned/thrown turn matrix, `MEM-14` compaction return/throw matrix and shutdown drain/fence over `MemoryIngestionObserver` completed in `ASLR-061` / Phase 9 | -| route/event/schema and composition/shutdown facts | compact machine-readable architecture baseline | per-slice integration gates and final architecture guard/baseline in `ASLR-091..092` | - -A large presenter fake created only to restate this table is not an acceptable substitute for the assigned typed -contract. E2E remains limited to user-observable smoke coverage; module and real-boundary integration tests are -the primary proof. - -## 7. Test gates by boundary - -### Control plane/data - -- `test/main/presenter/agentRepository.test.ts` -- `test/main/presenter/agentSessionPresenter/agentRegistry.test.ts` (retired after replacement contract exists) -- `test/main/presenter/agentSessionPresenter/agentSessionPresenter.test.ts` -- `test/main/presenter/agentSessionPresenter/integration.test.ts` -- session/table/import/search/export/usage tests touched by the slice -- malformed agent-row fixtures covering catalog visibility, default merge, filtering, unavailable errors and - current collision precedence - -### Loop/tool/context - -- all tests under `test/main/presenter/agentRuntimePresenter/` -- all tests under `test/main/agent/shared/` and `test/main/agent/deepchat/` -- relevant `test/main/presenter/toolPresenter/agentTools/` tests -- focused lifecycle-order/golden integration fixtures added by Phase 0 - -Critical existing coverage includes simple/tool/multi-round loop, skill refresh, prompt order, rate limit, -ViewManifest sequence/failure, context overflow recovery, compaction, queue/steer, permission/question resume, -ACP compatibility branches and stale-run cancellation. - -### ACP - -- `test/main/presenter/acpProvider.test.ts` -- `test/main/agent/acp/**` -- `test/main/presenter/sqlitePresenter/acpSessions.test.ts` -- agent-session ACP draft/subagent/transfer tests - -`ASLR-071` additionally requires focused proof for shared-owner lifetime, shutdown/refresh ordering, -descriptor-identity single-flight, prepare/workdir rollback, mode/config/command mapping, protocol-exit eviction, -permission settlement, regular/subagent prompt isolation, first-turn readiness, pending queue/steer ordering, -production prompt/projection/trace/rate/hook adapters and compatibility-provider reuse. Shutdown fixtures must -prove lazy materialization is fenced, in-flight identity hydration/prepare/send are drained, cleanup failure still -evicts the cache, and process shutdown is attempted after earlier cleanup failure. Rate fixtures must keep direct -waiters in the shared ACP QPS order while compatibility waiters are cancelled on provider rebuild/disable/remove. -The real session-manager fixture must replay initialization-time mode/config/command/session-info/usage updates -after record publication, discard updates emitted by failed resume/load attempts even when they share a persisted -session id, and replay only the successful attempt in original order. Cancellation fixtures must prove one caller -aborts all callers sharing a conversation-scoped initialization, owner shutdown completes without resolving a -stuck session-open RPC, and late SDK resolve/reject cannot publish, persist, restore a live map, leak handlers or -raise an unhandled rejection. Process-manager fixtures must also prove shutdown synchronously fences new -warmup/connection work without waiting for an unresolved spawn, late handles are disposed exactly once without -republication, and stale expected-handle cleanup preserves the replacement bound handle. - -`ASLR-072` route proof additionally covers: - -- real repository -> app-session lookup -> manager -> direct backend -> façade composition, including strict - missing/source-mismatched ACP config failure with zero DeepChat/provider fallback; -- ACP title projection invoking `summaryTitles` once without re-dispatching the primary turn through the - compatibility provider; -- exactly one ACP-backed subagent initialization retry with a new app-session id and failed-id runtime/shared - state/row cleanup; -- app-level pending queue/steer selection, remote active-generation cancel through manager generation facets, - and Cron detached-session/send wiring; -- lightweight list state without direct runtime hydration, direct transfer post-commit close, ACP target - pre-mutation rejection and direct-before-shared-owner shutdown order; -- descriptor-independent delete across missing agent rows, malformed/manual-commandless ACP rows, missing or - disabled registry rows and descriptor-valid/current-config-missing rows, with zero input resolution/process - launch/DeepChat message processing; malformed child recursion, normal DeepChat/direct deletion, durable ACP - metadata cleanup and runtime-error ordering are also locked by fixtures. - -`ASLR-073` contract proof additionally covers: - -- compile-time absence of migrated ACP runtime methods from `ILlmProviderPresenter` and explicit ownership by - session-control, permission and admin ports; -- zero compatibility-provider session-control calls for direct `kind=acp` routes; -- positive DeepChat + ACP-provider workdir, config/commands, permission and clear paths; -- provider admin warmup/process-config/debug routes through their explicit port; -- architecture rejection of retired legacy ACP backend symbols and factory overloads. - -### Tape - -- `tapeService.test.ts` -- `tapeFacts.test.ts` -- `tapeViewAssembler.test.ts` -- `tapeViewManifest.test.ts` -- `tapeViewPolicy.test.ts` -- structured message/session Tape tests - -### Memory - -- all `test/main/**` Memory tests, including runtime integration; -- `memoryInjectionPort.test.ts` for budget/sanitization; -- `memoryExtraction.test.ts`, `agent/deepchat/memory/memoryRuntimeCoordinator.test.ts` and - `agent/deepchat/memory/memoryExtractionChunks.test.ts` for cursor/serialization/chunking; -- `sqlitePresenter/deepchatMemoryIngestionProjection.test.ts` for projection/Tape/transaction behavior; -- `pnpm run test:main:memory-perf`; -- the dedicated native Memory CI job. - -## 8. Command gates - -### ASLR-062 Memory close-out record - -Comparison range: `c600f51b2adedb2147008bd1822cf8c72dac5f6c` (the parent of `ASLR-059`) through -the Phase 9 implementation head `c435746fc`. The ASLR-062 commit itself changes only documentation and test -stability, so the production comparison remains valid after it is applied. - -Contract comparison: - -- the complete `src/shared/contracts/routes` and `src/shared/contracts/events` trees are unchanged - (`1ac2dbd3c0aa3f91ab8a86ddf56bb3f52bea9b95` and - `3a62c4fa3a29e9883b39848ce26d2764360b79d5` respectively), proving no route/event DTO or Memory wire diff; -- `schemaCatalog.ts`, `schemaCatalogMetadata.ts`, `schemaTypes.ts` and the complete SQLite `tables` tree have - identical Git object IDs before and after Phase 9; the tables tree remains - `98e855ee04a5a362d4b62b76ef160b2fb51851d3`; -- Memory config surfaces remain identical: `agent-interface.d.ts`, `configPresenter/index.ts`, - `sqlitePresenter/tables/agents.ts` and `shared/types/agent-memory.ts` have no diff; -- the DuckDB sidecar implementation remains blob `cd2c68de92681b57c0075eca4aedc69750dae917`; -- `DEEPCHAT_MEMORY_INGESTION_PROJECTION_VERSION` is `1` at both endpoints and its source file has no diff. - -Validation results: - -- `pnpm run format`, `pnpm run i18n`, `pnpm run lint` and `pnpm run typecheck`: pass; -- focused coordinator/contributor/observer coverage: 3 files, 242 tests passed; -- `pnpm run test:memory:scope`: 56 classified, 4 exempt, pass; -- `pnpm run test:memory`: 43 files, 601 tests passed; -- native SQLite smoke plus `DEEPCHAT_REQUIRE_NATIVE_SQLITE=1` native suite: 6 files, 150 tests passed after - rebuilding the installed binding for local Node ABI 137 with the repository's CI command; no lockfile or - dependency manifest changed; -- `pnpm run test:memory:eval`: 1 file, 7 tests passed; hybrid Recall@5 `1.0`, MRR@10 `0.95`, nDCG@10 - `0.9630929753571458`; -- `pnpm run test:main:memory-perf`: the first run shared the machine with other suites and hit one recall-growth - timing assertion (`3.3584 > 3.3380`); the required isolated rerun passed 6 files and 12 tests. The same - implementation head also passed the isolated CI performance gate; -- `pnpm run test:main -- --run`: 353 files, 3850 tests passed; -- `pnpm run test:renderer -- --run`: 165 files, 1244 tests passed; -- `git diff --check`: pass. - -The local native rebuild exposed two pre-existing full-main test-harness assumptions. The Cron fixture now uses -the real temporary-directory API instead of the global mocked `fs`, and receipt assertions keep exact cardinality -plus both success/failure outcomes without assuming an order for equal timestamps. Production code and behavior -contracts are unchanged. The same implementation head is independently covered by -[PR run 29214356434, memory-native-validation job 86707391042](https://github.com/ThinkInAIXYZ/deepchat/actions/runs/29214356434/job/86707391042), -where scope, portable Memory, native rebuild/smoke/storage, retrieval eval and performance all passed. The x64 -build-check for that head also passed. This slice does not regenerate the architecture baseline; final baseline -regeneration remains assigned to `ASLR-092`. - -### ASLR-092 final close-out record - -Final validation ran on 2026-07-13 from `1e79ddc017a0e71d851f50d536b0ac59b5881105`, with -`origin/dev` verified as an ancestor. The canonical `pnpm run architecture:baseline` write produced the -schema-version-2 agent baseline from a clean relevant working tree: - -- all 24 expected current-owner and retained-boundary files exist; -- all 14 owner declarations exist exactly once; -- the 3 retired paths and 9 retired symbols/runtime-kind patterns have zero production matches; -- all DeepChat loop forbidden-import metrics are zero for Presenter, SQLite, Electron, routes and ACP; -- all eight refreshed report artifacts are included: - `agent-system-layered-runtime-baseline.json`, `archive-reference-report.md`, - `dependency-report.md`, `main-kernel-boundary-baseline.md`, - `main-kernel-bridge-register.md`, `main-kernel-migration-scoreboard.json`, - `main-kernel-migration-scoreboard.md` and `zero-inbound-candidates.md`. - The tracked `main-kernel-bridge-register.json` was also regenerated and remained byte-identical. - -Contract and storage comparison: - -- from the Phase 9 pre-Memory endpoint `c600f51b2` through the final implementation head, the route tree - remains Git object `1ac2dbd3c0aa3f91ab8a86ddf56bb3f52bea9b95`, the event tree remains - `3a62c4fa3a29e9883b39848ce26d2764360b79d5`, the SQLite tables tree remains - `98e855ee04a5a362d4b62b76ef160b2fb51851d3`, and the DuckDB sidecar remains - `cd2c68de92681b57c0075eca4aedc69750dae917`; -- `origin/dev...HEAD` has no route/event diff. Its only SQLite-table source diff exports the existing - `AgentCreateInput` and `AgentUpdateInput` TypeScript types; it changes no SQL, table identifier, migration or - stored row shape; -- `DEEPCHAT_MEMORY_INGESTION_PROJECTION_VERSION` remains `1`; -- the schema-version-2 aggregate hashes are - `38ab13b229502d09977b531d58ba33498f80b01e63656ea00b451e20fb33f594` for 59 route/event - contract files, `eb97de4264ffe061beaaaa7870051c8a808b81a056221de1f552e0c14f123e7f` for - 42 SQLite schema files and 51 table identifiers, and - `56d7d6a66e770ff389431935b67505199a7b8c01abc2fdf7be20f98bde8cf0c6` for the Memory - DuckDB sidecar. The old schema-version-1 JSON captured `8548b89a3`, before the latest `dev` baseline; its - aggregate hashes are not used as the migration diff. The Git object comparisons above prove the scoped - no-schema/no-wire result. - -Final gates: - -- `pnpm run format`, `pnpm run i18n`, `pnpm run lint`, `pnpm run typecheck`, - `pnpm run lint:architecture`, `pnpm run lint:agent-cleanup` and `git diff --check`: pass; -- architecture baseline/guard tests: 2 files, 24 tests passed; -- `pnpm run test:memory:scope`: 56 classified, 4 exempt, pass; -- `pnpm run test:memory`: 43 files, 601 tests passed; -- `pnpm run test:memory:eval`: 1 file, 7 tests passed; hybrid Recall@5 `1.0`, MRR@10 `0.95`, - nDCG@10 `0.9630929753571458`; -- `pnpm run test:main:memory-perf`: 6 files, 12 tests passed; -- native SQLite smoke plus the required native Memory suite: 6 files, 151 tests passed after the repository CI - command rebuilt the installed binding for local Node ABI 137; no lockfile or dependency manifest changed; -- `pnpm run test:main -- --run`: 354 files, 3856 tests passed; -- `pnpm run test:renderer -- --run`: 165 files, 1244 tests passed; -- `pnpm run build`: pass. Its required prebuild refresh updated the tracked provider database, ACP registry and - generated icon collection/whitelist, which are included as expected repository maintenance; -- `pnpm run e2e:smoke:ci`: 2 tests passed; a separate no-retry launch run passed 1 test; the credential-free - `09-main-ipc-boundary`, `17-acp-readonly-route` and `26-deepchat-agent-crud` run passed 3 tests. - Credential-required `02-chat-basic` and `03-session-persistence` were not configured and were not run. - -The first local E2E attempt exposed an environment transition and a real diagnostic weakness: the native -SQLite binding was still on Node ABI 137 while Electron 40 required ABI 143, and the startup alert caused -`electronApp.close()` to hide the 60-second main-window failure behind the 300-second test timeout. The binding -was explicitly rebuilt for Electron with the installed `@electron/rebuild`, after which all required E2E runs -passed. The shared Electron fixture now bounds graceful close to 10 seconds and force-kills the child before a -final bounded settle, so future startup failures retain their original cause. - -### Phase 3 composition order - -The mechanical owner moves retain the existing lifecycle ordering: - -```text -READY: presenter-initialization - -> AFTER_START: acp-registry-migration (priority 0) - -> AFTER_START: window-creation (priority 1) - -BEFORE_QUIT: mcp-shutdown (priority 5) - -> acp-cleanup (priority 6) - -> presenter-destroy (priority Number.MAX_VALUE) -``` - -`test/main/presenter/lifecyclePresenter/compositionOrder.test.ts` locks these relative boundaries. Hooks -with the same priority remain intentionally parallel under `LifecycleManager`; ASLR-033 does not impose a new -order inside a priority group. - -Focused PR gate: - -```bash -pnpm run format -pnpm run i18n -pnpm run lint -pnpm run typecheck:node -pnpm run test:main -- --run -``` - -Phase gate: - -```bash -pnpm run typecheck -pnpm run test:main -- --run -pnpm run test:renderer -- --run -``` - -Final gate: - -```bash -pnpm run format -pnpm run i18n -pnpm run lint -pnpm run typecheck -pnpm run test:main -- --run -pnpm run test:renderer -- --run -pnpm run test:main:memory-perf -pnpm run e2e:smoke:ci -``` - -If a local native binding prevents the native Memory job, CI remains required; the skipped local result is not -treated as a pass. - -## 9. Rollback policy - -- Each slice keeps one active owner; no long-lived dual writers. -- New facades/adapters may delegate backward, so rollback is code-only. -- No phase writes a new mandatory row/payload version before old readers can ignore it. -- This goal writes no new Tape lifecycle entry. Any future interaction/terminal entry requires a separate - data/behavior SDD and rollback contract. -- A failed phase is reverted before the next dependent phase starts; do not stack work on a red parity gate. -- A discovered behavior defect is recorded separately and the refactor preserves the baseline until that fix is - approved. - -## 10. Final architecture audit - -Before retirement completes, verify mechanically: - -- no import of retired presenter/runtime paths; -- no `kind=acp` session dispatch branch inside DeepChat loop/resource code; generic provider selection may - still resolve the ACP adapter for a DeepChat descriptor; -- no internal use of `agentType ?? type`; -- no shared optional capability mega-interface; -- no LoopEngine import of presenter root/Electron/route/concrete SQLite modules; -- no cross-session mutable map in LoopEngine; -- no second Tape store or raw request duplication; -- no MemoryPresenter dependency on DeepChat implementation; -- no stale active SDD plan competing with this goal. diff --git a/docs/architecture/agent-system-layered-runtime/modules/acp-runtime.md b/docs/architecture/agent-system-layered-runtime/modules/acp-runtime.md deleted file mode 100644 index 1ca9846d0d..0000000000 --- a/docs/architecture/agent-system-layered-runtime/modules/acp-runtime.md +++ /dev/null @@ -1,356 +0,0 @@ -# ACP 独立 Runtime - -> 状态:目标合同,不是 current API reference。ASLR-030..034 已收拢 ACP catalog、launch、client、 -> process、session、protocol 与 persistence owner;ASLR-070..073 已接入 typed direct runtime,并让 -> production router 只对 `kind=acp` 选择该路径。DeepChat 选择 ACP provider 的兼容路径仍保留。 -> 下文 interface、adapter 和迁移步骤是边界合同;只有实施进度明确列出的 slice 才代表当前代码。 - -## 1. 模块目的 - -ACP domain 管理 catalog/registry/install/launch/alias/debug、process/client、remote session、protocol -prompt、permission continuation、MCP 配置交付和 ACP turn metadata。`kind=acp` 共享 app session 与 -现有 message/Tape/event projection,但不共享 DeepChat 的 provider/tool round loop。 - -必须区分两个当前合法路径: - -```text -kind=acp -> direct AcpAgentInstance -kind=deepchat + providerId=acp -> DeepChat LoopEngine -> AcpAsLlmProviderAdapter -``` - -第二条在本目标内保留,因为 main route/config 和旧数据没有禁止该组合。 - -## 2. BEFORE - -当前 ACP 请求路径是: - -```text -AgentSessionPresenter - -> AgentRuntimePresenter - -> DeepChat request preparation / provider round - -> AcpProvider.coreStream() - -> ACP client/session prompt - -> external ACP agent loop -``` - -`AcpProvider` 需要把 ACP protocol event 翻译成 LLM stream event,让外层 DeepChat runtime 看起来在 -调用普通 provider。传入的 DeepChat `_tools` 并不是 ACP 工具注入来源;ACP 实际通过自己的 session -configuration/MCP 协议使用工具。 - -这造成两个问题: - -- 外层 DeepChat loop 与内层 ACP loop 的责任重叠; -- ACP 专属 process/session/mode/command/permission 状态被迫穿过 provider abstraction。 - -## 3. AFTER 的所有权 - -```text -ACP domain -├─ catalog/install/launch/alias/migration/debug boundaries -├─ provider-model refresh compatibility adapter -├─ AcpRuntimeOwner -│ └─ shared client/process/session runtime + AcpSessionController -└─ AcpAgentRuntime - ├─ AcpAgentInstance cache/lifecycle - ├─ AcpCompatibilityPromptBuilder - ├─ AcpPermissionBridge - ├─ AcpRequestTracePort -> existing provider trace persistence - └─ AcpCompatibilityProjectionAdapter -> current message/Tape/event writers - -AcpAgentInstance -├─ appSessionId -├─ agentDescriptor -├─ workdir -├─ process/client handle -├─ remoteSessionId -├─ mode/config options/commands -└─ active prompt + cancellation state -``` - -`AcpAgentRuntime` 是 keyed instance factory/cache 与 direct lifecycle;`AcpRuntimeOwner` 是 composition -级 client/session/process lifetime owner。协议运行状态由 `AcpAgentInstance` 持有。实例不 import -DeepChat Tape、compaction、Memory 或 `LoopEngine`;composition adapter 只通过窄 port 复用现有 -prompt resource 与 transcript writers。 - -“一个 ACP domain owner”不表示一个 God class。`AcpCatalogConfigAdapter`、 -registry/install/launch-spec、alias、debug route、provider model catalog/enable refresh 和 lifecycle -helper 分别保留窄 sub-owner,但不能继续散落成互不知情的第二实现。 - -Phase 3 收拢后的过渡所有权如下;后续 direct backend 只能替换 adapter 背后的实现,不能重新把这些 -职责搬回 generic presenter: - -| Concern | Transitional owner | -| --- | --- | -| ACP catalog、registry cache/icon、catalog migration | `src/main/agent/acp/catalog/` | -| install、launch spec、interactive setup terminal | `src/main/agent/acp/launch/` | -| agent-id alias compatibility | `src/shared/utils/acpAgentAlias.ts` | -| legacy config-store bridge | `ConfigPresenter/AcpCatalogConfigAdapter` boundary adapter | -| process、remote session、persistence、protocol mapping | `src/main/agent/acp/runtime/` + `client/` | -| debug actions、provider model refresh | `AcpProvider` compatibility adapter, delegated by `LLMProviderPresenter` | -| startup migration、shutdown cleanup | narrow `LifecyclePresenter` hooks delegating to ACP owners/adapters | - -## 4. Session 生命周期 - -```text -open app session - -> resolve typed ACP descriptor - manual: required command/args/env - registry: registry reference + nullable install overlay + resolved launch spec - -> resolve and validate workdir - -> create/reuse ACP client process - -> load remote session when current metadata allows - or create new ACP remote session - -> apply MCP servers and protocol config - -> publish initial mode/config/command capabilities - -send prompt - -> for regular sessions, build the current runtime/env/tool/skill/local-resource compatibility system prompt - (ACP-backed subagent keeps its current bypass) - -> create current user/assistant message and Tape projection through compatibility adapter - -> attempt the current ViewManifest write (fail-open) - -> pass rate admission and cancellation checks - -> open/reuse the ACP remote session - -> persist/mark ACP turn metadata start (`user_message_id` remains null) - -> connection.prompt(remoteSessionId, prompt) - -> map protocol events to existing stream/message/block types - -> update current structured message/Tape/event projection - -> bridge permission requests and await user decision - -> persist turn terminal state - -> update app session summary/status - -cancel/close - -> cancel active protocol prompt - -> settle terminal projection - -> release/reuse process according to current policy -``` - -load/resume/new 的判定、workdir、remote session id 复用和 process reuse 必须由现有 ACP tests/fixtures -锁定后原样迁移。 - -process exit 会立即驱逐 `AcpSessionManager` 的 live record 和 handlers,但保留 persisted remote -session id;下一次 public `getOrCreateSession` 仍按 resume -> load -> new 恢复,不能复用已退出 process 的 -connection。request timeout 与 cancel/process exit 分开结算:timeout 取消 protocol request、turn/session -进入 error;caller/process abort 的 turn 为 cancelled、session 回到 idle。 - -## 5. Backend 合同 - -```ts -interface AcpAgentRuntime { - getOrHydrate(input: AcpAgentRuntimeSessionInput): Promise - prepare(input: AcpAgentRuntimeSessionInput): Promise - send( - input: AcpAgentRuntimeSessionInput, - content: string | SendMessageInput - ): Promise - closeByAgent(agentId: string): Promise - closeAll(): Promise -} - -interface AcpAgentInstance extends AcpAgentSessionHandle { - readonly kind: 'acp' - - prepare(): Promise - updateWorkdir(workdir: string | null): Promise - setMode(modeId: string): Promise - setConfigOption(optionId: string, value: string | boolean): Promise - getModes(): { current: string; available: AcpMode[] } | null - getConfigOptions(): AcpConfigState | null - getCommands(): AcpSessionCommand[] -} -``` - -这些 ACP-only methods 是 required facet,不加入公共 `AgentSessionHandle` 变成 optional。 -`ASLR-071` 已把 mode/config/command、workdir prepare、pending queue/steer、readiness/snapshot 和 -regular/subagent production collaborators 接到 typed direct runtime;`ASLR-072` 已通过 discriminated -direct handle 将这些能力接到 app route。session-state、transcript mutation 和 Tape 仍是独立 shared -ports,不被塞回公共 handle 或 `AgentManager`。 -当前 route 与 ACP SDK 没有独立 `executeCommand` 操作证据;available commands 仍是 prompt 输入提示, -因此本设计不发明该 facet。若协议/route 后续增加命令执行能力,另立合同。 - -### ASLR-071 composition 与生命周期 - -- `LLMProviderPresenter` 创建唯一 lazy `AcpRuntimeOwner`;direct runtime 与 `AcpProvider` adapter 共享同一 - `AcpClientRuntime`、session manager、process manager、persistence、prompt controller 和 content mapper; -- provider disable/remove/rebuild 只清理 adapter-local permission continuation,不关闭 shared owner; -- rate admission 仍以一个 ACP provider state 保持全局 QPS/last-request order,只给 queued item 标记 - `provider` / `acp-direct` scope;compatibility adapter retirement 只 reject `provider` waiter,不删除 state - 或影响 direct waiter; -- agent refresh 顺序固定为 direct instance close -> remote session clear -> process release;global shutdown - 顺序固定为 lifecycle fence -> direct hydration/prepare/send drain + closeAll -> session clearAll -> process - shutdown,且 root lifecycle 是唯一全局关闭点;fence 开始后 lazy client/instance materialization 一律拒绝; - session-scoped initialization abort 会让共享同一 conversation 的 caller 一致失败,不等待挂起的 - getConnection/resume/load/new RPC;late SDK settlement 由 epoch fence 消费并清理,不得重新注册 handler、 - 写 persistence 或发布 capability;process manager 的 fence 在 shutdown 入口同步生效,不等待未决 - spawn/warmup,late handle 只进行幂等、identity-safe disposal;旧 handle 的延迟 unbind 只清理旧进程, - 不得移除或终止已替换的 bound handle; -- `AcpSessionController` 统一 open/prepare/workdir、mode/config/commands、capability events 与 - session-info/usage metadata mapping,compatibility provider 不再保留第二份 mapper;process initialization - 在 session record publish 前 flush 的 update 按 remote session 和 restore attempt 隔离;失败的 - resume/load attempt 整组丢弃,只有成功 record 对应 attempt 的 update 在 record 可见后按原序 - map/persist/publish; -- direct runtime 使用现有 `PendingInputCoordinator`,没有第二份 queue/store;idle initial queue 可直接 - claim,active steer promotion 等待旧 turn terminal 后按 steer-first drain,prepare 中 steer 保持 queued; -- descriptor/config identity 变化时严格拒绝当前 open,不关闭、替换或复用旧 instance;malformed - session id、kind、descriptor/config id/source、command 或 scope 同样在 hydration 前失败;并发 hydration - 按 app session single-flight, - close/prepare-cleanup 即使 session clear 失败也 finally 驱逐 identity-matched cache;prepare-only process - exit 同样只驱逐 live instance,保留 durable remote-session binding 供下一次 hydrate 恢复; -- regular prompt 保持当前 runtime/tool/skill/local-resource description,ACP-backed subagent 保持空 system/ - local-tool isolation;direct ACP 不新增 DeepChat callable tools、skills 或 Memory。 - -### ASLR-072 production route - -- root composition 对 `kind=acp` 只创建 `DirectAcpSessionBackend`,不存在 legacy/direct 双发; -- 每次 direct operation 从 app session、canonical alias、strict executable descriptor 和当前 enabled ACP - config 重建 input,id/source/command 不一致时返回 `AgentUnavailable`,不 fallback 到 DeepChat; -- app session 初始化仍写现有 shared state,网络 prompt、prepare、pending/steer、mode/config/commands、 - permission continuation、generation cancel 和 close 只由 direct ACP handle 驱动; -- lightweight session list 只读现有 state projection,不 materialize `AcpAgentInstance`; -- title 读取现有 structured transcript,并调用 `AcpProvider.summaryTitles`;primary direct turn 不因此再走 - compatibility provider; -- permission response 从持久化 assistant action block 取得 ACP request id,直接 resolve 当前 instance 的 - permission bridge; -- failed subagent initialization 只允许一次新 app-session-id retry,失败 id 的 runtime、shared state、 - pending binding 与 app session row 先清理; -- delete cleanup 不解析 descriptor/current launch config,也不 materialize owner/process。它关闭已存在的 - direct instance,或仅在 owner 已存在时清理 session-controller live binding,并始终通过 data port 删除 - `acp_sessions` durable remote binding;因此 missing/malformed/disabled catalog row 不会阻塞删除; -- ACP -> DeepChat 在 target validation、target context、app-session ownership update 和 state rebuild 成功后 - 才关闭旧 direct runtime;ACP target 在任何 mutation 前拒绝; -- `kind=deepchat + providerId=acp` 的 workdir、commands/config、permission、clear 和 prompt/resource branches - 继续使用 compatibility provider;`ASLR-073` 已将前述 session operations 迁到显式 - `AcpAsLlmProviderSessionControlPort` / `AcpAsLlmProviderPermissionPort`,admin routes 使用独立 - `AcpProviderAdminPort`。generic `ILlmProviderPresenter` 只保留普通 provider 能力。 - -## 6. MCP 与 tools - -ACP 通过 `AcpMcpDeliveryAdapter` 获取当前 agent/session 允许的 MCP server definitions,并转换为 ACP -protocol 所需配置。它不接收 DeepChat 的 `ToolDefinition[]`,也不调用 DeepChat local tool dispatcher。 - -```text -McpPresenter (owner) - -> agent/session policy query - -> AcpMcpDeliveryAdapter - -> ACP session config / protocol -``` - -server selection、manual/auto config、disabled state 和当前 collision/identity 语义必须保持。若 ACP -agent 自身再决定工具调用,那属于外部 ACP loop。 - -ACP provider 仍忽略 `_tools` 数组,但当前 regular ACP path 已经由外层 DeepChat runtime 把 -runtime/env/tool/skill/local-resource 描述放进首个 system message,`AcpMessageFormatter` 会把它发给 -ACP。direct backend 必须用 `AcpCompatibilityPromptBuilder` 重现这份 prompt;DeepChat + ACP-provider -路径继续由 DeepChat prompt assembly 产生。ACP-backed subagent 的当前 bypass 必须保留。以后若要删除 -这些兼容描述,另立行为 spec。 - -## 7. Permission 与输出映射 - -ACP permission request 的 continuation 由 `AcpPermissionBridge` 保存和恢复。它可以复用 renderer -现有 decision UI/output types,但不能被转成 DeepChat `tool_call` 后交给 DeepChat permission gate。 - -必须保留: - -- request id 到 active ACP continuation 的一一映射; -- allow/deny/cancel/timeout 的当前协议响应; -- renderer 丢失、session close 与 process exit 时的清理; -- regular ACP session 与 ACP-backed subagent 当前不同的展示/完成语义; -- protocol event 到 message/turn/status 的顺序和去重。 - -## 8. Transcript / Tape compatibility projection - -`acp_turns` 只有 turn id/status/stop reason 等 metadata,不能替代外层 runtime 当前写入的 user、 -assistant、tool/block、Tape、search/export projection。direct backend 必须通过一个 adapter 复用现有 -writer: - -```text -start prompt - -> ensure Tape/bootstrap under current rule - -> create user fact/projection - -> create mutable assistant projection - -> attempt ViewManifest at the current point (fail-open) - -> map/apply protocol events and refreshes - -> finalize assistant/tool facts and terminal status once -``` - -adapter 不运行 DeepChat provider/tool loop,也不发明新 transcript union;它只复现当前持久化/输出 -副作用。每种 protocol event 的 create/update/finalize 顺序由 golden fixture 锁定。 - -terminal projection 复用当前 ACP stop-reason mapper 和 DeepChat accumulator/settlement:prompt reject 先 -写入 `ACP: ...` error event,空 `end_turn` 写现有 `common.error.noModelResponse`,再由同一 writer 一次性 -落 final blocks、message status、Tape fact 和 completed/failed event,不能另开 direct-only error path。 - -direct backend 还必须调用 `AcpRequestTracePort`,它裁剪自当前 provider trace writer: - -```text -ViewManifest attempt - -> persist ACP turn start + current debug request event - -> trace write attempt: - endpoint=acp://session/prompt - headers={} - body={ sessionId: remoteSessionId, prompt } - -> connection.prompt -``` - -trace 继续使用现有 message/request correlation、redaction/truncation 和 opt-in context。persist failure -只 warning、保持 fail-open,不能阻止 ACP prompt;不能把 raw trace body复制进 Tape。 -旧 provider-private mock 通过主动 reject 证明 trace failure 的测试不是 production writer 合同;真实基线是 -base trace emitter / direct trace adapter 捕获 persistence error 后 warning 并继续 prompt。 - -## 9. 兼容迁移 - -直接删除 `AcpProvider` 风险过高,使用 adapter strangler: - -1. 为 `AcpProvider` 输入、protocol event、transcript、permission、terminal status 建 golden fixtures。 -2. 把 ACP client/process/session/persistence 代码收拢到新的 ACP owner,旧 provider 先委托它。 -3. 引入尚不被 production `AgentManager` 选择的 typed `AcpAgentInstance` slice,以 causal fake、 - prompt/projection/trace/permission golden fixture 验证边界;旧 `AgentRuntimePresenter` 路径保持原状。 -4. 收拢 Config/Provider/Lifecycle 中的 ACP registry/install/launch/alias/debug/model-refresh 边界。 -5. 建 `AcpCompatibilityPromptBuilder`、`AcpCompatibilityProjectionAdapter` 和 `AcpRequestTracePort`, - 分别锁定 regular/subagent prompt差异、现有 message/Tape/event projection 和 trace-before-prompt - parity。 -6. 让 `AgentManager` 仅对 `kind=acp` session 直接调用 `AcpAgentRuntime`。(`ASLR-072` 已完成) -7. 从 generic provider contract 迁出 ACP-only session/permission/admin operations,并删除不可达的 legacy - ACP backend glue。(`ASLR-073` 已完成) -8. 在 compatibility adapter 中继续生成旧 route/event DTO。 -9. 从 DeepChat runtime 删除 ACP-agent-session state/permission branches,但保留 generic ProviderPort 到 - `AcpAsLlmProviderAdapter` 的 DeepChat + ACP-provider 路径。 - -任何阶段都不能同时改变 ACP protocol SDK 版本、process policy 或 UI schema。 - -## 10. 失败、取消与恢复 - -- install 不可用、spawn 失败、handshake 失败、load session 失败使用当前可观察错误映射; -- remote session load 的 fallback 只能沿用当前规则,不能无条件创建新 session 隐藏错误; -- process exit 先终止 active continuation,再写 terminal projection,避免 permission 永久 pending; -- cancel 必须区分用户取消、app shutdown 和 protocol failure,外部 payload 保持兼容; -- close 是幂等的,重复 process exit/event 不得产生双 terminal turn; -- ACP turn persistence 与 transcript output 的相对顺序由 golden fixture 锁定。 -- projection adapter 的 partial failure 按当前 message/Tape recovery 处理,不能只写 `acp_turns` 后假装 - transcript 完整。 - -## 11. 验证矩阵 - -- manual、registry、installed、disabled、missing binary agent; -- manual required command/args/env 与 registry reference/nullable install overlay; -- new/load/resume remote session; -- workdir 正常、缺失、无权限和变化; -- mode/config option/command capability 更新; -- text、thought、tool/progress、plan、error、complete 等现有 protocol event; -- permission allow/deny/cancel/timeout/window close/process exit; -- cancel before prompt、during stream、during permission; -- regular session、ACP-backed subagent、transfer/import/restore; -- MCP zero/one/many servers 与 config update; -- renderer event、persisted ACP turns、app status 的迁移前后 snapshot parity; -- restart/history/search/export 与 structured message/Tape facts parity; -- `kind=deepchat + providerId=acp` regular compatibility prompt/resources 和 subagent bypass parity; -- provider catalog/enable refresh/debug route/trace/process refresh parity。 - -## 12. 明确不做 - -- 不把 ACP loop 重写成 DeepChat Tape loop; -- 不给 direct `kind=acp` backend 新增可调用的 DeepChat local tools、skills 或 Memory;但保留 regular - ACP 与 DeepChat + ACP-provider 已有 system-prompt resource descriptions; -- 不合并 ACP remote session id 与 app session id; -- 不在本次重构改变 ACP SDK、安装方式或协议语义; -- 不因为拆分而修正已有 regular/subagent 行为差异。 -- 不实现没有 route/SDK 证据的 ACP `executeCommand` facet。 diff --git a/docs/architecture/agent-system-layered-runtime/modules/agent-manager.md b/docs/architecture/agent-system-layered-runtime/modules/agent-manager.md deleted file mode 100644 index 1ed268666f..0000000000 --- a/docs/architecture/agent-system-layered-runtime/modules/agent-manager.md +++ /dev/null @@ -1,268 +0,0 @@ -# AgentManager 与顶层控制面 - -> 状态:目标合同,不是 current API reference。ASLR-020..026 已接入 catalog/app-session lookup、explicit -> kind router、typed handles、required facets 和 manager ports;ASLR-090 已删除 unified implementation 与 -> reflection legacy backend。下文类型与伪代码用于说明边界,不保证与 concrete API 逐项同名。 -> 上位合同:[总体设计](../README.md) · [规格](../spec.md) · -> [迁移与验证](../migration-and-validation.md) - -## 1. 模块目的 - -`AgentManager` 是 agent 子系统的应用控制面,只回答三个问题: - -1. 当前有哪些 agent,它们分别是什么 kind; -2. 一个 app session 应该由哪个 backend 打开和处理; -3. route、remote、cron、subagent 等入口怎样获得稳定且兼容的 session 操作。 - -它不是资源总管,也不是第三套 runtime。MCP、skills、memory、provider、plugin 仍由各自 Presenter -拥有。`AgentManager` 只持有它们暴露出来的窄引用,或者把引用交给具体 backend。 - -## 2. BEFORE - -当前顶层入口分散在三个位置: - -- `AgentSessionPresenter` 同时承担 route façade、session application service、kind dispatch 和大量 - DeepChat/ACP 专属操作; -- `AgentRegistry` 的生产注册表只有 `deepchat`,但 `resolveAgentImplementation()` 会把 - `deepchat` 和 `acp` 都解析为同一个 `AgentRuntimePresenter`; -- `ConfigPresenter -> AgentRepository` 才是真正给 UI 提供 agent catalog 的路径。 - -因此 registry、catalog 和 runtime routing 是三套不同事实源。`IAgentImplementation` 为了容纳双方 -能力,把大量方法设成 optional;调用方只有在运行时检查“有没有这个方法”。 - -当前简化关系: - -```text -route -> AgentSessionPresenter - ├─ AgentRegistry (one implementation) - ├─ ConfigPresenter -> AgentRepository (actual catalog) - └─ AgentRuntimePresenter (both kinds) -``` - -问题不在于缺少更多接口,而在于公共接口覆盖了并不公共的能力。 - -## 3. AFTER 的所有权 - -```text -AgentManager -├─ ExecutableAgentCatalog (strict lookup only) -├─ AppSessionLookupPort -└─ AgentBackendSet - ├─ DeepChatSessionBackend - └─ AcpSessionBackend -``` - -`AgentManager` 负责: - -- 按 canonical alias 查询 capability-strict executable descriptor; -- 根据 app session 持久化的 agent identity 解析明确 kind; -- 显式选择 backend 并返回 discriminated session handle; -- 暴露 transfer source/target、subagent 和 generation 等 required facet。 - -`AgentManager` 不负责: - -- agent CRUD、legacy DTO 映射或 catalog notification;这些属于 repository/config boundary; -- transcript、Tape、session state 或 Memory data access;这些是独立 shared ports; -- DeepChat provider/tool round; -- ACP process/protocol loop; -- prompt 拼装、Tape、compaction、Memory 策略; -- MCP server、skill、provider 或 plugin 的生命周期; -- Electron window 渲染与具体 SQLite statement。 - -## 4. 最小内部合同 - -```ts -type AgentDescriptor = DeepChatAgentDescriptor | AcpAgentDescriptor - -interface AgentDescriptorBase { - id: string - kind: 'deepchat' | 'acp' - name: string - enabled: boolean -} - -interface DeepChatAgentDescriptor extends AgentDescriptorBase { - kind: 'deepchat' - config: DeepChatAgentConfig -} - -interface AcpAgentDescriptorBase extends AgentDescriptorBase { - kind: 'acp' - source: 'manual' | 'registry' -} - -interface AcpManualAgentDescriptor extends AcpAgentDescriptorBase { - source: 'manual' - launch: { - command: string - args: string[] - env: Record - } -} - -interface AcpRegistryAgentDescriptor extends AcpAgentDescriptorBase { - source: 'registry' - registry: AcpRegistryReference - installState: AcpInstallState | null -} - -type AcpAgentDescriptor = AcpManualAgentDescriptor | AcpRegistryAgentDescriptor -``` - -内部 executable descriptor 只使用 `kind`。当前 DTO 中的 `type`、`agentType?` 只允许存在于 boundary -codec。catalog list 对 malformed legacy row 保持当前宽容/null/default/filter 语义;真正 open backend 时 -若 required capability 不成立,返回 typed `AgentUnavailable`,不能 fallback 到 DeepChat。 - -双方真正共享的 active-session 合同保持很薄: - -```ts -interface AgentSessionHandle { - readonly sessionId: AppSessionId - readonly kind: 'deepchat' | 'acp' - readonly lifecycle: AgentSessionLifecycleFacet - readonly pending: AgentPendingInputFacet - readonly settings: AgentSessionSettingsFacet - readonly toolInteractions: AgentToolInteractionFacet - - send(input: AgentSessionSendInput): Promise - cancel(): Promise - snapshot(options?: { lightweight?: boolean }): Promise - waitForFirstTurnReady(options?: { timeoutMs?: number }): Promise - close(): Promise -} -``` - -公共 facet 只收纳两边已经存在且 route 需要的共同操作;kind-specific 能力继续通过 discriminated -required facet 暴露: - -```ts -interface DeepChatSessionHandle extends AgentSessionHandle { - kind: 'deepchat' - deepchat: DeepChatControlFacet -} - -interface DirectAcpSessionHandle extends AgentSessionHandle { - kind: 'acp' - acp: DirectAcpControlFacet -} -``` - -`AgentManager` 本身不 proxy transcript/Tape,也不做 method-name lookup。DeepChat backend 显式组合 -`DeepChatAgentRuntime` 和 required presenter port;production ACP backend 则直接组合 `AcpAgentRuntime`。 -composition root 负责 backend wiring 和 `AcpAgentRuntime` 构造;作为保留的 DeepChat state/delegate -façade,`AgentRuntimePresenter` 继续初始化 `DeepChatAgentRuntime`。runtime 对象与 instance 的实现所有权仍在 -`agent/deepchat` 和 `agent/acp`,manager 不会重新长成 generic optional-method façade。 - -## 5. 路由规则 - -任何 session 入口都执行同一套解析顺序: - -```text -validate route DTO - -> load app session agentId (`new_sessions.session_kind` is only regular|subagent) - -> load current typed AgentDescriptor by agentId - -> verify any already-hydrated backend binding still matches the current descriptor - -> switch (descriptor.kind) - deepchat -> DeepChatAgentBackend - acp -> AcpAgentBackend - -> expose common or required kind-specific facet -``` - -禁止以下兼容捷径: - -- 按 agent id 字符串猜 kind; -- unknown kind 默认走 DeepChat; -- backend 缺方法时静默 no-op; -- 读取 mixed `Agent` 后在下游反复判断 optional fields; -- 为了满足 `kind=acp` 公共 backend 入口而把 ACP agent session 再包装回 `LLMProvider`;这不禁止下述 - DeepChat provider compatibility adapter。 - -`descriptor.kind` 与 DeepChat provider selection 正交:`kind=deepchat + providerId=acp` 是当前支持的 -组合,仍由 DeepChat backend 打开,再由 generic ProviderPort 选择 ACP compatibility adapter。 -`kind=acp` 才进入 direct ACP backend。 - -## 6. 与入口的关系 - -### Renderer / typed routes - -迁移期间 `AgentSessionPresenter` 仍保留公开 route façade,方法签名不变。方法体逐组变成:验证输入、 -调用 `AgentManager` 或 kind-specific backend、映射输出。最后才决定是否改名或删除 façade。 - -### Remote、cron 与 subagent - -这些入口不能绕过 manager 自行构造 runtime。它们传递明确的 `agentId`、`sessionId` 与 kind-specific -input,复用同一 descriptor/session 解析。现有 subagent 在 DeepChat 与 ACP 下的差异继续由对应 -backend 负责。 - -### Transfer - -`new_sessions` 没有 backend kind column。ACP -> DeepChat transfer 在一个明确 application operation 中: -先验证 target descriptor/provider/model 并拒绝所有 ACP target;然后在当前 DeepChat state owner 写入 target -context、更新 `agent_id`/project/tool policy 并重建可读 state。只有这些 commit steps 全部成功后,才关闭 -旧 direct ACP runtime;commit 前失败保留旧 runtime。legacy DeepChat + ACP-provider source 仍在相同 -post-commit 位置清 compatibility binding。该顺序由 transfer fixtures 锁定,不新增 `session_kind` 含义。 - -### Catalog 通知 - -catalog 的变更通知按 domain 发送: - -- DeepChat CRUD 只发送 DeepChat/catalog change; -- ACP registry/install/manual CRUD 只发送 ACP/catalog change; -- 若 UI 需要统一刷新,再由 `AgentCatalog` 聚合成通用 catalog revision。 - -不得继续用 `notifyAcpAgentsChanged()` 表示所有 agent 变化。 - -## 7. 错误、取消与关闭 - -- descriptor 缺失、kind 不匹配、backend 不可用使用明确 error code; -- 单个 malformed legacy catalog row 不让整批 list 失败;backend open 才执行 capability-strict decode; -- manager 不吞掉 backend 错误,只在 boundary 做当前 route 需要的错误映射; -- `cancel()` 委托当前 session instance,不能靠 manager 扫描不同 backend 的内部 Map; -- `close()` 是幂等操作,先阻止新输入,再由 backend 完成 run/process/drain 清理; -- delete 是 descriptor-independent cleanup,不是普通 routed `close()`:manager 不读 app session/catalog, - 而是对 DeepChat 与 ACP backend 各调用一次 non-hydrating cleanup,并等待两者 settle。direct ACP cleanup - 无论 live instance 是否存在都删除 durable remote binding;façade 随后按 shared state、permission、skills、 - app row 的顺序清理。runtime cleanup 失败时仍尝试 durable/shared cleanup,再保留原错误策略; -- app shutdown 不由 manager 枚举 handle;composition-owned runtime owner 关闭 direct instances,再清 - shared ACP sessions/processes,DeepChat/runtime/Memory 各由现有 owner 关闭。 - -## 8. 迁移步骤 - -1. 为当前 mixed DTO 写 characterization tests 与 boundary codec。 -2. 引入 discriminated `AgentDescriptor`,先不删除旧 route types。 -3. 从 `AgentRepository` 提取两个 typed repository view,物理表不变。 -4. 引入 `AgentCatalog`,让旧 `ConfigPresenter` 委托给它。 -5. 引入 `AgentManager`,仅接管 catalog 和 backend resolution。(`ASLR-020` 已完成) -6. 让 `AgentSessionPresenter` 的公共 open/send/cancel/close/snapshot 委托 typed handle。(`ASLR-022` - 已完成) -7. 按能力组迁移 kind-specific routes,删除 façade 内 optional capability checks。(`ASLR-023..026`、 - `ASLR-072` 已完成) -8. 两个 backend 均独立后删除 generic provider ACP methods 和 legacy ACP backend glue。(`ASLR-073` - 已完成) -9. 删除 DeepChat legacy backend、session-handle `runtimeKind` 分支和 `IAgentImplementation`。 - (`ASLR-090` 已完成) -10. façade 全部变成薄 adapter 后,再单独评估命名和目录移动。 - -每一步都必须保持旧 route、event、DTO 和数据库 schema 可用。 - -## 9. 验证 - -- 同一 catalog 返回的 id/name/enabled/config/install state 与基线一致; -- legacy `type` / `agentType` 的所有有效组合转换结果一致; -- manual ACP required launch fields 与 registry ACP reference/install overlay 分别 round-trip; -- malformed config/state JSON、missing manual command、invalid source×kind 和 source/id collision 保持当前 - list/default/filter/winner 行为; -- invalid/unknown combination 不会被错误路由; -- DeepChat session 只创建 DeepChat backend;ACP session 只创建 ACP backend; -- DeepChat descriptor 选择 ACP provider 仍创建 DeepChat backend,并保留 `MessageStartResult`; -- route、remote、cron、regular subagent、ACP-backed subagent 的 backend 选择有表驱动测试; -- delete/clear/cancel/close 重复调用保持幂等; -- DeepChat CRUD 不再误发 ACP-only 通知,同时 renderer 的统一刷新不回归。 - -## 10. 明确不做 - -- 不创建第三种 generic agent plugin API; -- 不让 renderer 直接依赖内部 descriptor; -- 不在本模块统一 DeepChat/ACP 的专属配置; -- 不更改 catalog/session table; -- 不借重构修复已有业务行为差异。 diff --git a/docs/architecture/agent-system-layered-runtime/modules/deepchat-agent-instance.md b/docs/architecture/agent-system-layered-runtime/modules/deepchat-agent-instance.md deleted file mode 100644 index 1fd0b108a2..0000000000 --- a/docs/architecture/agent-system-layered-runtime/modules/deepchat-agent-instance.md +++ /dev/null @@ -1,224 +0,0 @@ -# DeepChatAgentInstance - -> 状态:目标设计,不是 current API reference。一个 active/hydrated DeepChat app session 对应一个实例; -> 下文 class/interface 与流程伪代码描述目标边界,当前事实只以“实施进度”和 current architecture docs -> 明确列出的 slice 为准。 - -> 实施进度:ASLR-040..046 已接入 lazy runtime/instance shell,并迁入 identity、project、effective -> generation settings、status、first-turn readiness、pre-stream abort controller、active generation 与 -> stale-run guard、pending drain/steer merge state,以及 ordered interaction projection、response/resume -> guards、deferred-tool cancellation、live provider-permission continuation、message-scoped runtime skill -> selection、prompt/tool snapshot caches 和 compaction in-flight projection。持久 pending rows 已归入 -> `agent/deepchat/pending`,持久 interaction blocks、Skill/Tool/MCP owners、configured selections 和 -> persisted summary 仍是事实源。ASLR-090 让 typed DeepChat backend 直接组合 runtime/instance 与 required -> presenter port,并删除 reflection-based legacy backend;instance 只持有 stable Memory session handle。 -> Memory kernel/schema 保持在 `MemoryPresenter`,cursor/queue/epoch orchestration 已迁到 -> `MemoryRuntimeCoordinator`,既有 trigger policy 保持不变。 - -## 1. 模块目的 - -`DeepChatAgentInstance` 把当前 singleton 中按 `sessionId` 分散保存的状态收回到真实实例。它管理 -session 的长期运行态和 turn 排队,但不实现 provider/tool loop;每次 turn 的短期状态交给 -`LoopRun`。 - -## 2. BEFORE - -`AgentRuntimePresenter` 通过几十个 `Map` 保存 generation、abort、pending、permission、 -skill、Memory 和 provider recovery 等状态。方法接收 `sessionId` 后自行查多个 Map,状态创建与清理 -分散在 `processMessage`、`processStream`、`dispatch` 和 session routes 中。 - -这会产生四类负担: - -- 无法从对象边界判断一个 session 当前拥有什么; -- close/delete/clear 需要记住清理所有 Map; -- turn-local 状态容易泄漏成 session-global; -- unit test 必须构造整个 singleton 和大量 Presenter。 - -## 3. 实例边界 - -```ts -class DeepChatAgentInstance implements AgentSessionHandle { - readonly kind = 'deepchat' - readonly sessionId: AppSessionId - - private effectiveConfig: EffectiveDeepChatConfig - private status: DeepChatSessionStatus - private preStreamAbortController?: AbortController - private activeRun?: LoopRun - private pendingInputs: PendingInputQueue - private pendingInteractions: PendingInteraction[] - private sessionResources: SessionResourceSelection - - send(input: AgentInput): Promise - steer(input: AgentInput): Promise - retry(input: RetryInput): Promise - cancel(reason?: string): Promise - snapshot(): Promise - close(): Promise -} -``` - -构造函数只接收 session-scoped identity/config 和窄 ports。不得接收整个 Presenter registry 或 Electron -window。 - -## 4. 状态所有权 - -| 状态 | Owner | 说明 | -| --- | --- | --- | -| effective agent/session config | instance | config revision 变化时明确 refresh | -| session status | instance | idle/generating/waiting/error 等兼容状态 | -| pending/steer input queue | instance | 跨当前 turn 等待下一执行点 | -| ordered pending interactions | instance | 同一 tool batch 可有多个;响应首项后其余继续 pending | -| selected skills/MCP/extensions references | instance | 保存选择引用,不拥有资源内容 | -| pre-stream abort/status | instance | 在 tools/prompt/compaction/context 的长 await 前注册,但不冒充 active generation | -| active generation | instance -> `LoopRun` | assistant placeholder/context 之后才按当前时点注册 | -| abort signal、per-attempt requestSeq、outer providerRoundCount、round messages | `LoopRun` | turn 完成必须释放 | -| provider recovery/overflow flags | `LoopRun` | 不跨 turn 泄漏 | -| Tape/message/trace data | stores through ports | 不复制到实例作为事实源 | -| Memory rows/vector/maintenance | `MemoryPresenter` | instance 不拥有 | -| Memory chains/epochs/cooldown/access dedupe/cursor orchestration | runtime-scoped `MemoryRuntimeCoordinator` | instance 只保留 session handle | - -pending input 的事实源仍是现有 SQLite rows。`agent/deepchat/pending` 中的 store/coordinator 负责持久化、 -claim/order/recovery 与通知;instance 只持有 drain single-flight 和 rapid-steer merge id,避免把 rows -复制成第二份内存状态。 - -## 5. Hydration 与缓存 - -实例采用 lazy hydration:只有被 open/send/snapshot 的 session 才创建。hydration 顺序固定: - -```text -load app session metadata - -> verify kind=deepchat and agent descriptor - -> load effective config/resource selections - -> inspect persisted generation/interaction recovery state - -> ensure Tape bootstrap when operation requires it - -> publish ready snapshot -``` - -runtime 可以缓存 active instances,但 cache 不是事实源: - -- cache key 是 `AppSessionId`; -- concurrent `open()` 对同一 id 必须 coalesce; -- open/send/snapshot 等入口使用 `getOrHydrate()`;stale completion、cleanup 与只读 status guard 使用 - `getHydrated()`,不得在 close/delete 后重新创建空实例; -- idle eviction 只能发生在无 pre-stream work、active run、pending input、pending interactions 和 - background close fence 时; -- eviction 不删除持久化数据; -- delete/clear 显式进入 closing fence,阻止随后 lazy recreate 使用旧 epoch。 - -## 6. 输入与并发 - -`send()` 不直接展开 loop 细节: - -```text -validate instance state - -> apply current pending/steer policy - -> claim one input - -> register status + pre-stream AbortController - -> prepare resources/Tape/compaction/user/context and assistant placeholder - -> create/register LoopRun at the current active-generation point - -> await LoopEngine provider/tool rounds - -> commit instance-visible terminal status - -> drain next pending input when current rules allow -``` - -公开 `send()` 保留现有 `MessageStartResult`,包括 queued path 允许 `requestId/messageId=null` 的语义。 -普通 send 与 create-session initial send 当前等待/非阻塞差异也由 route fixtures 锁定,不能被一个新 -handle 偷偷统一。 - -必须保持当前规则:输入何时作为 pending、steer 如何影响 active generation、retry/rollback 从哪个 -projection/Tape point 开始、draft session 何时转成正式 session。重构不把它们统一成新的 queue 语义。 - -## 7. `LoopRun` 分界 - -```ts -interface LoopRun { - readonly runId: GenerationId - readonly sessionId: AppSessionId - readonly input: ClaimedAgentInput - readonly abortController: AbortController - readonly startedAt: number - - phase: LoopPhase - requestSeq: number - providerRoundCount: number - roundMessages: ProviderMessage[] - mutableAssistant?: MutableAssistantProjection - pendingToolBatch?: ToolBatch - pendingInteractions: PendingInteraction[] - terminal?: LoopTerminalOutcome -} -``` - -`LoopEngine` 可以修改 `LoopRun`,但不能持有跨 session Map。instance 只保留 active run reference;run -terminal 后先完成所需 commit,再清除 reference。 - -interaction pause 会 settle/clear 当前 run。用户响应时先按 origin 处理/持久化第一项;仍有 interaction -就保持 paused,不创建 run。只有最后一项解决后,instance 才从持久 message/Tape context 重建输入并 -注册一个新的 resume `LoopRun`;`originRunId` 只用于审计/stale guard,不是恢复旧 call stack 的 token。 - -## 8. 资源刷新 - -instance 保存 selection/revision,不缓存资源 owner 的可变内部对象: - -- skill catalog/content 由 `SkillPresenter` adapter 按 revision 读取; -- MCP/tools 由 `ToolPresenter`/MCP adapter 解析; -- provider config 由 provider port 解析; -- Memory 由 Memory adapter 按 session/persona/query 调用。 - -refresh 触发点必须与当前 turn/round 顺序一致。工具执行激活 skill 后,需要在下一 provider round 前刷新 -prompt/tools,不能延迟到下一 turn,也不能每个 token 重查。 - -## 9. 关闭与 epoch fence - -`close()` 与 data delete 不等价: - -1. 标记 closing,拒绝新输入; -2. cancel pre-stream work/active run,并处理全部 ordered pending interactions; -3. 按当前策略 settle/drain 或丢弃 pending inputs; -4. 等待 required Tape/output commits; -5. 与 Memory background queue 建立 drain/fence; -6. 释放 instance state。 - -clear/delete/rollback/retry 会改变 session lineage 时,必须推进现有 epoch/rewind 语义,防止旧 Memory -job 或旧 async callback 回写新 lineage。 - -## 10. 迁移步骤 - -1. 列出并测试 singleton 中全部 session-keyed maps 的创建/读取/清理点。 -2. 引入 instance shell,但最初委托旧 runtime 方法。(ASLR-040 已完成) -3. 先迁移 identity/config/status 等纯 session state。(ASLR-041 已完成) -4. 保留 pre-stream abort 与 active-generation 的当前注册边界,将两者迁入 instance。(ASLR-042 - 已完成;全局 run-id sequence 暂留 compatibility runtime,`LoopRun` 与 request/round/overflow state - 属于 ASLR-050) -5. 迁移 pending drain/steer merge state,并将持久 queue store/coordinator 归入 DeepChat pending - 模块。(ASLR-043 已完成) -6. 把 permission/question/post-call/skill-draft continuation 收敛为 instance ordered interaction state, - 保留 fresh-run resume。(ASLR-044 已完成基础 state ownership;ASLR-056 已完成 typed batch outcome、 - origin/order 与 persisted execution-state ownership) -7. 迁移 message-scoped runtime skill selection 与 prompt/tool snapshot caches;resource owner revision - 只使对应 snapshot 失效,不复制 Skill/Tool/MCP owner 内部状态。(ASLR-045 已完成) -8. 迁移 compaction in-flight projection;persisted summary 继续作为事实源。ASLR-046 当时只给 instance - stable Memory session handle;ASLR-059 后续把四组 orchestration state 迁到唯一 coordinator。 -9. 让 route/runtime cache 只通过 instance 操作 session。(`ASLR-090` 已完成 typed backend 接线) -10. 对每个旧 Map 做“无读写引用”检查后逐个删除。 -11. 最后把 loop orchestration 迁入 `LoopEngine`;Memory 接线在其他状态稳定后迁移。 - -## 11. 验证 - -- 同一 session concurrent open 只生成一个实例; -- 不同 session 的 pending、permission、abort、tool、provider recovery 状态隔离; -- normal、retry、regenerate、steer、pending drain、draft promotion 的序列 parity; -- multiple interactions 同批排序、逐项响应、中间项保持 paused、最后一项后的 fresh resume run parity; -- cancel at prepare/provider/tool/permission/finalize 各阶段均可终止且无悬挂状态; -- close/clear/delete 后不存在 active timers、continuations 或 background writes; -- idle eviction 后 rehydrate 的 snapshot 与未 eviction 一致; -- singleton map leak regression test:大量 open/close 后 instance/runtime state 回到基线。 - -## 12. 明确不做 - -- 不让 instance 变成包含所有 Presenter 的 service locator; -- 不在实例内复制 Tape、message 或 Memory 数据库; -- 不把 ACP 做成 `DeepChatAgentInstance` subtype; -- 不新增 actor framework、DI container 或通用 state machine library; -- 不在状态迁移时修改现有 pending/steer/retry 行为。 diff --git a/docs/architecture/agent-system-layered-runtime/modules/loop-engine-and-lifecycle.md b/docs/architecture/agent-system-layered-runtime/modules/loop-engine-and-lifecycle.md deleted file mode 100644 index f913a0b725..0000000000 --- a/docs/architecture/agent-system-layered-runtime/modules/loop-engine-and-lifecycle.md +++ /dev/null @@ -1,317 +0,0 @@ -# LoopEngine 与可等待生命周期 - -> 状态:目标设计,不是 current API reference。此 loop 只属于 `kind=deepchat` session,其中 provider -> 仍可按兼容合同选择 ACP。下文 stage/type 伪代码表达生命周期合同;当前 concrete API 以实施进度和 -> `src/main/agent/deepchat/loop/` 为准。 - -> Current ownership: `TurnCoordinator` owns initial/resume preparation, -> `DeepChatLoopRunner` owns provider-attempt execution and context-pressure recovery, and -> `DeepChatLoopEngine` remains the inner provider-round/tool-batch decision engine. The presenter is -> the composition root and compatibility façade; session/run state remains in -> `DeepChatAgentInstance`/`LoopRun`. -> -> Implementation progress: ASLR-050 introduced the per-turn `LoopRun` and narrow provider, tool, -> Tape, output, and context port contracts. That slice left the legacy `processStream` control flow -> unchanged while moving provider-round state, provider-attempt sequencing, and recovery flags into -> the same run object registered with the instance at the existing late -> active-generation boundary. ASLR-051 extracted the outer provider-round/tool-batch loop into -> `DeepChatLoopEngine`: it owns round and tool limits plus terminal/next-round decisions, while the -> compatibility `processStream` adapter still owns the existing accumulator, dispatch, interaction, -> and skill-refresh details in their original order. ASLR-052 added fixed `updateOutput`, -> `afterRoundPersisted`, and `settleTurn` callbacks. The engine now controls those commit points while -> the retained adapter continues to use the existing echo, message projection, stable per-fact -> `TapeRecorder`, and terminal writers. The engine invokes `settleTurn` exactly once. Within that adapter, -> a normal settlement write failure still enters the legacy error/abort fallback; a settlement -> failure while already handling a thrown round error is not replayed. ASLR-053 added separate -> `BasePromptAssembler` and `PostCompactionPromptAssembler` seams. The first is scoped to the -> captured DeepChat instance and remains before compaction intent preparation; the second fixes -> summary, reconstruction, and the awaited fail-open `MemoryPromptContributor` after normal compaction -> completion. ASLR-054 extracted typed input-preparation and context coordinators. Initial input now -> has a source-level history -> nullable intent -> compaction projection/user fact -> apply -> -> normal-return Memory-trigger order; resume passes the stale/abort checkpoint before refreshing -> history after its optional compaction seam and does not call that trigger; pressure recovery uses -> the extraction-enabled return/throw boundary. Manual -> compaction also remains outside compaction extraction. The context coordinator owns -> post-compaction view assembly and actual provider-attempt preflight, recovery, strict retry, -> request-sequence, ViewManifest, rate-gate and stream order. Presenter code supplies narrow retained -> algorithm/data adapters, while assistant placeholder creation and active-run registration remain -> at their existing late boundary. ASLR-055 connected the session-scoped `ToolCatalogPort`, -> `ToolExecutionPort`, and `ToolResultPort`: `processStream` and legacy dispatch now consume those -> capabilities instead of `IToolPresenter`, a normalization callback, or concrete `ToolOutputGuard`. -> ToolPresenter remains the sole merged/collision-resolved catalog and execution owner. ASLR-056 -> made the retained dispatcher return a discriminated `ToolBatchOutcome`: only pre-check permission, -> question interception, post-call permission, and post-success skill-draft confirmation can pause a -> batch. The outcome carries ordered interactions plus the persisted call/invocation/result state; -> the instance owns that state until the final response creates one fresh resume run. ASLR-057 -> replaced the mixed `ProcessHooks` callback bag with a typed notification observer, control -> collaborators, and an internal diagnostics seam. Notification delivery receives a detached -> snapshot and never awaits observer promises or thenables; synchronous throws and asynchronous -> rejection are logged without changing the loop outcome. `NewSessionHooksBridge` still delegates -> to the existing `HooksNotificationsService`, whose `queueMicrotask`, payload, command timeout and -> routing behavior remain unchanged. Interleaved-reasoning trace persistence remains an internal -> diagnostic and is not exposed as an external hook event. - -## 1. 模块目的 - -`LoopEngine` 把一个 DeepChat turn 的 prepare、context、provider round、tool batch、permission、persist -和 settle 组成固定、typed、可观察的执行序列。它使用现有 Tape 记录语义事实,并在少数明确 seam -允许 `await` 或 persistent pause。 - -它不是开放式 workflow engine,也不是任意插件可以重排的 hook bus。 - -## 2. BEFORE - -当前 lifecycle 被切在以下位置: - -- `AgentRuntimePresenter.processMessage()`:generation/session/resource/Tape/Memory/compaction 起点; -- `runStreamForMessage()`:prompt/context/provider attempt/preflight/manifest; -- `processStream()`:stream accumulation、tool batch、next round; -- `dispatch.ts`:tool execution、permission、pause、normalization、persistence、renderer echo。 - -同一个 turn 的状态依靠参数、closure 和 session-keyed Map 传递。当前 external hooks 通过 -`queueMicrotask` 触发,是 notification,不是 lifecycle gate。 - -## 3. 固定 stage - -```text -prepareTurn - -> prepareInput - -> assembleRequest - -> startProviderRun - -> enterProviderRound - -> beforeProviderRequest (1..n attempts within this outer round) - -> consumeProviderRound - -> executeToolBatch (0..n) - -> afterRoundPersisted - -> settleTurn -``` - -stage 名称是控制流骨架,内部 seam 才是模块参与点。执行顺序由代码声明,不提供 priority、dynamic -registration 或第三方 stage insertion。 - -## 4. Stage 合同 - -### `prepareTurn` - -- claim input; -- set `status=generating` and register the pre-stream AbortController; -- resolve effective config、session resource references、final ToolPresenter tool set; -- assemble base/runtime/env/skill/tooling/permission prompt sections。 - -此时尚未注册 externally visible active generation。 - -### `prepareInput` - -- ensure Tape and snapshot pre-turn history; -- prepare optional compaction intent using the already assembled base prompt; -- no intent: append user message fact; -- with intent: create compaction projection, append user fact, then await compaction apply; -- only after a normal initial compaction return, run the current compaction-to-Memory trigger; -- emit user refresh and fire `UserPromptSubmit` notification,绝不 await。 - -这个顺序是兼容合同:user fact 在 compaction apply 前,AbortError/throw 不触发 compaction Memory -extraction。 - -context-pressure recovery 同样是 extraction-enabled path;resume/manual compaction 不调用该 trigger, -resume terminal fallback 继续由 `MEM-13` 决定。 - -### `assembleRequest` - -- append summary/reconstruction and awaited fail-open Memory contribution; -- assemble effective Tape view/context; -- fit budget without changing existing policy; -- produce immutable `PreparedProviderRequest` for this attempt。 - -### `startProviderRun` - -- create mutable assistant placeholder; -- clear current plan state and re-check pre-stream abort; -- consume the claimed pending row and emit initial assistant refresh when current flags require; -- enter provider execution and register active generation/`LoopRun` at the current point。 - -### `enterProviderRound` - -- increment `providerRoundCount` at each outer-round entry; -- enforce `maxProviderRounds` before creating/reading the next stream; -- a strict provider overflow retry stays inside this outer round and does not increment this counter again。 - -### `beforeProviderRequest` - -- refresh prompt/final ToolPresenter tools for later rounds when current skill/resource rules require; -- context preflight / pressure recovery; -- increment `requestSeq` once per actual provider attempt, including strict overflow retry in the same outer - round; -- synchronously attempt request `ViewManifest`;write failure logs and remains fail-open; -- pass rate-limit gate; -- obtain provider stream using `ProviderPort`。 - -manifest attempt 与 provider request 的相对顺序是审计合同,不能异步补写;成功 entry 不是每次 request -必然存在的前置条件。 - -### `consumeProviderRound` - -- consume raw stream on hot path; -- update accumulator and throttled mutable output; -- normalize final assistant blocks/tool calls; -- never await generic observers per token/event; -- return terminal assistant response or `ToolBatch`。 - -### `executeToolBatch` - -- normalize/validate calls; -- run current pre-check permission when applicable; -- intercept question interactions without calling the underlying tool; -- execute according to current concurrency policy; -- capture post-call `rawData.requiresPermission` and post-success skill-draft confirmation; -- normalize/fit outputs, persist tool facts and renderer projection; -- return `completed` or `paused` with all ordered interactions and the batch execution state; -- refresh activated skills/resources before next round when required。 - -### `afterRoundPersisted` - -- ensure assistant/tool facts and projection required for this round are committed; -- emit round-complete internal callback; -- decide terminal versus next `assembleRequest`。 - -`providerRoundCount` 与 `requestSeq` 不是 commit counters:前者在 outer round 入口推进,后者在每个 -provider attempt 的 manifest preparation 处推进。strict retry 可让一次 outer round 对应多个 request -sequence。 - -### `settleTurn` - -- write current final assistant/message/status/event terminal projection; -- flush required output; -- on pause, persist ordered interactions and settle/clear the current run; -- otherwise clear run only after required commit; -- drain pending input under current rule; -- enqueue Memory ingestion under current eligibility; -- fire external notifications without blocking the loop。 - -## 5. Typed seam - -```ts -interface BasePromptAssembler { - assemble(input: BasePromptInput): Promise -} - -interface InputPreparationCoordinator { - prepare(input: PreparedTurnInput): Promise -} - -type ToolBatchOutcome = - | { type: 'completed'; results: ToolExecutionResult[] } - | { - type: 'paused' - interactions: PendingInteraction[] - executionState: PersistedToolBatchState - } - -type PendingInteractionOrigin = - | 'pre-check-permission' - | 'question' - | 'post-call-permission' - | 'skill-draft-confirmation' -``` - -实际实现使用这些窄 ports,不创建一个包含所有 callback 的 `LoopLifecycle` mega-interface。base prompt、 -input/compaction、post-compaction context、provider attempt、tool batch、round commit 和 turn settlement 的 -顺序在 source-level composition 中固定。 - -## 6. Await 与 pause 语义 - -`await` 表示当前阶段必须等依赖完成或收到 abort;它不自动产生可持久恢复状态。persistent pause 只能 -由上述四种合法 tool interaction origin 产生,不能由 generic observer 任意返回: - -```text -awaited wait: same call stack/run, abortable, no resume token -persistent pause: ordered interactions + execution state committed, current run settles -response: process/persist first interaction; stay paused while more remain -resume: after the final interaction, rebuild context and create one fresh run -``` - -compaction、Memory query、rate gate、provider preflight 只是 awaited wait;失败按各自 -fail-open/fail-closed policy 处理。pause 不保存 JavaScript call stack,也不假设旧 `runId` 可恢复。 - -## 7. 端口 - -LoopEngine 只依赖以下能力类型: - -- `ProviderPort`:prepare/stream/cancel provider request; -- `ToolCatalogPort`:只从 ToolPresenter aggregate 获取最终 merged/collision-resolved definitions; -- `ToolExecutionPort`:optional pre-check + execute,传递 exact options/current `AbortSignal` 并返回 raw - result;没有 owner 不支持的第二条 cancel channel; -- `ToolResultPort`:screenshot/result normalization、output preparation/offload 和 batch fitting; -- `BasePromptAssembler` 与 post-compaction context contributors:两个固定时点; -- `InputPreparationCoordinator` / `ContextCoordinator`:Tape view、budget、compaction; -- `TapeRecorder`:semantic fact/manifest/anchor; -- `MessageProjection` 与 `OutputSink`; -- typed tool interaction resolver; -- `MemoryPromptContributor` 与 `MemoryIngestionObserver`; -- clock/id/rate-limit 等窄 platform ports。 - -禁止 import Presenter root、Electron route/window、ACP-agent runtime 或 concrete database singleton。 -generic ProviderPort 可以解析 `providerId=acp`,但 LoopEngine 不据此把 session 当成 ACP agent。 - -## 8. 错误、取消与 terminal outcome - -```ts -type LoopTerminalOutcome = - | { type: 'completed' } - | { type: 'aborted'; reason: AbortReason } - | { type: 'failed'; error: NormalizedLoopError } - | { type: 'paused'; interactions: PendingInteraction[] } -``` - -- 所有 stage 在入口和长 await 后检查同一 `AbortSignal`; -- provider fallback/retry/overflow recovery 保持当前 attempt policy,不泛化成通用 retry middleware; -- stage 失败必须写与当前行为等价的 message/status/event terminal projection; -- tool 单项失败与 turn failure 的区分保持; -- terminal commit 幂等,late provider/tool callback 通过 run id/epoch 被拒绝; -- notification observer 错误只记录,不反向改变 terminal outcome。 - -## 9. Tape 与 replay - -LoopEngine 控制“何时记录”,`TapeRecorder` 控制“怎样记录”。本目标只包装现有 user、effective view -manifest、assistant/tool facts 与 compaction/handoff/memory audit anchors。interaction/terminal 继续由当前 -message/status/event projection 表达;不新增 Tape entry。raw token stream 不进入 Tape;mutable projection -不是永久事实。 - -replay 指审计时重建输入/输出因果 slice,不表示重新执行 provider 或 tool side effect。 - -## 10. 迁移步骤 - -1. 为当前完整 call order 增加 causal golden tests,不先抽象。 -2. 区分 pre-stream operation 与 late active-generation registration,再把 provider/tool turn-local state - 移入 `LoopRun`。 -3. 包装现有 provider、tool、Tape、output 为 ports,逻辑仍调用旧函数。 -4. 先收拢 `consumeProviderRound` hot path,再收拢 tool batch;保持输出节流。 -5. 按现有顺序建立固定 stages,逐个替换旧 orchestration。 -6. 把 pre-check/question/post-call permission/skill-draft 映射成 ordered `ToolBatchOutcome`。 -7. 分别接入 compaction 前的 base prompt 与 compaction 后的 summary/reconstruction/Memory context。 -8. 最后接入 Memory 两个 adapter,并跑专门 parity suite。 -9. 删除旧 `processMessage`/`processStream`/`dispatch` 中已无调用的 orchestration。 - -## 11. 验证 - -- 记录 stage + fact + renderer event 序列,迁移前后 golden diff 为零; -- provider 0/1/N tool rounds、parallel/sequential tools、tool error/oversized output; -- provider fallback、rate wait、overflow recovery、preflight failure; -- providerRoundCount outer-entry/max check 与 requestSeq per-attempt/strict-retry advancement; -- compaction intent/no-intent、user-before-apply、apply return false、apply throw/abort; -- ViewManifest success/fail-open; -- cancel/pause at every awaited seam; -- multiple ordered interactions、pre/post permission、question、skill draft、中间项保持 paused、最后一项后 - fresh-run resume; -- prompt/tool refresh after skill activation; -- external hooks 慢、失败或悬挂时不阻塞 turn; -- LoopEngine instance 无跨 session mutable map; -- import graph test 阻止 ACP、Electron 和 Presenter root 依赖。 - -## 12. 明确不做 - -- 不支持用户任意注册/reorder lifecycle stage; -- 不引入 generic event bus、middleware stack 或 workflow DSL; -- 不逐 token 写 Tape; -- 不把 `kind=acp` protocol loop 纳入此 engine;DeepChat + ACP-provider compatibility 仍通过 generic - ProviderPort; -- 不在重构中改变 tool 并发、provider fallback 或 compaction 策略。 diff --git a/docs/architecture/agent-system-layered-runtime/modules/memory-integration.md b/docs/architecture/agent-system-layered-runtime/modules/memory-integration.md deleted file mode 100644 index 15e7d7bcf8..0000000000 --- a/docs/architecture/agent-system-layered-runtime/modules/memory-integration.md +++ /dev/null @@ -1,273 +0,0 @@ -# Memory 集成与不可回归合同 - -> 状态:目标合同,不是 current API reference。ASLR-059..062 已接入 coordinator、prompt contributor、 -> ingestion observer 并完成 Memory 专项门禁;下文类型和流程仍是不可回归合同,不是 concrete API 清单。 -> 本模块只改变接线位置,不改变 `MemoryPresenter`、schema、retrieval、projection 或 maintenance。 - -> 实施进度:ASLR-046 在 instance 上建立 identity-only、per-instance stable Memory session handle; -> ASLR-054 固定 `MEM-14` 的 existing entry-point asymmetry。ASLR-059 已将 prompt/accounting、 -> extraction queue/chains/counter、epoch/cursor、projection fallback/cooldown 与 injection-access dedupe -> 机械提取到唯一 runtime-scoped `MemoryRuntimeCoordinator`。retained Presenter 只保留构造接线和调用委托, -> `MemoryPresenter` data orchestration 未迁移。AST architecture guard 以 class/property structure 固定唯一 -> owner,并拒绝 Presenter owner 回流;public seam tests 覆盖 128-turn access cap 与 256-session cooldown -> cap。ASLR-060 已让 coordinator 直接实现最小 `MemoryPromptContributor`,PostCompaction 固定 slot 传入 -> captured stable handle;Presenter 不再拥有私有 Memory injection callback。ASLR-061 已让同一 coordinator -> 直接实现 discriminated `MemoryIngestionObserver`,按 `MEM-13/14` 接入 terminal/compaction,并在 composition -> shutdown 先同步关闭 ingestion admission、fence epoch,再由 `MemoryPresenter.dispose()` abort provider-bound -> work,再 bounded-await existing chains 后关闭 SQLite;timeout 返回 typed pending diagnostics,不冒充 -> settled。coordinator epoch 与 disposed Memory operation fence 共同禁止 late commit。 -> ASLR-062 已完成 correctness/privacy/performance/native 收口,并证明 Phase 9 未改变 Memory schema、config、 -> route/event DTO、DuckDB sidecar 或 ingestion projection version。 - -## 1. 模块目的 - -Memory 对 DeepChat loop 有两个方向完全不同的参与点: - -```text -read path: awaited, fail-open prompt contribution before provider request -write path: background, serialized ingestion after the currently eligible returned turn/compaction outcomes -``` - -把两者放进一个万能 `MemoryHook` 会再次混合生命周期。目标由一个有状态 coordinator 暴露两个窄 -ports,并冻结当前所有语义。 - -## 2. BEFORE - -Memory 是近期进入 `AgentRuntimePresenter` 的复杂模块。runtime 负责 query/injection、view anchor、turn -selection/dedupe、effective Tape extraction、cursor、fallback、cooldown、epoch/rewind、delete cleanup、 -background queue 和 shutdown fencing。 - -这些调用虽集中在大 Presenter 中,但已经有严格的安全和 lineage 规则。本次不能把“代码移动成功” -误当成“行为等价”。Memory 必须用独立 causal fixtures 验证,并最后接线。 - -## 3. AFTER 的唯一 orchestration owner 与两个端口 - -一个 runtime-scoped `MemoryRuntimeCoordinator` 已接收现有 runtime 中的 mutable orchestration state, -成为唯一 owner: - -- `memoryExtractionChains`; -- extraction queue 与 monotonic queue counter; -- `memoryExtractionEpochs`; -- `memoryIngestionProjectionRetryAfter`; -- `memoryInjectionAccessByTurn`。 - -它还通过现有 DeepChat session state port 读写 cursor。`MemoryPresenter` 继续拥有 memory rows、 -persona/working-memory、retrieval/write、vector 与 maintenance;instance 只持 coordinator 的 session -handle,不复制上述 maps。 - -```ts -interface MemorySessionHandle { - readonly sessionId: AppSessionId -} - -interface MemoryPromptContributor { - contribute(input: { - session: MemorySessionHandle - basePrompt: string - query: string - messageId?: string | null - }): Promise -} - -interface MemoryIngestionDrainOutcome { - timedOut: boolean - pendingSessions: readonly string[] -} - -interface MemoryIngestionObserver { - afterTurnSettled(input: { - session: MemorySessionHandle - origin: 'initial' | 'resume' - outcome: - | { kind: 'returned'; status: 'completed' | 'paused' | 'aborted' | 'error' } - | { kind: 'thrown'; error: unknown } - }): void - afterCompactionApplyReturned(input: { - session: MemorySessionHandle - origin: 'initial' | 'context-pressure' - targetCursorOrderSeq: number - }): void - drainAndFence(): Promise -} -``` - -prompt contributor 属于 fixed ordered prompt composition;ingestion observer 接收 committed snapshot 后 -排入 coordinator 的 per-session background serialization。observer 的 enqueue 可以同步确认,实际提取 -不阻塞 turn terminal path。方法名使用 `ApplyReturned`,因为当前 `succeeded=false` 也触发,而 throw 不 -触发。 - -## 4. Read path 合同 - -```text -assemble request - -> build sanitized Memory query input - -> select active persona only + eligible working memory - -> MemoryPresenter retrieval - -> sanitize again at boundary - -> apply hard token/character budget - -> produce read-only PromptSection with provenance - -> best-effort audit/view anchor under current rule -``` - -必须同时满足: - -1. retrieval/injection fail-open;Memory 失败不能阻塞 provider request; -2. 注入文本经过现有 sanitization; -3. 有独立 hard budget,不能挤掉不可缺失的 system/tool context; -4. 只注入 active persona;working memory 独立处理; -5. draft session 不参与当前不允许的 Memory path; -6. prompt contribution 是 read-only,不在 query 时写 persona/working memory; -7. view/audit anchor 写失败不能反向移除已允许的 prompt contribution; -8. provenance 不泄露不应进入 provider/Tape 的内部字段。 - -## 5. Write path 合同 - -一次 ingestion job 分两段冻结,不能在 enqueue 时提前读取 cursor/tail: - -```text -settled turn or eligible settled compaction attempt (initial/context-pressure only) - -> evaluate current eligibility - -> enqueue the trigger path without capturing an epoch - compaction path additionally captures the existing targetCursorOrderSeq upper bound - -> when this task reaches the head of the per-session serial queue: - ensure/read the current epoch - read latest committed cursor - read latest fallback tail, or use captured compaction upper bound - build effective Tape projection/window + exact sourceEntryIds - freeze that built window for this task - -> MemoryPresenter extraction/upsert/audit/vector work - -> commit cursor only when result.ok === true and epoch/lineage still valid -``` - -禁止从 mutable assistant blocks 或 renderer event 构建窗口。读取“task 开始时的最新 committed -cursor/tail”是当前防重复语义,不能改成 enqueue-time snapshot;window 建成后才是 immutable。 - -## 6. 冻结不变量 - -权威合同是 [migration-and-validation.md 的 `MEM-01..14`](../migration-and-validation.md#4-memory-no-regression-contract)。 -特别注意两个容易混淆的事实: - -- `MEM-04` 是 prompt injection access accounting,不是 extraction dedupe:有 messageId 时执行当前 - session/message TTL/cap dedupe;pressure recovery 的 null messageId 调用保持每次记录; -- `MEM-07` 要求 queued task 开始时读取最新 cursor/tail,`MEM-06` 的 exact lineage 指随后构建并冻结 - 的 effective Tape window。 - -若产品希望修正 `MEM-13` 或 `MEM-14`,必须另写行为变更 spec。 - -## 7. Trigger mapping - -明确用现有调用点建立迁移表,而不是只按新 stage 名猜测: - -| 当前语义 | 新 seam | 执行方式 | -| --- | --- | --- | -| compaction 后、request 前 Memory query/injection | `MemoryPromptContributor` | awaited + fail-open | -| initial returns completed | `afterTurnSettled(origin=initial, outcome=completed)` | enqueue | -| initial returns aborted/paused/error or any throw | explicit outcome/error path | no enqueue | -| resume returns completed or aborted | `afterTurnSettled(origin=resume, returnedOutcome=...)` | enqueue | -| resume returns paused/error or throws | explicit outcome/error path | no enqueue | -| initial/context-pressure non-null compaction apply normally returns (`succeeded=true|false`) | `afterCompactionApplyReturned` | enqueue with intent target upper bound | -| initial/context-pressure has no intent or compaction apply throws | explicit no-op/error path | no enqueue | -| resume/manual compaction with any outcome | no compaction observer | no enqueue; resume terminal fallback remains under `MEM-13` | -| edit/delete/retry/rollback | instance/data operation | rewind + epoch fence | -| clear/destroy/shutdown | instance/runtime close | drain/fence before data close | - -`SettledTurnMemoryInput` 必须带 origin 和“returned outcome vs thrown error”;compaction callback 只在 -normal return 后调用。完整 outcome matrix 以 `MEM-13/14` 为准。 - -## 8. Queue、cursor 与 epoch - -- queue key 使用 app session id;同 session 严格串行,不同 session 可按当前限制并发; -- enqueue 时只保留 trigger path;compaction 额外捕获 current intent upper bound; -- task 到达 per-session queue 头部时才 ensure 当前 epoch;同一 job 的 chunk continuation 显式传递 - `expectedEpoch`,epoch 不在普通 enqueue 时冻结; -- task 真正开始时读取最新 cursor;fallback 同时读取最新 tail,compaction 使用已捕获 upper bound; -- task 在此时构建并冻结 effective Tape chunks/sourceEntryIds;后续 queued task 因最新 cursor 不会重复; -- 开始执行和提交前都重新验证 fence; -- `ok: false`、throw、abort、fallback projection 或 stale epoch 都不得前移 cursor; -- cooldown/queue/epoch/access-dedupe 由 `MemoryRuntimeCoordinator` 管理,LoopEngine/instance 不复制 maps; -- clear/delete/destroy 先建立 fence,之后的 late job 不能回写已重建的同 id session; -- shutdown 必须先永久关闭 coordinator admission、fence active epochs,再 dispose Memory 以 abort provider - work 并关闭 Memory operation fence,最后 bounded-await captured chains。timeout outcome 必须显式携带 - pending session diagnostics;composition root 记录 warning 后才可继续关闭 SQLite,不能把 timeout 表述为 - settled;late resolve/reject 仍不得写 row/audit/vector、触发 embedding/event 或前移 cursor/anchor。 - -当前 start-time epoch 语义存在一个已知边界:旧 session 的普通 queued task 若在 destroy 后才首次开始, -可能读取到复用后的同 session id epoch。ASLR-059 为保持 baseline 不改变该行为;session-id reuse 的产品 -语义与修复必须另写 behavior spec,不能隐含塞入 ownership refactor。 - -## 9. 删除与 lineage 变化 - -edit/delete/retry/rollback 会改变有效历史。新 instance/data operation 只调用 Memory adapter 的现有 -rewind/invalidation API,不自行计算 cursor。删除相关 rows/audit 与 vector cleanup 的事务/补偿语义保持; -部分失败必须能被现有 maintenance/retry 识别。 - -session clear/destroy 的 Tape、message、Memory 顺序以当前 tests 为准。不能因为新 owner 边界把它们 -改成无序 `Promise.all`。 - -## 10. 失败策略 - -| 失败点 | Foreground turn | Cursor | Audit/telemetry | -| --- | --- | --- | --- | -| query/retrieval | 继续,无 Memory section | 不相关 | 记录安全错误 | -| sanitization/budget | 丢弃或裁剪当前 section | 不相关 | 保持当前统计 | -| view anchor | 继续已构建 request | 不相关 | best-effort failure | -| projection fallback | turn 已完成,不回滚 | 不提交 | cooldown/current fallback audit | -| extraction/upsert | turn 已完成,不回滚 | 不提交 | retry/maintenance 可见 | -| vector cleanup | 不影响已完成 turn | 按当前 delete contract | compensation/maintenance 可见 | -| shutdown timeout | 阻止 unsafe DB close or fence per current policy | 不错误提交 | 明确 shutdown reason | - -composition root 在调用 `drainAndFence()` 后立即持有其 rejection handler,避免 Memory dispose 期间出现 -unhandled rejection;随后独立记录 dispose failure、drain failure 或 typed timeout,并继续尝试 SQLite -close。该 continue policy 的安全前提由生产实现固定:coordinator 在第一个 await 前永久关闭 admission 并 -fence epoch,`MemoryPresenter.dispose()` 也在第一个 await 前标记 disposed 与 abort provider。任一后续 -reject/timeout 都不能重新开放写入。 - -## 11. 迁移步骤 - -1. 在任何代码移动前冻结 `MEM-01..14` 和两个 outcome matrix 的 fixture/test。 -2. [ASLR-059 已完成] 从 runtime 机械提取唯一 `MemoryRuntimeCoordinator` owner,queue/counter、四组 - maps/cursor call sites 原样移动;instance 只取得 session handle。 -3. [ASLR-060 已完成] coordinator 直接实现 `MemoryPromptContributor`,以 stable session handle 接入现有 - PostCompaction slot,并对比 exact prompt/budget/sanitization/selection/accounting/anchor。 -4. [ASLR-061 已完成] 为 terminal/compaction 建只携带 trigger/origin/upper-bound 的 DTO;普通 task 在开始时 ensure epoch, - 只有同一 job continuation 携带 `expectedEpoch`;不要提前抓 cursor/tail/window。 -5. [ASLR-061 已完成] 接入 `afterTurnSettled`,分别验证 initial returned、resume returned 和 thrown paths。 -6. [ASLR-061 已完成] 只在 initial/context-pressure 接入 `afterCompactionApplyReturned`,覆盖 normal - `succeeded=true|false` return;throw/no-intent 不调用,resume/manual 不接入该 observer。 -7. [ASLR-061 已完成] 保留 edit/delete/retry/rollback/clear/destroy fence,并将 shutdown 接到两阶段 - fence/dispose/bounded-drain-outcome/SQLite close 顺序;timeout 记录 pending diagnostics,late work 由两层 - operation fence 拒绝。 -8. [ASLR-062 已完成] 删除 legacy runtime ingestion trigger 接线后统一运行 Memory correctness/privacy/ - performance/native gates,并对比 Phase 9 前后的 schema/config/wire/projection contracts。 - -## 12. 验证矩阵 - -- Memory disabled/enabled、无 persona、一个/多个 persona、active 切换; -- working memory empty/present/oversized/malicious content; -- retrieval success/throw/timeout、anchor write failure; -- repeated null-messageId pressure recovery access accounting,以及 130 个 non-null turns 对 128 cap 的 - dedupe/eviction/TTL parity; -- initial/resume returned completed、returned aborted、thrown AbortError、thrown non-abort error; -- initial/resume returned paused、returned error; -- turn with 0/1/N tools、duplicate renderer/message callbacks; -- initial/context-pressure compaction no intent、normal return succeeded=true、normal return - succeeded=false、thrown AbortError、thrown non-abort error;resume/manual 各 outcome 均无 compaction - extraction; -- projection primary/fallback/cooldown,以及 257 个 failed sessions 对 256 cap 的 oldest eviction; -- cursor success/failure/stale epoch; -- concurrent sessions 与 same-session serialized jobs; -- first job 尚未完成时连续触发第二 job并落入新 turn;验证第二 job 在执行时看见第一 job 推进后的 - cursor,且不重复 sourceEntryIds; -- edit/delete/retry/rollback/clear/destroy during queued/running job; -- shutdown with empty/queued/running/failed jobs; -- rows/audit/vector cleanup fault injection; -- exact effective Tape sourceEntryIds,以及 injection-access selected manifest ID accounting(non-null - message dedupe / null-message recovery non-dedupe)。 - -## 13. 明确不做 - -- 不改 Memory schema、embedding/vector provider、retrieval ranking 或 prompt 文案; -- 不把 ACP 接入 DeepChat Memory; -- 不将 ingestion 改成阻塞 turn; -- 不统一现存 initial/resume 或 compaction trigger 不对称; -- 不把 Memory data owner 搬进 `DeepChatAgentInstance` 或 `LoopEngine`。 diff --git a/docs/architecture/agent-system-layered-runtime/modules/permission-and-interactions.md b/docs/architecture/agent-system-layered-runtime/modules/permission-and-interactions.md deleted file mode 100644 index 8e66f611b5..0000000000 --- a/docs/architecture/agent-system-layered-runtime/modules/permission-and-interactions.md +++ /dev/null @@ -1,187 +0,0 @@ -# Permission 与可恢复交互 - -> 状态:目标合同,不是 current API reference。DeepChat ordered batch 已由 ASLR-056 接入,external -> notification observer 已由 ASLR-057 隔离;ASLR-070..073 已接入 direct ACP permission continuation。 -> 两者共享 decision UI,不共享 continuation 实现;下文类型是合同伪代码。 - -> Current ownership: `InteractionCoordinator.respond()` performs ordered DeepChat interaction -> settlement and calls the single `TurnCoordinator.resume()` boundary only after the final pending -> interaction. `DeepChatAgentInstance` remains the batch/interaction state owner. - -## 1. 模块目的 - -本模块把“等待用户决定”建模为明确 gate/interaction,而不是散落 callback。DeepChat tool permission、 -question tool 和 ACP protocol permission 可以共享 renderer decision/output contract,但必须由各自 backend -保存、恢复和终止 continuation。 - -## 2. BEFORE - -当前 DeepChat permission、tool dispatch、pause/resume、question 等逻辑主要分散在 runtime/dispatch 和 -session-keyed maps。ACP permission 经过 `AcpProvider` 翻译后也在统一 runtime 周边汇合,但实际响应的是 -ACP protocol continuation。 - -外部 `HooksNotificationsService` 同样叫 hook,却是 `queueMicrotask` fire-and-forget。它不能被混入 -permission lifecycle,也不能让 shell command 阻塞 agent。 - -## 3. 两种 interaction owner - -```text -DeepChat: - ToolBatchDispatcher -> ordered PendingInteraction[] - -> instance interaction state - -> fresh resume run after the final response - -ACP: - ACP protocol request -> AcpPermissionBridge -> AcpProtocolContinuation - -> ACP instance state - -Both -> InteractionOutputPort -> current renderer routes/events -``` - -共同部分只有 request/decision display model、输出 port 和安全的 id correlation。 - -## 4. DeepChat interaction outcome - -```ts -type ToolBatchOutcome = - | { type: 'completed'; results: ToolExecutionResult[] } - | { - type: 'paused' - interactions: PendingInteraction[] - executionState: PersistedToolBatchState - } -``` - -pre-check policy 可以 allow/deny/pause,但不是唯一来源。question 在调用 tool 前产生 interaction;某些 -tool 调用后通过 `rawData.requiresPermission` 产生 interaction;skill draft 在 tool 成功后产生 confirmation。 -dispatcher 保留已经发生的 execution state,避免 resume 重做 side effect。 - -ASLR-056 的 production seam 位于 `agent/deepchat/loop/ports.ts`。retained dispatcher 仍负责现有 -permission policy、question interception、tool execution 与 block projection,但它现在只返回 -`completed` 或 `paused` discriminated outcome。`paused` outcome 的 interaction 带内部 origin/order; -`PersistedToolBatchState` 记录 call order、已经调用、已经提交 result 与仍 pending 的 call ids,并由 -`DeepChatAgentInstance` 持有到最后一项解决。order 沿用现有 persisted action append 顺序;skill-draft -confirmation 仍在 output fitting 成功后 late append。renderer route 和 block schema 未增加字段, -process restart 后也不承诺恢复未持久化的内部 origin。 - -## 5. Interaction state - -```ts -interface PendingInteraction { - sessionId: AppSessionId - messageId: string - origin: - | 'pre-check-permission' - | 'question' - | 'post-call-permission' - | 'skill-draft-confirmation' - toolCallId?: string - order: number - originRunId?: GenerationId - createdAt: number - payload: InteractionPrompt -} -``` - -实例持有同一 batch 的有序数组。现有 renderer route 继续用 `sessionId/messageId/toolCallId` 响应,不要求 -新增 `interactionId/runId/epoch` wire fields;内部可以用 origin run 做 stale guard,但匹配必须兼容当前 -route。每次只解决第一项,剩余项继续 pending。raw continuation/function 不持久化,重启恢复能力不超过 -当前实现。 - -## 6. ACP permission - -ACP bridge 保存 protocol request id 与 live connection continuation 的关系: - -- renderer decision 映射回 ACP allow/deny/cancel response; -- timeout、process exit、connection error、session close 会终止 continuation; -- 相同 request 的重复 decision 幂等或返回明确 stale error; -- ACP permission 不生成 DeepChat tool result,也不进入 DeepChat Tape tool lifecycle; -- ACP turn/transcript 的 pending/terminal 映射保持当前事件顺序。 - -## 7. 生命周期顺序 - -DeepChat: - -```text -prepared tool batch - -> for each call in current order/concurrency policy - pre-check permission may allow/deny/pause - question is intercepted without tool execution - otherwise execute exactly once - inspect post-call requiresPermission - inspect post-success skill-draft confirmation - -> collect all ordered interactions + execution state - -> persist pending actions/current message projection -> settle current run - -> user decision - -> match current first interaction by session/message/toolCall under current route contract - -> execute deferred tool / deny / answer / confirm according to that interaction origin - -> persist its result/resolution and remove the first interaction - -> if ordered interactions remain: keep paused and return without a new run - else: rebuild context and create one fresh resume run -``` - -## 8. Hook notification 分界 - -下列 observer 继续 non-blocking:user prompt submitted、tool completed、turn settled 等外部 hook -notification。规则: - -- 使用 committed snapshot,不暴露 mutable continuation; -- 通过 `queueMicrotask`/现有队列触发; -- 慢、失败、超时不阻塞 loop; -- 不能返回 allow/deny/pause; -- 不能更改 Tape 或当前 run terminal outcome。 - -需要控制 loop 的内部逻辑必须是 typed lifecycle port,不能借 external hook 实现。 - -ASLR-057 的 production seam 是 `DeepChatLoopNotificationObserver`。`processStream`/dispatcher 只投递 -`PreToolUse`、`PostToolUse`、`PostToolUseFailure` 与 `PermissionRequest` 的 detached snapshot;observer -的 sync throw、Promise/thenable reject 或 never-settling wait 都不会被 loop await,也不能改变 terminal -outcome。auto-grant、auto-review、streaming permission continuation、skill activation 与 image cache 位于 -单独的 control collaborators;interleaved-reasoning trace 位于 internal diagnostics。现有 -`HooksNotificationsService` 仍是唯一 shell-command owner,route/config、agentId fallback、payload、顺序、 -`queueMicrotask` 和 30 秒 command timeout 均未改变。 - -## 9. 取消、关闭与 stale decision - -- cancel active run 时先标记 abort/closing,再终止/settle ordered interactions under current policy; -- session clear/delete/destroy 清理 persisted interactions,旧 renderer decision 必须被拒绝; -- close window 是否取消 interaction 按当前业务规则保持,不做新的全局假设; -- tool 已完成后到达的 decision 不得再次执行; -- process exit 后的 ACP decision 不得新建 connection 假装恢复; -- renderer event emit 失败不允许留下无法清理的 continuation。 - -## 10. 迁移步骤 - -1. 建立 pre-check、question、post-call permission、skill-draft、multiple interaction 与 ACP permission - 状态序列 fixtures。 -2. 引入统一的 display DTO/output port,不改变 renderer route/event。 -3. 把 DeepChat continuation 收敛到 instance ordered interaction state。(ASLR-044 已完成 state - ownership;持久 blocks 仍是事实源,typed outcome 不在此 slice) -4. 让 tool dispatcher 返回 typed `ToolBatchOutcome`,携带 interactions 和已发生的 execution state。 -5. 将 question tool 移到 ToolPresenter owner,但保留 dispatch interception;接入 post-call permission 和 - skill-draft origins。 -6. 把 ACP continuation 收敛到 `AcpPermissionBridge`。 -7. 删除 runtime 中 cross-backend permission maps 和 provider id 特判。 -8. 明确标注 external hook notification 为 observer,并加 non-blocking test。 - -## 11. 验证 - -- DeepChat tool allow/deny/always policy/ask/pause/resume; -- question answer/cancel/window close/session close; -- post-call permission 不重复执行已发生 side effect; -- post-success skill draft confirmation 保留成功结果和 draft; -- 同批 multiple interactions 按序逐项解决;中间项处理后保持 paused,最后一项解决后才创建 fresh run; -- ACP allow/deny/cancel/timeout/process exit; -- duplicate、wrong session/message/tool call 和 stale decision; -- cancel during dialog 与 decision/cancel race; -- persistence/output failure 不遗留 continuation; -- external hook sleep/failure 不增加 loop critical-path latency; -- regular session 与 subagent 现有 interaction payload parity。 - -## 12. 明确不做 - -- 不把 ACP continuation 转成 DeepChat tool continuation; -- 不增加 generic human-in-the-loop workflow engine; -- 不改变 permission 默认策略或 UI payload; -- 不让 external hooks 获得 blocking veto; -- 不承诺当前不支持的跨进程重启 continuation 恢复。 diff --git a/docs/architecture/agent-system-layered-runtime/modules/prompt-context-and-compaction.md b/docs/architecture/agent-system-layered-runtime/modules/prompt-context-and-compaction.md deleted file mode 100644 index 45e9655cc4..0000000000 --- a/docs/architecture/agent-system-layered-runtime/modules/prompt-context-and-compaction.md +++ /dev/null @@ -1,216 +0,0 @@ -# Prompt、Context 与 Compaction - -> 状态:目标设计,不是 current API reference。顺序与 budget policy 是兼容合同,不在重构中优化; -> 下文 contributor/type 伪代码表达边界,current concrete API 以实施进度和 loop source 为准。 - -> 实施进度:ASLR-046 已把 session-scoped compaction in-flight projection 迁入 -> `DeepChatAgentInstance`,persisted summary 仍是事实源,`compacting` projection 仍优先。ASLR-053 -> 增加了 scoped `BasePromptAssembler` 与固定 `PostCompactionPromptAssembler`:initial、resume 和 -> manual compaction 都在 intent preparation 前完成原有 base/runtime/env/skills/tooling/permission/ -> verification 组合;normal initial/resume/pressure paths 在 compaction 后固定按 -> summary -> reconstruction -> awaited fail-open Memory 顺序组合。initial tool-skill refresh -> 重用两个 phase;resume later-round refresh 继续保留原有 base-only 差异。manual compaction 仍只消费 -> base prompt,不新增 post-compaction request assembly。ASLR-054 introduced typed -> `InputPreparationCoordinator` and `DeepChatContextCoordinator` seams. They now own the existing -> initial/resume/pressure compaction order, post-compaction Tape view assembly, provider preflight, -> pressure recovery, strict retry, request sequence and synchronous fail-open ViewManifest attempt. -> Existing `CompactionService`, `buildTape*View`, context-budget and message/Tape adapters remain the -> algorithms and data owners. ASLR-060 replaced the fixed awaited Memory call with -> `MemoryPromptContributor`; ASLR-061 routes only initial/context-pressure normal compaction returns through -> `MemoryIngestionObserver.afterCompactionApplyReturned`, while resume/manual compaction retain their -> no-trigger baseline. No prompt, budget, schema or cache policy changed. - -## 1. 模块目的 - -这个模块把 prompt contributors、Tape effective view、context budget、provider preflight/recovery 和 -compaction 协调成明确的数据流。它参与 DeepChat `LoopEngine`,但不拥有 provider、skills、Memory 或 -Tape 的底层数据。 - -## 2. BEFORE - -当前 prompt/context 构建横跨 runtime、`contextBuilder`、Tape view、compaction service、provider -preflight 和 Memory orchestration。初始 turn、tool 后续 round、overflow recovery 与 resume path 的 -刷新时机并不完全相同。 - -这些差异有业务含义。把它们“统一得更优雅”会改变模型实际看到的请求,因此首先需要锁定 request -fixtures,而不是先抽一个 generic prompt pipeline。 - -## 3. AFTER 数据流 - -```text -session config + resource revisions + input - -> ordered BasePromptContributor[] - base/runtime/env/skills/final tooling/permission - -> ensure Tape/history + prepare optional compaction intent using that base prompt - -> append user fact (and compaction projection first when intent exists) - -> apply compaction intent when present - -> ordered ContextPromptContributor[] - summary/reconstruction/Memory at their current positions - -> effective Tape view (policy remains legacy_v1 until separately changed) - -> ContextAssembler / budget fit - -> assistant placeholder + active generation registration - -> provider-specific preflight/recovery - -> immutable PreparedProviderRequest - -> synchronous ViewManifest attempt (fail-open) - -> ProviderPort -``` - -`PreparedProviderRequest` 创建后不可由 unrelated observer 修改。provider adapter 可以做现有的 wire -format mapping,但不得重新查询资源并改变逻辑顺序。 - -## 4. 固定 prompt 顺序 - -顺序分成两个固定 phase,不能用一个统一 contributor array 放到 compaction 之后: - -```text -base agent prompt - -> runtime capabilities/instructions - -> system environment - -> skill catalog metadata - -> pinned/activated skill content - -> tooling/MCP instructions - -> permission/verification instructions - -> [prepare compaction intent; append user; apply compaction] - -> persisted summary/reconstruction sections - -> Memory persona/working-memory contribution at current insertion point - -> current effective conversation/tool context -``` - -若当前不同入口存在合法差异,则在 typed `PromptAssemblyMode` 中显式表达,而不是让 contributor 自行 -根据全局状态猜测。 - -## 5. Contributor 合同 - -```ts -interface BasePromptContributor { - readonly id: PromptContributorId - contribute(context: BasePromptContext): Promise -} - -interface ContextPromptContributor { - readonly id: PromptContributorId - contribute(context: PostCompactionPromptContext): Promise -} - -interface PromptSection { - id: string - source: 'base' | 'runtime' | 'environment' | 'skill' | 'tooling' | 'memory' | 'other' - content: string - visibility: 'provider' - provenance?: PromptProvenance -} -``` - -两组排序分别在 composition root 的固定数组中声明,不提供 runtime priority,也不允许 contributor -跨过 compaction 边界。contributor 必须: - -- 尊重 `AbortSignal`; -- 不直接写 renderer、Tape 或 session status; -- 返回 section/provenance,不拼接整份 prompt; -- 按自己的明确 policy fail-open 或 fail-closed; -- 不缓存超出 owner revision 的内容。 - -## 6. InputPreparation 与 ContextCoordinator - -```ts -interface ContextCoordinator { - assemble(input: PostCompactionContextInput): Promise - recoverFromPressure(input: ContextPressureInput): Promise -} -``` - -`InputPreparationCoordinator` 包装现有 Tape/history、compaction intent、user fact 和 apply 顺序; -`ContextCoordinator` 组合现有 `ContextBuilder`、Tape view policy 与 pressure recovery。职责包括: - -- 选取 effective Tape slice; -- 合并 summary/reconstruction 和 prompt sections; -- 计算当前 provider/model budget; -- 执行现有 fitting/truncation policy; -- pressure recovery 时按当前 base prompt/compaction/Memory rebuild 顺序尝试 compaction; -- 在 provider preflight/overflow 时执行当前 recovery policy; -- 返回 ViewManifest 所需 provenance。 - -它不负责发网络请求或执行 tools。 - -## 7. Compaction 边界 - -compaction 是 awaited operation,必须可取消,但不是 persistent pause。initial input path 明确包含: - -```text -base prompt + Tape/history - -> prepare nullable intent - -> if intent: create compaction projection - -> append user fact - -> apply intent (summarize + CAS/lineage + commit/reject) - -> on any normal return, invoke current compaction Memory trigger - -> on any throw, including AbortError, do not trigger -``` - -pressure recovery 没有新的 user fact,但同样只在 non-null intent 正常返回后触发。`succeeded=false` -仍是正常返回并触发;no intent 或 throw 不触发。权威矩阵见 -[MEM-14](../migration-and-validation.md#4-memory-no-regression-contract)。 - -resume 与 manual compaction 仍不触发 compaction extraction;resume 完成后的 terminal fallback extraction -由 `MEM-13` 单独决定,不能与 `MEM-14` 合并。 - -## 8. Memory 参与 - -Memory prompt contributor 是 awaited、read-only、hard-budgeted、fail-open 的 section source。它不控制 -context policy,也不修改 working memory/persona rows。当前 active persona、working memory、draft exclusion -和 sanitization 规则保持。 - -Memory ingestion 是 turn/compaction settle 后的 background observer,不属于 request assembly。 - -## 9. Round 刷新 - -每个 provider attempt 使用 immutable request snapshot。以下情形按当前规则重新 assemble: - -- tool batch 完成并激活/改变 skill/resource; -- provider fallback 需要不同 model/wire capabilities; -- overflow/preflight recovery 改变 context; -- resume/retry 从新的 lineage 继续。 - -普通 raw stream event 不触发 prompt refresh。refresh 前必须完成前一 round 所需的 Tape/tool persistence。 - -## 10. 错误与取消 - -- Memory query 失败:按现有 fail-open,不阻塞 request; -- required base/config/provider mapping 失败:fail-closed,产生兼容 error projection; -- initial/context-pressure compaction `succeeded=false`:执行当前 fallback并仍触发现有 compaction - Memory path;resume/manual 维持不触发; -- budget 无法 fit:沿用 provider preflight/overflow recovery 和 terminal error; -- abort:所有 awaited contributors/compaction/recovery 尽快终止,late result 通过 run/lineage fence 丢弃; -- ViewManifest 在 request 前同步尝试;写入失败记录 warning 并继续发送,不异步补写。 - -## 11. 迁移步骤 - -1. 捕获各入口、provider、tool round、Memory on/off、compaction on/off 的 request fixture。 -2. 为每个现有 prompt fragment 标注 owner、order、refresh trigger、failure policy。 -3. 分别引入 pre-compaction base contributors 与 post-compaction context contributors,输出仍交给旧 - builder。 -4. 包装 Tape view/context builder 为 `ContextCoordinator`。 -5. 把 Tape/history -> intent -> compaction projection/user fact -> apply 迁入 - `InputPreparationCoordinator`,保持 return/throw trigger matrix。 -6. 接入 provider preflight/recovery,比较 exact request/manifest。 -7. 最后迁移 Memory contributor,执行 Memory parity suite。 -8. 删除 runtime 中重复拼 prompt、重算 budget 的分支。 - -## 12. 验证 - -- provider request golden fixture 比较 role/order/content/tool definitions 和 budget结果; -- initial、tool follow-up、fallback、overflow recovery、resume、retry、draft/subagent; -- skill activation 后下一 round prompt/tool refresh; -- compaction no-op/success/failure/abort/stale CAS; -- Memory on/off/query failure/anchor failure/oversized retrieval; -- ViewManifest 与实际 request source refs 一致; -- ViewManifest write failure 仍发出 request; -- cancellation at every awaited contributor; -- external hooks 不参与也不能改变 request assembly。 - -## 13. 明确不做 - -- 不改 prompt 文案、section 顺序或 budget 数值; -- 不把 contributors 做成第三方 priority plugin system; -- 不更换 Tape view policy; -- 不重写 compaction algorithm; -- 不把 provider-specific wire mapping搬进通用 context。 diff --git a/docs/architecture/agent-system-layered-runtime/modules/shared-data-and-io.md b/docs/architecture/agent-system-layered-runtime/modules/shared-data-and-io.md deleted file mode 100644 index dc6dce195a..0000000000 --- a/docs/architecture/agent-system-layered-runtime/modules/shared-data-and-io.md +++ /dev/null @@ -1,232 +0,0 @@ -# Shared Data / IO 边界 - -> 状态:目标合同,不是 current API reference。ASLR-010..013、ASLR-021 与 ASLR-070..073 已接入 typed -> catalog/repository、app-session shell 和 shared projection ports;ASLR-060..061 已把 Memory prompt/ -> ingestion ports 接入 DeepChat lifecycle。共享的是物理数据与投影能力,不是两个 runtime 的领域实现。 - -## 1. 模块目的 - -这个模块给 `AgentManager`、DeepChat backend 和 ACP backend 提供最少的共享基础设施: - -- app session identity 与 metadata; -- typed agent catalog persistence; -- transcript/message projection; -- renderer/event output; -- session path、workspace 和 process 等真正跨 backend 的 platform ports。 - -核心约束是:shared table 不等于 shared domain repository,shared output 不等于 shared execution -loop。 - -## 2. BEFORE - -当前 `AgentRepository` 同时读写 DeepChat config、ACP manual/registry/install state,返回的 `Agent` -类型也同时容纳两边字段。上层通知语义因此被混在一起。 - -另一方面,`src/main/lib/agentRuntime` 的名字暗示它是一套 agent runtime,实际包含: - -- background process / shell environment / encoding / process tree / spawn guard / RTK; -- filesystem search; -- question tool; -- session paths; -- system environment prompt builder。 - -这些代码没有共同 lifecycle,只是历史上都被 agent 调用。 - -## 3. AFTER 的边界 - -```text -shared/ -├─ catalog persistence codecs -├─ app-session service -├─ adapters over existing message/Tape projection writers -├─ output/event sink ports -└─ platform ports - ├─ process/command runtime - ├─ workspace/search - └─ session storage paths -``` - -`shared/` 只能依赖 shared types、数据库基础层和 platform primitives。它不能 import DeepChat -`LoopEngine`、ACP client 或 Presenter root。 - -## 4. Typed repositories - -保持当前 `agents` 物理表和 migration 不变,在它上面提供两个 typed view: - -```ts -interface DeepChatAgentRepository { - get(id: string): Promise - list(): Promise - create(input: CreateDeepChatAgent): Promise - update(id: string, patch: DeepChatAgentPatch): Promise - delete(id: string): Promise -} - -interface AcpAgentRepository { - get(id: string): Promise - list(): Promise> - upsertManual(input: UpsertManualAcpAgent): Promise - updateInstallState(id: string, state: AcpInstallState): Promise - deleteManual(id: string): Promise -} -``` - -每个 repository 有自己的 codec,但读策略分两层: - -- catalog/legacy DTO read 保持当前宽容行为:单行 JSON parse failure 使用当前 null/default/filter 规则, - 不能让整次 `listAgents()` 失败; -- executable descriptor read 验证 kind/source 和 required capability。无法运行的 row 返回 typed - `AgentUnavailable`,不能猜成另一 kind,也不能伪造 manual command 或 registry launch spec; -- write path 对新数据保持严格验证。 - -DeepChat malformed config 继续按当前 default-merge 语义解析;manual ACP 缺 command、invalid -source/kind、registry install-state parse failure 和 source/id collision 各自由 Phase 0 fixture 固定当前 -catalog visibility、runtime availability 与 winner/filter 结果。旧 mixed `Agent` 只由 route compatibility -mapper 产生。 - -## 5. App session identity - -`new_sessions` 继续保存应用层 session metadata。内部使用带语义的标识,避免把本地 session id、ACP -remote session id 和 provider request id 混成一个 `string`: - -```ts -type AppSessionId = string & { readonly __brand: 'AppSessionId' } -type AcpRemoteSessionId = string & { readonly __brand: 'AcpRemoteSessionId' } -type GenerationId = string & { readonly __brand: 'GenerationId' } -``` - -这些 brand 只用于编译期,不改变数据库或 wire representation。 - -薄 `AppSessionService`/data port 复用当前 `new_sessions` owner,负责 title、project、pin、draft、window -binding、agent identity 和 session 清理的应用层事务。`session_kind` 仍只表示 `regular | subagent`; -backend kind 每次由 `agent_id -> AgentDescriptor` 解析。backend 不能复制一套 session table owner。 - -### ASLR-072 shared data ports - -production switch 没有把 shared data methods 塞进 `AgentManager`。session application coordinators 与 -direct ACP backend 通过四个独立 facet 访问现有 owner: - -```ts -interface AgentSharedDataPorts { - sessionState: AgentSessionStatePort - transcript: AgentTranscriptReadPort - transcriptMutation: AgentTranscriptMutationPort - tape: AgentTapePort -} -``` - -- `AgentSessionStatePort` 保留 init/destroy、full/lightweight state、permission、generation settings 和 - project-dir persistence; -- `AgentTranscriptReadPort` 服务 title/history/export/message lookup; -- `AgentTranscriptMutationPort` 服务 clear/edit/delete/fork/retry preparation,retry preparation 继续执行原 - summary 与 Memory invalidation,再把合法 input 交给当前 backend; -- `AgentTapePort` 服务 query/handoff/replay、显式 linked Tape view 与 production subagent link - finalization。 - -当前这些 port 由 `AgentRuntimePresenter` 作为过渡 adapter 实现,但 direct ACP 的网络 prompt、pending、 -permission、generation 和 ACP control 不通过该 presenter 执行。后续 ownership slice 可以替换 adapter, -无需修改 `AgentManager` 或 public route。Memory 的 data/state owner、schema 与触发规则在 `ASLR-072` -没有移动。 - -## 6. Transcript 与 output ports - -本目标不发明第四套 `TranscriptEvent` / message union。共享的是对当前 writer 的窄 adapter,类型直接 -复用现有 `MessageStartResult`、`LLMCoreStreamEvent`、message/block DTO、Tape fact input 与 typed -renderer events: - -```text -DeepChat LoopEngine - -> adapter over current MessageStore/TapeService/tapeFacts/event publisher - -ACP protocol event - -> current ACP event/content mapping (`LLMCoreStreamEvent` where already used) - -> AcpCompatibilityProjectionAdapter - -> the same current structured message/Tape/event writers - -ACP prompt request - -> AcpRequestTracePort - -> existing trace persistence/context (not a transcript type) -``` - -`AcpCompatibilityProjectionAdapter` 明确复现当前外层 runtime 的:Tape bootstrap、user/assistant message -create、stream block update、tool/message fact、ViewManifest attempt、terminal status/event 和 refresh -顺序。这样 restart/history/search/export 继续读取同一 structured projection。`acp_sessions`/`acp_turns` -仍是 remote binding/turn metadata,不是 transcript 内容库。 - -exact TypeScript port shape 在 Phase 0 从当前 writer call sites 裁剪;没有第二个真实调用需求的方法不 -进入接口。旧 renderer payload 继续由现有 typed boundary 生成。 - -## 7. 事务与一致性 - -- agent catalog write 和对应 catalog revision/notification 在一个明确 application operation 中完成; -- app session delete/clear 继续调用 backend-specific cleanup,再完成 shared metadata 清理;delete cleanup - 不要求 catalog descriptor 仍可执行,direct ACP durable metadata 通过独立 data port 删除; -- DeepChat Tape/message/trace 的事务边界保持现状,由 DeepChat data ports 管理; -- ACP session/turn metadata 持久化保持 ACP owner;direct ACP 的 transcript 仍写当前 structured - message/Tape projection; -- transcript projection 失败不能假装 backend fact 已经不存在,恢复策略沿用当前实现; -- 不引入跨 SQLite owner 的分布式 transaction abstraction。 - -## 8. `lib/agentRuntime` 的机械归属(ASLR-032 已完成) - -迁移只纠正 ownership,不改变逻辑: - -| 原内容 | 当前 owner | -| --- | --- | -| `backgroundExec*`、`shellEnvHelper`、`shellOutputEncoding`、`processTree`、`spawnGuard`、`rtk` | `src/main/agent/shared/process/` | -| `fffSearchService` | `src/main/agent/shared/workspace/` | -| `questionTool` | `src/main/presenter/toolPresenter/agentTools/questionTool.ts` | -| `sessionPaths` | `src/main/agent/shared/storage/sessionPaths.ts` | -| `systemEnvPromptBuilder` | `src/main/agent/deepchat/resources/systemEnvPromptBuilder.ts` | - -移动时在同一个机械 PR 内改完 imports/tests 并删除旧文件,不建立临时 compatibility 目录。机械 move -PR 不允许同时修改行为或公共类型。 - -## 9. 错误与安全 - -- row codec 错误带 agent id/kind 和安全的字段名,不记录 secret/config 全量; -- output sink 对 renderer 消失保持当前 fail-safe 语义; -- trace/request body 不进入通用 transcript; -- platform process port 不向 catalog/session 层暴露 raw child-process handle; -- workspace/path port 必须继续执行当前 path normalization 与 scope 检查; -- 删除 session 时遵循现有 Tape、message、Memory、ACP row 清理顺序,不由 shared 层猜测。 - -## 10. 迁移步骤 - -1. 冻结当前 table/schema/DTO/event 快照。 -2. 冻结 malformed `config_json`/`state_json`、missing manual command、invalid source×kind 和 id collision - 的 catalog/runtime matrix。 -3. 在原 repository 外增加两个 typed codec/view,不移动表;catalog tolerant read 与 executable strict - read 分开。 -4. 引入 `AppSessionService`,先由旧 Presenter 委托。 -5. 对现有 message/Tape/event/trace writers 裁剪窄 ports,并为 direct ACP 建 - `AcpCompatibilityProjectionAdapter` / `AcpRequestTracePort` characterization tests;不新增 canonical - event union。 -6. 把两个 backend 改为依赖 ports,而不是互相或 Presenter root。(direct ACP shared-data slice 已由 - `ASLR-072` 完成;compatibility ACP session/permission/admin port separation 已由 `ASLR-073` 完成; - DeepChat typed backend 与 legacy façade retirement 已由 `ASLR-090` 完成) -7. 逐项移动 `lib/agentRuntime` 文件,在同一 slice 更新 imports/tests 并删除旧路径。(已完成) -8. 所有 import 收敛后删除旧 mixed repository API 和旧目录。 - -## 11. 验证 - -- 对当前数据库 fixture 做 typed round-trip,字段与排序无 diff; -- malformed JSON、missing command、invalid source×kind、manual/registry id collision 不会让 catalog - batch 失败,且 runtime available/unavailable 与当前行为一致; -- DeepChat/ACP catalog CRUD 与通知分别测试; -- legacy mixed DTO snapshot 完全一致; -- transcript/event golden fixture 在迁移前后逐项相等; -- ACP restart/history/search/export 继续从现有 structured projection 得到相同内容,`acp_turns` 不被 - 误当成 transcript; -- session clear/delete/destroy 覆盖半完成 generation、active ACP process 和 Memory pending work; -- import graph guard 阻止 `shared -> deepchat`、`shared -> acp implementation`; -- secret、request body 和 tool raw payload 不因通用 projection 被额外持久化。 - -## 12. 明确不做 - -- 不合并 DeepChat 与 ACP 的事实表; -- 不重命名数据库表或 id; -- 不创建通用 ORM/repository framework; -- 不把所有 platform utility 塞进一个新的 `shared/utils`; -- 不更改 renderer transcript schema。 -- 不创建新的 canonical transcript/message/event model。 diff --git a/docs/architecture/agent-system-layered-runtime/modules/tape-and-observability.md b/docs/architecture/agent-system-layered-runtime/modules/tape-and-observability.md deleted file mode 100644 index 2207e1b21a..0000000000 --- a/docs/architecture/agent-system-layered-runtime/modules/tape-and-observability.md +++ /dev/null @@ -1,241 +0,0 @@ -# Tape 与可观测性 - -> 状态:目标合同,不是 current API reference。ASLR-052、ASLR-054、ASLR-080..081 与 ASLR-090 已接入 -> 下述 round commit、ViewManifest、causal observation 和 stable per-fact recorder slice。继续使用现有 -> Tape;不新建 event store 或 entry kind。 - -> Implementation progress: ASLR-052 placed the existing tool-round snapshot behind the fixed -> `afterRoundPersisted` callback. ASLR-090 retired that snapshot adapter: after the message projection -> commit, the callback now writes terminal tool-call/result facts through the stable per-fact -> `TapeRecorder.appendToolFact` port. Existing provenance, idempotency, pending exclusion, fact ordering and -> fail-open behavior remain unchanged. No Tape schema, entry kind, provenance field, or per-token write was -> added. ASLR-054 moved the existing -> ViewManifest attempt into the typed provider-attempt coordinator: each actual request sequence -> synchronously attempts its matching manifest before rate admission/provider streaming, and a -> persistence failure remains fail-open. -> ASLR-080 added a pure-read causal observation slice in the existing Tape service. It joins only persisted -> facts and reports the renderer event-history gap explicitly. ASLR-081 proved the reader does not mutate Tape, -> projections, replay state or Memory ingestion; the event-history gap remains explicit and unresolved. -> Subagent Tape lineage now separates true fork merge from production subagent finalization. True fork deltas -> and their receipt commit atomically; production children remain independent Tapes linked by a frozen-head -> `subagent/tape_linked` event and exposed only through an explicit, authorized, read-only View scope. - -## 1. 模块目的 - -Tape 是当前 agent transcript pipeline 的 append-only semantic ledger:在存续 lineage 内,以有序事实解释一个 turn -看到了什么、产生了什么、为何进入下一步。它与 message projection、trace storage 各有不同职责: - -```text -Tape semantic facts, anchors, manifests, lineage -Message store mutable renderer projection and compatibility history -Trace store sensitive/raw request-response diagnostics correlated by existing messageId/requestSeq -``` - -“append-only”不表示永久保存。session clear/delete/destroy 仍按当前合同删除 Tape。 - -## 2. BEFORE - -现有 `tapeService.ts` 已经提供 bootstrap、effective view、search、manifest、replay 和 fork。相关写入 -分散在 runtime、dispatch、compaction、handoff 与 Memory 接线中;mutable stream block 则主要存在于 -message projection。 - -所以本次不是重新设计 Tape,而是把调用时序和 provenance 收敛成一个窄 `TapeRecorder`,避免新 -`LoopEngine` 再次到处直接操作 store。 - -## 3. AFTER 的边界 - -```text -LoopEngine / DeepChatAgentInstance - │ semantic operation - ▼ -TapeRecorder -├─ TapeStore (existing schema/order) -├─ ViewManifest writer -└─ Replay/query service - -MessageProjection <--- projection mapper ---> Tape facts -TraceStore <--- existing messageId/requestSeq correlation ---> observation reader -``` - -`TapeRecorder` 是现有 service 的窄 application adapter,不拥有新的数据库。direct `kind=acp` 为兼容 -restart/search/export,也通过 `AcpCompatibilityProjectionAdapter` 写当前外层 pipeline 已经写入的 Tape -子集,但不运行 DeepChat LoopEngine。 - -## 4. 语义事实链 - -一个普通 turn 至少可以形成以下因果链: - -```text -user message fact - -> context/view manifest - -> assistant content/tool-call facts - -> tool result facts - -> optional compaction/handoff/memory audit anchors - -> current terminal message status + optional current runtime status -``` - -因果 observation 通过现有 `sessionId`、message/tool provenance、request sequence 联结 Tape、message、 -status 与 trace。renderer event history 当前没有 durable store,因此 read model 只能返回 -`eventHistory=not_persisted`,不得从 message status 推断 event。本目标不增加 -interaction/terminal/trace-reference Tape entry;若以后证明有缺口,单独建立 data/behavior SDD。 - -## 5. `TapeRecorder` 合同 - -```ts -interface TapeRecorder { - ensureSession(input: EnsureTapeSession): Promise - appendUserMessage(input: AppendUserMessageFact): Promise - appendViewManifest(input: AppendViewManifest): Promise - appendAssistantFact(input: AppendAssistantFact): Promise - appendToolFact(input: AppendToolFact): Promise - appendAnchor(input: AppendTapeAnchor): Promise - readEffectiveView(input: ReadEffectiveView): Promise -} -``` - -API 应贴近现有 store 能力。不能引入通用 `append(type, payload)` 让所有调用方重新依赖内部 schema。 - -每个 write 只携带当前 store 已经支持的 `sessionId`、message/tool provenance、anchor metadata 等字段。 -不为了新接口强加不存在的 `runId`/epoch column 或 trace ref payload。ViewManifest adapter 捕获当前 write -error、记录 warning 并返回 `null`,保持请求 fail-open。 - -## 6. 顺序与事务 - -- `entry_id` 继续提供 per-session monotonic order; -- user fact 必须在依赖它的 request manifest 之前; -- ViewManifest 必须描述实际交给 provider attempt 的 effective view; -- ViewManifest 在 request 前同步尝试,但 write failure 允许 request 继续; -- assistant/tool final facts 必须在 round persisted callback 之前可见; -- terminal message/status/event projection 只 settle 一次,Tape 不新增 terminal entry; -- message projection 与 Tape 无法单事务时,保持当前 commit/recovery 顺序并测试 crash window; -- Memory extraction 使用 effective Tape lineage,不从临时 mutable blocks 猜测最终事实。 - -### True fork 与 production subagent lineage - -- true fork 在创建时记录精确 parent head,在 merge 开始时冻结 fork head;只复制 cutoff 内的 semantic - delta,排除 `session/start` 与 `fork/start`; -- copied delta 与 parent `fork/merge` receipt 在同一 SQLite transaction 中 append;失败不留下部分 delta, - 已提交 retry 复用并校验既有 receipt; -- production subagent 是 durable child session/Tape,其 child entries 不进入 parent effective view。 - 结算统一调用 typed `linkSubagentTape()`,以 `completed | error | cancelled` outcome append - idempotent `subagent/tape_linked`; -- link receipt 冻结 child head/count,link event 同时冻结 child Tape incarnation identity。缺失 - capability 或 append 失败不得把任务标记为 Tape finalized; -- legacy external `fork/merge` 可在 direct-child ownership 成立时作为 completed link 读取;legacy - `fork/discard` 只保留 audit 语义。true fork receipt 不会被误判为 external child link。 - -### Cross-Tape recall - -- `AgentTapeViewScope` 只允许 `current | linked_subagents | current_and_linked`,默认仍是 `current`; -- linked source 每次读取都以 `new_sessions` 的 persisted direct-child relationship 授权,并同时校验 - link 的 Tape incarnation identity 与 frozen head;不递归 grandchildren,不接受任意 session id; -- search result 携带 source `sessionId` 并在所有 source 合并后应用一个 global limit;context 的 - `sourceSessionId` 每次只展开一个 Tape,窗口不跨 source; -- linked read 不触发 bootstrap、backfill、projection/FTS repair、Memory ingestion 或 event publish;已 - finalize 但被单独删除或 reset/rebuild 的 child 明确返回 unavailable,直到新 incarnation 被重新 - link; -- model surface 仍只有 `tape_search` 与 `tape_context`,没有第三个 tool,也不会把 linked child 自动注入 - provider context。 - -## 7. ViewManifest 与 trace - -ViewManifest 记录 context 的构成、policy/version、source entry references、summary/reconstruction 等可审计 -信息。它不能包含无需持久化的 secret。 - -provider request/response 的 raw body、headers 或敏感诊断继续进入 trace storage。现有 replay 使用 -message id/request sequence 查找 trace,并与 ViewManifest/Tape 联结;Tape 不新增 trace id、request hash -或 raw request 副本。这样 replay 能解释因果,但不会把 Tape 变成 request dump。 - -## 8. Mutable projection - -streaming 期间 assistant blocks 可以在 message projection 中节流更新。它们不是每次 mutation 都写 -Tape: - -```text -raw provider event -> in-memory accumulator -> throttled renderer/message projection - └------> final semantic Tape fact -``` - -retry、replacement、retraction、rollback 继续通过现有事实/anchor/lineage contract 表达。不能用 SQL -update 伪造已经发生的历史,也不能因为 Tape append-only 就禁止现有 session delete。 - -## 9. Replay 范围 - -支持的是 audit/replay slice:给定 session/message/request,联结 input view、effective assistant/tool facts -与当前 terminal message projection;interaction/runtime event history 不持久化。明确不支持: - -- 自动重放 provider 网络请求; -- 重做 tool side effect; -- 从逐 token log 恢复 UI 动画; -- 在缺少旧 trace retention 时凭空还原 raw request。 - -### ASLR-080 causal observation read model - -- request 是 `manifest_bound`、`manifest_missing`、`manifest_malformed` 或 `request_unavailable`; -- explicit `requestSeq` 优先,否则取 raw ViewManifest `source_seq` 与 positive trace `request_seq` 的最大值, - 忽略 interleaved-reasoning 的 `request_seq=0` sentinel; -- `manifest_bound` 复用现有 replay slice;missing/malformed 只返回同 sequence 的 metadata-only trace, - 不伪造 manifest,hash integrity invalid 仍保留 readable invalid record; -- assistant message/tool facts 没有 request sequence,output 固定声明 `message_only` correlation;terminal - assistant projection 只暴露 status/order/timestamps 和 content/metadata hashes; -- 默认不暴露 Tape payload/meta、trace headers/body 或 message content/metadata/blocks/errors;显式 opt-in - 只复用现有 `includeTapePayloads` / `includeTracePayload`; -- old sessions、pending messages 与未 hydrate runtime 分别返回 unavailable/partial、无 terminal message、 - current runtime unavailable,且读取不触发 bootstrap/backfill/projection/Memory/event side effect。 - -### ASLR-081 non-interference proof - -- repeated default、explicit/latest request、trace-only 与 payload opt-in reads 前后,Tape rows/max/order、 - ViewManifest rows、message/trace rows、effective view、existing replay slice/hash 完全相同; -- fixture 同时包含 replacement、retraction、pending assistant 与 final tool-call/result facts;observation - output 仍为 `message_only`,不会改变 effective fold; -- Memory ingestion projection meta/range 与 session cursor 不变,现有 projection/cursor write seams 为零调用; - TapeService 不拥有也不暴露 runtime cooldown,不能在本任务伪造 snapshot;cooldown 的公共合同与迁移属于 - `ASLR-059`; -- default serialization 不暴露 Tape payload/meta、trace headers/body 或 terminal message raw fields;opt-in - 只增加现有 replay flags 允许的 Tape/trace payload 以及由此派生的 replay slice hash; -- old sessions 不 bootstrap/backfill/insert,architecture guard 禁止 observation reader 引入 write、SQL、 - event subscription/publication、projection mutation 或 Memory runtime value-import/call edge; -- native SQLite 可用时,同一套 proof 直接比较现有 Tape/message/trace/Memory projection/session tables 与 - `sqlite_master` 的完整 user schema(排除 SQLite internal objects)。本 test/proof slice 没有 production - route/schema/raw-token log/durable event 改动。renderer event history 仍为 `not_persisted`。 - -## 10. Memory 与 Tape - -- `memory/*`、`persona/*` anchors 继续是 non-reconstruction; -- prompt injection 即使 view anchor 写入失败,也按当前 fail-open 合同处理; -- extraction 必须读取本次确定的 effective Tape slice 和 exact lineage; -- cursor commit 只发生在 Memory job `ok: true` 后; -- retry/rollback/edit/delete/clear/destroy 继续推进 rewind/epoch,拒绝 stale ingestion。 - -详见 [Memory integration](./memory-integration.md)。 - -## 11. 迁移步骤 - -1. 冻结现有 entry sequence、ViewManifest、trace/replay golden fixtures。 -2. 用 `TapeRecorder` 包住现有 service,不改变 schema。 -3. 按 user -> view -> assistant/tool 的顺序迁移 runtime call sites;terminal 继续使用现有 projection。 -4. 迁移现有 compaction/handoff anchors,不新增 interaction audit entry。 -5. 最后迁移 Memory anchors/extraction references。 -6. 删除 LoopEngine 对 concrete Tape store 的直接 import。 -7. 若发现现有 entry 无法满足新的审计需求,记录单独 SDD,不在本目标增加 entry type。 - -## 12. 验证 - -- normal/tool/pause/retry/rollback/compaction/handoff/Memory 的 ordered entry golden tests; -- ViewManifest source references 与真实 provider context 一致; -- ViewManifest write failure 继续发送 provider request且不异步补写; -- raw secrets/request body 不因迁移进入 Tape; -- crash/abort at projection-before-Tape 与 Tape-before-projection recovery; -- duplicate/late terminal projection callback 幂等且无新 Tape entry; -- replay slice 在迁移前后等价; -- clear/delete/destroy 完成 Tape/trace/message/Memory 当前清理合同; -- large stream 不产生 per-token Tape growth。 - -## 13. 明确不做 - -- 不创建第二个 Tape 或通用 event-sourcing framework; -- 不把 message store 整体替换为 Tape projection; -- 不逐 token 持久化; -- 不扩张 trace retention 或隐私范围; -- 不承诺可重放外部 side effect。 diff --git a/docs/architecture/agent-system-layered-runtime/modules/tools-skills-and-mcp.md b/docs/architecture/agent-system-layered-runtime/modules/tools-skills-and-mcp.md deleted file mode 100644 index 0b969fb2fc..0000000000 --- a/docs/architecture/agent-system-layered-runtime/modules/tools-skills-and-mcp.md +++ /dev/null @@ -1,190 +0,0 @@ -# Tools、Skills 与 MCP 资源接线 - -> 状态:目标设计,不是 current API reference。资源 owner 保持独立,loop 只消费解析后的能力;下文 -> resource model、adapter 和迁移步骤是目标合同,当前 concrete API 以实施进度明确列出的 slice 为准。 - -> 实施进度:ASLR-045 已把 message-scoped runtime skill selection、prompt snapshot 和 final tool-definition -> snapshot cache 迁入 `DeepChatAgentInstance`,全局 tool registry revision 由 `DeepChatAgentRuntime` -> 广播失效。ASLR-055 已把 session-scoped catalog、execution 和 result-normalization ports 接入现有 -> loop/process/dispatch:catalog cache 仍按 profile fingerprint 和 registry revision 失效,最终 definitions -> 只来自 `ToolPresenter.getAllToolDefinitions()`;execution/result adapters 等价委托现有 pre-check、call、 -> screenshot normalization 和 output guard。SkillPresenter、ToolPresenter、McpPresenter、configured -> selection 与 collision policy 的 owner 均未移动。ASLR-056 已把四种合法 pause origin 映射为 -> ordered typed batch outcome,并由 instance 持有当前 batch execution state。 - -## 1. 模块目的 - -这个模块定义 DeepChat 和 ACP 怎样使用 tools、skills、MCP,而不把这些 subsystem 搬进 agent -runtime。关键区分是 ownership、selection 和 delivery: - -```text -Presenter/service owns catalog/runtime -agent/session stores selection references -backend adapter resolves and delivers capabilities -``` - -## 2. BEFORE - -MCP 本身相对独立,但 `AgentRuntimePresenter` 在 prompt、tool refresh、dispatch、permission、skill -activation 等多个位置直接调用相关 Presenter。ACP 又通过自己的 session config 使用 MCP,而不是使用 -传给 `AcpProvider` 的 DeepChat tool list。 - -`src/main/lib/agentRuntime/questionTool` 还把一个具体 tool 放在名为 runtime 的混合目录里,进一步模糊 -ownership。 - -## 3. AFTER 的 owner - -| 能力 | 数据/运行 owner | DeepChat delivery | ACP delivery | -| --- | --- | --- | --- | -| local built-in tools | `ToolPresenter` | `DeepChatToolPort` | 不作为 direct ACP callable tools;regular direct ACP 与 DeepChat + ACP-provider 均保留当前 prompt descriptions | -| MCP servers/tools | `McpPresenter` + `ToolPresenter` aggregate | ToolPresenter 返回最终 provider definitions + dispatcher | direct ACP session MCP config | -| skill catalog/content | `SkillPresenter` | prompt sections、activation、skill tools | direct ACP 不新增 callable skill;regular/subagent 当前 system-prompt 差异保持 | -| plugin-provided capabilities | `PluginPresenter`/对应 owner | 经 Tool/Skill adapter | 仅经 ACP 明确支持的 adapter | - -`AgentManager` 只管理 selection reference 与 agent association,不复制 catalog/runtime。 - -## 4. Resource model - -```ts -interface SessionResourceSelection { - skillIds: string[] - mcpServerIds: string[] - extensionPolicy: AgentExtensionPolicy -} - -interface ResolvedDeepChatResources { - promptSections: PromptSection[] - tools: ResolvedTool[] - revision: ResourceRevision -} -``` - -selection 与 resolved resource 分离:前者可持久化且轻量,后者只对当前 turn/round snapshot 有效。resolved -object 不得跨 owner revision 永久缓存。 - -## 5. DeepChat resource assembly - -```text -load session/agent selection - -> query SkillPresenter for prompt/activation data - -> query DeepChatToolCatalogPort -> ToolPresenter for the final merged definitions - (ToolPresenter alone applies MCP/local/plugin scope and collision policy) - -> build prompt sections + use those final provider tool definitions - -> freeze resource revision for provider attempt -``` - -provider 返回 tool call 后: - -```text -normalize identity/arguments - -> resolve same revision or approved refresh mapping - -> pre-check permission when current policy applies - -> question interception or ToolPresenter execution - -> capture post-call permission / post-success skill-draft interaction - -> normalize/fix output size - -> persist fact/projection - -> apply skill activation/resource revision - -> refresh before next provider round when current rule requires -``` - -`processStream` 和 legacy `dispatch` 只持有 `ToolCatalogPort`、`ToolExecutionPort` 与 `ToolResultPort`, -不再直接持有 `IToolPresenter`、normalization callback 或 concrete `ToolOutputGuard`。ASLR-056 后, -legacy batch dispatcher 把 permission/question/post-call/skill-draft decision 映射成 ordered typed -outcome;adapter 不决定 pause。已调用与已提交 result 的 call ids 随 outcome 交给 instance,逐项响应期间 -不会重新运行整个 batch 或重放已提交 side effect。 - -tool collision priority、parallel eligibility、argument repair、result fitting 和 renderer payload 均保持当前 -行为。 - -## 6. Skills - -skill 有三种参与形式,必须分开: - -1. catalog metadata:让模型知道可用 skill; -2. pinned/activated content:按固定 prompt 顺序注入; -3. activation/tool result:可能改变下一 round 的 resource revision。 - -`SkillPresenter` 继续拥有 catalog、内容加载、activation policy 和关联数据。loop adapter 只查询并产生 -`PromptSection`/resource delta。禁止把 skill 文本复制进 instance 成为第二事实源。 - -agent-scoped extensions 的 enabled/disabled、global/agent/session 合并和 existing precedence 由现有合同 -锁定;本次只让调用点变得明确。 - -## 7. MCP - -`McpPresenter` 继续拥有 server definitions、connectivity、tool discovery 和调用状态。两个 backend 使用 -不同 adapter: - -```text -DeepChatToolCatalogPort: - selection -> ToolPresenter aggregate -> final collision-resolved definitions/dispatcher - -AcpMcpDeliveryAdapter: - selected servers -> ACP-compatible server configuration -> remote ACP session -``` - -不能为了复用让 ACP MCP 先变成 DeepChat `ToolDefinition[]` 再还原。server identity、enabled state、 -collision 和 refresh 行为必须保持。 - -## 8. Question 与交互 tool - -`questionTool` 是 `ToolPresenter` 管理的 DeepChat tool implementation,不属于 platform runtime。dispatch -按当前规则拦截它并产生 ordered interaction,不调用 underlying tool。post-call -`requiresPermission` 和 post-success skill-draft confirmation 也进入同一 ordered batch;instance 保存 -`PendingInteraction[]`,不是单个 current-run callback。tool 本身不直接访问 Electron window。 - -## 9. 错误与取消 - -- resource owner 查询失败按现有 required/optional policy 处理; -- 单个 disabled/disconnected MCP server 不得改变其他 server 的稳定顺序; -- tool execution 使用当前 `AbortSignal`,late result 通过 run/interaction epoch 拒绝; -- `ToolExecutionPort` 不另造 cancel channel;现有 owner 只支持把同一个 `AbortSignal` 传入 `callTool`, - deferred tool 也沿用该合同; -- skill refresh 失败不能使用未验证的半新 revision; -- tool 输出 normalization/fitting 失败按当前 tool error fact 处理; -- ACP MCP delivery 失败由 ACP backend 映射,不能落入 DeepChat tool error 分支。 - -## 10. 迁移步骤 - -1. 冻结 tool list/order/collision、skill prompt、MCP delivery 和 output fixtures。 -2. 给现有 Presenter 调用增加窄 read/execute ports,内部实现不动。 -3. 建 `SessionResourceSelection` 与 revision snapshot。 -4. 把 DeepChat prompt resolution 收敛到 resource adapter,tool list 只通过 ToolPresenter aggregate port。 -5. 把 dispatch 改为 `DeepChatToolPort`,保留 permission/output policy。 -6. 把 `questionTool` 移到 ToolPresenter owner,并在同一 slice 更新 imports/tests、删除旧路径。 -7. 给 ACP 建独立 MCP delivery adapter。 -8. 删除 runtime 对 Skill/MCP concrete Presenter 的散落调用。 - -## 11. 验证 - -- zero/one/many local、MCP、plugin tools 的 exact definition order; -- duplicate tool names 与当前 collision winner; -- enabled/disabled/disconnected MCP server; -- global/agent/session extension selection precedence; -- pinned、activated、missing、changed skill; -- skill activation 后下一 round prompt/tool revision; -- parallel/sequential tool execution 与 output fitting; -- permission allow/deny/pause/cancel; -- pre-check、question、post-call permission、skill-draft 和同批 multiple interactions; -- ACP MCP config snapshot 与 current baseline; -- owner shutdown/reconnect 时 session 不保留 stale callable object。 - -ASLR-055 的 typed-port 与 real-boundary proof 位于 -`test/main/presenter/agentRuntimePresenter/toolAdapters.test.ts`、`process.test.ts`、`dispatch.test.ts`、 -`toolOutputGuard.test.ts` 和 `test/main/presenter/toolPresenter/toolPresenter.test.ts`。它们锁定 -zero/one/many、collision、policy/cache/revision、parallel/sequential ordering、exact call options、 -normalization/offload/failure、skill refresh 和 abort forwarding。 - -ASLR-056 的 ordered-outcome proof 位于 `dispatch.test.ts`、`process.test.ts`、 -`agentRuntimePresenter.test.ts` 与 `deepChatAgentRuntime.test.ts`。它锁定现有 persisted action 顺序、 -四种 origin、live-store refresh、逐项 execution-state 演进、post-call no-replay,以及 final-item-only -fresh resume。 - -## 12. 明确不做 - -- 不合并 `McpPresenter`、`SkillPresenter`、`ToolPresenter`; -- 不将 resources 注册到 generic lifecycle plugin bus; -- 不给 direct `kind=acp` 自动新增可调用的 DeepChat-only tools/skills;不删除 regular ACP 或 DeepChat + - ACP-provider 已有 system-prompt descriptions; -- 不更改 collision、parallel 或 output fitting policy; -- 不新增 dependency 或通用 resource framework。 diff --git a/docs/architecture/agent-system-layered-runtime/spec.md b/docs/architecture/agent-system-layered-runtime/spec.md deleted file mode 100644 index 617fcf20e9..0000000000 --- a/docs/architecture/agent-system-layered-runtime/spec.md +++ /dev/null @@ -1,229 +0,0 @@ -# Agent System Layered Runtime — Specification - -> 状态:implementation complete;本文记录最终架构与已验证的兼容合同。 -> 总览:[README.md](./README.md);当前实现入口:[ARCHITECTURE.md](../../ARCHITECTURE.md) 与 -> [FLOWS.md](../../FLOWS.md)。 - -## 1. User need - -维护者需要用稳定、可观察、可暂停的心智模型理解和修改 agent runtime:顶层管理 agent 与 -session,具体实例管理自己的 session state,DeepChat loop 只负责 turn/round/tool 状态机。 - -迁移前实现把 ACP 与 DeepChat 合并到同一 `IAgentImplementation`,但生产只有一个 -`AgentRuntimePresenter` implementation;ACP 实际通过 `AcpProvider` 再进入外部 loop。这造成: - -- 类型充满 optional fields/methods; -- session state 通过 singleton 上的几十个 map 维护; -- prompt、tool、permission、compaction、memory、Tape lifecycle 没有统一的执行顺序; -- ACP 与 DeepChat 的差异被 provider id 特判隐藏; -- 新功能持续进入两个大 presenter,Memory 等已有模块承担不必要的回归风险。 - -## 2. Goals - -1. 建立一个薄的 `AgentManager` control plane,显式管理 agent kind、catalog、app session 与 backend - dispatch。 -2. 将 DeepChat 与 ACP 拆成两个 required、typed session backend;删除 fake unified runtime。 -3. 为每个 hydrated DeepChat session 建立一个 `DeepChatAgentInstance`,明确 session state owner。 -4. 建立 DeepChat-only `LoopEngine`,用固定 typed stages 组织 provider round、tool loop、pause/resume、 - cancellation 与 settlement。 -5. 将 MCP、skills、prompt、permission、compaction、Tape、Memory 接为显式 collaborator/adapter, - 保留原模块 ownership。 -6. 让内部 lifecycle seam 可以 `await`;只有明确的 tool interaction outcomes 可以产生持久 `pause`。 -7. 保留现有 Tape、ViewManifest、trace、replay 与 Memory correctness/privacy/performance 合同。 -8. 使用可逐步回滚的 delegation 迁移,不改变现有用户行为、wire contracts 或 storage schema。 - -## 3. Non-goals - -- 不新增 agent kind、agent marketplace 或 runtime plugin API。 -- 不设计 ACP 与 DeepChat 共用的 universal LoopEngine。 -- 不把 MCP、skills、plugins、memory 的持久化和生命周期搬进 AgentManager。 -- 不改变 provider、tool、permission、prompt、compaction、memory 的产品行为。 -- 不把 Tape 改成逐 token raw log、永久不可删除存储或 live side-effect replay engine。 -- 不清理/重命名数据库表,不迁移历史数据格式。 -- 不改变 renderer UI、route/event payload 或 remote/cron feature behavior。 -- 不在本目标修复审计中发现的行为不对称。 -- 不引入 DI container、generic middleware framework、priority number ordering 或新依赖。 - -## 4. Constraints - -### 4.1 Compatibility - -- Existing typed routes/events are public app contracts and remain unchanged during migration. -- Existing SQLite tables and DuckDB Memory sidecars remain readable/writable without migration. -- `providerId='acp'` / `modelId=agentId` compatibility stays until every current consumer is moved. -- `kind='deepchat' + providerId='acp'` is a supported legacy/domain combination in current data and main - routes. It remains a DeepChat session using the ACP-as-provider adapter; only `kind='acp'` selects the direct - ACP session backend. -- ACP aliases, workdir validation, modes/config/commands, permission timeout and transfer restrictions stay - unchanged. -- Legacy import, history search/export, remote control, cron and subagent call paths remain operational. - -### 4.2 Architecture - -- Dependency direction is `manager -> backend -> narrow ports`; no loop access to global presenter singleton. -- Common contracts contain only capabilities implemented by both backends. -- Kind-specific capabilities are required facets, never optional methods on a shared mega-interface. -- Shared physical storage may use one table, but codecs and domain repositories are kind-specific. -- Lifecycle stage order is defined in code and docs, not runtime-configured priority. - -### 4.3 Safety - -- Provider/tool side effects must not be retried merely to fit output or compare implementations. -- Streaming accumulator/echo remains non-blocking and is not exposed as awaited extension hook. -- Required persistence/settlement failures fail explicitly; documented optional contributors preserve their - current fail-open behavior. -- Memory delete/shutdown fencing and provider permission settlement complete before dependent resources close. - -## 5. Locked decisions - -| ID | Decision | -| --- | --- | -| D1 | 保留薄 common control plane;删除 unified implementation abstraction。 | -| D2 | 内部使用 `AgentDescriptor` discriminated union 与 explicit `switch(kind)`。 | -| D3 | `agents` / app session physical tables 保留;拆 typed codecs/repositories,不拆表。 | -| D4 | 每个 hydrated DeepChat session 有一个 instance;每个 turn 使用独立 `LoopRun` state。 | -| D5 | `kind=acp` 拥有独立 process/session/protocol backend,不进入 DeepChat LoopEngine;DeepChat agent 选择 ACP provider 的兼容路径保留。 | -| D6 | Loop lifecycle 使用固定 typed stages;无 generic event bus/priority registry。 | -| D7 | MCP/skills 通过 resource adapters 贡献定义/prompt/执行引用,但原 Presenter 继续做 owner。 | -| D8 | Tool interaction 是 typed batch outcome,compaction 是 input/context coordinator,Memory coordinator 暴露 prompt contributor + ingestion observer;不强迫它们实现一个万能接口。 | -| D9 | Tape 是 semantic ledger;mutable stream 是 projection;trace 存 request payload;不建第二个 Tape,本目标不新增 interaction/terminal/trace-ref entry。 | -| D10 | MemoryPresenter 及其 schema/retrieval/projection/maintenance 在本目标冻结,Memory integration 最后迁移。 | -| D11 | 无 schema/wire migration;发现行为 bug 另立 spec。 | -| D12 | 本目标 supersede `agent-runtime-presenter-split` proposal。 | - -## 6. Functional requirements - -### 6.1 Agent management - -- Catalog returns correctly typed DeepChat and ACP descriptors from the existing `agents` table. -- ACP descriptors are additionally discriminated by `source: 'manual' | 'registry'`; manual launch config and - registry/install metadata are not optional fields on one shape. -- Catalog reads remain tolerant per current malformed-row/null/default/filter behavior; executable descriptor - resolution is capability-strict and returns typed unavailable errors without failing the whole list or - guessing another kind. -- DeepChat CRUD only publishes generic/DeepChat catalog updates; it does not invoke ACP-specific refresh by - name or ownership. -- AgentManager loads the current app session `agentId`, resolves its descriptor kind and verifies any hydrated - backend binding; `new_sessions.session_kind` remains the unrelated `regular | subagent` field. -- Unknown/mismatched kinds fail fast with a typed internal error. -- App session list, activate/deactivate, pin/title/draft/window binding stay shared. - -### 6.2 DeepChat instance - -- Instance owns effective identity/config, status, pending input coordination, ordered pending interactions, - runtime-activated skills and session caches. -- Pre-stream cancellation/status exists before the externally visible active generation is registered; the - current registration point after assistant projection creation remains stable. -- Turn-local abort, run id, request sequence, provider recovery flags and mutable conversation state live in - one `LoopRun`, not in global maps. -- Lazy hydration preserves restored sessions and does not instantiate the complete history list at startup. -- Destroy/clear/edit/retry paths invalidate exactly the same generation, summary and Memory state as today. - -### 6.3 Loop engine - -- Provider request and tool-loop order matches the current `processMessage -> runStreamForMessage -> - processStream -> dispatch` behavior. -- Fixed stage callbacks may be async and are cancellation-aware. -- Awaiting a transform pauses in-memory progression. Persistent user pause is only produced by typed tool - interaction outcomes: pre-check permission, question interception, post-call `requiresPermission`, or - post-success skill-draft confirmation. -- A paused provider run settles. Each response performs and persists the first origin-specific action; if more - interactions remain the session stays paused, and only resolving the final item rebuilds context and starts - one fresh resume run. -- The engine preserves max provider/tool rounds, rate-limit gate, context recovery, tool parallelization, - skill refresh, output fitting, cancellation and stale-run guards. -- The engine imports ports/contracts only, not presenters, routes, Electron windows or concrete SQLite owner. - -### 6.4 ACP - -- ACP catalog/install/launch/alias/debug/process/session/protocol/persistence/mapping have one domain module - owner with route/provider compatibility adapters where still required. -- ACP uses its external loop and protocol permission continuation. -- Direct ACP writes through an `AcpCompatibilityProjectionAdapter` backed by the existing structured message, - Tape and renderer event writers; `acp_turns` remains metadata and cannot replace transcript persistence. -- Direct ACP uses an `AcpRequestTracePort` with the current endpoint/body, correlation, redaction/truncation and - fail-open trace-before-`connection.prompt` order. -- Direct regular ACP uses an `AcpCompatibilityPromptBuilder` to preserve the current runtime/env/tool/skill/ - local-resource first system message; ACP-backed subagent keeps its current bypass. -- Regular ACP and ACP-backed subagent compatibility behavior remain separately tested. -- `AcpProvider` remains as long as a DeepChat descriptor may select `providerId='acp'`. It delegates to the same - ACP core, but keeps the current DeepChat outer-loop compatibility prompt/resource behavior. - -### 6.5 Resources and cross-cutting behavior - -- Prompt section order is deterministic and equivalent to the baseline. -- ToolPresenter remains the tool source aggregate and collision/dispatch owner. -- DeepChat MCP delivery uses tool definitions; ACP MCP delivery uses ACP session-init config conversion. -- Skill discovery, pinning, runtime activation and agent-scoped policy keep current semantics. -- DeepChat ordered tool interactions and ACP protocol permission are separate continuations behind a shared UI - decision port only. -- Compaction CAS/anchor/message/event boundaries remain atomic at the same point. - -### 6.6 Tape and Memory - -- Existing Tape facts, replacement/retraction fold, bootstrap, ViewManifest, trace and replay contracts remain - intact. -- Lifecycle observability joins existing Tape/message terminal status/trace data and an optional current - non-hydrating runtime status. Renderer event history is not persisted and must be reported as unavailable, - never inferred from message state. Adding a new Tape entry kind or durable event history is a separate - data/behavior SDD, not part of this structural migration. -- Observation reads are proven non-interfering across existing Tape/message/trace/Memory projection tables: - they do not change effective view, replay privacy/hash state, ingestion projection/cursor state, complete user - schema or routes, and an AST guard prevents Memory runtime, event-subscription and storage-write edges. This - test/proof slice adds no production raw-token persistence. TapeService has no public projection-retry cooldown - seam; that contract remains assigned to `ASLR-059`. This proof does not close the persisted renderer - event-history gap. -- Memory injection remains opt-in, sanitized, hard-budgeted and fail-open. -- Memory extraction remains background, per-session serialized, exact-lineage and cursor-safe. -- Memory edit/delete/retry/clear/shutdown invalidation and fencing remain unchanged. - -## 7. Acceptance criteria - -### Architecture - -- [x] `AgentRegistry` no longer exists in production flow. -- [x] `IAgentImplementation` optional mega-interface is removed. -- [x] Internal descriptors use one canonical `kind`; legacy aliases are quarantined at boundary adapters. -- [x] `AgentManager` contains no prompt, provider round, tool execution, compaction or Memory logic. -- [x] `kind=acp` cannot be dispatched through DeepChat `LoopEngine`; the generic provider port still supports - the documented DeepChat + ACP-provider combination. -- [x] DeepChat session state has one instance owner and LoopEngine has no cross-session maps. -- [x] `src/main/lib/agentRuntime` no longer exists; every file has an explicit owner. - -### Behavior parity - -- [x] Normal, tool, multi-round, queue, steer, cancel, question and permission flows preserve event and - persistence order. -- [x] Prompt and provider request parity tests pass for initial, tool-loop, resume and overflow-recovery turns. -- [x] ACP regular/subagent, workdir, mode/config/commands, cancel and permission timeout tests pass. -- [x] DeepChat descriptor + ACP provider request/prompt/tool-resource compatibility fixtures pass. -- [x] Tape golden flow and replay/privacy tests pass. -- [x] All Memory correctness, native, performance and lifecycle gates listed in - [migration-and-validation.md](./migration-and-validation.md) pass. -- [x] Remote, cron, subagent, transfer, import, export, search and dashboard integrations pass. - -### Compatibility - -- [x] No unapproved shared route/event schema diff. -- [x] No database schema or migration version change is required by the refactor. -- [x] Old sessions restore and continue without eager migration. -- [x] Each implementation slice can be reverted without data rollback. -- [x] Architecture guards describe the new paths and reject resurrection of retired paths. - -## 8. Success measures - -Success is measured by ownership and parity, not by arbitrary file-count or line-count targets: - -- a developer can locate agent catalog, concrete session state, loop, Tape, ACP protocol and Memory adapter - from the directory tree without reading a god object; -- each state has one writer/owner; -- kind-specific code does not appear as optional shared capabilities; -- lifecycle order is tested directly; -- changing a resource adapter does not require constructing the full presenter graph; -- adding a future agent kind requires a new backend and explicit router branch, not ACP/DeepChat conditionals - inside LoopEngine. - -## 9. Open questions - -None. Every implementation-affecting choice is locked above. Any request to alter behavior, wire schema, data -schema or Tape/Memory semantics creates a separate SDD goal. diff --git a/docs/architecture/agent-system.md b/docs/architecture/agent-system.md index b6c9f3840a..eaafe89b08 100644 --- a/docs/architecture/agent-system.md +++ b/docs/architecture/agent-system.md @@ -1,217 +1,127 @@ -# Agent 系统架构详解 +# Agent 系统 -本文描述当前实现。迁移决策、兼容矩阵与最终验证记录见 -[Agent System Layered Runtime](./agent-system-layered-runtime/README.md)。更早的 presenter-split -提案已经被取代,历史内容通过 Git 记录查询。 +本文描述 `2026-07-16` 的当前实现。历史迁移方案、Presenter 拆分过程和已完成的分阶段 +SDD 已删除;需要时从 Git 历史查询。 -DeepChat 与 ACP 的执行路径对比见 -[deepchat-vs-acp-agents/](./deepchat-vs-acp-agents/)。 +## 两种 Agent backend -当前消息与权限边界由以下维护合同定义: - -- [Send Message Canonical Boundary](./send-message-canonical-boundary/spec.md):route/application - compatibility 与 runtime canonical input 的分界; -- [Permission Mode Policy Ownership](./permission-mode-policy-owner/spec.md):assignment、storage 与 - deferred execution 的权限策略 owner; -- [Provider Round Stop Contract](./provider-round-stop-contract/spec.md):AI SDK、ACP 与内部 provider - round terminal reason 的无损映射。 - -## Agent 类型与路由 - -DeepChat 支持两个 executable descriptor kind: - -- `kind='deepchat'`:in-process `DeepChatAgentRuntime` / `DeepChatAgentInstance`,由 DeepChat-only - `DeepChatLoopEngine` 驱动 provider/tool rounds。 -- `kind='acp'`:direct `AcpAgentRuntime` / `AcpAgentInstance`,通过 ACP SDK 与外部进程自己的 loop 通信。 - -内部使用 `DeepChatAgentDescriptor | AcpAgentDescriptor` discriminated union。ACP descriptor 再按 -`source='manual' | 'registry'` 区分 required launch/registry data。`type` / `agentType` 只存在于 storage、 -route DTO 和 renderer compatibility boundary;internal manager/backend 不做 alias fallback。 - -agent kind 与 DeepChat provider selection 正交: +`AgentManager` 只按 canonical `AgentDescriptor.kind` 选择 backend: ```text -kind=deepchat + ordinary provider -> DeepChat LoopEngine -> ordinary provider -kind=deepchat + providerId=acp -> DeepChat LoopEngine -> AcpProvider compatibility adapter -kind=acp -> direct ACP backend -> external ACP protocol loop +kind=deepchat -> DeepChatAgentRuntime -> DeepChatAgentInstance -> DeepChatLoopEngine +kind=acp -> AcpAgentRuntime -> AcpAgentInstance -> ACP process/protocol ``` -## 当前所有权 +`kind=deepchat + providerId=acp` 是兼容组合:它仍运行 DeepChat loop,只把 ACP 作为 provider。 +Direct ACP 不进入 `DeepChatLoopEngine`,也不通过 DeepChat ToolService 执行外部 agent 自己的 loop。 -```mermaid -flowchart TD - UI["Renderer / typed routes"] --> RouteOwners["Session route owners
search / translation / export / usage / catalog"] - UI --> Services["SessionService / ChatService"] - Services --> App["Session application coordinators"] - UI --> RoutePorts["Main route runtime
four separate session ports"] - RoutePorts --> App - App --> Sessions["AppSessionService"] - App --> Manager["AgentManager"] - Manager --> Catalog["strict executable catalog"] - Manager --> DeepBackend["DeepChatAgentBackend"] - Manager --> AcpBackend["DirectAcpSessionBackend"] - DeepBackend --> DeepRuntime["DeepChatAgentRuntime"] - DeepRuntime --> DeepInstance["DeepChatAgentInstance"] - DeepInstance --> Loop["DeepChatLoopEngine + LoopRun"] - AcpBackend --> AcpRuntime["AcpAgentRuntime"] - AcpRuntime --> AcpInstance["AcpAgentInstance"] - Loop --> Provider["ProviderPort"] - Loop --> ToolPorts["Tool ports"] - Loop --> Tape["TapeRecorder / OutputSink"] - Loop --> Memory["Memory prompt/ingestion ports"] -``` +内部 descriptor 使用 discriminated union。旧 `type` / `agentType` 只允许在 storage、route DTO 和 +renderer compatibility adapter 出现;manager/backend 不做反射式 fallback。 -所有权原则: - -- `AgentManager` 是薄 control plane,只做 catalog/app-session lookup、alias normalization、kind switch 和 - required facet selection。 -- `SessionLifecycleCoordinator`、`SessionTurnCoordinator`、`SessionAgentAssignmentCoordinator` 和 - `SessionProjectionCoordinator` 分别拥有 core session application invariants;typed Session/Chat、Remote - 和 Cron 通过 consumer-owned narrow ports 调用它们。 -- `AgentSessionPresenter` 与 main-process `IAgentSessionPresenter` 已退休;main route runtime、Tool、MCP、 - Floating 与 hooks 直接调用 composition-owned coordinators,不经过 aggregate façade。 -- typed session routes 直接组合 `SessionHistorySearch`、`SessionTranslation`、 - `AgentSessionExportService`、`UsageStatsService`、RTK runtime service 与 available-agent catalog policy; - lifecycle hooks 直接调用 startup migration/maintenance owner。 -- `AgentRuntimePresenter` 保留 DeepChat state/delegate façade,初始化 `DeepChatAgentRuntime`,并接线现有 - message/Tape/prompt/provider/tool/permission adapters。它不再实现 unified agent interface,也不构造 - `AcpAgentRuntime`。 -- composition root 负责 backend wiring 和 `AcpAgentRuntime` construction;runtime/instance 实现分别位于 - `agent/deepchat` 与 `agent/acp`,并且只构造一组 session application coordinators。 - -## 目录与职责 +## 生命周期和所有权 -```text -src/main/agent/ -├── manager/ -│ ├── agentManager.ts # explicit descriptor.kind router -│ ├── sessionHandles.ts # common + required kind facets -│ ├── deepChatAgentBackend.ts # typed DeepChat runtime/delegate adapter -│ └── directAcpAgentBackend.ts # direct ACP adapter -├── shared/ -│ ├── agentDescriptors.ts # canonical discriminated descriptors -│ ├── agentCatalogCodec.ts # tolerant catalog / strict executable decode -│ ├── agentCompatibilityMapper.ts # legacy route DTO boundary -│ └── appSessionService.ts # new_sessions application shell -├── deepchat/ -│ ├── instance/ # per-session state owner -│ ├── loop/ # LoopRun, engine and ports -│ ├── memory/ # runtime coordinator + two Memory seams -│ ├── pending/ # durable pending input coordination -│ └── resources/ # DeepChat resource helpers -└── acp/ - ├── instance/ # direct runtime/instance - ├── client/ # shared ACP client/runtime owner - ├── runtime/ # process/session/protocol/persistence/mapping - ├── launch/ # executable launch setup - └── catalog/ # registry/migration +```mermaid +flowchart TD + Entry["Desktop / Remote / Scheduler / Subagent"] --> Session["Session Lifecycle / Turn"] + Session --> Manager["AgentManager"] + Manager --> Deep["DeepChat backend"] + Manager --> ACP["ACP backend"] + Deep --> Instance["one instance per loaded Session"] + Instance --> Run["one LoopRun per provider/tool round sequence"] + Run --> Provider["ProviderRuntime"] + Run --> Tool["ToolService"] + Run --> Memory["Memory ports"] + Run --> Tape["Session Tape / transcript"] ``` -## AgentManager 合同 - -`AgentManager.resolveSessionHandle(sessionId)`: - -1. 从 `AppSessionService` 读取当前 `new_sessions.agent_id`; -2. canonicalize ACP alias; -3. strict resolve executable descriptor; -4. 按 `descriptor.kind` 选择 typed backend; -5. 返回 `DeepChatSessionHandle | DirectAcpSessionHandle`。 - -共同 handle 只包含双方已有的 lifecycle、send/cancel/snapshot/close、pending、settings 和 tool interaction。 -transfer target、subagent、generation control 和 ACP mode/config/commands/workdir 都是 required -kind-specific facet。不存在 optional-method reflection,也不存在 agent handle/backend -`legacy | direct runtimeKind`。 - -session delete 是 descriptor-independent cleanup:manager 不 hydrate、不读取 catalog,分别清理两个 -backend cache/durable binding;Lifecycle deletion transaction 再按 shared state、permission、skills、app -row 的既有顺序清理。 - -## DeepChat instance 与 lifecycle - -`DeepChatAgentRuntime` lazy hydrate,一个 session 只缓存一个 `DeepChatAgentInstance`。instance 拥有: - -- identity、project、effective generation settings、runtime status; -- pre-stream abort 与 active generation reference; -- pending/steer drain state、ordered pending interactions; -- runtime-activated skills、tool/prompt snapshots、compaction in-flight projection; -- stable Memory session handle。 +- Session 拥有长期身份、settings、transcript、Tape 和 pending input。 +- 已载入 Session 在一个 backend 中最多有一个 instance。 +- 每次执行创建独立 Run,Run 拥有取消信号、provider round、request sequence 和临时输出。 +- Window、Remote endpoint 和 Cron run 只保存各自 binding,不拥有 Agent instance。 +- 只读 list/query 不 hydrate backend;执行、完整 restore 或明确 backend 设置操作才允许 hydrate。 -turn-local state 位于 `LoopRun`:run id、abort signal、per-attempt request sequence、outer provider-round -count、round messages、stream state 和 overflow/recovery flags。LoopEngine 不保存跨 session mutable map。 +## DeepChat 执行合同 -固定 round lifecycle: +主要实现位于: ```text -input/context preparation - -> register LoopRun - -> enter provider round - -> provider attempt: context gate -> ViewManifest -> rate gate -> stream - -> update output projection - -> execute typed tool batch - -> message commit -> TapeRecorder tool facts - -> continue or settle - -> terminal projection / pending drain / Memory observer +src/main/agent/deepchat/ +├── instance/ # per-session runtime state +├── loop/ # LoopRun, input/context coordination and ports +├── runtime/ # provider/tool loop, interaction, compaction and projection +├── memory/ # prompt contribution and terminal ingestion +└── resources/ # system prompt resources ``` -只有 pre-check permission、question、post-call permission 和 post-success skill draft 能生成 ordered pause -outcome。pause 会 settle 当前 run;中间 response 保持 paused,最后一项后从持久 context 创建 fresh resume -run。Hooks notification 是 detached、non-blocking observer。 - -## ACP direct runtime - -`AcpAgentRuntime` 按 app session cache/hydrate `AcpAgentInstance`,验证 descriptor/config/workdir identity, -并在 shutdown/close/process-exit 时 fence/evict。instance 使用 consolidated ACP client/session/process/runtime -模块处理 session new/load/resume、prompt、cancel、mode/config/commands 和 protocol permission。 - -direct ACP 通过 `AcpCompatibilityProjectionAdapter`、request trace adapter 和现有 pending/rate/hook ports -写入相同 structured message、Tape、renderer event、trace pipeline,因此 restore/search/export 仍使用 -DeepChat app projection;`acp_turns` 只是 protocol metadata。 - -## Tape、Memory 与持久化 - -- `DeepChatMessageStore` 使用 message header + structured child tables,legacy JSON 仅是 read fallback。 -- Tape 保存 semantic facts/anchors/ViewManifest;trace 保存 opt-in raw request diagnostics。 -- `TapeRecorder.appendToolFact` 在 persisted round callback 中按 terminal call/result 写入,保持 provenance、 - monotonic order、idempotency 和 pending exclusion。 -- causal observation pure-read join Tape/ViewManifest、message terminal status 和 trace;renderer event history - 未持久化时明确返回 unavailable。 -- `MemoryRuntimeCoordinator` 是 runtime queue/epoch/cooldown/access/cursor owner,并实现 awaited - `MemoryPromptContributor` 与 background `MemoryIngestionObserver`;`MemoryPresenter` 继续拥有 Memory data、 - retrieval、write、vector、maintenance。 - -## 兼容边界 - -仍保留: - -- `AgentRuntimePresenter` state/delegate façade; -- `AcpProvider` 的 DeepChat + ACP-provider compatibility; -- `startupMigrations/LegacyChatImportService`、旧 conversations/messages、`SessionPresenter` - export/thread/data compatibility;current agent-session export 由 `AgentSessionExportService` 拥有; -- 现有 route/event/DTO/schema/table。 - -已经退休并由 guard 阻止回流: - -- `AgentSessionPresenter` compatibility façade 与 main-process `IAgentSessionPresenter`; -- fake `AgentRegistry`; -- unified optional implementation interface; -- reflection-based legacy backend; -- `src/main/lib/agentRuntime`; -- agent handle/backend legacy/direct runtime-kind branch。 - -## 调试入口 - -按问题选择入口: - -1. kind/session routing:`src/main/agent/manager/agentManager.ts` -2. core session application behavior:`src/main/presenter/sessionApplication/` -3. route composition:`src/main/routes/index.ts` -4. session search/translation:`src/main/routes/sessions/` -5. current session export:`src/main/presenter/exporter/agentSessionExporter.ts` -6. usage/startup/catalog:`src/main/presenter/{usageStatsService.ts,startupMigrations/}` 与 - `src/main/agent/shared/availableAgentCatalog.ts` -7. DeepChat session state:`src/main/agent/deepchat/instance/` -8. provider/tool round:`src/main/agent/deepchat/loop/`,再看 retained presenter adapters -9. direct ACP:`src/main/agent/acp/instance/` 与 `src/main/agent/acp/runtime/` -10. tool source/dispatch:`src/main/presenter/toolPresenter/` -11. Tape/message projection:`src/main/presenter/agentRuntimePresenter/{tapeService,messageStore}.ts` -12. Memory runtime seam:`src/main/agent/deepchat/memory/memoryRuntimeCoordinator.ts` +必须保持的合同: + +- runtime 只通过构造时注入的窄接口访问 Provider、Tool、Memory 和 Session data;不得反向读取 App、 + Desktop、Remote、Scheduler 或 route registry。 +- `providerRoundCount` 统计 outer round;`requestSeq` 统计真实 provider attempt。retry 不伪造新 round。 +- send input 在 Session 边界正规化为 canonical request;resume 不重新解释旧 route DTO。 +- permission mode 属于 Session assignment/settings;一次 Run 使用开始时的闭合快照,deferred tool + execution 仍需重新检查当前安全边界。 +- paused interaction 按顺序结算;最后一项完成后创建新的 resume Run,不复用已经 settle 的 Run。 +- no-progress tool loop 由 `noProgressToolLoopGuard.ts` 终止;usage 由 runtime accumulator 跨 round + 累加。 +- provider terminal reason 必须无损正规化;普通 `stop` 只有在确实解析出 tool call 时才能变为 + `tool_use`。 + +## ACP 执行合同 + +ACP 代码集中在 `src/main/agent/acp/`: + +- `catalog/`:registry、migration 和 settings; +- `launch/`:安装与 launch spec; +- `client/`:connection 和 protocol session owner; +- `runtime/`:capability、process、session、filesystem、terminal、permission 和 persistence; +- `instance/`:per-session ACP state; +- `compatibility/`:允许保留的外部格式 adapter。 + +ACP capability 必须来自 initialize result,未声明能力失败关闭。filesystem 和 terminal 请求受 +workspace/path guard 约束。取消、process exit、permission resolver 和 session persistence 都由 ACP +runtime 自己处理,不借用 DeepChat loop 状态。 + +## Subagent + +Subagent 可用性只由当前 Agent delegation policy、正规化 slot 和 `sessionKind` 决定: + +- 只有 regular DeepChat parent 且存在有效 slot 时才暴露 `subagent_orchestrator`; +- Subagent child 不能再次创建 Subagent; +- admission 后的 run 使用已固定的 task/slot snapshot,真正调用前仍重新检查 host policy; +- child 使用独立 Session、workspace authorization、Tool mapping、Memory namespace 和 permission state; +- 完成后父 Session 记录 child Tape 的 frozen-head link,不复制 child entries。 + +每个 Subagent run 有独立 deadline;默认 `300000ms`,允许 `1000-1800000ms`。一个 parent 最多同时 +拥有三个 nonterminal run。deadline 或手动取消会标记未完成 task、请求 child cancellation,并记录 +timeout/deadline/reason;run 的终止不能无限等待被阻塞的 child cleanup。Child handoff 固定包含 +`Result`、`Evidence`、`Changed Files`、`Validation`、`Unresolved`,调用方的 `expectedOutput` 只能追加 +指导,不能替换基本合同。 + +## 删除与 transfer + +删除不依赖当前 agent descriptor 是否仍有效:先清理两个 backend 的 cache/durable binding,再清 +Session data、permission、Skill selection 和 app-session row。ACP 与 DeepChat transfer 必须先验证目标并 +提交 ownership,再关闭旧 backend;失败时保留原 assignment。 + +## 防回归 + +`scripts/agent-cleanup-guard.mjs` 阻止旧 Agent/Session Presenter 路径和 import 回流。Backend、Session、 +Subagent 与 Tape 的行为约束由对应的 unit/integration tests 验证,不再维护全仓库启发式 architecture +guard。 + +`pnpm run test:agent:eval` 提供离线 deterministic Agent 行为基线,使用 scripted provider/tool 直接 +覆盖 production loop。场景包括 direct completion、多轮 tool、tool failure、permission pause、cancel、 +pending yield、round guard、no-progress、empty output 和 context/provider errors;断言 persisted run +metadata、usage/cache fields、provider/tool budgets,不调用真实 provider,也不写仓库 artifact。 + +关键入口: + +1. `src/main/agent/manager/agentManager.ts` +2. `src/main/session/turn.ts` +3. `src/main/agent/deepchat/instance/` +4. `src/main/agent/deepchat/loop/` +5. `src/main/agent/deepchat/runtime/` +6. `src/main/agent/acp/instance/` +7. `src/main/agent/acp/runtime/` +8. `test/main/agent/` diff --git a/docs/architecture/baselines/agent-system-layered-runtime-baseline.json b/docs/architecture/baselines/agent-system-layered-runtime-baseline.json index 0be9916f43..1baa3fce5b 100644 --- a/docs/architecture/baselines/agent-system-layered-runtime-baseline.json +++ b/docs/architecture/baselines/agent-system-layered-runtime-baseline.json @@ -1,7 +1,7 @@ { "schemaVersion": 2, "goal": "agent-system-layered-runtime", - "headCommit": "30d57c3873cec94e342ce616d8ccafe9f71676b3", + "headCommit": "d87cb4f69dca7af499e153d9a1d71015e1bfad60", "relevantWorkingTree": { "dirty": false, "files": [] @@ -18,18 +18,25 @@ "src/main/agent/acp/catalog/acpRegistryConstants.ts", "src/main/agent/acp/catalog/acpRegistryMigrationService.ts", "src/main/agent/acp/catalog/acpRegistryService.ts", + "src/main/agent/acp/catalog/data/settingsTable.ts", + "src/main/agent/acp/catalog/settings.ts", + "src/main/agent/acp/catalog/settingsDbStore.ts", "src/main/agent/acp/client/acpRuntimeOwner.ts", "src/main/agent/acp/client/connection/AcpConnectionManager.ts", "src/main/agent/acp/client/index.ts", "src/main/agent/acp/client/session/AcpPromptController.ts", "src/main/agent/acp/client/session/AcpSessionRuntime.ts", "src/main/agent/acp/client/types.ts", + "src/main/agent/acp/compatibility/adapters.ts", + "src/main/agent/acp/compatibility/dependencies.ts", + "src/main/agent/acp/createRuntimeOwner.ts", "src/main/agent/acp/instance/acpAgentInstance.ts", "src/main/agent/acp/instance/acpAgentRuntime.ts", "src/main/agent/acp/instance/index.ts", "src/main/agent/acp/instance/ports.ts", "src/main/agent/acp/launch/acpInitHelper.ts", "src/main/agent/acp/launch/acpLaunchSpecService.ts", + "src/main/agent/acp/routes.ts", "src/main/agent/acp/runtime/acpCapabilities.ts", "src/main/agent/acp/runtime/acpCompatibilityPromptBuilder.ts", "src/main/agent/acp/runtime/acpConfigState.ts", @@ -49,6 +56,7 @@ "src/main/agent/acp/runtime/mcpTransportFilter.ts", "src/main/agent/acp/runtime/types.ts", "src/main/agent/deepchat/deepChatAgentRepository.ts", + "src/main/agent/deepchat/defaults.ts", "src/main/agent/deepchat/instance/deepChatAgentInstance.ts", "src/main/agent/deepchat/instance/deepChatAgentRuntime.ts", "src/main/agent/deepchat/loop/contextCoordinator.ts", @@ -61,9 +69,38 @@ "src/main/agent/deepchat/memory/memoryIngestionObserver.ts", "src/main/agent/deepchat/memory/memoryPromptContributor.ts", "src/main/agent/deepchat/memory/memoryRuntimeCoordinator.ts", - "src/main/agent/deepchat/pending/pendingInputCoordinator.ts", - "src/main/agent/deepchat/pending/pendingInputStore.ts", "src/main/agent/deepchat/resources/systemEnvPromptBuilder.ts", + "src/main/agent/deepchat/resources/systemPromptBuilder.ts", + "src/main/agent/deepchat/runtime/accumulator.ts", + "src/main/agent/deepchat/runtime/compactionRuntimeCoordinator.ts", + "src/main/agent/deepchat/runtime/compactionService.ts", + "src/main/agent/deepchat/runtime/contextBudget.ts", + "src/main/agent/deepchat/runtime/contextBuilder.ts", + "src/main/agent/deepchat/runtime/contextWindowError.ts", + "src/main/agent/deepchat/runtime/deepChatLoopRunner.ts", + "src/main/agent/deepchat/runtime/deepChatRuntimeCoordinator.ts", + "src/main/agent/deepchat/runtime/deferredToolExecutor.ts", + "src/main/agent/deepchat/runtime/dispatch.ts", + "src/main/agent/deepchat/runtime/echo.ts", + "src/main/agent/deepchat/runtime/generationSettings.ts", + "src/main/agent/deepchat/runtime/imageGenerationBlocks.ts", + "src/main/agent/deepchat/runtime/interactionCoordinator.ts", + "src/main/agent/deepchat/runtime/interactionProjection.ts", + "src/main/agent/deepchat/runtime/messageTracePayload.ts", + "src/main/agent/deepchat/runtime/noProgressToolLoopGuard.ts", + "src/main/agent/deepchat/runtime/process.ts", + "src/main/agent/deepchat/runtime/providerPermissionCoordinator.ts", + "src/main/agent/deepchat/runtime/runtimeMetadata.ts", + "src/main/agent/deepchat/runtime/sessionSettingsCoordinator.ts", + "src/main/agent/deepchat/runtime/sessionUpdates.ts", + "src/main/agent/deepchat/runtime/tapeViewAssembler.ts", + "src/main/agent/deepchat/runtime/tapeViewPolicy.ts", + "src/main/agent/deepchat/runtime/toolAdapters.ts", + "src/main/agent/deepchat/runtime/toolOutputGuard.ts", + "src/main/agent/deepchat/runtime/toolPermissionReviewer.ts", + "src/main/agent/deepchat/runtime/toolResolver.ts", + "src/main/agent/deepchat/runtime/turnCoordinator.ts", + "src/main/agent/deepchat/runtime/types.ts", "src/main/agent/manager/agentManager.ts", "src/main/agent/manager/deepChatAgentBackend.ts", "src/main/agent/manager/directAcpAgentBackend.ts", @@ -76,7 +113,6 @@ "src/main/agent/shared/agentSessionHandle.ts", "src/main/agent/shared/agentSessionIds.ts", "src/main/agent/shared/agentSessionNormalization.ts", - "src/main/agent/shared/agentSharedData.ts", "src/main/agent/shared/appSessionService.ts", "src/main/agent/shared/assistantModelSelection.ts", "src/main/agent/shared/availableAgentCatalog.ts", @@ -89,18 +125,13 @@ "src/main/agent/shared/process/shellOutputEncoding.ts", "src/main/agent/shared/process/spawnGuard.ts", "src/main/agent/shared/storage/sessionPaths.ts", - "src/main/agent/shared/workspace/fffSearchService.ts", - "src/main/presenter/agentRuntimePresenter/dispatch.ts", - "src/main/presenter/agentRuntimePresenter/index.ts", - "src/main/presenter/agentRuntimePresenter/messageStore.ts", - "src/main/presenter/agentRuntimePresenter/process.ts", - "src/main/presenter/agentRuntimePresenter/tapeService.ts", - "src/main/presenter/index.ts", - "src/main/presenter/llmProviderPresenter/providers/acpProvider.ts", - "src/main/presenter/sessionApplication/agentAssignmentCoordinator.ts", - "src/main/presenter/sessionApplication/lifecycleCoordinator.ts", - "src/main/presenter/sessionApplication/projectionCoordinator.ts", - "src/main/presenter/sessionApplication/turnCoordinator.ts" + "src/main/provider/providers/acpProvider.ts", + "src/main/session/assignment.ts", + "src/main/session/data/tape.ts", + "src/main/session/data/transcript.ts", + "src/main/session/lifecycle.ts", + "src/main/session/query.ts", + "src/main/session/turn.ts" ], "expectedFiles": { "src/main/agent/acp/instance/acpAgentInstance.ts": true, @@ -112,6 +143,9 @@ "src/main/agent/deepchat/memory/memoryIngestionObserver.ts": true, "src/main/agent/deepchat/memory/memoryPromptContributor.ts": true, "src/main/agent/deepchat/memory/memoryRuntimeCoordinator.ts": true, + "src/main/agent/deepchat/runtime/deepChatRuntimeCoordinator.ts": true, + "src/main/agent/deepchat/runtime/dispatch.ts": true, + "src/main/agent/deepchat/runtime/process.ts": true, "src/main/agent/manager/agentManager.ts": true, "src/main/agent/manager/deepChatAgentBackend.ts": true, "src/main/agent/manager/directAcpAgentBackend.ts": true, @@ -119,17 +153,13 @@ "src/main/agent/shared/agentCatalogCodec.ts": true, "src/main/agent/shared/agentDescriptors.ts": true, "src/main/agent/shared/appSessionService.ts": true, - "src/main/presenter/agentRuntimePresenter/dispatch.ts": true, - "src/main/presenter/agentRuntimePresenter/index.ts": true, - "src/main/presenter/agentRuntimePresenter/messageStore.ts": true, - "src/main/presenter/agentRuntimePresenter/process.ts": true, - "src/main/presenter/agentRuntimePresenter/tapeService.ts": true, - "src/main/presenter/index.ts": true, - "src/main/presenter/llmProviderPresenter/providers/acpProvider.ts": true, - "src/main/presenter/sessionApplication/agentAssignmentCoordinator.ts": true, - "src/main/presenter/sessionApplication/lifecycleCoordinator.ts": true, - "src/main/presenter/sessionApplication/projectionCoordinator.ts": true, - "src/main/presenter/sessionApplication/turnCoordinator.ts": true + "src/main/provider/providers/acpProvider.ts": true, + "src/main/session/assignment.ts": true, + "src/main/session/data/tape.ts": true, + "src/main/session/data/transcript.ts": true, + "src/main/session/lifecycle.ts": true, + "src/main/session/query.ts": true, + "src/main/session/turn.ts": true }, "ownerEvidence": { "agentManager": { @@ -192,28 +222,28 @@ "exists": true, "declarationCount": 1 }, - "sessionProjectionCoordinator": { - "file": "src/main/presenter/sessionApplication/projectionCoordinator.ts", + "sessionQuery": { + "file": "src/main/session/query.ts", "exists": true, "declarationCount": 1 }, - "sessionAgentAssignmentCoordinator": { - "file": "src/main/presenter/sessionApplication/agentAssignmentCoordinator.ts", + "sessionAssignment": { + "file": "src/main/session/assignment.ts", "exists": true, "declarationCount": 1 }, - "sessionTurnCoordinator": { - "file": "src/main/presenter/sessionApplication/turnCoordinator.ts", + "sessionTurn": { + "file": "src/main/session/turn.ts", "exists": true, "declarationCount": 1 }, - "sessionLifecycleCoordinator": { - "file": "src/main/presenter/sessionApplication/lifecycleCoordinator.ts", + "sessionLifecycle": { + "file": "src/main/session/lifecycle.ts", "exists": true, "declarationCount": 1 }, - "retainedDeepChatStateDelegateFacade": { - "file": "src/main/presenter/agentRuntimePresenter/index.ts", + "deepChatRuntimeCoordinator": { + "file": "src/main/agent/deepchat/runtime/deepChatRuntimeCoordinator.ts", "exists": true, "declarationCount": 1 } @@ -223,7 +253,12 @@ "src/main/agent/manager/legacyAgentBackends.ts": 0, "src/main/lib/agentRuntime": 0, "src/main/presenter/agentSessionPresenter": 0, - "src/shared/types/presenters/agent-session.presenter.d.ts": 0 + "src/main/presenter/index.ts": 0, + "src/main/presenter/lifecyclePresenter": 0, + "src/main/presenter/sessionPresenter": 0, + "src/shared/lifecycle.ts": 0, + "src/shared/types/presenters/agent-session.presenter.d.ts": 0, + "src/shared/types/presenters/session.presenter.d.ts": 0 }, "symbols": { "AgentRegistry": 0, @@ -256,18 +291,17 @@ "promptContributor": "src/main/agent/deepchat/memory/memoryPromptContributor.ts", "ingestionObserver": "src/main/agent/deepchat/memory/memoryIngestionObserver.ts" }, - "presenterBoundaries": [ - "src/main/presenter/agentRuntimePresenter/dispatch.ts", - "src/main/presenter/agentRuntimePresenter/index.ts", - "src/main/presenter/agentRuntimePresenter/messageStore.ts", - "src/main/presenter/agentRuntimePresenter/process.ts", - "src/main/presenter/agentRuntimePresenter/tapeService.ts", - "src/main/presenter/index.ts", - "src/main/presenter/llmProviderPresenter/providers/acpProvider.ts", - "src/main/presenter/sessionApplication/agentAssignmentCoordinator.ts", - "src/main/presenter/sessionApplication/lifecycleCoordinator.ts", - "src/main/presenter/sessionApplication/projectionCoordinator.ts", - "src/main/presenter/sessionApplication/turnCoordinator.ts" + "runtimeBoundaries": [ + "src/main/agent/deepchat/runtime/deepChatRuntimeCoordinator.ts", + "src/main/agent/deepchat/runtime/dispatch.ts", + "src/main/agent/deepchat/runtime/process.ts", + "src/main/provider/providers/acpProvider.ts", + "src/main/session/assignment.ts", + "src/main/session/data/tape.ts", + "src/main/session/data/transcript.ts", + "src/main/session/lifecycle.ts", + "src/main/session/query.ts", + "src/main/session/turn.ts" ] }, "contracts": { @@ -332,130 +366,37 @@ "src/shared/contracts/routes/window.routes.ts", "src/shared/contracts/routes/workspace.routes.ts" ], - "sha256": "a7eef369f151c3679ba6f36e12041fed36fab124f5cd33a897c2c06c9925bcb8" + "sha256": "406ef501353454e8677f773c556b8938141d931229eebabae6fcb9b0c37abe3c" }, "storage": { "sqlite": { "files": [ - "src/main/presenter/sqlitePresenter/schemaCatalog.ts", - "src/main/presenter/sqlitePresenter/schemaCatalogMetadata.ts", - "src/main/presenter/sqlitePresenter/schemaTypes.ts", - "src/main/presenter/sqlitePresenter/tables/acpSessions.ts", - "src/main/presenter/sqlitePresenter/tables/acpTurns.ts", - "src/main/presenter/sqlitePresenter/tables/agentMemory.ts", - "src/main/presenter/sqlitePresenter/tables/agentMemoryAudit.ts", - "src/main/presenter/sqlitePresenter/tables/agentMemoryFtsPolicy.ts", - "src/main/presenter/sqlitePresenter/tables/agentMemoryStateSql.ts", - "src/main/presenter/sqlitePresenter/tables/agents.ts", - "src/main/presenter/sqlitePresenter/tables/attachments.ts", - "src/main/presenter/sqlitePresenter/tables/baseTable.ts", - "src/main/presenter/sqlitePresenter/tables/configTables.ts", - "src/main/presenter/sqlitePresenter/tables/conversations.ts", - "src/main/presenter/sqlitePresenter/tables/cronJobDeliveries.ts", - "src/main/presenter/sqlitePresenter/tables/cronJobRuns.ts", - "src/main/presenter/sqlitePresenter/tables/cronJobs.ts", - "src/main/presenter/sqlitePresenter/tables/deepchatAssistantBlocks.ts", - "src/main/presenter/sqlitePresenter/tables/deepchatMemoryIngestionProjection.ts", - "src/main/presenter/sqlitePresenter/tables/deepchatMessageSearchResults.ts", - "src/main/presenter/sqlitePresenter/tables/deepchatMessageTraces.ts", - "src/main/presenter/sqlitePresenter/tables/deepchatMessages.ts", - "src/main/presenter/sqlitePresenter/tables/deepchatPendingInputs.ts", - "src/main/presenter/sqlitePresenter/tables/deepchatSearchDocuments.ts", - "src/main/presenter/sqlitePresenter/tables/deepchatSessionMetadata.ts", - "src/main/presenter/sqlitePresenter/tables/deepchatSessions.ts", - "src/main/presenter/sqlitePresenter/tables/deepchatTapeEffectiveSemantics.ts", - "src/main/presenter/sqlitePresenter/tables/deepchatTapeEntries.ts", - "src/main/presenter/sqlitePresenter/tables/deepchatTapeSearchProjection.ts", - "src/main/presenter/sqlitePresenter/tables/deepchatUsageStats.ts", - "src/main/presenter/sqlitePresenter/tables/deepchatUserMessageFiles.ts", - "src/main/presenter/sqlitePresenter/tables/deepchatUserMessageLinks.ts", - "src/main/presenter/sqlitePresenter/tables/deepchatUserMessages.ts", - "src/main/presenter/sqlitePresenter/tables/legacyImportStatus.ts", - "src/main/presenter/sqlitePresenter/tables/messageAttachments.ts", - "src/main/presenter/sqlitePresenter/tables/messages.ts", - "src/main/presenter/sqlitePresenter/tables/newEnvironmentPreferences.ts", - "src/main/presenter/sqlitePresenter/tables/newEnvironments.ts", - "src/main/presenter/sqlitePresenter/tables/newProjects.ts", - "src/main/presenter/sqlitePresenter/tables/newSessionActiveSkills.ts", - "src/main/presenter/sqlitePresenter/tables/newSessionDisabledAgentTools.ts", - "src/main/presenter/sqlitePresenter/tables/newSessions.ts", - "src/main/presenter/sqlitePresenter/tables/settingsActivity.ts" + "src/main/data/schemaCatalog.ts", + "src/main/data/schemaCatalogMetadata.ts", + "src/main/data/schemaTypes.ts" ], - "tableIdentifiers": [ - "acp_sessions", - "acp_turns", - "agent_mcp_selections", - "agent_memory", - "agent_memory_audit", - "agent_memory_fts", - "agent_memory_fts_meta", - "agent_settings", - "agents", - "app_settings", - "attachments", - "config_migrations", - "conversations", - "cron_job_deliveries", - "cron_job_runs", - "cron_jobs", - "deepchat_assistant_blocks", - "deepchat_memory_ingestion_projection", - "deepchat_memory_ingestion_projection_meta", - "deepchat_message_search_results", - "deepchat_message_traces", - "deepchat_messages", - "deepchat_pending_inputs", - "deepchat_search_documents", - "deepchat_session_metadata", - "deepchat_sessions", - "deepchat_tape_entries", - "deepchat_tape_search_fts", - "deepchat_tape_search_fts_meta", - "deepchat_tape_search_projection", - "deepchat_tape_search_projection_meta", - "deepchat_usage_stats", - "deepchat_user_message_files", - "deepchat_user_message_links", - "deepchat_user_messages", - "legacy_import_status", - "mcp_servers", - "mcp_settings", - "message_attachments", - "messages", - "model_configs", - "model_status", - "new_environment_preferences", - "new_environments", - "new_projects", - "new_session_active_skills", - "new_session_disabled_agent_tools", - "new_sessions", - "provider_models", - "providers", - "settings_activity" - ], - "sha256": "16882d6b4c22ec84e0e28c4eb7f5aeb7a867503648e67eeb42461f4f838fe198" + "tableIdentifiers": [], + "sha256": "411d0c6078cf4264f2241e1198315e52dcf085342975d344e5d8967333b8129b" }, "memoryDuckDbSidecar": { "files": [ - "src/main/presenter/memoryPresenter/infra/memoryVectorStore.ts" + "src/main/memory/infra/memoryVectorStore.ts" ], "tableIdentifiers": [ "embedding_meta", "memory_vector" ], "versionContract": "embedding identity stored in embedding_meta; no numeric schema version", - "sha256": "fdcb17375f3c938763aff162c179f04bc98f0392feebdc0b30b1fd9f48cdc025" + "sha256": "1f1e48a568223a67f1dc55e3901728fbefb3f741427882bde941d98ddccefc56" } }, "compositionAndShutdown": { "files": [ - "src/main/presenter/index.ts", - "src/main/presenter/lifecyclePresenter/hooks/beforeQuit/mcpShutdownHook.ts", - "src/main/presenter/lifecyclePresenter/hooks/beforeQuit/presenterDestroyHook.ts", - "src/main/presenter/lifecyclePresenter/index.ts" + "src/main/app/composition.ts", + "src/main/app/mainProcess.ts", + "src/main/appMain.ts" ], - "sha256": "d42c0862ef9e41fa77cb9296dcb625f5559b5ae594bd1fbba3ba248b2a4e5203" + "sha256": "dc13adbe8b76f0c4bb463605c780fc83e4a78696b8875d6765e767a4c2dcf70f" }, "dependencyMetrics": { "loopFiles": [ diff --git a/docs/architecture/baselines/archive-reference-report.md b/docs/architecture/baselines/archive-reference-report.md deleted file mode 100644 index e84a257fc2..0000000000 --- a/docs/architecture/baselines/archive-reference-report.md +++ /dev/null @@ -1,5 +0,0 @@ -# Archive Reference Baseline - -Generated on 2026-07-14. - -- Total references: 0 diff --git a/docs/architecture/baselines/dependency-report.md b/docs/architecture/baselines/dependency-report.md deleted file mode 100644 index c0092a7b91..0000000000 --- a/docs/architecture/baselines/dependency-report.md +++ /dev/null @@ -1,161 +0,0 @@ -# Dependency Baseline - -Generated on 2026-07-14. - -## main - -- Total files: 565 -- Internal dependency edges: 1537 -- Cycles detected: 36 - -### Top outgoing dependencies - -- `presenter/index.ts`: 69 -- `presenter/agentRuntimePresenter/index.ts`: 48 -- `presenter/sqlitePresenter/index.ts`: 40 -- `presenter/sqlitePresenter/schemaCatalog.ts`: 37 -- `routes/index.ts`: 36 -- `presenter/configPresenter/index.ts`: 27 -- `presenter/lifecyclePresenter/hooks/index.ts`: 23 -- `presenter/toolPresenter/agentTools/agentToolManager.ts`: 22 -- `presenter/memoryPresenter/index.ts`: 20 -- `presenter/llmProviderPresenter/index.ts`: 17 -- `agent/acp/runtime/index.ts`: 15 -- `presenter/filePresenter/mime.ts`: 14 -- `presenter/remoteControlPresenter/index.ts`: 14 -- `presenter/agentRuntimePresenter/dispatch.ts`: 12 -- `presenter/skillSyncPresenter/adapters/index.ts`: 12 - -### Top incoming dependencies - -- `presenter/index.ts`: 49 -- `routes/publishDeepchatEvent.ts`: 42 -- `presenter/remoteControlPresenter/types.ts`: 38 -- `presenter/sqlitePresenter/tables/baseTable.ts`: 38 -- `agent/shared/agentSessionIds.ts`: 31 -- `events.ts`: 30 -- `eventbus.ts`: 29 -- `presenter/memoryPresenter/types.ts`: 23 -- `presenter/remoteControlPresenter/services/remoteBindingStore.ts`: 22 -- `presenter/sqlitePresenter/index.ts`: 21 -- `presenter/memoryPresenter/ports.ts`: 20 -- `presenter/remoteControlPresenter/services/remoteConversationRunner.ts`: 16 -- `presenter/memoryPresenter/domain/types.ts`: 15 -- `presenter/filePresenter/BaseFileAdapter.ts`: 13 -- `presenter/memoryPresenter/context.ts`: 12 - -### Cycle samples - -- `agent/acp/runtime/index.ts -> agent/acp/runtime/acpCompatibilityPromptBuilder.ts -> agent/acp/instance/ports.ts -> agent/acp/runtime/index.ts` -- `agent/acp/client/acpRuntimeOwner.ts -> agent/acp/client/index.ts -> agent/acp/client/acpRuntimeOwner.ts` -- `presenter/memoryPresenter/core/injectionPort.ts -> presenter/memoryPresenter/types.ts -> presenter/memoryPresenter/injection.ts -> presenter/memoryPresenter/core/injectionPort.ts` -- `presenter/sqlitePresenter/index.ts -> presenter/startupMigrations/legacyChatImportService.ts -> presenter/sqlitePresenter/index.ts` -- `presenter/sqlitePresenter/index.ts -> presenter/startupMigrations/legacyChatImportService.ts -> presenter/agentRuntimePresenter/messageStore.ts -> presenter/sqlitePresenter/index.ts` -- `presenter/agentRuntimePresenter/messageStore.ts -> presenter/agentRuntimePresenter/tapeFacts.ts -> presenter/agentRuntimePresenter/tapeViewManifest.ts -> presenter/agentRuntimePresenter/contextBuilder.ts -> presenter/agentRuntimePresenter/messageStore.ts` -- `presenter/index.ts -> presenter/windowPresenter/index.ts -> presenter/index.ts` -- `presenter/index.ts -> presenter/windowPresenter/index.ts -> presenter/tabPresenter.ts -> presenter/index.ts` -- `presenter/index.ts -> presenter/windowPresenter/index.ts -> presenter/windowPresenter/FloatingChatWindow.ts -> presenter/index.ts` -- `presenter/index.ts -> presenter/shortcutPresenter.ts -> presenter/index.ts` -- `presenter/index.ts -> presenter/llmProviderPresenter/index.ts -> presenter/llmProviderPresenter/managers/providerInstanceManager.ts -> presenter/llmProviderPresenter/providers/githubCopilotProvider.ts -> presenter/githubCopilotDeviceFlow.ts -> presenter/index.ts` -- `presenter/index.ts -> presenter/llmProviderPresenter/index.ts -> presenter/llmProviderPresenter/managers/providerInstanceManager.ts -> presenter/llmProviderPresenter/providers/ollamaProvider.ts -> presenter/llmProviderPresenter/aiSdk/index.ts -> presenter/llmProviderPresenter/aiSdk/runtime.ts -> presenter/index.ts` -- `presenter/index.ts -> presenter/sessionPresenter/index.ts -> presenter/index.ts` -- `presenter/index.ts -> presenter/sessionPresenter/index.ts -> presenter/sessionPresenter/managers/conversationManager.ts -> presenter/index.ts` -- `presenter/index.ts -> presenter/upgradePresenter/index.ts -> presenter/index.ts` -- `presenter/index.ts -> presenter/mcpPresenter/index.ts -> presenter/mcpPresenter/serverManager.ts -> presenter/mcpPresenter/mcpClient.ts -> presenter/index.ts` -- `presenter/index.ts -> presenter/mcpPresenter/index.ts -> presenter/mcpPresenter/serverManager.ts -> presenter/mcpPresenter/mcpClient.ts -> presenter/mcpPresenter/inMemoryServers/builder.ts -> presenter/mcpPresenter/inMemoryServers/deepResearchServer.ts -> presenter/index.ts` -- `presenter/index.ts -> presenter/mcpPresenter/index.ts -> presenter/mcpPresenter/serverManager.ts -> presenter/mcpPresenter/mcpClient.ts -> presenter/mcpPresenter/inMemoryServers/builder.ts -> presenter/mcpPresenter/inMemoryServers/autoPromptingServer.ts -> presenter/index.ts` -- `presenter/index.ts -> presenter/mcpPresenter/index.ts -> presenter/mcpPresenter/serverManager.ts -> presenter/mcpPresenter/mcpClient.ts -> presenter/mcpPresenter/inMemoryServers/builder.ts -> presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts -> presenter/index.ts` -- `presenter/index.ts -> presenter/mcpPresenter/index.ts -> presenter/mcpPresenter/serverManager.ts -> presenter/mcpPresenter/mcpClient.ts -> presenter/mcpPresenter/inMemoryServers/builder.ts -> presenter/mcpPresenter/inMemoryServers/builtinKnowledgeServer.ts -> presenter/index.ts` - -## renderer-main - -- Total files: 280 -- Internal dependency edges: 494 -- Cycles detected: 2 - -### Top outgoing dependencies - -- `App.vue`: 29 -- `pages/ChatPage.vue`: 29 -- `i18n/index.ts`: 20 -- `components/message/MessageItemAssistant.vue`: 19 -- `pages/NewThreadPage.vue`: 18 -- `components/chat/ChatStatusBar.vue`: 17 -- `views/ChatTabView.vue`: 12 -- `components/WindowSideBar.vue`: 9 -- `components/chat/ChatInputBox.vue`: 9 -- `components/ChatConfig.vue`: 8 -- `components/markdown/MarkdownRenderer.vue`: 8 -- `components/sidepanel/WorkspacePanel.vue`: 8 -- `components/sidepanel/viewer/WorkspacePreviewPane.vue`: 8 -- `components/mcp-config/components/McpServers.vue`: 7 -- `components/mcp-config/components/index.ts`: 7 - -### Top incoming dependencies - -- `components/chat/messageListItems.ts`: 21 -- `stores/ui/session.ts`: 17 -- `stores/ui/agent.ts`: 15 -- `stores/providerStore.ts`: 14 -- `stores/theme.ts`: 14 -- `stores/artifact.ts`: 13 -- `components/use-toast.ts`: 12 -- `stores/uiSettingsStore.ts`: 12 -- `stores/modelStore.ts`: 11 -- `stores/ui/sidepanel.ts`: 10 -- `stores/mcp.ts`: 8 -- `components/icons/ModelIcon.vue`: 6 -- `lib/onboardingResume.ts`: 6 -- `stores/language.ts`: 6 -- `lib/utils.ts`: 5 - -### Cycle samples - -- `components/json-viewer/JsonValue.ts -> components/json-viewer/JsonObject.ts -> components/json-viewer/JsonValue.ts` -- `components/json-viewer/JsonArray.ts -> components/json-viewer/JsonValue.ts -> components/json-viewer/JsonArray.ts` - -## renderer-settings - -- Total files: 110 -- Internal dependency edges: 123 -- Cycles detected: 0 - -### Top outgoing dependencies - -- `main.ts`: 20 -- `components/ModelProviderSettingsDetail.vue`: 10 -- `components/skills/SkillsSettings.vue`: 9 -- `components/KnowledgeBaseSettings.vue`: 7 -- `components/MemorySettings.vue`: 6 -- `components/CommonSettings.vue`: 5 -- `components/ModelProviderSettings.vue`: 5 -- `components/BedrockProviderSettingsDetail.vue`: 4 -- `components/SettingsOverview.vue`: 4 -- `components/skills/SkillAgentsTab.vue`: 4 -- `components/DataSettings.vue`: 3 -- `components/MemoryListView.vue`: 3 -- `components/PromptSetting.vue`: 3 -- `components/skills/SkillSyncDialog/ImportWizard.vue`: 3 -- `App.vue`: 2 - -### Top incoming dependencies - -- `components/control-center/SettingsPageShell.vue`: 13 -- `components/skills/toolIcon.ts`: 6 -- `components/memoryRedesignUtils.ts`: 5 -- `lib/guidedOnboardingSettings.ts`: 3 -- `components/ProviderDialogContainer.vue`: 2 -- `components/ProviderModelManager.vue`: 2 -- `components/ProviderRateLimitConfig.vue`: 2 -- `components/ProviderSettingsShell.vue`: 2 -- `components/common/SettingToggleRow.vue`: 2 -- `components/skills/SkillDetailDialog.vue`: 2 -- `components/skills/SkillSyncDialog/ConflictResolver.vue`: 2 -- `App.vue`: 1 -- `components/AboutUsSettings.vue`: 1 -- `components/AcpDebugDialog.vue`: 1 -- `components/AcpSettings.vue`: 1 - -### Cycle samples - -- None diff --git a/docs/architecture/baselines/main-kernel-boundary-baseline.md b/docs/architecture/baselines/main-kernel-boundary-baseline.md deleted file mode 100644 index 46e38738d6..0000000000 --- a/docs/architecture/baselines/main-kernel-boundary-baseline.md +++ /dev/null @@ -1,108 +0,0 @@ -# Main Kernel Boundary Baseline - -Generated on 2026-07-14. -Current phase: P5. - -## Metric Snapshot - -| Metric | Value | -| --- | --- | -| `renderer.usePresenter.count` | 0 | -| `renderer.business.usePresenter.count` | 0 | -| `renderer.quarantine.usePresenter.count` | 0 | -| `renderer.windowElectron.count` | 0 | -| `renderer.business.windowElectron.count` | 0 | -| `renderer.quarantine.windowElectron.count` | 0 | -| `renderer.windowApi.count` | 0 | -| `renderer.business.windowApi.count` | 0 | -| `renderer.quarantine.windowApi.count` | 0 | -| `renderer.quarantine.sourceFile.count` | 0 | -| `hotpath.presenterEdge.count` | 8 | -| `runtime.rawTimer.count` | 211 | -| `migrated.rawChannel.count` | 0 | -| `bridge.active.count` | 0 | -| `bridge.expired.count` | 0 | - -## Renderer Single-Track Split - -- Business layer: `src/renderer/src/**`, `src/renderer/settings/**` -- Retired quarantine layer: `src/renderer/api/legacy/**` must remain deleted - -| Legacy surface | Business layer | Quarantine layer | Total | -| --- | --- | --- | --- | -| legacy presenter helper | 0 | 0 | 0 | -| `window.electron` | 0 | 0 | 0 | -| `window.api` | 0 | 0 | 0 | - -## Quarantine Exit Snapshot - -- Retained capability family: none; `renderer legacy transport` is retired -- Source files: 0 / 0 -- Delete condition: already satisfied; a recreated quarantine directory is a regression. - -- None - -## Phase Gates - -| Phase | Gate indicator | Current signal | Status | -| --- | --- | --- | --- | -| `P0` | Retired quarantine path `src/renderer/api/legacy/**` must remain deleted and baseline emits business/retired split metrics | `src/renderer/api/legacy/**` deleted; split metrics emitted | ready | -| `P1` | Business layer direct legacy presenter helper / `window.electron` / `window.api` counts must reach `0` | legacyPresenter=0, window.electron=0, window.api=0 | ready | -| `P2` | Business layer `configPresenter` and `llmproviderPresenter` hits must reach `0` | configPresenter=0, llmproviderPresenter=0 | ready | -| `P3` | Business layer window/device/workspace/project/file/browser/tab presenter hits must reach `0` | window=0, device=0, workspace=0, project=0, file=0, browser=0, tab=0 | ready | -| `P4` | Business layer session residual / skill / mcp / sync / upgrade / dialog / tool presenter hits must reach `0` | agentSession=0, skill=0, mcp=0, sync=0, upgrade=0, dialog=0, tool=0 | ready | -| `P5` | Business layer direct legacy access must be `0`, and retired quarantine source files must stay at `0` | businessLegacy=0/0/0, quarantineSourceFiles=0/0 | ready | - -## Hot Path Direct Dependencies - -- Direct edge count: 8 - -- `src/main/presenter/agentRuntimePresenter/index.ts -> src/main/eventbus.ts` -- `src/main/presenter/index.ts -> src/main/eventbus.ts` -- `src/main/presenter/index.ts -> src/main/presenter/agentRuntimePresenter/index.ts` -- `src/main/presenter/index.ts -> src/main/presenter/llmProviderPresenter/index.ts` -- `src/main/presenter/index.ts -> src/main/presenter/sessionPresenter/index.ts` -- `src/main/presenter/llmProviderPresenter/index.ts -> src/main/eventbus.ts` -- `src/main/presenter/sessionPresenter/index.ts -> src/main/eventbus.ts` -- `src/main/presenter/sessionPresenter/index.ts -> src/main/presenter/index.ts` - -## Renderer legacy presenter helpers - -- Total count: 0 - -- None - -## Renderer window.electron - -- Total count: 0 - -- None - -## Renderer window.api - -- Total count: 0 - -- None - -## Raw Timers - -- Total count: 211 - -- `src/main/presenter/githubCopilotDeviceFlow.ts`: 6 -- `src/main/presenter/browser/BrowserTab.ts`: 5 -- `src/main/presenter/devicePresenter/index.ts`: 5 -- `src/main/presenter/llmProviderPresenter/aiSdk/runtime.ts`: 5 -- `src/main/presenter/remoteControlPresenter/index.ts`: 5 -- `src/renderer/src/pages/ChatPage.vue`: 5 -- `src/main/presenter/memoryPresenter/infra/vectorStoreManager.ts`: 4 -- `src/main/presenter/memoryPresenter/services/maintenanceService.ts`: 4 -- `src/renderer/src/components/message/MessageToolbar.vue`: 4 -- `src/renderer/src/composables/message/useMessageScroll.ts`: 4 -- `src/main/agent/acp/launch/acpInitHelper.ts`: 3 -- `src/main/agent/shared/process/backgroundExecSessionManager.ts`: 3 - -## Migrated Path Raw Channel Literals - -- Total count: 0 - -- None diff --git a/docs/architecture/baselines/main-kernel-bridge-register.md b/docs/architecture/baselines/main-kernel-bridge-register.md deleted file mode 100644 index e75f67130d..0000000000 --- a/docs/architecture/baselines/main-kernel-bridge-register.md +++ /dev/null @@ -1,11 +0,0 @@ -# Main Kernel Bridge Register - -Updated on 2026-04-20. -Current phase: P5. - -- Active bridges: 0 -- Expired bridges: 0 - -## Entries - -- None diff --git a/docs/architecture/baselines/main-kernel-migration-scoreboard.json b/docs/architecture/baselines/main-kernel-migration-scoreboard.json deleted file mode 100644 index abb756d14b..0000000000 --- a/docs/architecture/baselines/main-kernel-migration-scoreboard.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "program": "main-kernel-refactor", - "generatedOn": "2026-07-14", - "currentPhase": "P5", - "metrics": { - "renderer.usePresenter.count": 0, - "renderer.business.usePresenter.count": 0, - "renderer.quarantine.usePresenter.count": 0, - "renderer.windowElectron.count": 0, - "renderer.business.windowElectron.count": 0, - "renderer.quarantine.windowElectron.count": 0, - "renderer.windowApi.count": 0, - "renderer.business.windowApi.count": 0, - "renderer.quarantine.windowApi.count": 0, - "renderer.quarantine.sourceFile.count": 0, - "hotpath.presenterEdge.count": 8, - "runtime.rawTimer.count": 211, - "migrated.rawChannel.count": 0, - "bridge.active.count": 0, - "bridge.expired.count": 0 - }, - "phaseGates": [ - { - "phase": "P0", - "indicator": "Retired quarantine path `src/renderer/api/legacy/**` must remain deleted and baseline emits business/retired split metrics", - "current": "`src/renderer/api/legacy/**` deleted; split metrics emitted", - "status": "ready" - }, - { - "phase": "P1", - "indicator": "Business layer direct legacy presenter helper / `window.electron` / `window.api` counts must reach `0`", - "current": "legacyPresenter=0, window.electron=0, window.api=0", - "status": "ready" - }, - { - "phase": "P2", - "indicator": "Business layer `configPresenter` and `llmproviderPresenter` hits must reach `0`", - "current": "configPresenter=0, llmproviderPresenter=0", - "status": "ready" - }, - { - "phase": "P3", - "indicator": "Business layer window/device/workspace/project/file/browser/tab presenter hits must reach `0`", - "current": "window=0, device=0, workspace=0, project=0, file=0, browser=0, tab=0", - "status": "ready" - }, - { - "phase": "P4", - "indicator": "Business layer session residual / skill / mcp / sync / upgrade / dialog / tool presenter hits must reach `0`", - "current": "agentSession=0, skill=0, mcp=0, sync=0, upgrade=0, dialog=0, tool=0", - "status": "ready" - }, - { - "phase": "P5", - "indicator": "Business layer direct legacy access must be `0`, and retired quarantine source files must stay at `0`", - "current": "businessLegacy=0/0/0, quarantineSourceFiles=0/0", - "status": "ready" - } - ], - "hotPathEdges": [ - "src/main/presenter/agentRuntimePresenter/index.ts -> src/main/eventbus.ts", - "src/main/presenter/index.ts -> src/main/eventbus.ts", - "src/main/presenter/index.ts -> src/main/presenter/agentRuntimePresenter/index.ts", - "src/main/presenter/index.ts -> src/main/presenter/llmProviderPresenter/index.ts", - "src/main/presenter/index.ts -> src/main/presenter/sessionPresenter/index.ts", - "src/main/presenter/llmProviderPresenter/index.ts -> src/main/eventbus.ts", - "src/main/presenter/sessionPresenter/index.ts -> src/main/eventbus.ts", - "src/main/presenter/sessionPresenter/index.ts -> src/main/presenter/index.ts" - ], - "migratedRawChannels": {} -} diff --git a/docs/architecture/baselines/main-kernel-migration-scoreboard.md b/docs/architecture/baselines/main-kernel-migration-scoreboard.md deleted file mode 100644 index 25b0a98e31..0000000000 --- a/docs/architecture/baselines/main-kernel-migration-scoreboard.md +++ /dev/null @@ -1,35 +0,0 @@ -# Main Kernel Migration Scoreboard - -Generated on 2026-07-14. -Current phase: P5. - -Phase 0 establishes the comparison baseline. Later phases should update this report and compare against this checkpoint. - -| Metric | Value | Status | -| --- | --- | --- | -| `renderer.usePresenter.count` | 0 | baseline | -| `renderer.business.usePresenter.count` | 0 | baseline | -| `renderer.quarantine.usePresenter.count` | 0 | baseline | -| `renderer.windowElectron.count` | 0 | baseline | -| `renderer.business.windowElectron.count` | 0 | baseline | -| `renderer.quarantine.windowElectron.count` | 0 | baseline | -| `renderer.windowApi.count` | 0 | baseline | -| `renderer.business.windowApi.count` | 0 | baseline | -| `renderer.quarantine.windowApi.count` | 0 | baseline | -| `renderer.quarantine.sourceFile.count` | 0 | baseline | -| `hotpath.presenterEdge.count` | 8 | baseline | -| `runtime.rawTimer.count` | 211 | baseline | -| `migrated.rawChannel.count` | 0 | baseline | -| `bridge.active.count` | 0 | baseline | -| `bridge.expired.count` | 0 | baseline | - -## Phase Gate Status - -| Phase | Status | Current signal | -| --- | --- | --- | -| `P0` | ready | `src/renderer/api/legacy/**` deleted; split metrics emitted | -| `P1` | ready | legacyPresenter=0, window.electron=0, window.api=0 | -| `P2` | ready | configPresenter=0, llmproviderPresenter=0 | -| `P3` | ready | window=0, device=0, workspace=0, project=0, file=0, browser=0, tab=0 | -| `P4` | ready | agentSession=0, skill=0, mcp=0, sync=0, upgrade=0, dialog=0, tool=0 | -| `P5` | ready | businessLegacy=0/0/0, quarantineSourceFiles=0/0 | diff --git a/docs/architecture/baselines/zero-inbound-candidates.md b/docs/architecture/baselines/zero-inbound-candidates.md deleted file mode 100644 index 899b7bf731..0000000000 --- a/docs/architecture/baselines/zero-inbound-candidates.md +++ /dev/null @@ -1,101 +0,0 @@ -# Zero Inbound Candidates - -Generated on 2026-07-14. - -These files have no in-repo importers inside their scope and need manual classification before deletion. - -## main - -- Candidate count: 17 - -- `backgroundExecUtilityHostEntry.ts` -- `env.d.ts` -- `fileWatcherUtilityHostEntry.ts` -- `lib/system.ts` -- `lib/terminalHelper.ts` -- `presenter/browser/BrowserContextBuilder.ts` -- `presenter/configPresenter/aes.ts` -- `presenter/llmProviderPresenter/oauthHelper.ts` -- `presenter/mcpPresenter/agentMcpFilter.ts` -- `presenter/searchPrompts/searchPrompts.ts` -- `presenter/sessionPresenter/persistence/conversationPersister.ts` -- `presenter/sessionPresenter/persistence/messagePersister.ts` -- `presenter/sessionPresenter/tab/tabAdapter.ts` -- `presenter/sessionPresenter/types.ts` -- `presenter/sqlitePresenter/tables/attachments.ts` -- `presenter/workspacePresenter/fileCache.ts` -- `schedulerUtilityHostEntry.ts` - -## renderer-main - -- Candidate count: 48 - -- `components/ChatConfig.vue` -- `components/ChatConfig/ConfigSwitchField.vue` -- `components/FileItem.vue` -- `components/ModelSelect.vue` -- `components/ScrollablePopover.vue` -- `components/artifacts/ArtifactBlock.vue` -- `components/chat-input/SkillsIndicator.vue` -- `components/chat-input/VoiceCallWidget.vue` -- `components/chat-input/components/ToolbarButton.vue` -- `components/chat-input/composables/useAgentMcpData.ts` -- `components/chat-input/composables/useContextLength.ts` -- `components/chat-input/composables/useDragAndDrop.ts` -- `components/chat-input/composables/useInputHistory.ts` -- `components/chat-input/composables/useInputSettings.ts` -- `components/chat-input/composables/usePromptInputFiles.ts` -- `components/chat-input/composables/useRateLimitStatus.ts` -- `components/chat/composables/useVoiceInput.ts` -- `components/editor/mention/PromptParamsDialog.vue` -- `components/editor/mention/mention.ts` -- `components/editor/mention/slashMention.ts` -- `components/mcp-config/AgentMcpSelector.vue` -- `components/mcp-config/const.ts` -- `components/message/MessageActionButtons.vue` -- `components/message/MessageItemPlaceholder.vue` -- `components/message/ReferencePreview.vue` -- `components/settings/ModelConfigItem.vue` -- `composables/message/useMessageScroll.ts` -- `composables/useArtifactCodeEditor.ts` -- `composables/useArtifactContext.ts` -- `composables/useArtifactExport.ts` -- `composables/useArtifactViewMode.ts` -- `composables/useSearchConfig.ts` -- `composables/useViewportSize.ts` -- `env.d.ts` -- `lib/cloudSyncForm.ts` -- `lib/float.cursor.ts` -- `lib/gemini.ts` -- `lib/sanitizeText.ts` -- `main.ts` -- `stores/floatingButton.ts` -- `stores/prompts.ts` -- `stores/providerDeeplinkImport.ts` -- `stores/shortcutKey.ts` -- `stores/sync.ts` -- `stores/systemPromptStore.ts` -- `types/vuedraggable.d.ts` -- `utils/maxOutputTokens.ts` -- `views/SettingsTabView.vue` - -## renderer-settings - -- Candidate count: 16 - -- `components/AcpDependencyDialog.vue` -- `components/AcpProfileDialog.vue` -- `components/AcpProfileManagerDialog.vue` -- `components/AcpTerminalDialog.vue` -- `components/common/AutoCompactionSettingsSection.vue` -- `components/common/DefaultModelSettingsSection.vue` -- `components/prompt/PromptSettingsHeader.vue` -- `components/skills/SkillFolderTree.vue` -- `components/skills/SkillSyncDialog/SyncResult.vue` -- `components/skills/SkillsHeader.vue` -- `components/skills/SyncPromptDialog.vue` -- `components/skills/SyncStatusSection.vue` -- `icons/MaximizeIcon.vue` -- `icons/MinimizeIcon.vue` -- `icons/RestoreIcon.vue` -- `main.ts` diff --git a/docs/architecture/chat-scroll-ownership/plan.md b/docs/architecture/chat-scroll-ownership/plan.md index 4c52cc1cfd..0193181cf4 100644 --- a/docs/architecture/chat-scroll-ownership/plan.md +++ b/docs/architecture/chat-scroll-ownership/plan.md @@ -120,7 +120,7 @@ Route paths through the controller in this order: 7. Spotlight/trace navigation. 8. TipTap/editor scroll containment. -After each step, prohibit the migrated direct write through tests or an architecture guard. +After each step, prohibit the migrated direct write through focused ownership tests. ### 7. Preserve composer and plan behavior @@ -316,7 +316,7 @@ DOM rows, and recorded first-paint/session-switch latency. | Long-list regressions | Keep `useMessageWindow`; migrate ownership, not the data model | | Auto-follow feels delayed | One rAF coalescing adds at most one frame and removes duplicate writes | | Viewport resize still clamps near bottom | Controller handles resize by mode: preserve anchor or follow bottom before paint | -| Migration creates two conflicting owners | Architecture guard and phase-by-phase removal of direct writes | +| Migration creates two conflicting owners | Focused ownership tests and phase-by-phase removal of direct writes | | Browser suite is slow | Keep a small critical Chromium suite separate from fast jsdom tests | | Recent-session cache increases memory | Use a small LRU plus memory budget and observable eviction | | Atomic preparation delays first paint | Gate on messages only; attach pending/plan/metadata later | diff --git a/docs/architecture/chat-scroll-ownership/spec.md b/docs/architecture/chat-scroll-ownership/spec.md index b4595b0e61..ba0e2b5e68 100644 --- a/docs/architecture/chat-scroll-ownership/spec.md +++ b/docs/architecture/chat-scroll-ownership/spec.md @@ -29,14 +29,14 @@ GitHub issue sync was not requested and is not part of this work. - Same-session loads are fenced by live mutation revisions. Streams remain session-scoped before commit and are ordered and settled by request identity; stale terminals cannot clear newer streams and duplicate terminals cannot start duplicate refreshes. -- Unit, component, architecture-guard, and opt-in Electron smoke coverage protect the new contract. +- Unit, component, focused ownership, and opt-in Electron smoke coverage protect the new contract. ## Validation record - Review-hardening scroll, page, keyed-parent, cache, and architecture suites passed 120/120 tests. - Full renderer suite with four workers and a 30-second per-test budget: 168 files and 1267 tests passed. -- Format, i18n, lint including architecture guards, Node/Web typecheck, and production build passed. +- Format, i18n, lint, Node/Web typecheck, and production build passed. - Playwright successfully discovers the opt-in real Electron scroll scenario at `test/e2e/specs/31-chat-scroll-ownership.smoke.spec.ts`. - The real Electron/provider scenario and manual macOS trackpad matrix remain outstanding; no @@ -349,7 +349,7 @@ secondary path 9. Streaming scroll work is coalesced to animation frames; token frequency does not equal scroll write frequency. 10. Search, history, and Spotlight navigation complete without visible intermediate window swaps. -11. Existing renderer unit suites, typecheck, lint, i18n, and architecture guards pass. +11. Existing renderer unit suites, focused ownership tests, typecheck, lint, and i18n pass. 12. Chromium integration scenarios show no flash, rollback, native clamp, or scrollbar theft. 13. On the agreed reference machine, warm cached session switches reach first meaningful message paint at p95 within 100ms, uncached local switches within 250ms, and initial chat shell @@ -361,7 +361,7 @@ secondary path ## Maintainability acceptance criteria - `ChatPage.vue` becomes an orchestration shell rather than a scroll implementation owner. -- Direct message viewport writes are architecture-guarded to the controller module. +- Direct message viewport writes are restricted to the controller module by focused ownership tests. - Scroll state transitions are pure and unit-testable. - Feature composables request typed scroll intents instead of sharing mutable timers. - Each asynchronous operation is scoped by session epoch and cancellable cleanup. @@ -369,8 +369,5 @@ secondary path ## Linked specifications -- `docs/architecture/chat-scroll-windowing/spec.md` - `docs/issues/chat-history-search-scroll-coordinates/spec.md` -- `docs/features/markstream-chat-rendering-optimization/spec.md` -- `docs/issues/chat-search-highlight-flicker/spec.md` -- `docs/issues/mac-native-feel-audit/spec.md` +- `docs/ARCHITECTURE.md` Desktop platform contract diff --git a/docs/architecture/chat-scroll-ownership/tasks.md b/docs/architecture/chat-scroll-ownership/tasks.md index d8be2fdb7f..0b5c106f47 100644 --- a/docs/architecture/chat-scroll-ownership/tasks.md +++ b/docs/architecture/chat-scroll-ownership/tasks.md @@ -38,7 +38,7 @@ - [x] Route history compensation and measurement anchoring through the controller. - [x] Route search and Spotlight navigation through the controller. - [x] Match programmatic scroll events by request ID/expected target instead of time windows. -- [x] Add an architecture guard preventing new direct viewport writes outside the controller. +- [x] Add a focused ownership test preventing new direct viewport writes outside the controller. ## Phase 4: Isolated page geometry @@ -86,7 +86,7 @@ - [ ] Manually verify trackpad and mouse-wheel behavior on macOS in short and long conversations. - [ ] Record before/after performance and scroll-write metrics. - [ ] Record cold first-load, warm cached-switch, uncached-switch, and rapid-switch race metrics. -- [ ] Update the retained chat windowing and issue specifications with final implementation results. +- [ ] Update this specification and the retained scroll-coordinate issue with final implementation results. ## Phase 8: Review hardening diff --git a/docs/architecture/chat-scroll-windowing/spec.md b/docs/architecture/chat-scroll-windowing/spec.md deleted file mode 100644 index 0efc6b4f44..0000000000 --- a/docs/architecture/chat-scroll-windowing/spec.md +++ /dev/null @@ -1,225 +0,0 @@ -# Chat Scroll Windowing Specification - -> The retained bounded-layout contract in this document remains valid. Scroll ownership, isolated -> page geometry, first-load staging, and session-switch performance are now specified by -> `docs/architecture/chat-scroll-ownership/`. - -## User Need - -DeepChat's chat page must remain fast and smooth for long conversations while preserving reliable message anchors for future features such as a chat minimap. The solution must not use a fully opaque virtual list model that makes anchor scrolling, search jumps, trace jumps, or minimap positioning depend on whether a message currently exists in the DOM. - -## Goal - -Design a chat-specific windowed rendering and scroll model that provides virtual-list-like performance without sacrificing stable message addressing, bottom-first chat behavior, user-controlled auto-scroll behavior, or smooth streaming output. - -## Current Context - -The current chat page renders through this path: - -```text -ChatTabView - -> ChatPage - -> MessageList - -> MessageListRow - -> MessageItemUser / MessageItemAssistant - -> MessageBlockContent - -> MarkdownRenderer -``` - -Relevant current files: - -- `src/renderer/src/views/ChatTabView.vue` -- `src/renderer/src/pages/ChatPage.vue` -- `src/renderer/src/components/chat/MessageList.vue` -- `src/renderer/src/components/chat/MessageListRow.vue` -- `src/renderer/src/composables/message/useMessageWindow.ts` -- `src/renderer/src/components/message/MessageItemAssistant.vue` -- `src/renderer/src/components/message/MessageBlockContent.vue` -- `src/renderer/src/components/markdown/MarkdownRenderer.vue` -- `src/renderer/src/stores/ui/message.ts` -- `src/renderer/src/stores/ui/stream.ts` -- `src/renderer/src/stores/uiSettingsStore.ts` - -Important existing behavior and risks: - -- `ChatPage` owns a bounded message window. Histories above the windowing threshold render only the - active range plus overscan, while `MessageList` uses before/after spacers from - `useMessageWindow` to preserve the full logical scroll extent. -- Full DOM rendering causes long conversations, Markdown rendering, code blocks, Mermaid, artifact parsing, tool-call blocks, and layout reads/writes to accumulate cost. -- Streaming currently updates reactive stream state and also applies streaming blocks into the message cache, causing repeated conversion, parsing, markdown rendering, scroll updates, and layout work. -- The UI setting `autoScrollEnabled` exists in `useUiSettingsStore()` and must be respected by any new scroll model. -- The spacer model is only reliable when estimated heights, measured heights, visual row spacing, - and container-coordinate conversion use the same layout units. - -## Required Behavior - -### 1. Bottom-first chat entry - -When the user opens an existing chat session, the page should quickly show the latest part of the conversation and land at the bottom. - -This initial bottom positioning is distinct from the auto-scroll setting: - -- Opening a chat should default to the bottom so users can see the latest context. -- This behavior should not be disabled merely because `autoScrollEnabled` is false. - -### 2. Respect auto-scroll setting during generation - -The existing `autoScrollEnabled` setting controls generation-time following behavior. - -When `autoScrollEnabled` is true: - -- During generation/streaming, the chat view should follow the bottom. -- Streaming content growth should be coalesced into efficient bottom-follow updates. -- The user should see new output without manual scrolling. - -When `autoScrollEnabled` is false: - -- Streaming/generation must not pull the user to the bottom. -- The user's current reading position, or "line of sight", should remain stable. -- Streaming output may continue below the viewport, but the viewport should not jump. - -### 3. Preserve line of sight - -The scroll system must be able to identify and preserve the user's current viewport anchor when auto-follow is not active. - -A viewport anchor should be based on stable message identity rather than raw DOM availability: - -```ts -type ViewportAnchor = { - messageId: string - offsetWithinMessage: number -} -``` - -When message heights change because of streaming, Markdown hydration, artifact rendering, image load, code block rendering, or history insertion, the system should compensate scroll position to keep the anchor visually stable unless the active mode is bottom-follow. - -### 4. Virtual-list-like performance without full virtual opacity - -The implementation should avoid painting all heavy message DOM for long conversations, but should retain full logical addressability. - -Use a chat-specific bounded window backed by DeepChat's own layout model: - -```text -complete loaded message data - -> stable layout model for every loaded message - -> viewport + overscan selects a bounded rendered range - -> before/after spacers preserve the full logical extent - -> rendered rows refine estimates through measured heights - -> search/jump/minimap consumers address every loaded row by messageId -``` - -Rows outside the active window are represented by spacers rather than mounted heavy DOM. Each -loaded message still has: - -- stable `messageId` -- ordering information -- estimated height -- measured height when available (committed only once the row has been painted) -- logical top/bottom offsets - -The layout contract is explicit: - -- a row's logical footprint includes all visual spacing owned by that row; -- estimates and DOM measurements use the same footprint; -- spacer heights are derived only from logical entry boundaries; -- entry positions are relative to a stable message-window origin and must be converted before they - are requested from the isolated message viewport controller; -- programmatic jumps carry a typed reason, session epoch, request ID, and expected target; matching - scroll events are attributed without elapsed-time windows; -- batched measurements preserve a logical message anchor in the same frame as the height-map - commit, before the revised spacers can be painted, but only when bounded windowing is active; -- short fully rendered conversations update measurement caches without measurement-driven - `scrollTop` writes, and scroll-state classes do not change page-wide visual effect tokens; -- session restore uses one controller-owned bottom transaction plus coalesced geometry notices; - any user gesture cancels active and pending restore work without a repeated frame loop; -- history loading chrome does not participate in message flow, and top pagination requires both a - full initial history window and pre-existing upward user intent. - -### 5. Future minimap compatibility - -This change must not block a future minimap. - -The future minimap should be able to rely on a logical layout model, not on querying every message DOM node. Therefore: - -- Do not make a third-party virtual scroller the sole source of truth for item heights or positions. -- Do not require all message DOM nodes to exist for anchor scrolling. -- Keep message positions addressable by `messageId`. -- Search, trace jumps, and future minimap jumps should operate through a message layout model. - -### 6. Smooth and continuous scrolling - -Scrolling should feel continuous for both normal and long conversations. - -Requirements: - -- Normal scrolling should not stutter from excessive Markdown mount/unmount work. -- Large or fast scrolls should not show large blank gaps caused by under-rendered virtual ranges. -- Overscan should adapt to scroll velocity and generation state. -- Heavy content hydration may be delayed while fast scrolling, then completed after scroll settles. - -### 7. Long chat first load must be fast - -Long conversations should not require full history or full DOM hydration before the chat becomes usable. - -Preferred behavior: - -1. Load and render the latest page/window first. -2. Position at the bottom. -3. Make input and latest messages interactive quickly. -4. Defer older history loading, metadata preparation, measurement refinement, and optional pre-hydration. - -### 8. Streaming must stay smooth - -Generation smoothness is a first-class requirement. - -Streaming updates should not force the entire message list to recompute or remount. The currently streaming assistant message should be treated as a live row or live layer that is isolated from stable historical rows as much as possible. - -The scroll/layout system should batch work during streaming: - -- Coalesce `scrollToBottom` operations with `requestAnimationFrame` or equivalent batching. -- Batch height measurement commits. -- Avoid synchronous full-list layout recalculation on every token/chunk. -- Apply dynamic throttling/debouncing to Markdown rendering for long streaming content. - -## Acceptance Criteria - -1. Opening a long chat renders quickly and lands at the latest/bottom content. -2. Long chats avoid full heavy DOM rendering for all loaded messages. -3. `autoScrollEnabled = true` causes generation to follow the bottom. -4. `autoScrollEnabled = false` prevents generation from forcing the viewport to the bottom. -5. With auto-scroll disabled, the user's current reading position remains stable while generation continues. -6. Fast scrolling through long chats does not show large blank areas. -7. Streaming output remains smooth and is not blocked by full-list recomputation or excessive layout work. -8. Search, trace jumps, and future minimap jumps can target messages by `messageId` even if the target is outside the current render window. -9. Loading older messages at the top preserves viewport position. -10. The design leaves a reusable message layout model for future minimap work. -11. Short conversations cannot trigger older-history pagination from incidental layout scrolls. -12. Short fully rendered conversations do not move because a row measurement settles after scroll. -13. User scrolling always wins over restore, follow, resize, and measurement requests. - -## Non-Goals - -- Implementing the minimap itself. -- Replacing all chat message rendering components. -- Changing LLM/provider streaming semantics. -- Removing the existing `autoScrollEnabled` setting. -- Requiring full conversation history to load before the chat becomes usable. -- Relying solely on a third-party virtual scroller as the long-term architecture. - -## Constraints - -- Use Vue 3 Composition API patterns already present in the renderer. -- Keep changes localized to chat rendering, message layout, and scroll behavior where possible. -- Do not weaken existing message actions, trace behavior, search behavior, or read-only session behavior. -- Do not introduce user-facing strings without i18n keys. -- Avoid synchronous expensive work during streaming. -- Keep future minimap support data-driven rather than DOM-driven. -- Keep explicit chat search and message jumps immediate/default rather than smooth, matching the - desktop native-feel regression contract. - -## Review Notes - -The preferred architecture is the bounded renderer window introduced by the long-chat reliability -work, backed by a DeepChat-owned layout model. A third-party virtual scroller must not become the -sole owner of positions. Search, spotlight, history anchoring, capture, and future minimap behavior -continue to consume stable message identities and logical coordinates from DeepChat. diff --git a/docs/architecture/chatpage-decomposition/plan.md b/docs/architecture/chatpage-decomposition/plan.md new file mode 100644 index 0000000000..a64217b489 --- /dev/null +++ b/docs/architecture/chatpage-decomposition/plan.md @@ -0,0 +1,34 @@ +# Plan: ChatPage 分解与竞态治理 + +## 实施顺序 + +按"每抽一个 composable 即 typecheck + 测试"的节奏,从耦合最少的关注点开始, +逐步收编模块级令牌;滚动仲裁族(`useChatScrollController` 等)保持不动,只归拢胶水调用。 + +1. **死代码清理** — 删除零引用的 `composables/message/useMessageScroll.ts`(339 行, + 旧 vue-virtual-scroller 实现)及其孤儿测试、`ScrollInfo` 类型。 +2. **useDisplayMessages + usePlanFloatLifecycle** — 记录→DisplayMessage 转换、 + 稳定/流式分离、占位符四态机(见 spec 决策记录)、Plan 快照生命周期。 +3. **useChatSearch** — 会话内搜索,包装 `lib/chatSearch` 的 rAF/highlight 调度。 +4. **useListGestures + useMessageVirtualization** — 手势原子与虚拟化原子, + 再组合接入主文件;composable 声明顺序移到会话 watch 之前。 +5. **useComposerSubmit** — 发送/排队/steer/命令/compaction 统一提交路径, + 自持 `attachmentFilterToken`;四个提交入口的重复守卫收敛为 `canSubmitNow()`。 +6. **useSessionRestore** — 会话恢复 epoch(`restoreRequestId`)、`canWriteSessionView` + 写 gate、启动延迟恢复调度、`deactivate()` 卸载语义;主文件通过 + `currentRestoreRequestId()` 读取最新令牌。 + +## 验证策略 + +- 每步:`pnpm run typecheck:web` + `ChatPage.test.ts`(82 用例)。 +- 收尾:`pnpm run format` / `lint` / `i18n` / `test:renderer` 全量, + 与 origin/dev 对照隔离既有失败。 +- 行为零漂移由测试 + 逐行 diff 审查共同保证;竞态令牌语义(闭包内最新值 vs 快照) + 单独走一轮对抗性 review。 + +## 回滚 + +每个 composable 一个独立 commit。因抽取提交之间存在依赖(后一个 composable 的接入 +依赖前面已建立的装配结构与导入),回滚须按提交的**逆序**进行,或连同依赖它的后续 +提交一并回滚,不能孤立 revert 中间某一步。滚动仲裁契约(`useChatScrollController` +及其协作模块)全程未动,整体回滚到基线不影响该子系统。 diff --git a/docs/architecture/chatpage-decomposition/spec.md b/docs/architecture/chatpage-decomposition/spec.md new file mode 100644 index 0000000000..b4d9f444e1 --- /dev/null +++ b/docs/architecture/chatpage-decomposition/spec.md @@ -0,0 +1,64 @@ +# ChatPage 分解与竞态治理 + +## 背景 + +`src/renderer/src/pages/ChatPage.vue` 已膨胀到 3050 行,单个 ` diff --git a/src/renderer/settings/components/AboutUsSettings.vue b/src/renderer/settings/components/AboutUsSettings.vue index 1d5f5bf328..45ec8486b3 100644 --- a/src/renderer/settings/components/AboutUsSettings.vue +++ b/src/renderer/settings/components/AboutUsSettings.vue @@ -189,13 +189,12 @@ :disabled="upgrade.isChecking || upgrade.isDownloading || upgrade.isRestarting" @click="handlePrimaryAction" > - +