fix(memory,loop,capacity): 打通记忆链三条空壳(P0-3 / P0-4 / P0-17,审计 §4.3)
- P0-3 持久向量索引**不再空壳**:Store.Add 落库后真做 Embed+Add(失败只记日志、绝不让记忆写入失败); NewRetriever 拿到 WithVectorIndex 时**自动接上写入路径**(无需改组合根);索引无命中 → WARN + **回退内存余弦**; 新增每进程一次的**回填**(索引确实为空时把已落库条目向量化,分批 64、上限 512) - P0-4 工作记忆**按会话接线**:回合落定时写入(owner 会话 + 带会话 id 的窗口,LRU 上限 64), 组装上下文时按会话读取 ⇒ 前导块真的出现;原单窗口 API 语义不变 - P0-17 L4 学习回环**真回写**:新增 capacity.Judge(只跑判定、不施人格覆盖/不发事件/不改状态); loop 在回合落定"只写 owner 会话 + 跳过子代理分支",写 ScopeUser(L4 的读域)+ 结构化溯源 - 顺带红线加固:意图记录是主人**原话**的机器可读形态 ⇒ **绝不注入群聊/陌生人块**(design/03 §5) - 复核:三条各自的包 build/vet/test 绿;「真生效」断言含生产构造路径(vector.New + 真实 SQLite 后端 + n-gram embedder)、跑完一轮窗口非空、L4 回写后能唤醒一个自身无强信号的表达; **5 条变异反证**(其中两条专门打「受限会话零写入」与「群聊不泄露主人原话」)均已还原 - 未做(登记):Index.Search 无 ID 过滤(客户端按候选集过滤,极端情况走回退);每条记忆一次 Embed
This commit is contained in:
1 parent
983bc49218
commit
fbad02899a
10 files changed
+1020
-81
No files matched your search
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/capacity"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace"
|
||||
@@ -77,6 +78,10 @@ func (l *Loop) memoryBlock(ctx context.Context, messages []model.Message) string
|
||||
// (design/03 §5「不泄露主人敏感信息」+ 主人 2026-10-03
|
||||
// 裁决「用户习惯记忆与不敏感数据:可查、不可写」)。
|
||||
//
|
||||
// 同样按敏感项排除的是 L4 意图记录(capacity.RecordIntent 的回写格式,
|
||||
// 内容是主人的逐句原话):它虽落在共享的用户习惯域以便 L4 闭环学习,
|
||||
// 但群聊/陌生人只该看到「不敏感的习惯结论」,不该看到主人原话。
|
||||
//
|
||||
// 召回本身包在一个 memory span 里(裁决 11.4.2,design/19 §1
|
||||
// 旁路节点):span 只记元数据(作用域/命中条数/注入块字节数/耗时),
|
||||
// 绝不记召回了什么内容。
|
||||
@@ -95,9 +100,17 @@ func (l *Loop) retrieveMemoryBlock(ctx context.Context, env SessionEnv, personaI
|
||||
}
|
||||
shared := make([]memory.Result, 0, len(results))
|
||||
for _, r := range results {
|
||||
if sharedScope(r.Item.Scope) {
|
||||
shared = append(shared, r)
|
||||
if !sharedScope(r.Item.Scope) {
|
||||
continue
|
||||
}
|
||||
// L4 意图记录(capacity.RecordIntent 的回写格式)是主人原话的
|
||||
// 机器可读形态,属主人敏感信息:共享域可查的是「不敏感的用户
|
||||
// 习惯」,主人的逐句原话绝不注入群聊/陌生人上下文
|
||||
// (design/03 §5「不泄露主人敏感信息」)。
|
||||
if capacity.IsIntentRecord(r.Item.Content) {
|
||||
continue
|
||||
}
|
||||
shared = append(shared, r)
|
||||
}
|
||||
if len(shared) == 0 {
|
||||
l.finishMemoryRecallSpan(span, "restricted", 0, 0, nil)
|
||||
@@ -162,7 +175,11 @@ func (l *Loop) assembleContext(ctx context.Context, messages []model.Message, ro
|
||||
leading = append(leading, l.currentTimeBlock())
|
||||
}
|
||||
if l.working != nil {
|
||||
if s := l.working.Summarize(); s != "" {
|
||||
// 读**本会话**的窗口(回合落定时由 recordWorkingMemory 写入):
|
||||
// 进程内只有一个 WorkingMemory,按会话读才不至于把别的会话
|
||||
// (尤其是群聊/陌生人)的工作记忆混进主人的上下文。该会话
|
||||
// 尚无窗口(如首轮之前)时摘要为空,前导块自然不出现。
|
||||
if s := l.working.SummarizeSession(loopSession(ctx)); s != "" {
|
||||
leading = append(leading, model.TextMessage(model.RoleSystem, "工作记忆: "+s))
|
||||
}
|
||||
}
|
||||
@@ -237,7 +254,13 @@ func (l *Loop) extractMemory(ctx context.Context, messages []model.Message, step
|
||||
// 被提取(只读隔离,design/02 §3 / design/11 §2),缺少
|
||||
// extractor 或 store 时为空操作。提取失败与 store 失败会
|
||||
// 带循环的来源记录日志,绝不向上传播。
|
||||
//
|
||||
// 它同时是 loop 侧**回合落定**的记忆钩子入口(run 路径每个已落定
|
||||
// 的 turn 调用它一次):无论提取器是否配置,都在此先落下工作记忆
|
||||
// 与 L4 判定回写(见 recordTurnMemory)。两者各自带门(工作记忆写
|
||||
// 会话窗口、L4 只写 owner 会话),因此不依赖这里的 extractor 门。
|
||||
func (l *Loop) extractMemoryAsync(ctx context.Context, messages []model.Message, step int) {
|
||||
l.recordTurnMemory(ctx, messages, step)
|
||||
env := sessionEnvFrom(ctx)
|
||||
if env.Restricted() {
|
||||
l.log.Debug("memory extract skipped: read-only context", "step", step, "env", env.ActorEnv)
|
||||
@@ -318,6 +341,100 @@ func (l *Loop) persistMemories(ctx context.Context, items []memory.MemoryItem, s
|
||||
l.finishMemoryWriteSpan(span, written, failed, nil)
|
||||
}
|
||||
|
||||
// recordTurnMemory 是回合落定时 loop 拥有的记忆钩子:把该会话的当前
|
||||
// 对话窗口写进工作记忆(三层记忆的层①),并把该轮的 L4 判定回写进
|
||||
// 记忆(design/14 §2.2 L4 / §2.4 闭环学习)。它由 extractMemoryAsync
|
||||
// 在每个已落定的 turn 上调用一次 —— 落点是 run 路径既有的 turn 结束
|
||||
// 钩子,因此 sub-agent 与主会话走同一条路径。
|
||||
//
|
||||
// 两个钩子各自带门(见 recordWorkingMemory / recordIntent),因此
|
||||
// 未配置 capacity、未配置检索器、或处于群聊/陌生人环境时它们
|
||||
// 安静跳过,绝不因为「钩子跑了」而多写一行记忆。
|
||||
func (l *Loop) recordTurnMemory(ctx context.Context, messages []model.Message, step int) {
|
||||
l.recordWorkingMemory(ctx, messages)
|
||||
l.recordIntent(ctx, messages, step)
|
||||
}
|
||||
|
||||
// recordWorkingMemory 把该会话的对话窗口记进工作记忆(design/03 §0
|
||||
// 层①「当前会话的对话窗口(滑动窗口 + 摘要)」),使 assembleContext
|
||||
// 的「工作记忆: …」前导块与 Summarize 真的有内容 —— 接线之前生产
|
||||
// 代码从不调用 Add,窗口恒空、摘要恒 ""(审计 P0-4)。
|
||||
//
|
||||
// 两道门:
|
||||
// - 只写**主人会话**(MemWritable):群聊/陌生人的对话不入工作记忆
|
||||
// (design/02 §3、design/11 §2 的只读隔离)。
|
||||
// - 只写带会话 id 的窗口:进程内只有一个 WorkingMemory 实例,无
|
||||
// 会话键的写入会污染别的会话的读点。
|
||||
//
|
||||
// 传入的是该回合的完整对话(含 checkpoint 还原出的历史),窗口因此
|
||||
// 就是「最近 N 条」滑窗;会话恢复时同一条路径把历史回填进窗口,
|
||||
// 而不是从空白重启(design §1 durable execution)。
|
||||
func (l *Loop) recordWorkingMemory(ctx context.Context, messages []model.Message) {
|
||||
if l.working == nil || len(messages) == 0 {
|
||||
return
|
||||
}
|
||||
if !sessionEnvFrom(ctx).MemWritable {
|
||||
return
|
||||
}
|
||||
sessionID := loopSession(ctx)
|
||||
if sessionID == "" {
|
||||
return
|
||||
}
|
||||
l.working.RecordSession(sessionID, messages)
|
||||
}
|
||||
|
||||
// recordIntent 把该轮的 L4 判定写回记忆(design/14 §2.2 L4「写入记忆…
|
||||
// 让意图理解越用越准」、§2.4 闭环学习)。接线之前 capacity.RecordIntent
|
||||
// 在生产代码里零调用,判定层因此只读不写,「越用越准」永远不成立
|
||||
// (审计 P0-17)。
|
||||
//
|
||||
// 红线:**只写 owner 会话**。群聊/陌生人有独立的记忆域且只读不写
|
||||
// (design/02 §3、design/11 §2、design/03 §5),子代理的内部回合也
|
||||
// 不是用户本人的表达习惯(经 flowBranch 识别),两者都不写。
|
||||
// 判定层禁用、无角色、缺 store 时为空操作;判定为非任务时
|
||||
// capacity.RecordIntent 返回 "",跳过写入(不堆噪声记忆)。
|
||||
//
|
||||
// 条目落进**用户习惯域**(ScopeUser,design/03 §3「用户习惯记忆全局
|
||||
// 共享」):L4 学的正是主人的任务表达习惯,写入与读取因此在同一个
|
||||
// 作用域里闭环(capacity L4 只采信 user/persona/project 域的召回)。
|
||||
func (l *Loop) recordIntent(ctx context.Context, messages []model.Message, step int) {
|
||||
if l.capacity == nil || !l.capacity.Enabled() || l.memory == nil || l.retriever == nil {
|
||||
return
|
||||
}
|
||||
if !sessionEnvFrom(ctx).MemWritable {
|
||||
return
|
||||
}
|
||||
if _, isBranch := flowBranch(ctx); isBranch {
|
||||
return
|
||||
}
|
||||
personaID := l.personaFor(ctx)
|
||||
if personaID == "" {
|
||||
return
|
||||
}
|
||||
judgment := l.capacity.Judge(ctx, capacity.Input{
|
||||
Messages: capacityTexts(messages),
|
||||
Awakening: l.awakeningFor(ctx),
|
||||
Recall: l.capacityRecall(ctx, messages),
|
||||
})
|
||||
content := l.capacity.RecordIntent(judgment)
|
||||
if content == "" {
|
||||
return
|
||||
}
|
||||
item := memory.MemoryItem{
|
||||
PersonaID: personaID,
|
||||
Scope: memory.ScopeUser,
|
||||
Kind: memory.KindRelationship,
|
||||
Content: content,
|
||||
Confidence: judgment.Confidence,
|
||||
SourceConversation: memory.FormatSourceConversation(loopSession(ctx), loopTurn(ctx)),
|
||||
}
|
||||
if _, err := l.memory.Add(ctx, item); err != nil {
|
||||
l.log.Warn("memory: L4 intent writeback failed", "step", step, "error", err)
|
||||
return
|
||||
}
|
||||
l.log.Debug("memory: L4 intent writeback stored", "step", step, "task_kind", judgment.TaskKind)
|
||||
}
|
||||
|
||||
// queryFromMessages 从对话导出检索查询:最后一个用户
|
||||
// turn 的文本,回退到最后一条消息的文本,再回退到 ""。
|
||||
func queryFromMessages(messages []model.Message) string {
|
||||
|
||||
Reference in new issue
Block a user