9.9 KiB
项目历史召回插件设计方案
状态: 设计方案,非实施代码。本文件定义插件的能力边界与集成方式;代码实现需另行授权。
Goal: 用一个插件同时解决两个需求:(1) 会话项目归属(project_id),(2) 项目内历史的按需关键词+语义检索。
定位: Footprint Ladder 第 4 级 —— 独立插件,不动核心文件,不落 plugins/memory/(该目录已关闭新 provider)。装在 ~/.hermes/plugins/project-history-recall/ 或独立仓库 pip 包。
一、为什么用一个插件而不是两个
两个需求共享同一套底座:会话→项目的归属解析 + state.db 只读访问 + 检索结果格式化。拆成两个插件会重复这份底座并引入"哪个插件负责归属"的模糊边界。一个插件、两个工具,归属逻辑写一次,两个工具都吃它。
与 V3/V4.1 的关系: 本插件方案替代它们的"改核心 session_search"路径。V3/V4.1 的归属语义(project_id、移动只改归属、cwd 分离)和检索设计(项目过滤下推、混合检索、溯源)全部保留,但落点从核心 session_search 改为插件的独立工具,核心零改动。
二、插件结构与发现
~/.hermes/plugins/project-history-recall/
├── plugin.yaml # name/version/description,不动核心
├── __init__.py # register(ctx):注册两个工具
├── attribution.py # 归属解析(读 projects.db + sessions 行)
├── search.py # 关键词+语义混合检索
└── config_schema.py # embedding 配置项声明(如有)
发现路径:PluginManager(hermes_cli/plugins.py)扫描 ~/.hermes/plugins/,later-wins 发现 register(ctx)。
注册方式(对齐 plugins/spotify/__init__.py:28-31 的现有模式):
def register(ctx) -> None:
ctx.register_tool(name="project_history_search", toolset="project",
schema=SEARCH_SCHEMA, handler=handle_search,
check_fn=check_available, emoji="📂")
ctx.register_tool(name="project_session_move", toolset="project",
schema=MOVE_SCHEMA, handler=handle_move,
check_fn=check_available, emoji="📁")
两个工具都在 project toolset(会话作用域,桌面端 GUI 平台自动折叠进可用集合,不依赖 HERMES_DESKTOP 环境变量)。
三、需求一:项目归属(project_id 语义)
归属数据源: 复用现有 projects.db(hermes_cli/projects_db.py)+ 会话行的 cwd/git_repo_root(hermes_state_common.py:305-307)。
归属解析(attribution.py): 纯函数,无 IO 写入。
- 读
projects.db的projects+project_folders→ 构建文件夹索引(最长祖先匹配,_FolderIndex同款逻辑,插件内重新实现,不从 tui_gateway 复制——它是显示层,不是共享内核)。 - 当前会话行 →
cwd/git_repo_root→ 匹配显式项目文件夹 →Project.id。 - 无显式项目命中 → 用
git_repo_root的 path_key 作为 auto 项目身份(Tier 2)。 - 无 cwd 且无 git_repo_root → Home 桶,返回明确空结果,不检索。
移动(project_session_move 工具): 更新会话的归属记录。归属与 cwd 一致移动——会话移到哪个项目,工作目录就跟到哪个项目的目录。通过现有 session.workspace.move RPC 完成(它已有持久化 cwd/git_repo_root 的完整路径,见 tui_gateway/methods_session.py:840-876),插件在其上叠加归属语义(校验目标项目存在、写入 project_id)。核心零改动。
归属语义(保留 V3 合同,cwd 跟随移动):
- 移动会话 = 归属与 cwd 一起切到目标项目目录(workspace.move 的现有行为,插件叠加归属校验)。
- 移动前开始的检索可以完成;移动后的新检索用新归属和新 cwd。
- Home 桶(无 cwd)不检索,返回空并说明原因。
- 跨 profile 归属不合并。
四、需求二:项目内历史检索(关键词 + 语义)
工具: project_history_search,两个模式由 mode 参数控制(默认 hybrid)。
4.1 关键词路径
插件直接只读查询 state.db(SessionDB 的只读实例,或 sqlite3.connect(..., uri=True) 只读模式):
SELECT m.id, m.session_id, m.role, m.content, m.timestamp, s.title, s.started_at
FROM messages m JOIN sessions s ON m.session_id = s.id
WHERE s.<归属匹配条件> AND m.content LIKE / FTS5 MATCH ...
ORDER BY rank / timestamp
LIMIT ?
归属匹配条件由 attribution.py 解析出的项目会话集合生成(s.project_id = ? 或 s.cwd LIKE '...%' 或 s.git_repo_root = ?,取决于归属是否已持久化)。
只读约束是硬性的: 插件对 state.db 只有 SELECT,绝不 INSERT/UPDATE/DELETE。state.db 是核心的权威存储,插件写入会破坏核心的一致性假设(FTS 索引、token 计数、lineage 状态)。
4.2 语义路径
向量索引为插件自有派生数据,存在插件自己的 SQLite 文件(~/.hermes/plugins/project-history-recall/embeddings.db)——不碰 state.db schema。
索引构建:
- 按归属解析出项目会话集合 → 读 messages 的 user/assistant 文本 → 按消息切片(长消息分段)→ 调用 embedding 模型 → 存向量。
- 首次索引按项目分批执行;后续增量(新消息)在每次检索前检查并补建。
- 索引记录
session_id + message_id + chunk_offset + content_hash + model_fingerprint + vector。 - 移动会话后向量不重算——归属解析在查询时重新执行,向量只关联 session_id。
检索流程:
- 归属解析 → 项目会话集合。
- 关键词:state.db FTS5 查询,限定在集合内。
- 语义:embeddings.db 向量查询,限定在集合内(先 JOIN 过滤再算相似度,不全库 Top-K 后过滤)。
- 混合:Reciprocal Rank Fusion 按消息锚点归并两路结果。
- 展开:对命中消息用 state.db 读原文窗口(前后各 N 条),返回
session_id + message_id + @session:<id> 链接 + 原文片段。
模式:
hybrid(默认):关键词 + 语义,语义不可用时退化为关键词并注明。keyword:仅关键词,用于精确符号/错误码查询。semantic:仅语义,用于不同措辞的相似问题追溯。
4.3 结果格式
JSON 字符串返回(对齐现有工具约定):
{
"success": true,
"mode": "hybrid",
"project": {"kind": "explicit", "id": "p_abc", "name": "Hermes"},
"results": [{
"session_id": "20260901_143022_a1b2",
"link": "@session:20260901_143022_a1b2",
"title": "session_search 项目过滤设计",
"message_id": 47,
"snippet": "……项目归属应该持久化在 sessions 表……",
"score_source": "both",
"when": "2026-09-01 14:30"
}],
"sessions_in_scope": 12,
"semantic_status": "ready"
}
semantic_status:disabled / consent_required / indexing / ready / partial / unavailable。
五、Embedding 配置与隐私
embedding 模型配置通过插件自己的配置文件(~/.hermes/plugins/project-history-recall/config.yaml)或 config.yaml 的 plugins.project-history-recall 节,走 hermes tools 的插件配置 UX。密钥进 .env。
隐私(保留 V4.1 的对话确认设计):
- 首次需要远程 embedding 且无授权记录时,工具返回
consent_required状态,不发送任何内容。 - Agent 在对话中说明服务方、发送内容、项目范围,用户确认后写入配置。
- 授权持久化,同范围后续不重复询问;换服务/加项目重新确认。
- 未确认时关键词路径正常工作。
六、与核心的交互边界
| 插件做的 | 插件不做的 |
|---|---|
| 读 projects.db、读 state.db(只读) | 写 state.db 的 sessions/messages 表(移动除外——workspace.move 有核心自己的写路径) |
| 自己的 embeddings.db(向量存储) | 改 state.db schema |
注册两个工具到 project toolset |
修改核心 session_search 或 inline executor |
| 归属解析纯函数 | 修改 project_tree.py 或桌面端代码 |
| embedding 调用(自有配置) | 新增核心 HERMES_* 环境变量 |
核心配合需求:无。 移动会话归属与 cwd 一致移动,复用现有 session.workspace.move RPC 的完整路径(持久化 + git 元数据更新),插件只在其上叠加项目校验和归属语义。核心零改动。
七、实施顺序
| 步骤 | 内容 | 验证 |
|---|---|---|
| B0 | 验证 state.db 只读访问 + projects.db 读取 + workspace.move 的归属叠加路径 | 真实临时 HERMES_HOME 下跑通 |
| P1 | attribution.py 归属解析纯函数 + 单测 | 显式项目/自动 repo/Home 桶三态 |
| P2 | project_history_search 关键词路径 + project toolset 注册 | P/Q 项目隔离、Home 桶空结果、溯源链接 |
| P3 | project_session_move 移动 + 归属更新 | 移动后新检索用新归属 |
| P4 | embeddings.db + 语义路径 + 混合检索 | 同义词命中、关键词保底、移动不重算向量 |
| P5 | 对话确认链路 + 隐私控制 | 未确认不发送、确认后自主检索 |
八、不做的事
- 不改核心 session_search、inline executor、SessionDB schema。
- 不做项目摘要、会话摘要、自动上下文注入。
- 不做旧会话自动目录归类。
- 不做权限撤销协议、版本表、跨 profile 联合检索。
- 不动全局 MEMORY/USER 的项目隔离。
九、评审自审
- Footprint Ladder: 第 4 级插件,核心零改动,符合"能力在边缘"。
- 一个插件两个工具: 归属底座共享,不重复。
- 只读 state.db: 插件绝不写核心存储,向量索引自有。
- prompt cache: 工具结果通道,不碰系统提示词。
- 诚实边界: 移动时 cwd 跟随归属是已确认的行为(workspace.move 现有路径),无悬而未决项;语义检索的性能包络待 P4 实测,不预先承诺。