feat(memory): P1 长期记忆升级 —— 异步攒批 Consolidate + 软删 + importance/last_seen #1
Reference in New Issue
Block a user
Delete Branch "feat/wails3"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
wails3重构桌面端
把"逐轮盲写抽取"升级为 Mem0 式对账(方案见 memory_industry_analysis.md 落地节): mcp-go: - Profile 加 Importance(1~10, poignancy) + LastSeenAt(为 Generative Agents 读路径 Score=w1·Relevance+w2·Recency+w3·Importance 铺路)。 - Upsert 收 importance + 每次置 last_seen(印证);新增 Delete(软删,BaseModel.DeletedAt 已具备,失效不物删可审计)+ Touch;memory_upsert 透传 importance、新增 memory_delete 工具。 dispatcher: - extractMemory → consolidateMemory:一次 LLM 调用同时做 抽取+对账,输出 [{op:ADD|UPDATE|DELETE|NOOP,key,value,importance}];ADD/UPDATE→upsert、DELETE→软删; sanitizeOps 防幻删(DELETE 须命中已有)/夹 importance[1,10]/同key保末个/丢 NOOP。 - 攒批:每 3 轮(per-session 计数)才 consolidate 一次,省成本,对齐 ChatGPT 周期整理。 从根上解决 exact-key 盲写的记忆腐烂。 验证:parseOps/sanitizeOps/parseProfile 纯逻辑单测;store 集成测试(真 PG)覆盖 importance/last_seen 写入 + 软删(live 0 / 物理 1);dispatcher -race 全过。 (注:完整多轮 LLM consolidate 未做实跑,属构造性验证 + 沿用已证 pool.Chat 模式。) P2 待做:读路径按 Score(Recency+Importance) 排序/衰减/截断 + 桌面端记忆面板。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>agent 节点带 autonomous:true 时走 ReAct(flow/agent/react),模型在 推理-工具循环里自行决定调哪些 MCP 工具,而非只跑画死的工具节点。 - 新增 eino/react_agent.go:mcpTool 把 MCP 工具(NATS)适配成 components/tool.InvokableTool;agentTools 暴露 wiki_search + recall_user_memory(context 参数 user_id 服务端注入,不暴露给模型); runReactAgent 流式回流答复,工具调用经适配器落 ExecEvent 轨迹。 - LLM 接口 + *llm.Pool 增 ToolCallingModel()(openai 组件实现 model.ToolCallingChatModel);fakeLLM 同步桩(返回 nil → 降级普通对话)。 - graph.go:agent 节点按 autonomous 开关分流 ReAct / 普通;自主 agent 不预注入画像(让其经 recall_user_memory 工具按需自取)。 关键修复:默认 StreamToolCallChecker 只看首个流片段,deepseek 常先吐 文本再给 tool call 致漏判 → 改 streamHasToolCall 扫整段流(命中率 ~0→7/7)。 验收:自主工具调用 7/7 命中,args={} 证明注入生效,完整闭环跑通; make test-go 全绿。已知 eval 看不到工具结果会误判 agent(Phase C 修)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>bus.ConsumeTasks 由单条串行改为限并发分发:每个任务进独立 worker goroutine, 并发上限 = 信号量 + 消费者 MaxAckPending(DISPATCHER_CONCURRENCY,默认 8)。 - 背压:并发满则在 select{sem, ctx.Done} 处等空位;关停时留消息不 ack 待重投。 - 健壮:worker 内 recover panic → Term(避免崩溃循环);ack/nak/span 收口在 worker。 - 并发安全已核:CircuitBreaker 有锁、Orchestrator.turns 有 turnMu、pool RWMutex、 evaluate 本就异步。 效果:一个 HITL 待审任务(Handle 阻塞至多 5min)或长 LLM 生成不再冻结后续任务。 验证:单测 TestConcurrentConsume(A 阻塞时 B 完成);live 实测 HITL 停 waiting 期间 普通任务 3s 跑完且 HITL 不受影响。全模块 build+vet+test 全绿。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>agent 可对数据库执行只读查询。三重防护: - 静态校验:仅单条 SELECT/WITH,词边界拒 insert/update/delete/drop/alter/create/ truncate/grant 等写/DDL 关键字(不误伤 created_at 之类列名)。 - 只读事务(sql.TxOptions{ReadOnly:true}):Postgres 引擎级强制只读,硬兜底。 - 行数(100)+超时(10s)+单元格(200 rune)上限。 连接:SQL_QUERY_DSN 优先(生产应指向专用只读库/账号),未设回退服务已解析的平台 PG DSN (经 NewGateway 传入 g.pgDSN,不再裸读 env)。独立小连接池(8)。 测试:单测 validateReadOnlySQL(放行 SELECT/WITH/列名含 created;拒写/DDL/多语句)。 live 自主 agent 实测:查 sundynix_task=157 行、sundynix_model=2 行。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>按「工具只产数据、渲染交前端」设计: - 后端 chart 工具(mcp-go):校验并返回规范化图表 JSON(type=bar/line/pie + labels + series, 校验类型/长度一致/pie 取首系列)。工具说明指示 agent 用 ```chart 围栏原样包裹返回的 JSON。 - 前端:lib/chartspec.ts 从输出抽取 ```chart 块(解析失败回退为文本不丢内容); components/ChartView.tsx 自绘 SVG 柱/线/饼图(无第三方图表依赖); BottomDrawer 输出区含图表块时分段渲染(文本 + SVG),否则纯文本。 测试:前端 chartspec 单测 12 例(isChartSpec 校验、分段抽取、非法块回退、多块、hasChart); tsc 干净,vitest 48 过。live 自主 agent:chart 工具产出 {"type":"bar",...}, agent 正确用 ```chart 围栏嵌入答复,前端据此渲染。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>vLLM 与 Ollama 都暴露 OpenAI 兼容 API(底层 go-openai 请求 {base}/chat/completions), 故统一走 openai 客户端,按 provider 归一化连接参数: - Ollama/vLLM 的 OpenAI 端点固定在 /v1,BaseURL 漏写自动补全(否则打到 /chat/completions 404) - 本地后端默认不校验 api_key → 缺省补占位(ollama→"ollama",vllm→"EMPTY";openai 客户端要求非空) - 显式 key 一律尊重(vLLM --api-key 启动);在线 provider 原样不动(DeepSeek 两种都收) reasoning_content 适配:ChatStream 增 onReasoning 回调,捕获 reasoning 模型 (本地 Qwen3 思考 / DeepSeek-R1 / QwQ、在线 deepseek-v4-pro)的思考分片。思考阶段分片 Content 为空本就不污染答案;runAgent 把思考累计后 surface 到 exec 轨迹「推理过程」事件。 控制台 ModelManager 加 ollama 选项;llm 包补 normalizeBaseURL / apiKeyOrPlaceholder 单测。 四模块全绿。live:经 admin 配 ollama qwen2.5:0.5b → dispatcher 热切(日志 base 自动 .../v1) → POST /v1/chat/completions 200 端到端出答案;切回 deepseek-v4-pro → exec 轨迹现「推理过程:思考26字…」。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>问题:历史任务超 Redis 流 10min TTL 后,SSE 回放在空流上永久阻塞 → 运行页卡「流式中…」、 轨迹/工具/输出全空。 修复:收尾把最终输出 + 执行轨迹持久化到 PG,历史复盘改读库(不依赖 Redis TTL): - store:Task 加 output/trace 两列;SaveTaskOutput/SaveTaskTrace/GetRunDetail。 trace 用 type:text(不是 jsonb)——否则提交时空串 "" 入 jsonb 列会 INSERT 失败、整条任务不落库。 (已 ALTER 既有 trace 列 jsonb→text。) - gateway:token/exec 录制器在 done 时把累计的输出/轨迹快照落库。 - 新增 GET /tasks/:id/replay 返回持久化的 {output, exec}。 - RunsView:选中历史运行改 runReplay() 读库(秒回、phase 立即 done/error),不再 SSE 回放。 即便旧任务无持久化数据,也是 done+空态,绝不再卡「流式中…」。 live:新任务落库 output 305 字(含表格) + 轨迹 5 事件,/replay 正确返回;tsc+vite、gateway 全绿。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>接了 checkpoint 后端时,审批节点从「阻塞 goroutine 等 5min」改为持久化中断: 首次执行发待审 + 置 waiting + compose.Interrupt → compose 把整图状态(含 board) 落进 checkpoint store 并返回中断错误 → Handle 释放 goroutine、任务停在 waiting, 不收尾不评测不判 done。抗 dispatcher 重启。 - orchestrator: 新增 errInterrupted 哨兵 + checkpoints 字段 + SetCheckpoints setter(沿用 guard/usageSink 的 setter 注入,不动构造签名);Handle 识别 errInterrupted → 释放 goroutine、保留 waiting、SSE 流不关。 - compose_compiler: 编译挂 WithCheckPointStore + WithGraphName("root"),Invoke 带 WithCheckPointID(task_id);审批节点接 checkpoint 时改走专用 approvalInterruptLambda(须把中断错误作节点返回值上抛,泛型 lambda 会吞掉); ExtractInterruptInfo 识别中断 → 上抛 errInterrupted。 - graph.go: 抽出 approvalSummary / applyApprovalDecision,阻塞式与中断式审批共用, 杜绝两路文案/语义漂移。 双路径并存:未接 checkpoint 后端(store=nil)维持阻塞模型,行为不变;main 暂不 接,生产保持阻塞,待增量3 resume 闭环补齐再打开(建在 flag 后)。 测试:中断半边端到端——errInterrupted + 置 waiting + checkpoint 落 KV(key=task_id) + 下游 agent 不执行。既有阻塞式审批/等价测试全绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>把 HITL 的恢复半边补上:人工决定到达后从 checkpoint 重入图、喂给审批节点续跑。 至此中断→恢复全闭环在 compose 路径打通(in-process 端到端测试钉死)。 - runComposeGraph 重构为统一入口 execComposeGraph(rc):rc==nil 全新执行、rc!=nil 从 checkpoint resume,两路共用建图/编译,仅三处分叉——①记忆注入仅 fresh(resume 时黑板由 checkpoint 还原,重注入会覆盖已积累态);②Invoke ctx(resume 经 ResumeWithData 注入决定);③终态黑板来源。 - live 捕获:resume 时 compose 用 checkpoint 还原的实例作 local state(非闭包 b), 故节点 ProcessState 内捕获 live 指针,Invoke 后据此读终态(fresh 仍读 b)。 - resume 记录:中断时把 {interruptID, Task} 落 KV(pending:task_id),供决定在另一 goroutine/重启进程独立重建任务并续跑;终态清记录 + checkpoint(幂等)。 - ResumeApproval(t, dec) 入口:载记录定位中断点 → execComposeGraph resume。续跑 语义同 fresh:再遇审批→errInterrupted、批准跑完→稿+refs、拒绝→errRejected。 测试:批准(黑板无损还原 + 下游执行 + 状态拨回 running + 清记录/checkpoint)、 拒绝(errRejected + 拒绝语 + 下游不跑 + 清 checkpoint)。这条用例也透过 eino 真实 checkpoint 路径端到端验证了 board 序列化(2a)。全量 go test ./... 绿。 下一步 3b:审批决定改 JetStream 持久投递 + orchestrator 持久消费者触发 ResumeApproval + main 接 Bus.Checkpoints 打开开关。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>复现:登录恢复待审任务时若捞到一个无法续跑的任务(如早期 KV 冒号 bug 留下的无 resume 记录的孤儿),点批准 → 决定发出但消费者找不到记录 → 任务永远 waiting → 审批条 busy 状态成功后从不复位 → 永卡「提交中…」spinner,看似整个应用卡死。 - decide 成功后置 submitted 并在 finally 复位 busy(此前只在 catch 复位)。 - submitted 时显示「决定已发出,等待续跑…(若长时间无响应,该任务可能已失效)」 而非停在 spinner —— 诚实反映状态,不误导成 hang。 - Bar 加 key={taskId}:换审批任务时重挂载,避免上一个的 submitted 残留。 (孤儿任务本身已在 PG 标记 failed 清理;冒号 bug 早已修复,不再产生新孤儿。) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>把已通的多智能体后端(MULTI_AGENT.md)接上 UI,用户可在画布拖出协调者图。 - nodeCatalog: 新增 `coordinator`「多智能体协调」节点(indigo) + 新字段类型 `agentList` + Specialist 接口({name,use,system,tools}),与后端 parseSpecialists 对齐。 - Inspector: AgentListField —— 可增删的子智能体卡片(名/用途/系统提示词/工具逗号分隔)。 - dsl 校验: agentList 需 ≥1 个有名字的专家、名字不重复。 - RunsView: 「工具调用」面板纳入专家派发(kind=agent),改名「工具/专家」,专家项 用 Users 图标 + indigo「专家」徽标区分(此前只筛 kind=tool,漏掉多智能体派发)。 - 导出: agents 数组经 exportDsl 原样透传进 DSL → 后端 parseSpecialists 直接消费。 tsc + vitest(19) 绿;UI 同款结构 DSL 后端可跑(协调链路 live 已验)。 DEPTH_ROADMAP T0.2 ✅,T0 激活 2/2 完成。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>- ParseAndAssemble 加 validateTopology:拦重复/空节点 id + 悬挂边 (source/target 指向不存在节点),避免坏图进编排后被 compose 静默跳过 - 保守策略:空图 / 报告任务 {topic} / 非标准载荷一律宽松放过,不误伤合法提交 - 单测覆盖合法/重复id/空id/悬挂边×2/空端点/放过场景 - live:悬挂边 POST /tasks → 422(错误指明具体边与缺失节点);合法图 → 202 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>failover/熔断的运行时态原来只在 dispatcher 日志、admin 看不到 —— 本次接到概览可见: - harness: CircuitBreaker.Snapshot() 只读观测访问器(state + fails,不动状态机) - llm: Pool.ModelHealth() 上报主备链每模型 {provider,model,role,state,fails}; buildWithFallbacks 把模型名↔breaker 配对(同包直接读 failoverModel.breakers); newFailoverModel 改返回具体类型以便读 breakers - dispatcher 心跳 payload 加 models[] - gateway /admin/overview 独立超时 Ping dispatcher,合并进 models.health - admin 概览「模型路由」新增「运行时链路态(实时)」:逐模型状态点 (🟢在线/🔴熔断中+失败数/🟡半开探测/单点) - 单测:Snapshot、Pool.ModelHealth(名字↔态配对/单点/空) - live:配坏主→提交任务打熔断→概览显示 broken-demo「熔断中·失败3」,备用在线 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>引入 Space 中间容器(租户>Space>成员),资源作用域从 owner 改为 space_id, 让"个人私有/项目临时组队/整租户共享"出自同一模型(设计见 SPACE_DESIGN.md)。 先在纯 PG 的 Agent 上打样,零存储风险,验证协作+RBAC+切换 UX。 后端: - 新表 Space{tenant_id,name,kind,creator,archived} + SpaceMember{space_id,user_id,role} (如 Tenant 般不 isTenantScoped);User.ActiveSpaceID;Agent 作用域 owner→space_id, owner 降级为创建人(供 UI 显示 / 删他人鉴权) - store/space.go:个人空间幂等/活跃空间解析/切换/列表/建/成员CRUD/归档 - 迁移顺序坑:结构体只放非唯一 index,MigrateAgentSpaces 回填 space_id 后再建唯一 索引 idx_agent_sn + DROP 旧 idx_agent_on(否则存量空 space_id 撞车);启动序4步幂等 - 中间件 SpaceContext(注入 space_id) + RequireSpaceRole(照 RequireTenantRole) - handler/space.go 空间端点 + 路由;agent.go 改空间作用域(删/覆盖他人需 admin) - 计费零改动(Space 与 ResolveBillingTenantID 正交) 桌面端: - api.ts space 接口;顶栏 SpaceSwitcher(含新建项目空间);StudioView 随空间切换 重拉编排 + viewer 禁保存;Agent 列表显示创建人 + 按 mine 控删除 验证:中间件6门控单测 + DB迁移(13个人空间/9 Agent全re-key/索引换新) + 后端HTTP全 场景(member见他人编排/删他人403、viewer存403、非成员切空间400+隔离、owner删他人200) + 浏览器实机(切换器3空间/Studio空间编排随切换隔离刷新/创建人显示/console无错) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>从 wails/v2 v2.12.0 迁到 v3 v3.0.0-alpha2.117,采用 v3 工程规范: Go 侧: - main.go:wails.Run(options.App) → application.New(Options{Services,Assets,Mac}) + app.Window.NewWithOptions(WebviewWindowOptions) + app.Run();窗口选项照搬 (1440×900/min 1100×700/#0b0d12/Mac TitleBarHiddenInset) - app.go:App 结构体绑定 → v3 Service(application.NewService);去 startup/ctx 注入; SaveFileDialog 改 v3 链式 API(application.Get().Dialog.SaveFile().SetFilename(). AddFilter().PromptForSingleSelection());5 个原生方法保留 - go.mod wails/v2→v3、go 1.25 构建: - wails.json → Taskfile.yml + build/(config.yml 填产品信息 + 各平台配置) - 删移动端脚手架(ios/android),桌面端只留 darwin/windows/linux - .gitignore 改为只忽略 build/bin(build/ 构建系统需提交) 前端: - vite.config 接 @wailsio/runtime/plugins/vite 插件 + WAILS_VITE_PORT - package.json 加 @wailsio/runtime;wails3 generate bindings → frontend/bindings/ - desktop.ts:window.go.main.App → import { App } from bindings;桌面态检测改 window._wails.environment(原生注入);isMacDesktop 用 System.IsMac() - version.ts:runtime.BrowserOpenURL → @wailsio/runtime Browser.OpenURL;测试同步更新 - tsconfig 加 allowJs(消费 JSDoc 绑定) 验证进度:Go 编译过 + tsc 干净 + 48 前端测试绿 + 前端 vite build 通过。 **未完成:整体 wails3 build/dev 原生窗口实跑 + 原生能力(另存为/打开/外链)联调**——下次续。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>把多智能体协调画成「协调者居中 + 专家工位环绕」,让最难读的一段——并行派发与综合 ——变得一眼可读。执行轨迹保持默认视图;「团队」tab 只在多智能体运行时出现, 普通任务不塞多余 tab。 - lib/run.ts 加 isMultiAgent()(据 coordinator: 节点判定) + deriveTeam(): 从现有执行事件流派生协调者/工位模型。与 deriveNodes 的区别是保留 start/end 时间戳, 才能画并发时间轴;并解析专家收尾 detail("简报 X → Y")还原简报与产出。 - components/TeamView.tsx:径向布局(工位按角度均分环绕,>6 个降级为网格)、 协调者实时思考流(接 token 流 + 闪烁光标)、并发时间轴(重叠一眼可见)。 动效只标活跃态(派发链路流动/工位脉冲)——是状态编码不是装饰,带 prefers-reduced-motion 守卫。 专家(kind=agent)与工具(kind=tool)图标区分。 - 零后端改动:数据全部来自现有 exec 事件(coordinator:/agent:/tool:)。 补 9 个单测覆盖派生逻辑(简报解析/失败态/时间窗/lead 不混入工位)。 实机经回放路径验证渲染(径向布局/简报/并发条/tab 条件出现)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>承接上一个提交(报告终于落库进运行历史),把「所有执行归口运行页」做完整: 只能看不能存,不算归口。 - 运行页的输出面板:报告类运行显示「报告正文」并在右上角出 Word/Markdown 导出。报告页那两个导出按钮依赖它自己一切页面就没的本地状态,所以从历史 里找回来的报告,只能在运行页导。 - 运行历史列表显示报告主题。此前只有 report_<hex>,谁也认不出是哪份报告。 主题从 graph->>'topic' 取(报告的 graph 就是占位 DSL {"topic":"…"},普通 任务 DSL 没有顶层 topic → 空串,不会误伤)。 - isReportRun() 按 task_id 的 report_ 前缀判定 —— 这是既有契约,导出接口 /reports/:id/export 本来就按同一个 id 寻址。+4 单测(含"不能只看是否包含 report"的误判防线)。 - reportFilename 从 ReportView 提到 lib:运行页也要用,不能私藏在一个 view 里。 live 验证(真账号,桌面端实机):运行历史首条显示「Redis 缓存穿透的三种解法」 → 点开复盘 8 节点 + 全文 → 点 Word → **原生另存为对话框弹出**(文件名预填 主题)→ 保存 → 落盘 5KB,file 认 Microsoft Word 2007+,解 zip 得 word/document.xml,正文 2446 字。 **顺带把 wails3 迁移最后一个盲区验掉了**:v3 把 runtime.SaveFileDialog 重写成 application.Get().Dialog.SaveFile().SetFilename().AddFilter() .PromptForSingleSelection(),此前从没被点过一次,记忆里只敢写"理应工作"。 现在实测通了。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>