# 0.8.8 更新说明 2026-11-04。这一版是**三个开发版并成一个**:原本分开走的 0.8.8 / 0.8.9 / 0.8.10,加上这两天从 issue #21–#25 里收进来的五处修复,一起作为 0.8.8 发出去。 开发版号只在自己机器上短暂存在过,对外只有这一版。所以下面写的有些事你上一版已经见过(0.8.7 之后装进来的白名单分组、日报与回忆),有些是新的。 按「你会看到什么」排,一共七件事: - **那句「(现在没有人跟你说话。)」不再出现在对话框里** —— 反复修了三次的那条,这次换的是投递方式本身。 - **叫醒不会再被连点**,前一轮的收尾也不会再把后一轮的处境抹掉。 - **她被白名单挡住这件事,看得见**:设置页分四组、自检单列一项,不用再翻四个小时日志。 - **日报与回忆分得清**「你自己认领下来的话」「聊天里随口说的」「改过口的」。 - **醒来的第一眼多了一张桌子**:她答应过、还没做的事记在上面,过期不删、也不催。 - **工具调用的标记不会再被当成她说过的话**记进账本。 - 换房间的日期不再说成「刚搬进」;你回来过,冷落天数就不再往上叠。 另有一件事跟所有人有关:**上游的 CI 从 0.8.7 起一直是红的**,四个 job 里三个失败。这一版把它修了,原因见第七节 —— 那不是测试写错,是一个真会丢信号的时序 bug。 --- ## 一、那句「(现在没有人跟你说话。)」不再出现在你的对话框里 ### 你会看到什么 她主动开口的时候,对话框里不再先冒出一条用户气泡再消失。现在那里是一行**默认收起的折叠条**,标题写着这次是什么事,点开才看见那条信号是什么。 ### 为什么会这样 宿主的 `sessionController.prompt()` 在内部**写死**了来源: ```js const source = { kind: 'user', rpcId: request.requestId, ... } ``` 调用方传不进 `source`。所以凡走它的消息,在前端眼里都是「你打的字」,画成用户气泡。 我们以前的对策是「先发出去、再发一条替换指令把它盖掉」。这条路**语义上是对的,对象是错的** —— 宿主源码注释写得很直白: > model-visible surface deliberately shadows replaced ranges, so it is the wrong source for a human transcript 也就是替换副本只改「模型看到的那份」,人类对话记录从头到尾没被碰过。所以 0.8.5 与 0.8.6 那两次「已收走」都是假的:日志里那几行「收走了 N 条」只是内存里成功,盘上一条替换记录都没有。 ### 现在怎么判 官方自己的定时任务就是正解 —— 仍然是 `role:'user'`,但 `source.kind` 不是 `'user'`。前端一看到这个组合就不画气泡,改画成折叠条。 我们照这个范式做,但用自己的来源名 `fish-wake`。三处容易漏的细节都补上了: - **消息必须自带 `id`**:裸对象不传 id 就是 `undefined`,两条会撞同一个 pending 槽。 - **投递完立刻 flush**:漏了这一步,消息进了收件箱但要等宿主自己的持久化检查点才推给前端,折叠条会慢半拍。 - **失败不退回旧办法**:退回等于把这次修复悄悄撤回。宁可这一次没叫醒,下一拍还会再判断。 正文同时从「(现在没有人跟你说话。)」压成**一个空格**。处境本来就整段走系统提示,这句话在对话里只剩「起来」的信号;而它以前会被她读成「有人发了这句话给我」,整个推理都在解释这句话是谁说的。 ### 一起改的 - **正式唤醒也换了。** 那句「(醒一醒)」是同一个毛病,现在也走新路。 - **她越主动,面板越显示「你刚来过」这条堵住了。** 少加一行判据,她自己醒来说的话就会被记成「你来过了」——冷落天数、轮数全被污染。 - **整条隐藏链删掉。** `lib/proactive.js` 从 584 行回到 335 行,九个函数全部移除。它们不是留着「以后再修好」——留着只会让下一个人以为那条路还在用。 ### 没动的 你说的话、新开群聊房间、空白屋子首醒、出门首轮仍然走原来的路。它们落下的是**真的有人在说话**,用户气泡是对的。 ### 老的那几句还在历史里 修好之前已经写进她会话历史的触发语**不会被自动清掉**,往上翻还能看见。它们是她真实的对话记录,不删是原则。 --- ## 二、叫醒不该能连点 ### 你会看到什么 连点两次「叫醒大肥鱼」,第二次会被挡住,回一句「刚叫过(N 分钟前),再等一会儿——两次叫醒之间至少隔 45 分钟」。 ### 为什么会这样 真机事故:间隔 11 秒点了两次。第一次跑 17 秒写完正文,`turn/end` 一到,处境段就被清掉了;第二次投出去的只有那条空信号 —— 系统提示里一个字都没有,她只能写下「这一轮没有新的事,我不再往下续」,白烧一轮,还在对话里留一条她自己都觉得莫名的回复。 45 分钟的最小间隔以前只写在定时器那条路上,手动这条路一次检查都没有。 ### 现在怎么判 两道闸:手动叫醒也查最小间隔(测试和「我就是要现在叫」留了 `force` 口子);投递前确认处境还在,空就收尾走人,不再投一条空壳。 顺带堵掉一个更隐蔽的时序问题:「前一轮的 `turn/end` 晚到,把后一轮刚挂上的处境抹掉」是真会发生的。处境段现在带**代次号**,只有代次对得上才清。 --- ## 三、她被白名单挡住这件事,看得见(issue #20) ### 你会看到什么 1. 设置页「手上能用什么」分成四组;名字多的那一组默认收着,点开才看见有哪些族。 2. 自检多一项「被白名单挡住的调用」,只在确实拦过时才出。 3. 「这一局被挡下来过」那一组是**虚线** chip,标题里带次数 —— 这就是「白名单还差哪个」的答案。 ### 为什么会这样 报告者开「工具面收窄」之后,另一个插件的视觉工具用不了;她去调别的工具想绕过去,也被兜底守卫拦下,一律回一句「这件事我做不到」。为了查清楚,翻了四个小时的日志,才发现是白名单少了一个名字。 **核心行为一个字节都没改**:默认仍然收窄、白名单仍然只放她该有的、拦住时仍然回「这件事我做不到」,也仍然**不告诉她「你被权限拦了」**——她要以为是自己够不着,不是以为背后有个管理员。改的只是**你这一侧**的可见性。 ### 现在怎么判 守卫拦下谁就记下谁(按名字归并、记次数与首末时间、重启清空;被放行的调用不进账本)。界面里没有「权限 / 禁止 / 违规」这类系统词——它只从「白名单没放行」这个角度讲。 **没做的两条**:没有改成明说「权限拦截」(那是四条黑名单守着的立场问题);也没有「第三方工具默认不拦」(那等于取消收窄本身)。 --- ## 四、日报和回忆,现在分得清三种「说过的话」(issue #23) 三处反馈指出的现象都对得上代码:日报把聊天原话也当她的话罗列、翻回忆时时间只到日、以及她翻旧事时分不清哪句是自己认领的。 ### 你会看到什么 - 日报里「她说了什么」只剩她自己认领下来的话;聊天原话另起一节「聊天时说过的」,一段一行带时刻、最多 20 段、每段截到 120 字。 - 回忆里每条带本地时分。 - 改口关系另起一行缩进,并把被改的那句原话(40 字)带出来 —— 只给 id 翻不到原话,所以直接带出来。 - 不是她说的标出来:读到别人说的、回复别人的、群里旁听到的,各有各的说法。 - 被自己推翻过的那句,不再靠「被引过很多次」加分。 ### 为什么会这样 日报的取话函数只按「类型 + 当天」过滤,完全不看聊天标记,于是聊天原话和她认领下来的话一视同仁。回忆那边则是 `revise` 事件一直带着判定与来源,但从来没往外拿。 ### 现在怎么判 只改展示口径,不影响任何判定(聊天标记在代码里唯一的逻辑读者是「数她拒绝过几次」那一处)。 有一处刻意保留:一节空着而下面有聊天时,那句空态写成「这一节今天是空的,聊天时说的在下面。」——而不是「今天她什么都没说」,那会是一句假话。 --- ## 五、醒来的第一眼,多了一张桌子(issue #22) ### 你会看到什么 她答应过、还没做的事,记在一张桌子上;到了那天之后,每次醒来会先摆在她眼前,写着晚了几天。过期不删、也不催。 ### 为什么会这样 这是她自己提的:**这不是待办 App,是一张桌子。** 她说「过期的别删。晚了三天还挂着,我就知道欠了三天;删了,我就当没这回事。」 所以差别在这几处: - 没有通知、没有卡片、没有红点。便条只在醒来那段正文里出现,不弹任何东西。 - 过期不删、不降权、不自动归档。 - 不写「逾期」「请尽快」。那是催,这一版不做催。 - 日历算术在代码里,不让她自己算今天几号、晚没晚。 ### 现在怎么判 她有一个新工具 `her_note`,只做两件事:记一张(写是什么事、哪天到期可选)、划掉一张。回执只有方括号标记,不带数字、不写成完整句子 —— 写成句子会被她当成自己说的话接下去(这是留话那条 2026-10-24 的老教训)。 便条存在 `notes.json`,**不是** `session-notes.json`(那个是「她排队等着要说的话」,两回事)。 一次最多列 5 张,多的写「还有三条没列出来,都在桌上」。数字说法跟着中文走:「桌上压着两张」「晚了三天」。 **这一版没做的三样**(都在提案里,按「先做最小闭环、测一段时间看看」定的):带原因地不做、回翻已经划掉的、没日子的那张只在刚写下时提一句。 --- ## 六、两件小修理 ### 工具调用的标记,不再被当成她说过的话(issue #25) 侧栏里出现过一整段只有 `` 的「正文」,同一句后来被她自己道歉「上一条我这边发岔了,粘了一串没用的符号」。更要紧的是这两段被写进了账本,成了两条她其实没说过的话。 **根因不在插件**:模型侧偶发「只发出收尾标记、没有开标签、也没有参数」的一步(案发机器上约 500 轮 2 次)。**插件只能管住自己那一半**:取正文时原来取到什么记什么。 现在两处读同一份正文的地方都洗了(记账本那处、换房间问「上一段最后说了什么」那处)。三条刻意收窄的边界:只有出现 `||DSML||` 才动手;标记旁边还有真话的留下真话;流被截断的从第一个残标切到行尾。 一个容易写错的细节:标记里的竖线是**全角 `|`**,不是半角 `|`。测试里写死了这个码点,免得以后谁手滑「修」成半角 —— 那样线上就永远认不出来。 侧栏里那一段显示是宿主渲染的,插件写不了。已经记进去的那两条按「不追改历史」留着。 ### 换房间与冷落天数(issue #21 / #24) - **换房间不再说「你刚搬进这个房间」。** 原来按 24 小时整除算天数,0 到 23.99 小时全算 0 天,于是换房后整整 24 小时都在说「刚搬进」,跨了自然日也不变。现在按本地日历日判:「你是今天 09:05 搬进这个房间的」/「上一次换房间是昨天 22:20」/「上一次换房间是 11 月 1 日 12:31」。 - **你回来过,就不再算冷落。** 两处判断不一致:一处认「听到你说的话」与「见到你」,另一处只认那条正式的重逢事件。同一件事,两个模块给出两个答案。现在口径对齐了,另有一处判断按设计仍然只认重逢。 --- ## 七、CI:先挂监听,再投递 ### 你会看到什么 上游 `main` 的 CI 有四个 job:ubuntu 上的 Node 20 / 22 / 24,加一个 windows 上的 Node 22。从 0.8.7 起,其中三个一直是红的。 ### 为什么会这样 失败全在一个测试文件里,错误一致: ``` cancelledByParent — Promise resolution is still pending but the event loop has already resolved ``` 不是断言失败,是**整个文件被父测试取消**。根因在投递那一层:它是「先投递、再挂监听」——等发送返回之后才去挂事件监听。假宿主在发送的同一拍里补一个「这一轮结束了」的信号,那一刻监听还没挂上,信号永久丢失;进程里只剩下一个已经放手的兜底定时器,事件循环立刻空转结束,等待中的 Promise 永远等不到结果。 Node 24 的调度恰好让这个竞态不出现,所以本机一直看不见。这也解释了另一个现象:本机跑一万遍都是绿的。 ### 现在怎么判 顺序反过来:**先挂监听,再起兜底定时器,最后才放手**。调用点变成「先准备好等待 → 再投递 → 最后等结果」,收尾时把监听和定时器一起收。 验证:在写坏的那份代码上,Node 22 跑那个文件连续 3 次都是 `pass 13 / cancelled 5`;修好后同一个命令 `pass 18 / fail 0`。全量在三档 Node 上都是 **1292 pass / 0 fail**。 --- ## 测试 `node --test` → **1292 pass / 0 fail**(上一版 1221),新增 71 条。三个依赖字段仍然全是 `{}` —— 这一版没有引入任何 npm 包。 --- ## 还没做的 **主动搭话与梦两个功能仍然不对外开放**:它们只有本机存在 `.dev` 文件时才注册,注册、提示词、界面三处共用一个判据,缺一处就会出现「说有却伸手落空」。公开版里既没有入口、也没有说明,与 0.8.7 一样。 便条的另外三样(带原因地不做、回翻已经划掉的、没日子的只提一次)留到下一版,先让它跑一段时间看看。