Files
HomeAgent/internal/sdk/events.go
JianFeeeee 9036db9321 feat(memory): 图数据库自报真实读写事件,取代「按工具名猜节点」
## 问题(生产实证)

星图的「跟随 agent 活动」此前靠**工具名词元猜节点**:把 `memory_recall`
拆成 [memory, recall] 去匹配实体名。而图谱里的实体名是**中文概念**
(小宅/对话/待命),工具名是英文 —— 两套命名空间永不交集。

实测(生产库)每个工具名命中 **0** 个节点:

    hint=memory_recall  → memory/recall  → 0
    hint=files_read     → files/read     → 0
    hint=knowledge_search → knowledge/search → 0

匹配失败后走「补齐」分支,按 mention_count 取前 8 个 ⇒ **无论 agent 在干什么,
点亮的都是同一批无关节点**(小宅/对话/待命/投递/图片/文档/插件/老大)。
这就是「星图没有读写反馈」的真身。

## 改法:让图数据库在真正读写处上报

新增 `events.EventMemoryAccess` + `GraphDB.AccessHook`,在读写入口埋点上报
**真实块 ID**(不再猜):

| 操作 | 埋点 | 上报 |
|---|---|---|
| 读 | RecallSorted | blocks |
| 写 | commit | blocks + **created**(真正新建的) |
| 删 | DeleteEntity | removed |
| 合 | MergeBlocks | merged(from→to) + removed |

三处关键设计:

1. **钩子用独立锁** `hookMu`。用 `g.mu` 会**自死锁** —— 埋点方法已持该锁,
   而 Go 的 RWMutex 不可重入。
2. **上报在锁外**。拆 `commit`→`commitLocked` 两层解决 `defer Unlock` 的时序:
   钩子内部可能要回调内存系统(如 SSE 订阅者补数据),持锁调用必死锁。
3. **区分「新建」与「更新」**(`putBlockTx` 返回 isNew)。用 SELECT 判存在性
   而非 `RETURNING (xmax = 0)` —— 后者在**事务 + ON CONFLICT** 下报
   `no such column: xmax`(实测:autocommit 可用、包进 tx 就不可用),
   而本函数只在事务里被调用。重复提交同一事实不会重复播「生长」动画。

接线:`bootstrap.newMainAgent` 里 `memDB.SetAccessHook(...)` → `events.Bus`。
只在 `evBus != nil` 时接,把「内核进程」与「独立 CLI 工具」
(homed-graph-migrate / memgc 也用 NewGraphDB)区分开 —— 后者不该为一次
批量迁移向不存在的订阅者发几万条事件。

## 验证

- `access_hook_test.go`:钉住 commit/recall/delete 三类埋点真的触发、
  且重复提交不误报新建
- `access_hook_race_test.go`:4 写 + 4 读 + 钩子热替换并发,`-race` 下无竞态、
  无死锁;**断言事件数 > 0**(否则「钩子从未触发」也会假通过)
- 该测试起初是 flaky 的(读者可能在写者落库前查询 ⇒ 空结果不报事件 ⇒
  断言随机失败)。修法是先播种确定数据再并发读,已连跑 3 次稳定
- 生产实例端到端:CLI 注入一句话 → SSE 收到 6 条 memory_access,
  带真实块 ID 与 op/created
2026-10-08 10:41:07 +08:00

33 lines
1.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package sdk
import "github.com/JianFeeeee/HomeAgent/internal/events"
// 事件类型/事件类型别名:插件经 SDK 订阅/发布内核事件,
// 无需直接 import internal/events(内核事件总线仅通过 SDK 暴露)。
type EventType = events.EventType
type Event = events.Event
const (
EventRawInput = events.EventRawInput
EventAgentOutput = events.EventAgentOutput
EventAgentLLMChain = events.EventAgentLLMChain
EventToolCall = events.EventToolCall
EventReasoning = events.EventReasoning
EventStage = events.EventStage
EventSystem = events.EventSystem
EventTerminalOutput = events.EventTerminalOutput
EventAll = events.EventAll
// 流式增量事件(token 级):核心 process() 流式化后每收到一个增量块发布。
// 客户端可选订做真逐 token 渲染;聚合事件仍照常发布,旧订阅者不受影响。
EventReasoningDelta = events.EventReasoningDelta
EventContentDelta = events.EventContentDelta
// EventMemoryAccess 是图记忆的真实读写事件(哪个块被读/写/删/合)。
//
// 星图靠它做「反馈」:此前只能拿 tool_call 的工具名猜节点,而工具名是英文、
// 实体名是中文概念(实测命中 0),只能退化成点亮一批固定节点。
EventMemoryAccess = events.EventMemoryAccess
)