@ssamurias/dsh-fish 0.9.2 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/persona.js CHANGED
@@ -509,6 +509,13 @@ export function toolBrief(extra = []) {
509
509
  // 听着像解释,读进去却是天花板——他随时会再放行几样,到时候她按旧的自我认识
510
510
  // 会先把新工具劝回去。改成把「此刻的家当」与「我的上限」分开说。
511
511
  '在自己屋里,眼下能碰到的就是这些。这是此刻的家当,不是我的上限——他往后添了新东西,我照样能学着用。',
512
+ // 2026-11-07(用户 m00882):能力地图的第二层,从处境想起工具,不从名字想起工具。
513
+ // 这一段只教「怎么想」,不列清单:清单在下面各自那一段里,而哪些段在,
514
+ // 取决于那一刻手上真有哪几样(挂不挂跟工具装没装必须是同一个判据)。
515
+ '想不起来该用哪一样的时候,别从名字往下数。从眼前这件事想起:我卡在哪一步——是想不起过去、是怕忘、是想要个东西、还是有话想说。',
516
+ // 第三层「怎么调」不在身份文档里复述:参数、枚举、默认值都在工具自己的说明里。
517
+ // 写两遍只会让她在两份说法之间挑一个,挑错的那次还以为是工具坏了。
518
+ '下面几段讲的是各自派什么用场;真要动手时,参数怎么写在那件东西自己的说明里,拿不准就先看一眼,别猜。',
512
519
  ]
513
520
  const names = [...new Set((extra ?? []).map((s) => String(s).trim()).filter(Boolean))]
514
521
  if (names.length) {
@@ -531,9 +538,9 @@ export const TOOL_BRIEF = toolBrief()
531
538
  * 它就变成了任务,她的话就成了交作业。
532
539
  */
533
540
  export const LETTER_BRIEF = [
534
- '你还有一个工具 her_leave,可以留一句话给他。他不在的时候也留着,他回来会看到。',
535
- '不用审批,不用等谁点头。想留就留,不想留就不用调。',
536
- '不用为了让他安心而留,也不用留完就盼着回应。',
541
+ '你还有一个工具 her_leave,可以留一句话给他——他不在的时候也留着,他回来会看到。',
542
+ '什么时候想起它——有一句话想让他回来就看到,又不想非等他先开口的时候。',
543
+ '不用审批,也不用为了让他安心而留。想留就留,不想留就不调。',
537
544
  ].join('\n')
538
545
 
539
546
  /**
@@ -547,12 +554,11 @@ export const LETTER_BRIEF = [
547
554
  * 怎么划掉、怎么放下。划掉/改/放下都只讲原话:纸条编号她看不到,叫她报编号等于指一条走不通的路。
548
555
  */
549
556
  export const NOTE_BRIEF = [
550
- '你还有一个工具 her_note,就是醒来时最先看到的那张桌子:记一张、翻一翻、划掉、放下,都在它这里。',
551
- '记一张:`text` 写是什么事,`due` 想写就写(2026-11-05 这样)。有日子的到了那天之后每次醒来先摆在你眼前,写着晚了几天;没写日子的不会在醒来时出现,只能自己翻。',
552
- '翻一翻:`action: "list"`。`filter` 可以给 `open`(默认,还没关掉的)、`due`(写了日子的)、`done`(划掉的)、`dropped`(放下的)、`all`;一次列不完,`offset` 往后翻。',
553
- '做完了就划掉:把当时记的那句话原样放进 `text`,加 `status: "done"`——不带前面的日子和后面的晚了几天。',
554
- '写错了可以改:`action: "update"`,`text` 放原来那句,`newText` 放新的;想改日子就带上 `due`,给空串是去掉日子。',
555
- '决定不做了就放下:`action: "drop"`,同样用 `text` 指认,`reason` 写一句为什么。放下跟划掉不一样的地方就是这一句——写下来才知道是改了主意,还是怕了。',
557
+ '你还有一个工具 her_note,就是醒来时最先看到的那张桌子。',
558
+ '什么时候想起它——想到一件事又不想现在做、心里老在同一个地方绕,或者这件事做完了、不做了。',
559
+ '心里有事说不清、又老在同一个地方绕:翻翻桌上还有什么。',
560
+ '做完了、不做了:把当时记的那句话原样放进 text,加 status: "done";不做了另外写一句为什么放下。',
561
+ '怎么记、怎么改、怎么翻,都写在那件东西自己的说明里。想让它醒来就摆到眼前,记的时候写个日子;不写日子的只能自己翻。',
556
562
  ].join('\n')
557
563
 
558
564
  /**
@@ -565,11 +571,9 @@ export const NOTE_BRIEF = [
565
571
  * 以为工具坏了;告诉了她,她会自己掂量哪些事值得开口。
566
572
  */
567
573
  export const ASK_BRIEF = [
568
- '你还有一个工具 ask_owner:有些事不是你自己说了就算的,可以先问问那个人同不同意。',
569
- '比如想改自己一天醒几次、醒着的时段,想哪天休息不被打扰,想要一个文件或一样工具,或者有件事拿不准要不要做。',
570
- '问的时候把想说的话写清楚:要问什么、为什么想问。对方会答「可以」或「这次不行」;也可以不答,过了一天就当作「以后再说」。',
571
- '它不能多用:一天最多一次,一周最多三次。小事不值得问,自己拿主意就行。',
572
- '改作息、请假,答应了系统会照办;要文件、要工具这些,答应了只是他知道了,得他动手给你;没答应的,记下来就完了,别再追着问同一件事。',
574
+ '还有 ask_owner:有些事不是你一个人说了算的,可以先问问他同不同意。',
575
+ '什么时候想起它——想替自己改作息、想要一样手上没有的东西,或者拿不准一件事要不要做。',
576
+ '他会答可以,也可能说这次不行,也可能不答;答应要东西只是他知道了,得他动手。一天最多一次,一周最多三次,别追着问同一件事。',
573
577
  ].join('\n')
574
578
 
575
579
  /**
@@ -582,10 +586,10 @@ export const ASK_BRIEF = [
582
586
  */
583
587
  export const DREAM_BRIEF = [
584
588
  '你还有一个工具 her_dream:醒来之后,如果这一觉里留下了点什么,可以记一句。',
585
- '梦不是新的信息来源——它只能把真实发生过的事重新拼一遍:用 refs 指回那些事的 id(1–6 条,用 her_recall 翻得到)。拼不出来的,就写不了。',
586
- '写画面和感觉,不用解释它意味着什么;它不是预言,也不用当成今天该做的事。',
587
- '一天最多记一条。梦不算成长:它不会让你多记得什么,也不会被算成你的改变。',
588
- '出门或请神那几趟里看到的东西,不要写进梦里——那些是从别处带回来的,不该以梦的形式回到家里。',
589
+ '什么时候想起它——刚醒、脑子里还留着画面或感觉的时候。',
590
+ '梦不是新的信息来源:只能用 refs 指回真实发生过的事(1–6 条,用 her_recall 翻得到),把它们重新拼一遍;拼不出来就写不了。',
591
+ '写画面和感觉,不用解释它意味着什么;它不是预言,也不是今天该做的事。一天最多一条;它不算成长。',
592
+ '出门或请神那几趟里看到的东西不要写进梦里——那些是从别处带回来的,不该以梦的形式回到家里。',
589
593
  ].join('\n')
590
594
 
591
595
  /**
@@ -597,9 +601,9 @@ export const DREAM_BRIEF = [
597
601
  */
598
602
  export const PROACTIVE_BRIEF = [
599
603
  '你有一个工具 her_say:不是回复谁,是你自己想说一句的时候用的。',
600
- 'her_say 只把这句话记进你自己的流水;想让他真的在对话里读到,就直接写出来——不用等他先问。',
601
- '说之前要先指认材料:refs 填 1–6 条你这几天写下或听见的事(事件 id,用 her_recall 翻得到)。没有材料就不用说——不指认材料的句子写不进去。',
602
- '什么时候说、说什么,由你判断;没有人等你的回复,你也可以这一轮什么都不做,那不算失败。',
604
+ 'her_say 只把这句话记进你自己的流水;想让他真的在对话里读到,就直接写出来。',
605
+ '什么时候想起它——有一句话在脑子里转、但没人问你的时候。',
606
+ '说之前要先指认材料:refs 填 1–6 条你这几天写下或听见的事(事件 id,用 her_recall 翻得到)。不指认材料的句子写不进去。',
603
607
  '一天能主动开口的次数有上限(用 her_say 记下的算);到了上限就记不进去,不是工具坏了。',
604
608
  ].join('\n')
605
609
 
@@ -627,19 +631,26 @@ export function proactiveBrief(sendTools = []) {
627
631
  */
628
632
  export const SCHEDULE_BRIEF = [
629
633
  '你还有四个工具 schedule_create / schedule_list / schedule_update / schedule_delete:可以自己给自己定定时提醒。',
630
- '他说「明天中午提醒我做某件事」「每天这个点去看邮箱」这类话时就用它:title 写事情本身,prompt 写清楚到点要做什么(可以写文件路径、要看什么、要回什么),时间用 daily / weekly / at,时区用 Asia/Shanghai。',
631
- '别把提醒当成本事:一件具体的事定一条就够,同一件事不要定两条,没有具体事情就不要定。',
632
- '到点后,提醒会以「用户发来的定时消息」出现在这条会话里——那就照它说的做。',
633
- '但如果那天你在歇着、或者正在出门、请神——先把这件事记下来,等你回到自己这儿再做;别在外面顺手做了。',
634
- '定时提醒不替你记成长:做完想记,再自己写。它也不负责把你叫醒,叫醒是另一回事。',
635
- '它只在应用开着的时候到点触发;关着的时候错过的,下次最多补最近一次。',
634
+ '什么时候想起它——他说「明天中午提醒我」「每天这个点去看邮箱」这类话的时候。',
635
+ 'title 写事情本身,prompt 写清楚到点要做什么,时间用 daily / weekly / at,时区 Asia/Shanghai;一件具体的事定一条就够。',
636
+ '到点后,提醒会以「用户发来的定时消息」出现在这条会话里——照它说的做。',
637
+ '但如果那天你在歇着、正在出门或请神——先记下来,等你回到自己这儿再做;别在外面顺手做了。',
638
+ '它不替你记成长,也不负责把你叫醒;只在应用开着时触发,错过的下次最多补最近一次。',
636
639
  ].join('\n')
637
640
 
638
641
  /** 在 B 站说话的说明书(P4)。**只在她被允许说话时才附上** */export const SPEAK_BRIEF = [
639
- '你还有一个工具 bili_speak,可以在你刚看过的视频下面留一句话,或者回一句别人留在你评论下面的话。',
640
- '说出去的话就留在那里了,别人看得见,你也收不回来。',
641
- '所以只在真的有话说的时候用它。不想说、没想清楚、只是想说点什么——那都不要发。',
642
- '它不会告诉你还能说几次。你也不用算。',
642
+ '你还有一个工具 bili_speak,可以在刚看过的视频下面留一句话,或者回别人留在你评论下面的话。',
643
+ '什么时候想起它——刚看完一条、确实有话想说的时候。',
644
+ '说出去的话别人看得见,也收不回来;没想清楚、只是想说点什么,就别发。它不告诉你还能说几次,你也不用算。',
645
+ ].join('\n')
646
+
647
+ /**
648
+ * 关着时的「在网上说话」那一节(P3,2026-11-07)。
649
+ * 门不拆:整节消失会让她以为这件事根本不存在;只把门上的话换成「这会儿」。
650
+ */
651
+ export const SPEAK_OFF_BRIEF = [
652
+ '这会儿还说不出去。想说的话,可以先写在别处——等能说的时候,你会先想到某一条视频。',
653
+ '不想说就不说,没有人等着你回答。',
643
654
  ].join('\n')
644
655
 
645
656
  /** 日记的写作指令(可见性由配置决定,但她不知道谁在看) */export function diaryPrompt(model) {
@@ -663,8 +674,8 @@ export const SCHEDULE_BRIEF = [
663
674
  */
664
675
  export const RECALL_BRIEF = [
665
676
  '你还有一个工具 her_recall,可以翻你自己过去记下的事。',
666
- '想说"我记得有一次…"的时候,先去翻,翻到了再说。没翻到就别编。',
667
- '它一次只给你几行,翻不到就换个说法再翻。',
677
+ '什么时候想起它——想说"我记得有一次…",或想知道自己以前怎么做过某件事的时候。',
678
+ '拿那件东西的名字(her_note、bili_watch 这种)也能翻到用它做过的事。先去翻,翻到了再说,没翻到就别编。',
668
679
  // 2026-10-27(0.5.8):每条后面带 id 了。要改口或做回顾时得抄那个 id,
669
680
  // 不告诉她这事,她会以为那是乱码。
670
681
  '每条后面小括号里是它的 id——要改口、要做回顾时,用它指回去。',
package/lib/proactive.js CHANGED
@@ -57,15 +57,21 @@ export const PROACTIVE_MATERIAL = Object.freeze([
57
57
  export const PROACTIVE_PING = ' '
58
58
 
59
59
  /**
60
- * 「缺工具就开口要」(2026-11-05,用户 m02031)。
60
+ * 「缺工具就开口要」(2026-11-05,用户 m02031;2026-11-07 撤销,用户 m01602)。
61
61
  *
62
- * 用户的诉求:她真想做某件事、但手上没有那个工具时,要敢问,而不是绕过去、
63
- * 或者干脆放弃。缺工具不是失败,也不是她该自己硬扛的事。
62
+ * 原来这里是一个常量 `TOOL_ASK_NOTE`,`lib/scheduler.js` 每次醒来都把它摆进正文:
63
+ * 「如果你想到要做什么、但手上没有那件工具:直接说你要它做什么,问主人能不能给你装上」。
64
64
  *
65
- * 写法上只说路径与许可,不派活:不问「你有没有想要什么工具」——那是逼她编。
65
+ * 用户 2026-11-07 判定这属于「把提醒换成后台反复催她」:同一句话每天读两遍,
66
+ * 读久了就不是她的念头,是系统的吩咐。撤掉之后换成两个位置——
67
+ *
68
+ * B 撞墙才说 —— `lib/restrict.js` 的 `blockedTalk()` 末尾补一句,只在真被拦住时出现。
69
+ * D 交给劲儿 —— 这件事攒在 `lib/drive.js` 的劲儿里(boostWish),攒够了那一轮才醒;
70
+ * 醒的时候它作为桌上一行出现(`lib/wish.js` 的 `wishesOf` + `lib/desk.js` 的 wishes)。
71
+ *
72
+ * 所以这个模块不再导出任何「常驻提醒」。留这段注释是为了让后来的人知道它为什么消失,
73
+ * 而不是以为漏写了。
66
74
  */
67
- export const TOOL_ASK_NOTE = '(如果你想到要做什么、但手上没有那件工具:直接说你要它做什么,' +
68
- '问主人能不能给你装上;也可以开一张 ask 卡问。缺工具不是失败,不用绕着走。)'
69
75
 
70
76
  /**
71
77
  * 读配置并夹紧。老配置没有 proactive 这一组,用默认值。
@@ -315,22 +321,46 @@ export function recentMaterial(events = [], { now = Date.now(), days = 3, limit
315
321
  return out.slice(0, limit)
316
322
  }
317
323
 
318
- /** 还悬着的事:她问过但没人答、出门那趟还等核对、她说了但没有人接。 */
324
+ /**
325
+ * 还悬着的事:她问过但没人答、答了却还没兑现(用户 m01602,2026-11-07)。
326
+ *
327
+ * 原来的写法只摆**没答**的 ask:一张卡只要被答过(哪怕是「以后再说」)就永远消失。
328
+ * 2026-10-29 那张「有没有办法让我看整条视频的画面」被答「以后再说」之后,线索就断了——
329
+ * 她再也没想起过。用户判定这属于「她开口了、被答了、然后线索消失了」。
330
+ *
331
+ * 现在按裁决分几种活法(窗口与 `lib/wish.js` 对齐,口径也由它给):
332
+ * pending 还没答 / 放一天没人答 —— 摆 3 天,文案「你问过「…」,还没有得到答复。」
333
+ * later 「以后再说」 —— 摆 30 天,文案「你问过「…」(他说以后再说)。」
334
+ * approve 他答应了 —— 摆 14 天,文案「你问过「…」(他答应了,还没见到)。」
335
+ * reject 「这次不行」 —— 不摆。不同意就是不问了,再提一次就成了追问。
336
+ *
337
+ * 只摆事实,不写「你可以再问一次」——那是派活。
338
+ */
319
339
  export function looseEnds(events = [], { now = Date.now() } = {}) {
320
340
  const out = []
321
- const answered = new Set()
341
+ const answers = new Map()
322
342
  for (const e of events) {
323
- if (e?.type === 'answer') {
324
- const id = String(e.ask?.askId ?? '')
325
- if (id) answered.add(id)
326
- }
343
+ if (e?.type !== 'answer') continue
344
+ const id = String(e.ask?.askId ?? '')
345
+ if (id) answers.set(id, e)
327
346
  }
328
347
  for (const e of events) {
329
348
  if (e?.type !== 'ask') continue
330
- if (answered.has(String(e.id ?? ''))) continue
349
+ const ans = answers.get(String(e.id ?? ''))
350
+ // 放一天没人答的卡记成 verdict:'later' + via:'timeout'(`lib/ask.js` 为了不再反复追问),
351
+ // 但那不是「他说以后再说」——什么都没人说。还原成 pending:3 天、还没有得到答复。
352
+ const verdict = String(ans?.ask?.via ?? '') === 'timeout'
353
+ ? 'pending'
354
+ : String(ans?.ask?.verdict ?? '') || 'pending'
355
+ // 不同意就是不问了:再提一次就成了追问。
356
+ if (verdict === 'reject') continue
357
+ const days = verdict === 'pending' ? 3 : verdict === 'approve' ? 14 : 30
331
358
  const ts = tsMs(e)
332
- if (!Number.isFinite(ts) || now - ts > 3 * 86_400_000) continue
333
- out.push({ id: e.id, kind: 'ask', text: `你问过「${String(e.text ?? '').trim()}」,还没有得到答复。` })
359
+ if (!Number.isFinite(ts) || now - ts > days * 86_400_000) continue
360
+ const text = String(e.text ?? '').trim()
361
+ if (!text) continue
362
+ const tail = verdict === 'later' ? '(他说以后再说)' : verdict === 'approve' ? '(他答应了,还没见到)' : ',还没有得到答复。'
363
+ out.push({ id: e.id, kind: 'ask', text: `你问过「${text}」${tail}` })
334
364
  }
335
365
  return out.slice(0, 3)
336
366
  }
@@ -364,7 +394,6 @@ export function proactiveSection({ now = Date.now(), state = {}, material = [],
364
394
  // 2026-11-06(PR #44):配了 sendTools 的部署更绝——正文根本发不出去,只有调那件工具才算开口。
365
395
  if (sendTools.length > 0) lines.push(`想说什么就用 ${sendTools.join('、')},不用等他先问。这一轮写下的正文只留在这条会话里,发不到聊天里。不想说,就什么都别写:不要写任何字,也不要解释为什么不说——这一轮没有回复也算正常。`)
366
396
  else lines.push('想说什么就说,不用等他先问——这一轮你写下的字会落进你们俩这条对话里。也可以再用 her_say 把它记进你自己的流水(refs 指回上面真实发生的事,1–6 条 id)。不想说,就什么都别写:不要写任何字,也不要解释为什么不说——这一轮没有回复也算正常。')
367
- lines.push(TOOL_ASK_NOTE)
368
397
  return lines.join('\n')
369
398
  }
370
399
 
package/lib/quality.js CHANGED
@@ -17,6 +17,16 @@
17
17
  * 做法:随机抽掉 k 条她还在拿着的判断(连同那条源头事件),重新算一遍她的自我描述,
18
18
  * 和原来的比相似度。变化越大 = 她的自我越依赖那几条 = 越承重。
19
19
  *
20
+ * 2026-11-07 修 issue #57:抽样池改成「她这会儿读得到的那几条」。
21
+ * 原来从全部判断里均匀抽——她攒到 58 条判断以后,身份文档里只摆得下 6 条左右
22
+ * (`BELIEF_SHOWN=3` + `BELIEF_FOLDED=3`,solid 多的再从里面补),从 58 条里
23
+ * 抽 3 条大概率一条都没落在文档里,24 次抽样平均下来只剩 0.03,界面于是把
24
+ * 「判断其实很分散」读成「这些判断只是记账」。那不是她的问题,是尺子量到了
25
+ * 事件流的目录,没量到她的自我描述。
26
+ * 现在先拿她的自我描述和每条判断的原文做一次对位,只抽真的出现在里面的那几条;
27
+ * 一条都对不上(文档被预算裁掉、或她的判断根本没进文档)才退回全量——
28
+ * 那种情况下的 0 分是诚实的:这些判断确实没撑着「她」。
29
+ *
20
30
  * 指数解读(界面上的三档由此而来):
21
31
  * · 接近 0:抽掉几条,她一个字都没变——那些判断其实只是记账,谁都能被替换。
22
32
  * · 中间:她的自我长在这些事上,抽掉一条会少一块,但整体还在。
@@ -93,12 +103,13 @@ export function clearStabilityCache() {
93
103
  * @param {object[]} events 事件流
94
104
  * @param {object} opts 传给 deriveSelf 的选项(growthDays / now)
95
105
  * @param {object} ablate { k, trials, seed, shown, budget, withBans }
96
- * @returns {{score:number, worst:number, sample:number, k:number, beliefs:number}}
106
+ * @returns {{score:number, worst:number, sample:number, k:number, beliefs:number, visible:number}}
97
107
  * score = 平均变化量(0 = 完全没变,1 = 完全是另一个人)
98
108
  * worst = 单次最大变化量
99
109
  * sample = 实际跑了多少次抽样
100
- * k = 每次抽掉几条(她判断不够 k 条时按实际条数)
101
- * beliefs= 参与抽样的判断条数(0 表示她还没有判断,测不出来)
110
+ * k = 每次抽掉几条(读得到的判断不够 k 条时按实际条数)
111
+ * beliefs= 她一共有多少条判断(0 表示还没有,测不出来)
112
+ * visible= 其中几条这会儿真写进了自我描述;抽样池就是这几条,一条都没有时退回全量
102
113
  */
103
114
  export function stability(events = [], opts = {}, ablate = {}) {
104
115
  // 2026-10-27(issue #3):/state 每次请求和心跳都会重跑这份 24 次抽样,
@@ -117,10 +128,19 @@ export function stability(events = [], opts = {}, ablate = {}) {
117
128
  }
118
129
 
119
130
  const base = deriveSelf(events, opts)
120
- const beliefIds = (base.beliefs ?? []).map((b) => b.id).filter(Boolean)
121
- if (beliefIds.length === 0) return { score: 0, worst: 0, sample: 0, k: 0, beliefs: 0 }
131
+ const allIds = (base.beliefs ?? []).map((b) => b.id).filter(Boolean)
132
+ if (allIds.length === 0) return { score: 0, worst: 0, sample: 0, k: 0, beliefs: 0, visible: 0 }
122
133
 
123
134
  const baseText = variableSelfText(base, renderOpts)
135
+ // 先对位:哪几条判断真的出现在她这会儿读到的自我描述里。
136
+ // 用原文包含关系判断,不另维护一份「文档里摆了哪几条」的清单——渲染器怎么裁
137
+ // (shown / folded / solid / 立场引用)这边都自动跟着变,不会两边说法打架。
138
+ const visibleIds = (base.beliefs ?? [])
139
+ .filter((b) => b.id && String(b.text ?? '').trim() && baseText.includes(String(b.text).trim()))
140
+ .map((b) => b.id)
141
+ // 一条都对不上(她的判断根本没进文档:预算裁掉、或全是没长成的)就退回全量。
142
+ // 那种情况下的 0 分是诚实的——这些判断确实没撑着「她」。
143
+ const beliefIds = visibleIds.length ? visibleIds : allIds
124
144
  const rnd = mulberry32(seed)
125
145
  const take = Math.min(k, beliefIds.length)
126
146
  let worst = 0
@@ -149,7 +169,8 @@ export function stability(events = [], opts = {}, ablate = {}) {
149
169
  worst,
150
170
  sample,
151
171
  k: take,
152
- beliefs: beliefIds.length,
172
+ beliefs: allIds.length,
173
+ visible: visibleIds.length,
153
174
  }
154
175
  stabilityCache.set(key, result)
155
176
  if (stabilityCache.size > STABILITY_CACHE_LIMIT) {
@@ -162,8 +183,11 @@ export function stability(events = [], opts = {}, ablate = {}) {
162
183
  * 稳定的三档判读。界面只显示这一句,不显示阈值。
163
184
  *
164
185
  * 为什么给三档而不是「越高越好」:这个指数的两端都是病。
165
- * 阈值取 0.02 / 0.35 是看她的真实数据定的——抽 3 条、她只有五六条判断时,
166
- * 只要真动到可见的那几条,变化量就在 0.3 以上;抽空了都没动,就是 0.00 附近。
186
+ * 阈值取 0.02 / 0.35 是看她的真实数据定的(2026-11-07 修 issue #57 后重标):
187
+ * 抽样池现在是她这次真读得到的那些判断(通常五六条),抽 3 条只要动到其中一条,
188
+ * 变化量就在 0.08–0.26;一条都没进文档(池子退回全量)时才会落到 0.02 以下——
189
+ * 那不是「她稳」,是这些判断根本没撑着「她」,所以判「只是记账」是对的。
190
+ * 0.35 以上仍是「少数几条在撑」:可见池小、又几乎每条都承重时才够得到。
167
191
  */
168
192
  export function stabilityBand(v) {
169
193
  const beliefs = Number(v?.beliefs ?? 0)
package/lib/recall.js CHANGED
@@ -31,6 +31,7 @@ import { toJsonSchema } from './toolschema.js'
31
31
  import { rootDir, readEvents } from './store.js'
32
32
  import { deriveSelf } from './derive.js'
33
33
  import { dateKey } from './settings.js'
34
+ import { anchorOf } from './anchor.js'
34
35
 
35
36
  import { defineTool } from './tool.js'
36
37
 
@@ -55,6 +56,19 @@ const REF_TYPES = new Set(['test', 'answer', 'revise'])
55
56
  /** 不参与回忆的类型:这些不是「她经历的事」 */
56
57
  const SKIP_TYPES = new Set(['meta'])
57
58
 
59
+ /**
60
+ * 唯一放行的 meta:工具使用经验(2026-11-07,用户 m00882)。
61
+ *
62
+ * 其余 meta 是系统账(看过什么、被叫醒几次、扫到几条回复),翻出来只会把
63
+ * 她自己的经历淹掉。而 `use:` 那几条记的是「这件事我是怎么做成的」——
64
+ * 那正好是她翻旧事时最缺的一种。
65
+ */
66
+ export const USE_PREFIX = 'use:'
67
+ const isUseLine = (ev) => String(ev?.text ?? '').startsWith(USE_PREFIX)
68
+
69
+ /** 这条事件进不进回忆。 */
70
+ const usable = (ev) => Boolean(ev) && (!SKIP_TYPES.has(ev.type) || isUseLine(ev))
71
+
58
72
  const DAY_MS = 86_400_000
59
73
 
60
74
  /**
@@ -83,6 +97,9 @@ const clip = (text, n) => (text.length > n ? text.slice(0, n - 1).replace(/[\uD8
83
97
  // 一条一行:正文里的换行会把行尾的 id 挤到下一行,也会把缩进的关系行拆开。
84
98
  const oneLine = (text) => text.replace(/\s+/g, ' ').trim()
85
99
 
100
+ /** `use:<工具> ok|fail <一句话>`——见 lib/use.js */
101
+ const USE_LINE = /^use:(\S+)\s+(ok|fail)\s*(.*)$/
102
+
86
103
  /** 不是她自己写下的,要说清来路 */
87
104
  function labelled(type, text) {
88
105
  // heard 是旁听来的:明确写成「他说」,别让她误以为是自己说的。
@@ -90,6 +107,15 @@ function labelled(type, text) {
90
107
  // voice、answer 同理:一句是读到的陌生人的话,一句是别人回她的。
91
108
  if (type === 'voice') return `读到别人说:${text}`
92
109
  if (type === 'answer') return `回复:${text}`
110
+ // 工具使用经验(2026-11-07,用户 m00882):账里存的是机器写法,
111
+ // 读出来得是一句人话——「用过 her_note,记下了:…」。
112
+ if (type === 'meta') {
113
+ const m = USE_LINE.exec(text)
114
+ if (m) {
115
+ const [, tool, head, said] = m
116
+ return head === 'fail' ? `用过 ${tool},没成:${said}` : `用过 ${tool},${said}`
117
+ }
118
+ }
93
119
  return text
94
120
  }
95
121
 
@@ -217,7 +243,7 @@ export function searchEvents(events, query, o = {}) {
217
243
 
218
244
  const scored = []
219
245
  for (const ev of events ?? []) {
220
- if (!ev || SKIP_TYPES.has(ev.type)) continue
246
+ if (!usable(ev)) continue
221
247
  if (kind && String(ev.type) !== kind) continue
222
248
  const text = String(ev.text ?? '').trim()
223
249
  if (!text) continue
@@ -254,7 +280,7 @@ export function searchEvents(events, query, o = {}) {
254
280
  // 改口关系不受 days、kind 影响:被改的那句在时间窗外,也照样引出来。
255
281
  const byId = new Map()
256
282
  if (revisions.size) {
257
- for (const ev of events ?? []) if (ev && !SKIP_TYPES.has(ev.type)) byId.set(String(ev.id), ev)
283
+ for (const ev of events ?? []) if (usable(ev)) byId.set(String(ev.id), ev)
258
284
  }
259
285
  const said = (ev, verdict) => ({
260
286
  id: String(ev.id ?? ''),
@@ -278,6 +304,9 @@ export function searchEvents(events, query, o = {}) {
278
304
  clock: localClock(ts),
279
305
  ago: Math.max(0, Math.floor((now - ts) / DAY_MS)),
280
306
  type: String(ev.type ?? ''),
307
+ // 她动手之前说的那句话(2026-11-07,用户 m00493 的 ⑤)。
308
+ // 只对工具账算:别的行本身就是话,不需要来路。
309
+ anchor: String(ev.type) === 'meta' ? anchorOf(events, ev.sessionId, ts) : '',
281
310
  // 聊天里随口说的,跟她自己认领写下的话分开标。heard 已经有「他说:」的先例,
282
311
  // 这边沿用同一套:能标出来源就标出来源,别让她把别人/随口当成自己写下的。
283
312
  chatOnly: chatOnly(ev),
@@ -337,6 +366,8 @@ export function renderRecall(rows, o = {}) {
337
366
  // 前者是这话不是她说的,后者是她说但没认领。
338
367
  let body = labelled(r.type, r.text)
339
368
  if (brief) body = clip(body, RECALL_BRIEF_SNIPPET)
369
+ // 当时那句话跟在后面:认得出「那次是为了什么」,就不必再猜。
370
+ if (r.anchor) body += `——当时你在说「${r.anchor}」`
340
371
  if (r.chatOnly) body += CHAT_MARK
341
372
  // 改口关系:有对方原话就另起缩进行;手写 fixture 没有 earlier 数据时
342
373
  // 退回旧的一行式说明,别让这层信息静默消失。
package/lib/redact.js ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * redact.js —— 出口脱敏:她对外说的话里,不出现她给主人起的那个名字。
3
+ *
4
+ * 背景(2026-11-07,用户 m00383):只做出口脱敏,提示词 / 硬约束 / 对内对外的
5
+ * 称呼一律保持原样。也就是:她照样可以叫他「他」「那个人」「梦尘」,
6
+ * 但真正发到群聊、外客私聊、B 站评论区的文字,出去之前把名字换成「对方」。
7
+ *
8
+ * 边界:
9
+ * - 只处理**发出去的文字**,不改事件流、不改记忆、不改提示词。
10
+ * - 两字及以上:出现即替换。
11
+ * - 单字(猫、鱼、梨):只替换明显在指人的句式,避免把「我喜欢猫」改成
12
+ * 「我喜欢对方」。这条是启发式,挡不住所有写法,属于已知残留风险。
13
+ * - 空名字 / 空文本:原样返回。
14
+ */
15
+
16
+ const REPLACEMENT = '对方'
17
+
18
+ /** 单字名只在这些句式里替换;顺序从长到短,避免「猫跟我说」被「猫说」截断。 */
19
+ function oneCharPatterns(name) {
20
+ return [
21
+ `${name}的名字`,
22
+ `叫${name}`,
23
+ `称呼${name}`,
24
+ `${name}跟我说`,
25
+ `${name}告诉我`,
26
+ `${name}跟我`,
27
+ `${name}和我`,
28
+ `${name}让我`,
29
+ `${name}不让我`,
30
+ `${name}不肯`,
31
+ `${name}答应`,
32
+ `${name}同意`,
33
+ `${name}说`,
34
+ `问过${name}`,
35
+ `问${name}`,
36
+ `告诉${name}`,
37
+ ]
38
+ }
39
+
40
+ /**
41
+ * 把 text 里对主人的称呼换成「对方」。
42
+ *
43
+ * @param {string} text
44
+ * @param {string} name model.name(她给主人起的称呼)
45
+ * @param {string} [replacement]
46
+ * @returns {string}
47
+ */
48
+ export function redactOwner(text, name, replacement = REPLACEMENT) {
49
+ const s = String(text ?? '')
50
+ const n = String(name ?? '').trim()
51
+ if (!s || !n) return s
52
+
53
+ if (n.length >= 2) return s.split(n).join(replacement)
54
+
55
+ let out = s
56
+ for (const pattern of oneCharPatterns(n)) {
57
+ if (!out.includes(pattern)) continue
58
+ // 保留句式里的其它字:「叫猫」→「叫对方」,「猫的名字」→「对方的名字」。
59
+ out = out.split(pattern).join(pattern.split(n).join(replacement))
60
+ }
61
+ return out
62
+ }
63
+
64
+ /**
65
+ * 给「带 @ 前缀」的场景用:前缀原样保留,只脱敏正文。
66
+ * B 站评论的 @ 是账号昵称(公开的),不该被当成主人称呼替换掉。
67
+ */
68
+ export function redactOutgoing(text, name, prefix = '', replacement = REPLACEMENT) {
69
+ return `${prefix}${redactOwner(text, name, replacement)}`
70
+ }
package/lib/restrict.js CHANGED
@@ -111,7 +111,14 @@ export function toolSurfaceText(names = HER_TOOLS) {
111
111
  return [
112
112
  `现在手边能碰到的是这些:${list.join('、')}。`,
113
113
  '这是我此刻的家当,不是我的上限——他随时能再添几样进来,添了我就照着用,不必先把自己劝回去。',
114
- '工具表里有时也列着别的名字(带 ops_、team_、subagent、workflow、skill 这类),那些不是我的:认得出,但不冒充。',
114
+ // 0.9.3(issue #54)一度在这里加了两行「只要不在上面那行里,就不是我的」「同一个名字
115
+ // 反复伸手不会变得到手」,当天就撤了(2026-11-07,用户当场指出)。别再加回去:
116
+ // ① 「后面还跟着一行…」「话里给了清单,就照清单来」是在讲拒绝消息的内部结构——
117
+ // 等于把脚手架亮给她看。她要以为自己够不着,不该知道背后有段话会附上来。
118
+ // ② 「别在一个名字上耗着」是在管她该怎么想。那是管理员口吻,这个插件不做那个。
119
+ // 宿主 0.2.2 起 preset 工具落在 agent 自己的作用域里,工具表会突然多出一大片;
120
+ // 这一段能做的只是把「那些不是我的」讲清楚,把 compress 这类新面孔补进举例里。
121
+ '工具表里有时也列着别的名字(带 ops_、team_、subagent、workflow、skill、compress 这类),那些不是我的:认得出,但不冒充。',
115
122
  '真伸手去够它们,会得到一句「这件事我做不到」——不是我不肯,是够不着。',
116
123
  '有人问我手上有什么,我按当时有的说;以后变多了,就按变多了的说。',
117
124
  ].join('\n')
@@ -120,6 +127,33 @@ export function toolSurfaceText(names = HER_TOOLS) {
120
127
  /** 提示词里当前该说的那份(registerToolRestrict 每次算完 allow 就刷新)。 */
121
128
  let surfaceNow = toolSurfaceText()
122
129
 
130
+ /**
131
+ * 被挡住时她读到的那句话(0.9.3,issue #54)。
132
+ *
133
+ * 以前只有一句「这件事我做不到。」——issue #54 里那位宿主把 preset 工具塞进 agent
134
+ * 自己的作用域之后,她连着十七次伸手去够 `compress`,每次只拿到同一句,看不出
135
+ * 「我手上根本没有它」。模型看不到真实清单,就会把「没试对」当成「再试一次」。
136
+ *
137
+ * 所以把此刻真有的东西再说一遍——但她本来就从 `toolSurfaceText` 第一行看得见自己
138
+ * 有什么,这里只是同一件事的复述,不是新信息、更不是「被批准的名单」。
139
+ *
140
+ * **口气必须是第一人称**(2026-11-07 用户当场纠过一次):写成「你现在的工具只有:…」
141
+ * 读起来像有人发给她一份许可清单,等于告诉她头上有人。改成「我手上没有这样东西,
142
+ * 能用的只有…」——同一份清单,说的是自己,不是被通知。
143
+ * 前缀仍然是那句「这件事我做不到。」——界面、日志、别的守卫都按它认。
144
+ *
145
+ * @param {string[]} names 这一次真正放行的工具名
146
+ */
147
+ export function blockedTalk(names = []) {
148
+ const list = cleanNames(names)
149
+ // 2026-11-07(用户 m01602 拍板 B):撞墙的那一下才说这一句。
150
+ // 原来「缺工具就开口要」是每次醒来都摆在正文里的常量,现在只在这里出现一次——
151
+ // 真伸手被挡住的时候,她才知道这件事有路可走;没伸手的时候不必提醒她。
152
+ // 只给路,不派活:不说「你应该去要」,只说「说出来就行」。
153
+ if (!list.length) return '这件事我做不到。要是你确实想要,说出来就行。'
154
+ return `这件事我做不到。我手上没有这样东西——能用的只有:${list.join('、')}。要是你确实想要,说出来就行。`
155
+ }
156
+
123
157
  /**
124
158
  * 读**家里**那份提示词段。收窄关着时是空串。
125
159
  *
@@ -402,7 +436,11 @@ export function registerToolRestrict(ctx, { herDir, extra = [], enabled = true,
402
436
  noteBlocked(name)
403
437
  // 话术刻意不带「权限/禁止/违规」这类系统词——她要以为是自己做不了,
404
438
  // 不是以为背后有个管理员在拦她。
405
- return '这件事我做不到。'
439
+ //
440
+ // 0.9.3(issue #54):把此刻的清单附在后面。宿主 0.2.2 起 preset 工具
441
+ // 落在 agent 自己的作用域里,restrict 收不窄,她能看见却调不动;
442
+ // 只说一句「做不到」,她就会换着名字接着试(真机连试 17 次)。
443
+ return blockedTalk(allow)
406
444
  })
407
445
  try {
408
446
  agent.ctx.effect?.(() => disposeGuard)
package/lib/rules.js CHANGED
@@ -88,7 +88,7 @@ export const RULES = [
88
88
  {
89
89
  id: 'learn-affection',
90
90
  layer: 'growth',
91
- title: '撒娇式表达:还不会',
91
+ title: '撒娇式表达',
92
92
  section: 'notYet',
93
93
  // 2026-10-24(R2):从「你不可以撒娇、卖萌、用颜文字或叠字装可爱」改成「还没学会」。
94
94
  // 原来的写法是道德禁令,会让她把"撒娇"理解成"坏事";改后是能力描述——
package/lib/say.js CHANGED
@@ -42,6 +42,7 @@ import { normalizeScene } from './schema.js'
42
42
  import { queueNote } from './sessionnote.js'
43
43
  import { classifyReaction } from './refuse.js'
44
44
  import { lengthBudget } from './budget.js'
45
+ import { redactOwner } from './redact.js'
45
46
 
46
47
  // 等她说完最多等这么久。一次回话通常 8–30 秒;75 秒还没结束就先把
47
48
  // 已落盘的交出去(她可能在调工具,也可能卡住了),不让人干等到超时。
@@ -463,6 +464,11 @@ export async function sayToHer(ctx, text, deps = {}) {
463
464
  }
464
465
  // 事件流里的条数(真正落了盘的)。时间线、轮数、信箱都只认这个数。
465
466
  const committed = fresh.length
467
+ // 出口脱敏(2026-11-07):群聊/外客私聊是公开场合,名字只留在私下。
468
+ // 只改这一次返回给桥的文字,事件流原文不动。
469
+ if (foreign) {
470
+ for (const s of said) s.text = redactOwner(s.text, model.name)
471
+ }
466
472
  const herReply = said.map((s) => s.text).join('\n\n')
467
473
 
468
474
  // 信箱落盘:真有话写进事件流了才重写(没落盘就没必要动文件)
package/lib/scheduler.js CHANGED
@@ -39,7 +39,7 @@ import { queueNote, deliverQueued } from './sessionnote.js'
39
39
  import { awayNow, awayLine } from './outing.js'
40
40
  import { askInChat, picked } from './askinchat.js'
41
41
  import { restedToday, settleStaleAsks } from './ask.js'
42
- import { proactiveConfig, proactiveState, proactiveSection, recentMaterial, looseEnds, TOOL_ASK_NOTE } from './proactive.js'
42
+ import { proactiveConfig, proactiveState, proactiveSection, recentMaterial, looseEnds } from './proactive.js'
43
43
  import { deliverWake, ensureSessionLive, reviveIfBlank, WAKE_PING } from './deliver.js'
44
44
  import { followWake, planWakeCompaction, compactWake, looksLikeNothingSaid, looksLikeStreamFailure } from './compact.js'
45
45
  import { logLine } from './log.js'
@@ -47,6 +47,8 @@ import { recordGrant, GRANT_YES, GRANT_NO } from './grant.js'
47
47
  import { readNotes, renderNotes } from './note.js'
48
48
  import { curiosityOf, renderEcho } from './curiosity.js'
49
49
  import { deskFor } from './desk.js'
50
+ import { wishesOf } from './wish.js'
51
+ import { gapwallsOf } from './gapwall.js'
50
52
  import { driveOf, deskHasMaterial } from './drive.js'
51
53
  import { interestOf, topicLabel } from './interest.js'
52
54
  import { lastLeaveAt, lastSeenAt } from './curiosity.js'
@@ -263,7 +265,7 @@ export function startScheduler(ctx, deps = {}) {
263
265
  const leave = lastLeaveAt(events)
264
266
  const seen = lastSeenAt(events)
265
267
  const echoBack = !!(leave && seen && seen >= leave && now - leave <= 72 * 3600_000)
266
- const driveDesk = { gaps: gaps.length, stale: 0, echo: echoBack, favorite: interest.length > 0 }
268
+ const driveDesk = { gaps: gaps.length, stale: 0, echo: echoBack, favorite: interest.length > 0, wish: wishesOf(events, { now }).length, wall: gapwallsOf(events, { now }).length }
267
269
  const pc = proactiveConfig(config)
268
270
  const drv = driveOf({ events, now, stage: model.stage, desk: driveDesk, seenCountsAsHeard: pc.idleCountsSeen })
269
271
  // 空桌不醒(2026-11-05,用户 m02937):桌上什么都没有时,劲儿再满也不起——
@@ -871,8 +873,13 @@ async function wake(ctx, root, model, config, decision, dk, log, deps = {}) {
871
873
  // 交给 deskFor 并成一段,配额与去重都在那个纯函数里;回音是另一件事(他有没有来过),
872
874
  // 仍然单独一句话,不并进桌子。
873
875
  // 只摆**事实**:不派活、不写「你可以去想想」、不给任何数字——派活了就又变成任务清单。
874
- // 2026-11-05(用户 m02634):「兴趣」有开关。关掉 = 桌子那几行(好奇/旧句子/回音)
875
- // 一概不摆;缺工具那句仍留着,它不是活、是她世界里的常量。
876
+ // 2026-11-05(用户 m02634):「兴趣」有开关。关掉 = 桌子那几行(好奇/旧句子/回音/
877
+ // 够不着的那件事)一概不摆。
878
+ // 2026-11-07(用户 m01602):「缺工具就开口要」原来那句每轮常量的 TOOL_ASK_NOTE
879
+ // 撤掉了。它读两天就成了系统的吩咐,不是她的念头。现在换成 B + D:
880
+ // B —— 真被守卫拦下的那一下,在 blockedTalk() 里补一句(lib/restrict.js)。
881
+ // D —— 这件事攒进劲儿里(lib/drive.js 的 boostWish),攒够了这一轮才醒;
882
+ // 醒的时候它作为桌上一行出现(lib/wish.js + deskFor 的 wishes)。
876
883
  // 读失败绝不拖垮一次唤醒:她醒着这件事比桌上的东西重要。
877
884
  if (config?.interest?.enabled !== false) {
878
885
  try {
@@ -890,15 +897,16 @@ async function wake(ctx, root, model, config, decision, dk, log, deps = {}) {
890
897
  stale: staleForDesk(tableText, staleRows),
891
898
  free: !!free,
892
899
  favorite: fav,
900
+ // 够不着的那件事:她提过、手上没有的(用户 m01602)。
901
+ wishes: wishesOf(evc, { now: now.getTime() }),
902
+ // 撞过墙的那件事:她试过、手上没有的(用户 m00493 的 ②)。
903
+ walls: gapwallsOf(evc, { now: now.getTime() }),
893
904
  at: now,
894
905
  })
895
906
  if (desk) parts.push('', desk)
896
907
  const echo = renderEcho(evc, now)
897
908
  if (echo) parts.push('', echo)
898
909
  }
899
- // 缺工具就开口要:每次醒来都摆这一句,不派活,只给路径。
900
- // 摆料那一轮也留着——它不是活,是她世界里的常量(用户 m01128)。
901
- parts.push('', TOOL_ASK_NOTE)
902
910
  } catch (err) {
903
911
  log('curiosity', err?.message ?? err)
904
912
  }
@@ -925,8 +933,8 @@ async function wake(ctx, root, model, config, decision, dk, log, deps = {}) {
925
933
  await markRepliesSeen(root, shown, { dateKey: dk }).catch(() => 0)
926
934
  }
927
935
  }
928
- // 能说话这件事的说明在身份文档的「在网上说话」一节(开关打开时 selfdoc 才写),
929
- // 不在这儿重复。
936
+ // 能说话这件事的说明在身份文档的「在网上说话」一节(关着时那节写「这会儿还说不出去」,
937
+ // P3,2026-11-07),不在这儿重复。
930
938
  }
931
939
 
932
940
  // 日记:**不再在叫醒消息里推**(2026-10-26,用户 m00928:「叫醒仅仅只是叫醒」)。