feat: 段页式记忆流转、coldcontext 重命名与安全治理

记忆层:
- MemoryBlock 承载 FrameTS/PageSeq/PageKind/BlockRole/CallID
- coldcontext 归档与回填按完整页传递,工具调用参数和配对键不丢失
- 召回先估算总 token,超预算时按全局页相似度标记 inactive
- inactive 仅是 context 渲染期状态,不落盘、不进入 coldcontext,Prune 后重置
- Peek/确认/Remove 两阶段召回,失败保留冷上下文副本
- stablemem 记录 last_recalled_at,删块连带清理边

命名:
- internal/memory/document → internal/memory/coldcontext
- Doc/DocEvent/DocHit → ColdDoc/ColdEvent/ColdHit
- ContextToColdContext/RecentColdContexts/FindColdContexts
- 保留既有 JSON 标签、doc_ 文件前缀与 doc_count 状态键,兼容存量数据

安全治理:
- 工具策略门禁、安全配置、事件总线发布者身份与入站脱敏
- 声明式 SecurityAPI 与 QQ owner/guest profile
- 日志保留策略与相关 e2e/回归判据

验证:go build ./...、go vet ./...、go test ./...(62 包);
生产副本 e2e:7013 块迁移后 ID/向量/指纹 0 变化、0 缺失。
This commit is contained in:
JianFeeeee
2026-10-29 23:53:48 +08:00
parent a3e50b395f
commit b850feaef2
113 changed files with 6512 additions and 790 deletions

View File

@ -0,0 +1,118 @@
package core
// 术语改名的判据(plan.md 裁定 #11:`coldcontext_*` / `stablemem_*`,不再用 doc/graph)。
//
// ## 为什么必须钉(2026-10-29 现场)
//
// 裁定 #11 只落实了一半:`coldcontext_*` 已改,而**图记忆的工具仍叫 `memory_*`**。
// 改名不是纯文字工作,它有三个会静默失效的面:
//
// ① 路由:toolroute 按前缀分派,改前缀不改路由 ⇒ 工具"存在但调不到"
// ② 提示词:BuildToolPrompt / skill / 工具描述里写旧名 ⇒ 模型照着调不存在的工具
// ③ 安全策略:生产配置用的是通配符 `memory_*`,命中不了 `stablemem_*`
// ⇒ 硬私有策略静默失效(用户能读私人记忆)
//
// ③ 最危险:它不报错、不告警,只是拦不住。所以本文件也钉一条策略匹配判据。
import (
"strings"
"testing"
agentAPI "github.com/JianFeeeee/HologramAgent/internal/agent/api"
"github.com/JianFeeeee/HologramAgent/internal/memory"
"github.com/JianFeeeee/HologramAgent/internal/toolroute"
)
// ① 面向模型的稳定记忆工具必须统一用 stablemem_ 前缀。
func TestStableMemoryToolsUseStablememPrefix(t *testing.T) {
a := newPreemptAgent(t, newPreemptProvider())
a.memory = nil // 只需要名字,不需要真库
// 旧名不得再出现于任何工具定义(会造成"提示词教旧名、定义是新名")。
//
// ★ 此处曾有例外 `def.Name != "memory_document_query"`(2026-10-29 移除)。
// 当时以为那个工具"仍在 memory_ 名下",实际它是改名前的死分支:
// 没有工具定义、Dispatch 也不分派它。例外一旦存在,就会掩盖真问题 ——
// 所以现在**零例外**。
for _, def := range a.ToolDefinitions() {
if strings.HasPrefix(def.Name, "memory_") {
t.Errorf("发现未改名的稳定记忆工具: %s(裁定 #11 要求 stablemem_*)", def.Name)
}
}
}
// ② 路由必须认 stablemem_ 前缀(否则工具存在但调不到)。
//
// ★ 这里直接调 Dispatch,而不是某个"IsBuiltinTool"前缀表 —— 后者曾有**两份**
// 手抄副本(toolroute 与 core 各一份),都零调用方,却在改名时制造了
// "改了 Dispatch 却漏了副本"的真实故障。判据钉**行为**(能不能分派),
// 不钉"某份表里有没有这个字符串"。
func TestStablememToolsAreRouted(t *testing.T) {
h := &routeProbeHandlers{}
for _, n := range []string{"stablemem_recall", "stablemem_purge"} {
if _, handled := toolroute.Dispatch(h, agentAPI.ToolCall{Name: n}, nil); !handled {
t.Errorf("%s 未被 Dispatch 分派 —— stablemem_ 前缀漏了,工具会变成\"存在但调不到\"", n)
}
}
// ★ 旧前缀 memory_ **不再被分派**(这是改名的语义,不是遗漏)。
//
// 我最初在这里断言"memory_ 应保留兼容分派",它变红了 —— 而红得对:
// Dispatch 里从来没有 memory_ 分支,改名后只有 stablemem_。
// 若为"兼容"而加一条 memory_ 分支,反而会让**旧名重新可用**,
// 与裁定 #11(不再用 graph/doc 时代的名)直接冲突。
//
// 真正需要兼容的是**配置与提示词**(那些已另行处理),不是工具路由:
// 存量插件注册的是自己的工具名,内核的稳定记忆工具由内核定义,
// 不存在"外部还在用旧名调内核工具"的情况。
if _, handled := toolroute.Dispatch(h, agentAPI.ToolCall{Name: "memory_recall"}, nil); handled {
t.Error("memory_ 不应再被分派 —— 旧名重新可用会与裁定 #11 冲突")
}
// 反向:不是内置工具的名字必须返回 handled=false(否则插件工具会被吞掉)。
if _, handled := toolroute.Dispatch(h, agentAPI.ToolCall{Name: "qq_get_message"}, nil); handled {
t.Error("非内置工具不应被 Dispatch 吞掉")
}
}
// routeProbeHandlers 记录 Dispatch 选中了哪个族,全部返回空串即可。
type routeProbeHandlers struct {
memoryCalls int
}
func (h *routeProbeHandlers) ExecutePersonaTool(agentAPI.ToolCall) string { return "" }
func (h *routeProbeHandlers) ExecuteMemoryTool(agentAPI.ToolCall, []string) string {
h.memoryCalls++
return ""
}
func (h *routeProbeHandlers) ExecuteSocialTool(agentAPI.ToolCall) string { return "" }
func (h *routeProbeHandlers) ExecuteKnowledgeTool(agentAPI.ToolCall) string { return "" }
func (h *routeProbeHandlers) ExecuteColdContextTool(agentAPI.ToolCall) string { return "" }
func (h *routeProbeHandlers) ExecuteSecretTool(agentAPI.ToolCall) string { return "" }
func (h *routeProbeHandlers) ExecuteOutputSend(agentAPI.ToolCall) string { return "" }
func (h *routeProbeHandlers) ExecuteOutputSendHelp(agentAPI.ToolCall) string { return "" }
func (h *routeProbeHandlers) ExecuteOutputListChannels() string { return "" }
func (h *routeProbeHandlers) ExecuteInputChannels(agentAPI.ToolCall) string { return "" }
func (h *routeProbeHandlers) ExecuteResidentAgents(agentAPI.ToolCall) string { return "" }
func (h *routeProbeHandlers) ExecuteNotifyParent(agentAPI.ToolCall) string { return "" }
func (h *routeProbeHandlers) ExecuteInputchNote(agentAPI.ToolCall) string { return "" }
func (h *routeProbeHandlers) ExecutePluginReload() string { return "" }
func (h *routeProbeHandlers) ExecuteGetPluginTools(string) string { return "" }
// ③ ★ 提示词里不得再教模型调旧工具名。
//
// BuildToolPrompt 是**硬编码字符串**(不是从定义生成),工具一改名它就过期。
// 这类不一致模型会当成事实执行,最终表现为"反复调用不存在的工具"。
func TestToolPromptDoesNotTeachRenamedTools(t *testing.T) {
// BuildToolPrompt 是 Indexer 上的**硬编码字符串**(不是从 toolDefs 生成),
// 工具一改名它就过期 —— 这正是本次改名后真实失败的那个测试。
idx := memory.NewIndexer(nil)
prompt := idx.BuildToolPrompt()
if prompt == "" {
t.Skip("BuildToolPrompt 返回空")
}
for _, stale := range []string{"memory_recall", "memory_commit", "memory_purge", "memory_introspect"} {
if strings.Contains(prompt, stale) {
t.Errorf("工具提示词仍在教旧工具名 %q —— 模型会照着调用不存在的工具", stale)
}
}
}