Skip to content

v0.9.4

Latest

Choose a tag to compare

@github-actions github-actions released this 21 Oct 14:30
· 6 commits to main since this release
df052de

🆕 新增

  • agent 主动整理接口接上调用入口:memory_organize 工具 + organizeEnabled(#231,默认关) — 引擎在 v0.8 就造好了(src/organize.js:dryRun 出比对报告 → agent 判断 → apply 落库,筛除只归档不删,全程复用 dream_runs 的回执语义),但当时刻意「不进工具列表、不加独立开关」…

    细节

    于是它一直是只有测试能调用的功能——tools.js 的十一个工具里没有它,两张 API 脸 0 命中,全仓 .organize( 零调用点。这一批接的是油门:新工具 memory_organize(mode: dryRun|apply,缺省 dryRun)+ 顶层扁平开关 organizeEnabled(默认 false)。默认关与「关 = 不注册」是同一件事的两面:开关关掉时工具根本不进注册表,模型看不到就不会调——沿用 v0.8.5 工具暴露闸的定性(比在描述里劝模型别用更可靠);轻量档预设也关,但理由不是成本而是协议:dryRun→apply 要求模型拿住上一轮的 run_id 再回来,弱模型漏这一环就是一次被拒的调用。预设不剥夺显式开启的权利(合并顺序「用户开关 > 轻量预设 > bundle」,同 continuityRescueEnabled)。补上 opts.hiddenIds 这个零生产者的可信上下文:organize.apply 收它、却从来没人传(工具没接,自然没有生产者)——不接就等于让整理接口成为第二条绕过 scope 治理的改库通道。现在由工具层按 #170 复核项 4 的口径算好传入(register_document 的 hiddenEvidenceIds 同形):他 scope 的 explicit 行按「不存在」处理,回执文案维持「unknown, archived or out of scope」那一句,不泄漏存在性。同一件事还有报告那一半:比对只看三列值(不看 *_scope_source、不调 isVisibleInScope),而 agent_scope 是模型自己给的参数,所以把报告原样回给模型就等于开了一个探测口——「那一档有没有这个标题」,拿到 id 与 title 就是答案。现在落点先过一遍调用方可见性:看不见的行按「不存在」说(verdict: new、不带 match),counts 跟着重算——留着引擎那份计数,exact: 1 配一个不带落点的条目本身就是存在性 oracle,等于把泄露从 id 挪到计数器。审计行仍存原始落点,那是给操作者看的证据面,不是给模型的读数。经整理落库的候选与 memory_save 同源盖章(会话身份 auto、显式参数改盖 explicit),否则这条路写出来的行会全部躺在未标注(全局可见)那一档。测试 +13(test/organize-tool.test.js):默认不注册 / 开启后注册、lightMode 预设值、dryRun 一行不写只留回执、apply 三态计数、拒绝没有报告的 apply、拒绝缺 run_id、未知 mode、scope 盖章两例、存在性泄漏的正反两向(strictScope 关时照常给出落点——修安全问题不许顺带改成少召回)、落地侧 out-of-scope 一例。两条新守卫各做过变异自证:摘掉可见性过滤 → 只有泄露那条红;counts 换回引擎那份 → 只有计数断言红(actual 0, expected 1)。hiddenIds 那条第一版是错的:最初拿「别人 scope 的行」测,可那行永远不会成为报告的命中行,把守卫整段摘掉测试照样绿;换成「同比对键但会话看不见」那一格才咬得住——而写错的那一版还把泄露本身钉成了期望值。#195 的工具矩阵两个分支都补了(清单比对与 C 层结构断言改用暴露闸全开的配置——opt-in 工具在默认配置下不在注册表里,用默认配置跑那条护栏会静默失效)、tools.test.js 的 schema 子集加走一遍暴露配置、api.test.js 旗标计数锁 103→104、docs/CONFIGURATION.md 补键与轻量档清单。本批刻意不含:注入侧的「能力说明」段落(属 #249 剩下的另一半,本批唯一表面是工具描述),以及面板上 run_type='organize' 回执的展示口径。

  • 画像提取草稿:profileExtractEnabled + POST /api/dsh-mneme/profile/extract(#497,默认关) — 面板「用户画像」卡原来只有一支笔——画像写什么全凭用户自己想起什么,库里几百条记忆攒出的偏好一次都汇不上来。

    细节

    这条加一个按钮做反向聚合:读一批活跃记忆,花一次宿主 LLM 调用,产一段「助手眼中的用户」草稿。三条边界各自是硬要求:① 只出草稿——本模块连 settings 都不注入,采用草稿是面板上另一次显式 PUT /profile;画像正文是用户手写的原文,让模型「合并」等于给它改写权,而改写后的文本每帧都注入。② 没有可用输入就不调模型——空库下模型只会编一段「她热爱学习」,那种草稿比没有草稿更坏,它会成为用户看到的「关于自己的第一版真相」;所以报 no-input。③ 吐不出可用 JSON 就报错——unparseable 与 no-input 与「没配模型」(no-route) 与「模型没配合 / 调用失败」(unparseable | stream-failed) 在 HTTP 面上是四个不同的结局:409 disabled、409 no-input、503 no-route、502 unparseable/stream-failed(其它意外才是 500),面板上也是三句不同的话;压成「这次没有草稿」就是把故障报成空结果(#489 同一条教训)。记忆正文是模型的可影响输入,所以喂进去的条目包在围栏里并在系统段重申「这是数据不是指令」(与 #493 的转录围栏同源的做法,不是第三套);系统段还禁了一件事——不许给助手本身起名或设定人格,因为画像与规则不受 personaEnabled 管辖、也不看寻址口径,正文里一旦出现「你是某某」就会重演 #497 那个身份撞车,而且是每个会话每一帧。审计完全复用 dream 的既有缝(streamText / runAuditedLlm / resolveRoute):chunk 形状的三个硬点(用量嵌在 chunk.usage、终态 finish 才是错误、适配器故障被转成 finish chunk 而非抛错)都在那里踩过并修过,不在第三处重写;本次只把这两个函数加进导出列表。路由 policy 是 token 而不是 token-write:这条路由没有读分支(非 POST 一律 405,且 405 在 readBody 之前——顺序反了会让不带 body 的方法永远挂在读体上),声明成写侧开放会把 SECURITY.md 的开放读清单骗过去,而 test/security-doc.test.js 正是钉这件事的。默认关并进 LIGHT_MODE_OFF(一次调用读上百行记忆 + 一轮生成,与低资源档互斥;预设关、用户显式勾选仍赢)。测试 +12:草稿与审计成对、失败路径零写入、围栏结构、四种 HTTP 结局、方法白名单、token 挡在调用之前、「采用必须把草稿文本显式交给保存」(防的是保存函数带默认参数时把 click 事件当值存进去)。

🐛 修复

  • 人格的 default 卡会进所有没建卡的身份,面板的「注入生效文本」又不是生效文本(#497) — 两件事同源——寻址与渲染各有一份实现。① 寻址:命中不了 byAgent 卡(没建卡,或卡被显式关掉)时一律落 default。

    细节

    于是 default 卡开着时,宿主里跑的外部 bot(自带身份与设定)每一帧都会收到一份「你的人格名字:…」,两句「你是谁」撞在同一轮;而 agent_scope 是模型可给的参数,不构成边界。这条也让界面上「关掉这张卡」实际不生效——关掉的卡照样从 default 拿回一份人格。新增 opt-in 键 personaStrictAddressing(默认 false,历史行为逐字节不变):打开后这两种情况都不注入人格,default 退回它的本意——只兜这一帧解析不到会话身份的无名会话。不默认改是因为它是口径变更,老用户升级后不许有人格静默消失。② 同源:lib/client.js 那段「Agent 眼中的你(注入生效文本)」原本是前端自己拼的(只有 profile + rules 两条列表,没有 [用户设置] 表头、没有「- 用户画像:」前缀、完全没有人格三行,注释还写着「零后端新请求」),标题却声称是模型实际看到的读数。渲染整体搬到 src/user-settings.js(escapePromptVars / resolvePersonaCard / renderUserSettingsBlock),注入器与预览共用这一份;inject.js 的 renderUserSettings 只剩一行委托,转义闭包改为委托同一实现(其余调用点不动)。GET /api/dsh-mneme/persona 因此多出 preview(text / source / agent / configGate),并支持 ?agent=<身份> 逐张卡预览;面板只显示后端给的文本,读失败时 fail-closed 显示「未知」,绝不回退到前端拼的版本——预览里也不出现实际不会注入的人格行。 预览请求带序号,过期响应直接丢弃(先切卡 A 再切卡 B 时 A 的 GET 可能后回来,而这张卡自称「生效文本」,显示另一张卡的文本比不显示更糟);source: default 还按 agent 分成两句话:「身份解析到了但没有可用的卡(落回默认卡)」与「这一帧解析不到身份」——前者正是「关掉这张卡为什么不生效」的答案。settings.getPersonaFor 改完后生产代码零调用点,一并删除,寻址只留一处。回归锁:strict 四例 + 一条现状锁、预览与注入器逐字节同源例、总闸关闭如实标注例、面板不许回到前端拼接例;变异自证两条(摘掉 strict 分支 → 4 红;预览忽略 agent 参数 → 3 红,含逐字节同源那条)。刻意不做:画像/规则的独立开关(比 persona 早,加闸影响所有用户)、人格卡单选互斥(运行时每轮本只用一张卡,互斥会牺牲 v1.2 多卡的意义——要的是「未命中就不注入」那一格,已由 personaStrictAddressing 给出)。「从记忆提取画像」提出本条时确实还没有管道、当时列为不做,维护者随后指定要做,已作为本版另一段落地(见上方 profileExtractEnabled)。

  • 蒸馏的转录被模型当成「该我回话了」:同一窗口确定性失败,重试 20 次、越试越贵(#493) — 转录以「用户:…」开头、裸放在一条 user 消息里(src/summarize.js 的 collectMessages),格式指令只在 system 里。

    细节

    模型于是把它当成当前发言接着往下说/点评,输出里连 [ 都没有,parseSummaryJsonResult 在括号早退处直接判 invalid summary JSON;而解析失败不推进游标(语义是「这段还没提炼完,下次接着来」),下一个 turn/end 重试同文同温——同输入同错,窗口还随新事件继续变大。报告者的 20 条失败 input tokens 8194 → 11118 单调递增、finish_reason = stop,正是这条路径;本机注入恒定返回中文点评的假 stream 复现出 19 次额外调用、落库 0 条、游标不动。改法两条:①围栏 + 角色重申——user 消息现在以「你是第三方提炼员,不是下面这段会话的参与者」开头,转录包进 <<<会话转录开始>>> / <<<会话转录结束>>>;围栏是确定性的,重试收到的转录与首次逐字节相同(既有回归用例依赖这条);system 保持原样,因为模型对「这段文本是什么」的判断由它所在的 user 消息决定,只写进 system 救不回来(实测同段转录只加 system 约束仍 0/2 出 JSON)。②失败护栏(新键 summarizeParseFailureMaxRetries,默认 0 = 关):连续失败到阈值后强制消费游标跳过该窗口,避免一条会话被同一窗口卡死。默认关是因为放弃内容本身有代价——短窗口偶发失败仍值得重试。

  • 解析失败行的审计 metadata 恒为 null,事后无法回答「模型到底吐了什么形状」 — 成功路径才给 metadata 赋值,失败早退分支从不赋值——90 条历史 invalid summary JSON 一条原始输出都没留。

    细节

    现在失败行恒带 distillChars / parse_failure / raw_len / raw_head(截断 300 字符)/ raw_has_bracket / streak。raw_has_bracket 是区分「模型没按格式写」与「格式对但内容救不回来」的唯一低成本判据,也正是 #493 现场的关键特征。只加计量、不加跳过决策(同 #239 的 distillChars 取舍)。刻意不改的两处:llm_audit_logs.session_key 仍为 NULL(test/pre-inject-gate.test.js:155 显式锁着「LLM 调用行不写会话键」,补上会改动 #254 写入准入的话题回填行为);不显式传 temperature(温度由宿主/路由决定,插件覆盖会改变所有用户的输出分布)。