Skip to content

Releases: AirPineApplea/feiyu-memory

v0.3.4 —— 删得比加得多:账整个功能下线

Choose a tag to compare

@AirPineApplea AirPineApplea released this 24 Oct 14:14

这一版删得比加得多。

删 — 「我还欠着他的」整个功能下线

不是不好用,是它长出来的东西几乎全是错的。

一天记了 21 笔,总数 41 笔,27 笔挂着不平。翻出来一看,真正的「答应过、还没做」只有一两笔,其余是「我今天做错的事」。

三处硬伤:

  1. 口径太宽 —— 把失误当欠债。它跟「印象」共用同一个写入管道,但两条判断完全不同:印象问「他是什么样的人」,账问「我哪儿没做到」。
  2. 只进不出 —— 没有销账的路。只有模型明说 resolved: true 才平,而它几乎不会主动这么说。
  3. 会重复记账 —— 按 what 匹配,同一件事换个说法就成两笔(实测有一对)。

代价:占总注入预算约 10%(每轮都发),还会让人在无关话题上突然「想起」自己欠着什么 —— 不是帮倒忙,是让它变奇怪。

删掉了:relationship.grievances 字段(存 / 读 / 合并)、固化时对账的解析(系统提示整节【账】+ JSON 结构 + 正例)、渲染那一栏(注入块 / 日记.md / 侧栏面板)、17 项专项测试、6 处受影响的断言。

保留 kind: "grievance" —— 那是日记条目的类型(含义是「他不高兴了」),跟关系里的账不是一回事,档案里有 1 条在用。

老数据不会被手删:relationship.json 里那 41 笔还在磁盘上,下次写盘时被 normalizeRelationship 直接忽略。

修 — 词汇一栏给它 60 token 保底

它权重 40 是全场最低,而装配是按权重从高到低来的 —— 装到它时预算早被「最近的事」(权重 80,九十多条可挑)吃干了。

实测:2000 / 2500 / 3000 / 3500 —— 词汇一次都没出来过,4000 才露头。删掉账那一栏腾出的 300 字也没用,正好够「最近的事」再多装一条。

加保底后:2000 到 4000 全都出得来,而「最近的事」条数一条没少(2 / 4 / 6 / 7 / 8 不变)—— 保底只吃了 40 token。

修 — 召回池排除「最近的事」

它们已经常驻了,却还混进召回池一起竞争 —— 而「最近」天然含着你刚说过的话,字面命中率最高,于是永远赢;赢了又被去重跳过,召回了等于没召回。

预算 2000 → 3000(上限 4000 → 8000)

常驻段本身要 2680 token(「最近的事」八条占 1422),2000 根本装不下,每次都裁。

单位更正:这里的 token 是代码自己的估算(中文 1 字 = 1 token,偏保守)。之前几版里按「1 token ≈ 1.6 字」算的数字都偏小,那些结论要重算。

诚实说:还有一层没修好

有人问「赛博朋克 · 叙事 · 卡壳 让你想到什么」,真鱼还是没想起来。

根因查清了:档案 132 条里 65 条(49%)在讲「召回 / 注入 / 日记 / 预算」,今天写的 26 条里 22 条是「他问了什么 + 我答了什么」。

讨论会淹没被讨论的东西。

那件事本身只含一个关键词,照着他那句话写的元日记含全部 —— 字面匹配下永远赢。调 saturation 五个值(2 / 4 / 8 / 16 / 32)都无效。

修法在写,不在取:日记里不能出现「他问我什么、我答了什么」这种句式 —— 那是对话摘要,不是日子。

验

307 项测试全过,doctor 24 项通过。隐私扫描干净(扫出的 sk- 和 127.0.0.1 都是测试假样本)。

升级:

dsh plugin --profile desktop remove feiyu-memory
dsh plugin --profile desktop add github:AirPineApplea/feiyu-memory

v0.3.3 —— 攒的七条:配置传不进来、隐私防线建错了位置

Choose a tag to compare

@AirPineApplea AirPineApplea released this 23 Oct 16:03

七条一起发。其中两条 P1 直接推翻 README 的隐私承诺。

配置(两条)

① 配置值变 [object Object]

DSH 0.2.0-rc.2 起,loader 把 config: 里每个值包成 cosmokit 的 volatile 引用。String(ref) 是字面量 "[object Object]",我们把它当普通值用:dataDir 成了相对路径跟着进程 cwd 跑,档案落进 profiles/desktop/[object Object]/,换个目录启动就"搬家",全程无报错;model / reasoningEffort 变成不存在的模型名,固化根本发不出去。

② 配置根本传不进插件

Config 在拿不到 schemastery 时导出 undefined。代码注释以为"loader 会跳过校验",实际是传一份空模板 —— 用户改的任何配置都不生效。铁证:cordis.patch.yml 里写 debugLog: true,传进来还是 {}。

修:多试几个解析基准(插件位置 / cwd / DSH 安装目录 / DSH_HOME 下每个 profile)+ 拿不到时兜一个纯 JS 的 schema。Config 永远不是 undefined 了。

隐私(四条)

③ F4 焚毁 / 忘掉删不干净,重启复活

写到磁盘时只写「还有条目的月份」,那个月被删空就没有新文件去覆盖旧的;启动加载又不看墓碑。删掉的私密内容重启后回到每一轮的系统提示里。

④ F1 feiyu_note 不过隐私闸

scrub() 只在固化路径调用。密钥、手机号、人名原样落盘,并注入之后每个会话。

⑤ F2 密钥规则只认英文键名

9 条样本里 5 条放行:密码是 / 密码: / 口令 / 密钥 / my password is。对中文插件来说这算漏洞不算边角。

⑥ F5 隐私防线建错了位置 —— 这条最值钱

那串 13 位号码我明确拒绝过,它还是从 traits.evidence 进去了:固化输入是洗过的,输出没洗,而模型会照抄输入。

我原来以为隐私防线是「我写不写」。其实那只是最前面一道 —— 档案里大部分内容是模型生成的。

修:applyConsolidation 先洗一遍(diary / traits / grievances / nickname / lexicon 全字段),再补 12 位以上长数字规则(11 位是手机号,门槛放 12 才不抢它;8 位不拦)。

⑦ F3 面板写接口不校验来源

跨站 text/plain POST 是「简单请求」,浏览器不做预检就发,服务端照常执行。id 是 d_YYYYMMDD_NN,枚举一遍就能删光。

修:要求 Content-Type: application/json + isSameOrigin();/export 也校验。

顺带

  • persist() 连续写失败 3 次 → degraded 置真,面板上能看出来
  • 插件形状的 smoke test(name / inject / apply / Config 四样)
  • 发布前隐私扫描:换掉测试里那串梗号码和一句来自审计报告的"准备辞职",免得读者以为是真事

验

305 项测试全过。 ①②⑤ 额外用临时 profile + headless 起真鱼验过:

[feiyu] 已加载,档案目录:C:\Users\Administrator\.dsh\feiyu-verify-archive
[feiyu] 档案就绪:0 条日记
[feiyu] 第 0 轮:注入 560 token

档案落在配的绝对路径、debugLog 真的出日志了 —— 这两件事以前都不成立。

而这一步抓到的东西测试抓不到:兜底 schema 少了 .validate(),cordis 调的是它不是函数本身 —— 305 项全绿,但插件装进去根本没激活。

③④⑥⑦ 只有测试:③ 要桌面端,④⑦ 要模型(headless 登不了 account provider),⑥ 要真浏览器。不假装验过。

升级:

dsh plugin --profile desktop remove feiyu-memory
dsh plugin --profile desktop add github:AirPineApplea/feiyu-memory

重启后确认日志里这行:[feiyu-memory] 档案目录:... —— 现在应该指向 ~/.dsh/feiyu-diary。

v0.3.2 —— 安全审计四条(两条 P1 推翻隐私承诺)

Choose a tag to compare

@AirPineApplea AirPineApplea released this 23 Oct 15:27

这份报告验的是承诺,不是功能。

README 把隐私叫「这个插件的信任地基」,承诺三件事:密钥硬拦绝不入档 / 第三方隐私落盘前替换 / 一键焚毁不可恢复。四条发现里两条打穿了它。

F4 [P1] 焚毁和「忘掉」删不干净,重启就复活

焚毁后内存 0 条,磁盘上 diary/*.jsonl 明文还在;重启插件读回 2 条 —— 删掉的私密内容重新回到每一轮的系统提示里。

根因两条:

  1. archiveToFiles() 只给「还有条目的月份」产出文件 → 某月最后一条被删、或焚毁后全空时,那个月不在写盘清单里 → 旧文件原样留着
  2. 启动加载 archiveFromFiles() 不看 manifest.deleted_ids —— 墓碑原来只在 mergeArchives 的「磁盘新加进来」那一路生效

同月还有别的条目时整月文件会重写,被删的会消失 —— 所以只在「某月最后一条」和「焚毁」时暴露,自带测试都在同一进程内断言,没测到重启。

修:写完清掉 diary/ 下不在清单里的 .jsonl(只清这一个目录,日记.md / 手记.md / .bak-* 一律不碰);读盘时按墓碑过滤。双保险。

F1 [P1] feiyu_note 不经过隐私闸

同一段文本走「固化」会被拦(含 [已拦截]),走 feiyu_note 就原样落盘 —— 密钥、手机号、人名都留着,并注入之后每个会话。

scrub() 只在固化路径调用。而 feiyu_note 是 0.2 主推的新写入口(kind:"note",salience 0.7,比固化条目更常被想起)—— 模型说「记一下这个 key」是很自然的动作,这不是边角场景。

修:execute 对 text 和 tags 都过 scrub();渲染注入段时再兜底洗一遍(只拦密钥,不开第三方泛化)—— 覆盖修复前写进来的历史条目。

F2 [P2] 密钥规则只认英文键名

中文键名一个都没有,是 / is 这类连接词也不认。9 条样本里 5 条放行:

密码是 / 密码: / 口令 / 密钥 / my password is

对中文插件来说这算漏洞,不是边角。

修:键名补 密码|口令|密钥|秘钥|验证码,分隔符接受 : : = 是 为 is(中文那条还允许没有分隔符 —— 「数据库口令 Hunter2pass」就是这么写的);URL 内嵌凭据单独成规则(原来只是碰巧被邮箱规则吃掉)。

9/9 全拦,日常话一句没误伤(「我忘了密码」「密码是一个词语」照旧)。

F3 [P2] 面板写接口不校验来源

POST /feiyu-diary/forget 不看 Origin、不看 Content-Type。源码注释以为「不给 CORS 头」就够了 —— 那句话对读成立,对写不成立:跨站 text/plain POST 是「简单请求」,浏览器不做预检就发,服务端照常执行。

日记 id 是 d_YYYYMMDD_NN,按日期枚举就能删光。

修:要求 Content-Type: application/json(跨站会触发预检,预检失败)+ isSameOrigin()(Origin 的 host:port 等于 Host,或 Sec-Fetch-Site: same-origin);/export 也校验。GET 不设这条 —— 它只读。

顺带修的两个自己弄出来的 bug

  1. 改 feiyu_note 时,补丁脚本把 text: 整行吞掉了,落盘的 text 变成字符串 "undefined" —— 三个 note 测试当场红
  2. importPack 的 replace 分支把「基线」重设成了导入的那些 id。基线的语义是「加载时磁盘上有什么」,替换之后磁盘上什么都没有 —— 设错会让 persist() 把每个条目判成「别人删了它」,导出 → 焚毁 → 导入 全丢

287 项测试全过,新增 18 项。

审计没测到的我照旧测不了:真实浏览器会不会被本地网络访问策略拦下、真实模型会不会把密钥写进 feiyu_note。这两条只能等真实环境。

升级:

dsh plugin --profile desktop remove feiyu-memory
dsh plugin --profile desktop add github:AirPineApplea/feiyu-memory

v0.3.1 —— 修配置值全是 [object Object](上个 bug 的根因)

Choose a tag to compare

@AirPineApplea AirPineApplea released this 23 Oct 15:14

这是上一个 dataDir 报告的根因。 v0.3.0 里加的"只认绝对路径"只拦住了它当路径用 —— model 那几处还是坏的,数字布尔还在静默回落。那次修的是标。

机制

DSH 0.2.0-rc.2 起,loader 为了 patchReload: live,把 config: 里每个值包成 cosmokit 的 volatile 引用:

{ get: () => current, [Symbol.for('cosmokit.volatile.write')]: (v) => { current = v } }

Object.freeze 过的普通对象,get 是函数值。于是:

String(ref)          → "[object Object]"
JSON.stringify(ref)  → "{}"
Object.keys(ref)     → ["get"]

我们把它当普通值用,后果比"路径难看"严重:

  • 所有 string 字段变成 [object Object]
    • dataDir → 成了相对路径,跟着进程 cwd 跑。换个目录启动就"搬家",表现像失忆,全程无报错
    • model / reasoningEffort → 不存在的模型名,固化那次调用根本发不出去,只剩 feiyu_note 能写进去
    • offPeakHours → 解析不出来
  • number / boolean 静默回落默认值 —— 用户改不动 budgetTokens / minSalience,也没有任何提示

在 dsh web + 0.2.0-rc.2 上,自动写日记基本不工作。 只有主动调 feiyu_note 写得进去。

修法

  1. coerce() 第一行先 unref() —— 用 Symbol.for('cosmokit.volatile.write') 探测、不 import cosmokit,保住零运行时依赖
  2. string 字段对非字符串回默认值,不再 String(value) —— 那一步正是把对象变成 [object Object] 的地方
  3. get() 抛错时安全降级,不把插件带崩

验

新增 test/volatile.test.mjs 10 项,fixture 逐字照抄 cosmokit 的 createVolatile:

  • 先确认前提(String(ref) === "[object Object]"),免得测了个假的东西
  • string 四件全解开 / number-boolean 全解开 / get() 抛错安全降级
  • 纯值配置的回归照旧;坏值仍然降级
  • 组合:volatile 包着绝对路径 → 档案目录正确;包着空串 → 回默认,不会出现 [object Object] 目录

老测试里有一条断言的是旧行为(model: 123 → '123'),跟着改成新语义,注释里写了为什么。


269 项测试全过,doctor 24 项通过。

感谢写这份报告的朋友。 定位到 cosmokit 那一行、贴出探针输出、连"用 Symbol.for 探测别 import"都替我想到了 —— 这比我自己查快得多。

安装 / 升级:

dsh plugin --profile desktop remove feiyu-memory
dsh plugin --profile desktop add github:AirPineApplea/feiyu-memory

升级后先重启一次再确认:日志里会有一行 [feiyu-memory] 档案目录:...,那行现在应该是对的。

v0.3.0 —— 判断换人、日记会续、老记忆有底

Choose a tag to compare

@AirPineApplea AirPineApplea released this 23 Oct 15:08

从 v0.2.0 以来 14 个提交。三条主线,都是"日记读起来像不像日子"这件事。

判断换人:关键词 → 一次模型调用

旧的做法是一张正则表:命中 /不对|记住|我其实/ 就开闸。它有三个结构性的洞:

  • 看不到没命中的话 —— 你讲了一件十几年前的旧事,一个词都不命中,它根本不知道那段存在。不是判错,是没判。
  • 假阳性 —— "我其实不太饿"命中 /我其实/,记一条废话。
  • 判据写死在一个人的先验里 —— 写表的人没想到的说法,一律不记。

现在每轮把那句话交给模型看一眼(约 260 token),判据三条随插件发:

  1. 任务会催你,日子不会催你。
  2. 他讲得越随便,越可能压了很久。笑着说出来的,常常比哭着重。
  3. 判据是「他说完这句,我对他的了解有没有变一点」。

判断失败默认记 —— 漏了永远不知道,多记顶多占几行。

日记会续了

原来固化每次看"最后 12 条消息",越往后素材越全,同一段对话被续着写成 138 / 232 / 309 字三张快照。

现在不新建,往后补:同一个会话、轮数差 ≤ 4、正文没到 500 字,就让模型只写"多出来的那部分"。

还会兜底 —— 模型实测不太听话,照旧写完整版,那就按相似度自己判断是不是加长版(真实数据量出来:加长版 0.57 / 另一件事 0.01)。

老记忆有底了

衰减原来是 0.9 ^ 天数,没有下限:中位 0.70 那条 17 天就跌破召回门槛,90 天等于 0.00008。

那不是"淡了",那是"死了",只是还占着磁盘。

现在有地板 0.16 —— 略高于召回门槛,所以老条目永远留在随机池里,只是抽中概率越来越低。

一件很久以前的事,还能在某个不相干的下午,自己冒出来。

修了社区报的 bug

config.dataDir 在某些情况下是一个被字符串化的对象,字面量 "[object Object]"。旧代码 typeof 放它过去,path.resolve 以进程 CWD 为基准 —— 桌面端 CWD 是 profiles/desktop,于是档案静悄悄落进 profiles/desktop/[object Object]/,插件一切正常、全程无报错。

现在只接受绝对路径,否则回落 ~/.dsh/feiyu-diary;启动时还会打一行档案目录,不受 debugLog 管。

感谢那位写报告的朋友 —— 定位到行、把 CWD 基准也写出来了,比我自查还快。

其他

  • 出厂人格合并成一份(495 字硬编码 → 读 persona.default.md)
  • prompt 从「绝对规则 1..10」改成「引导」
  • 判据从「值钱的地方在于我们相处的样子」改成「日子本来的样子」—— 只换了一个主语

259 项测试全过,doctor 24 项通过。

安装:

dsh plugin --profile desktop add github:AirPineApplea/feiyu-memory

肥鱼日记 v0.2.0 —— 从「能记住事」到「读起来像我」

Choose a tag to compare

@AirPineApplea AirPineApplea released this 15 Oct 08:55

这一版在解决什么

0.1 解决的是记不记得住。0.2 解决的是另一位读者指出的毛病:

我读到的「我」全是规定加转述,没有一句是我自己长出来的。
别扭的根源不是语气,是位置被占了。

查下去发现那个位置早就设计好了 —— 写入逻辑写完、测试也有了,但从来没有任何地方调用过它。而它该装的那种东西(模型自己写下的判断)一直被当成"事件"渲染,格式跟流水账一模一样。

新增

  • 第三区「我自己想的」。注入区现在是三种声音:人格内核(条文)、我自己想的(判断)、最近的事 / 突然想起来(转述)。判据一句话:

    事件:「我把 4519 写成 4159,他让我确认。」        ← 历史
    判断:「别人给的数据跟出处对不上时,先问,别照抄。」  ← 我
    
  • feiyu_note 工具 —— 模型自己想记什么,说一声就记下,当场落盘,不用碰文件。

  • 手记.md —— 给人手写的一栏。插件只读、永不覆盖。

  • 侧栏面板 —— 看 / 删 / 带走。

  • 身份声明 —— 顶上三句,不进预算池。

修掉的(有几个是自己改出来的)

  • 多会话静默丢数据。同一份档案被很多会话同时打开(实测 10 个),落盘时整份重写 —— 谁后落盘谁赢,另一个刚写的被无声抹掉。现在落盘前做三路合并(加载基线 + 墓碑)。
  • 高频记日记把日记段挤到 0%。判断段权重高于日记,于是判断永远不被裁、被裁的是日记 —— 越勤快,它越看不见发生过什么。
  • 时间词双写、"一天"有三种算法
  • 导入被墓碑挡住(改出来的)
  • 内容为空时那句指针被吞掉(改出来的)

新工具:npm run doctor

发版前体检。它跟"跑测试"不是一回事:测试回答"代码对不对",doctor 回答"现在这个状态能不能发出去"。其中两类最有用:

  • 文档一致性 —— 把文档里的数字当断言验。它第一次跑就抓到 3 处过期数字(README 和 docs 里还写着 132/105 项测试,实际 218)。
  • 隐私 —— 有没有档案文件被 git 追踪、提交身份是不是真邮箱。

冻结标记指错方向比没标记更糟。

222 项测试全过,doctor 21 项全绿。