v2-refactor-temp/docs/ai/tool-result-db-trim.md
更新日期:2026-08-02(已实施,见文末「实施结论」) 范围:
src/main/ai/runtime/aiSdk/params/features/contextBuild.ts、packages/aiCore/src/core/context/*(vendored)、src/main/services/VfsBlobService.ts、src/main/data/services/MessageService.ts、src/main/services/file/*、src/shared/data/types/message.ts、src/shared/data/types/file.ts目的:记录「现在就做 message.data 大工具结果裁剪」前提下,blob 存储从 VFS 临时目录迁到 FileManager、GC 从 TTL 改为引用式的设计结论。
今天的双副本结构里,VFS 临时文件是可再生缓存:message.data 存全量,截断中间件每次请求重算,vfs_<sha256>.txt 被清掉会在下一次请求自动重写。所以 7 天(或 30 天)TTL 只是磁盘旋钮,无正确性影响。
一旦裁剪 DB 副本落地,message.data 只剩 head/tail + marker,blob 成为被截断内容的唯一副本:
FileManager.createInternalEntry({ source:'bytes', cleanupPolicy:'delete_when_unreferenced' }),落盘 {userData}/Data/Files/{uuid}.txt,contentHash(xxh3-64)自动计算。findInternalByContentHash(FileManager.ts:1021)查重:同内容跨轮次、跨 regenerate/分支兄弟只存一份 —— 这继承了今天 vfs_<sha256> 内容寻址的语义,只是寻址键从文件名变成 entry。VFSStorageAdapter(offloader.ts)接口归我们所有,可演进:per-request 适配器携带本轮 assistantMessageId,write = xxh3 → find-or-create → 写 provisional ref(见 §3);filename→entry 映射内存持有,跨重启用 name(存 vfs_<sha256[:16]>)兜底查询。超过 truncateThreshold(默认 50k 字符)的 tool-result,持久化时 output 替换为:
{ kind: 'persisted-text', fileEntryId, head, tail, totalChars, totalLines }
(具体挂在 src/shared/data/types/message.ts 的 tool part output 变体上。)
data/api/handlers/files.ts)按 entry 取全文。<persisted-output> marker 文本(路径 = entry 物理路径)。同字段 → 同字节,前缀稳定不再依赖中间件重算。chatMessageRoles(src/shared/data/types/file.ts:465)从 ['attachment'] 扩为 ['attachment','tool_output'];cmfr_role_check CHECK 变更走追加迁移(pnpm db:migrations:generate)。表已注册在 persistentFileRefTablesBySourceType(fileRelations.ts:189),零引用反连接、级联、覆盖测试全部自动生效。extractChatMessageFileEntryIds(MessageService.ts:210)增加对 persisted 部分 fileEntryId 的收集;replaceChatMessageFileRefsTx(MessageService.ts:237)按部分类型选 role,delete-and-reinsert 语义不变。MessageServiceBackend.persistAssistant 是终态一次性落库,但 assistant placeholder 行在 turn 开始就存在(createUserMessageWithPlaceholders)。offload 时立刻写一条 tool_output ref 指向 placeholder,防止长于 1h grace 的 turn 中 0-ref 条目被 reaper 收走;终态 replace 收敛到真实集合。turn 出错时 ref 随 placeholder 存续,随消息删除级联 —— 只是轻微过保留,无泄漏。entryCleanup.ts:55)删行删 blob;行删/文件删之间崩溃由每周 FS 孤儿清扫(orphanSweep.ts)兜底。lastUsedAt、不新增定时器。blob 移入 Data/Files 后不能再给目录级 root(会暴露附件/画作)。context build 渲染 marker 时把本请求出现的 entry 物理路径收集进 RequestContext(同 fileAttachments 模式),FsReadTool.allowedRoots 换成精确路径集合校验。模型只能读本请求 prompt 里引用过的 blob,注入猜 uuid 无效。
feature.context_build.vfs.temp 路径键、getRoot 皆无消费者,随改动删除(v1 residue 规则)。contentHashBackfillJobHandler 先例)渐进改写旧行为 persisted 部分。注意每个旧会话首次改写会有一次性 prefix cache bust。pnpm db:migrations:check、schema 测试。setupTestDatabase)。三个开放决策的定案:①临时聊天/一次性 streamPrompt 不落盘(纯内联 head/tail 截断,VfsBlobService 整体退役,仅留启动时对旧 temp 目录的一次性清理);②本期只裁新写入,旧行靠在飞行截断兼容,不回填;③放弃 30 天 TTL,纯引用式 GC。
与设计稿的主要差异/细化:
message.data 存结构化 $persistedToolOutput(src/shared/ai/transport/persistedToolOutput.ts:fileEntryId + vfsFilename + head/tail + counts + shape),prompt 组装时由 renderPersistedToolOutputs(ai/messages/persistedOutputRendering.ts,挂 toModelMessages 首步)确定性重建 marker——字节等同由契约测试钉死。shape: 'text' | 'mcp-content');结构化 JSON 大输出仍存全量(toModelOutput 二次包装问题),由在飞行截断兜底,后续可加 shape:'json'。MessageServiceBackend.persistAssistant(仅 SQLite 路径)在同步 finalize 事务前 await trimOversizedToolOutputs;refs 与 data 同事务写入。逐 part 容错,存储失败保留全量。createFileManagerStorageAdapter(ai/contextBuild/persistedOutputAdapter.ts)write = 去重建 entry + 立即写 provisional tool_output ref 指向 placeholder 行(1h grace 降为兜底);锚定判定 = message 行存在(临时聊天有合成 uuid 无行,只看 messageId 会 FK 违规)。RequestContext.persistedOutputPaths,历史信封 + 在飞行新增;realpath 比较,字面成员优先使 blob 失联时报 not-found)。copyPathRowsTx 无需补 ref 复制——唯一调用方 TopicService.duplicate 已按 source-id map 整体复制 ref 行(role 保真,tool_output 自然覆盖);在 copyPathRowsTx 内重导会撞 (entry, source, role) 唯一索引。$deferredToolResult 传输链,投影时附带 excerpt,取回失败降级为摘录 + 注记。Breaking-changes 条目:v2-refactor-temp/docs/breaking-changes/2026-08-02-tool-output-excerpt-storage.md。
Backfill 接缝:维护任务 walk message.data → trimOversizedToolOutputs → messageService.update(自动重导 refs);本期未实现。