@furongjun1999/dsh-memory 0.7.1 → 0.7.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.
Files changed (125) hide show
  1. package/README.md +58 -48
  2. package/codebuddy/CODEBUDDY.md +9 -9
  3. package/docs/README.md +148 -144
  4. package/docs/discipline/harnesses.yaml +16 -8
  5. package/docs/discipline/templates/zcode-user.md.tmpl +49 -0
  6. package/docs/eval/AGI/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_/345/205/255/345/256/266/350/256/260/345/277/206/347/263/273/347/273/237/345/220/214/345/260/272/345/256/236/346/265/213_v3.1.md +340 -0
  7. package/docs/eval/N225_/347/264/242/345/274/225/346/227/245/345/277/227/351/235/236/345/257/271/350/261/241/350/243/205/350/275/275/351/235/242/347/261/273/345/236/213/351/227/270_v1.0.md +2 -2
  8. package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +1 -1
  9. package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.1.md +1 -1
  10. package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/344/270/216/351/224/256/347/261/273/345/236/213/351/227/270_v1.1.md +1 -1
  11. package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/345/212/240/350/275/275/344/270/216/345/207/255/346/215/256/346/230/216/346/226/207/351/230/237/345/210/227_v1.0.md +2 -2
  12. package/docs/eval/issue50_/345/205/203/346/225/260/346/215/256/351/200/217/344/274/240/344/270/216/345/205/234/345/272/225_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +5 -5
  13. package/docs/eval/issue50_/345/215/212/351/207/215/345/244/215/345/276/205/345/256/232/345/244/215/346/240/270_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +9 -9
  14. package/docs/eval/issue50_/345/276/205/345/256/232/345/244/215/346/240/270/345/205/245/351/230/237_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +4 -4
  15. package/docs/eval/issue50_/351/207/215/350/246/201/345/272/246/345/220/214/346/272/220/344/270/216/344/277/235/346/212/244/350/257/255/344/271/211_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +1 -1
  16. package/docs/eval/issue51_/344/270/200/351/224/256/345/256/211/350/243/205/345/244/261/350/264/245_/345/275/222/345/261/236/345/210/244/345/256/232_v1.0.md +1 -1
  17. package/docs/eval/issue52_/346/235/241/344/273/266/345/205/210/350/241/214/344/270/216/346/210/252/346/226/255/345/217/257/350/247/202/346/265/213_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +3 -3
  18. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/242/345/217/230/346/233/264/345/215/225/344/270/216/345/233/236/346/273/232/345/216/237/350/257/255_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +3 -3
  19. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/243/345/207/206/345/205/245/350/257/273/346/225/260_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +7 -7
  20. package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/200/346/211/271_/346/216/245/347/272/277/344/270/216/347/255/211/344/273/267/345/217/230/346/215/242_v1.0.md +2 -2
  21. package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/211/346/211/271_/351/227/250/347/246/201/350/275/254/346/255/243/344/270/216/350/260/203/345/272/246/346/255/242/350/241/200/344/270/216/351/227/250/346/216/247/346/224/266/345/217/243_v1.0.md +1 -1
  22. package/docs/eval//344/274/230/345/214/226/347/254/254/344/272/214/346/211/271_/344/276/235/350/265/226/351/200/217/344/274/240/344/270/216/345/257/271/346/213/215/345/217/243/345/276/204/344/270/216/350/264/237/347/274/223/345/255/230_v1.0.md +7 -7
  23. package/docs/eval//345/207/272/350/264/247/351/235/242/345/206/222/347/203/237_/350/277/233/350/264/247/351/227/250/347/246/201_v1.0.md +10 -10
  24. package/docs/eval//345/217/221/345/270/20307_/345/244/226/351/203/250/346/212/245/345/221/212/345/233/233/346/211/271/344/277/256/345/244/215/344/270/216/346/217/222/344/273/266/351/235/242/345/212/240/345/233/272_v1.0.md +4 -0
  25. package/docs/eval//345/217/221/345/270/20308_/350/207/252/350/277/255/344/273/243/344/270/216/347/235/241/347/234/240_/345/233/276/346/243/200/347/264/242/350/267/257/344/270/216/346/235/203/351/207/215_v1.0.md +1 -1
  26. package/docs/eval//345/217/221/345/270/20309_/346/243/200/347/264/242/351/235/242/344/270/211/346/211/271/346/224/266/345/217/243_v1.0.md +1 -1
  27. package/docs/eval//345/217/221/345/270/20310_/350/257/273/351/235/242/346/215/237/345/235/217UTF8/345/256/266/346/227/217/346/224/266/345/217/243_v1.0.md +71 -0
  28. package/docs/eval//345/217/221/345/270/20311_/345/244/226/346/212/245/345/205/255/350/277/236/344/277/256/344/270/216/344/270/226/347/225/214/346/250/241/345/236/213/345/272/225/345/272/247_v1.0.md +91 -0
  29. package/docs/eval//345/275/222/344/270/200/345/261/202/347/274/272/347/234/201/347/277/273/345/205/263_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +2 -2
  30. package/docs/eval//347/224/250/346/210/267/350/243/201/345/206/263/345/244/204/347/275/256_/347/254/254/344/270/200/346/211/271_v1.0.md +195 -0
  31. package/docs/eval//347/224/250/346/210/267/350/243/201/345/206/263/345/244/204/347/275/256_/347/254/254/344/272/214/346/211/271B6_/346/234/254/346/234/272/350/267/257/345/276/204/347/233/270/345/257/271/345/214/226_v1.0.md +78 -0
  32. package/docs/eval//347/253/257/345/210/260/347/253/257LoCoMoQA/345/220/214/345/217/243/345/276/204/345/257/271/347/205/247_v1.0.md +1 -1
  33. package/docs/eval//347/253/257/345/210/260/347/253/257/345/271/262/346/211/260/346/261/240/350/257/204/346/265/213_/347/241/256/345/256/232/346/200/247/350/243/201/345/206/263vsLLM_judge_v1.1.md +3 -3
  34. package/docs/eval//347/254/2543/345/261/202stg/347/273/223/346/236/204/347/264/242/345/274/225_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +1 -1
  35. package/docs/eval//347/274/226/347/240/201/351/235/242/345/211/215/347/275/256_/345/205/245/345/217/243/350/207/252/344/277/235/350/257/201UTF8/344/270/216/346/226/207/346/234/254open/345/256/210/345/215/253_v1.0.md +1 -1
  36. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v26.md +196 -0
  37. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v27.md +259 -0
  38. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v28.md +217 -0
  39. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v29.md +270 -0
  40. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v30.md +231 -0
  41. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v31.md +281 -0
  42. package/docs/hive//345/256/211/345/205/250/345/256/241/350/256/241/345/256/236/351/224/232_v0.1.md +1 -1
  43. package/docs/hive//350/234/202/345/267/242M6_ingest/345/256/236/346/226/275/350/256/241/345/210/222_v0.1.md +1 -1
  44. package/docs/hive//350/234/202/345/267/242/345/217/214/345/256/236/344/276/213/344/272/222/351/252/214_/350/256/276/350/256/241/345/256/232/347/250/277.md +1 -1
  45. package/docs/hive//350/234/202/345/267/242/350/256/276/350/256/241_/347/220/206/350/256/272/345/257/271/351/275/220_v0.1.md +17 -17
  46. package/docs/hive//350/234/202/345/267/242/350/277/255/344/273/243_/345/256/217/350/247/202/344/270/216/347/276/244/344/275/223/350/260/203/345/272/246_v0.1.md +1 -1
  47. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +2 -2
  48. package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +42 -41
  49. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +29 -8
  50. package/docs/mdcg//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +71 -1
  51. package/docs/plans//345/205/250/344/270/255/346/226/207/347/274/226/347/240/201/344/270/216/350/234/202/345/267/242/344/273/273/345/212/241/346/240/207/350/257/206/345/245/221/347/272/246_v2.0.md +1 -1
  52. package/docs/plans//345/244/232/344/270/273/344/275/223/344/270/226/347/225/214/346/250/241/345/236/213_/345/257/271/351/275/220/350/257/204/344/274/260/344/270/216/350/220/275/345/234/260/350/256/276/350/256/241_v0.1.md +141 -0
  53. package/docs/plans//347/235/241/347/234/240/344/270/216/350/207/252/350/277/255/344/273/243_/345/212/237/350/203/275/344/274/230/345/214/226/350/256/276/350/256/241_v0.4.md +2 -2
  54. package/docs/plans//350/257/255/344/271/211/346/227/266/347/251/272/345/233/276/350/241/245/345/205/250_/344/270/226/347/225/214/346/250/241/345/236/213/345/212/237/350/203/275/347/253/257_/350/256/276/350/256/241_v0.1.md +107 -0
  55. package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +9 -8
  56. package/docs//347/247/244_/350/256/260/345/277/206/347/263/273/347/273/237/350/257/204/346/265/213/350/247/204/350/214/203_v2.1.md +225 -0
  57. package/dsh/cordis-patch-profile-web.example.yml +2 -2
  58. package/lib/bridge.js +9 -0
  59. package/lib/hooks.d.ts +8 -1
  60. package/lib/hooks.js +94 -32
  61. package/lib/lib/hook_audit.d.ts +55 -0
  62. package/lib/lib/hook_audit.js +208 -0
  63. package/lib/lib/mutual.js +5 -1
  64. package/lib/lib/roleplay_web.js +17 -2
  65. package/md_cg/auditview.py +248 -0
  66. package/md_cg/bench6_arms.py +10 -6
  67. package/md_cg/bench_e2e_judge.py +4 -2
  68. package/md_cg/branches.py +29 -3
  69. package/md_cg/census.py +2 -1
  70. package/md_cg/consistency.py +38 -13
  71. package/md_cg/datapath.py +38 -11
  72. package/md_cg/forgetting.py +15 -2
  73. package/md_cg/freshness.py +32 -11
  74. package/md_cg/ghostref.py +130 -0
  75. package/md_cg/judgment_manifest.py +7 -0
  76. package/md_cg/mcp_server.py +58 -1
  77. package/md_cg/mdcg.py +4923 -4764
  78. package/md_cg/mdcos.py +40 -15
  79. package/md_cg/mreview/locate.py +1 -1
  80. package/md_cg/nodefile.py +77 -13
  81. package/md_cg/readcache.py +29 -4
  82. package/md_cg/rollback.py +12 -3
  83. package/md_cg/routing.py +10 -1
  84. package/md_cg/selfreport.py +12 -3
  85. package/md_cg/sleep.py +129 -13
  86. package/md_cg/sources.py +1 -1
  87. package/md_cg/state_events.py +131 -0
  88. package/md_cg/sustain.py +1436 -1436
  89. package/md_cg/test_auditview.py +277 -0
  90. package/md_cg/test_autonomy_modes.py +2 -1
  91. package/md_cg/test_boot_openblas_guard.py +197 -0
  92. package/md_cg/test_ccg_form_parity.py +95 -4
  93. package/md_cg/test_cons200_scan_selection.py +4 -2
  94. package/md_cg/test_corrupt_utf8_read_surfaces.py +451 -0
  95. package/md_cg/test_datapath_device_name.py +62 -3
  96. package/md_cg/test_generation_guard.py +20 -11
  97. package/md_cg/test_ghostref.py +182 -0
  98. package/md_cg/test_health_corrupt_utf8.py +37 -0
  99. package/md_cg/test_index_crossprocess_reload.py +39 -24
  100. package/md_cg/test_interop.py +1 -1
  101. package/md_cg/test_issue52_scan_condition_first.py +1 -1
  102. package/md_cg/test_issue53_cipher_health_hash.py +385 -0
  103. package/md_cg/test_lock.py +16 -7
  104. package/md_cg/test_m3_h9_semantic_guard.py +1 -1
  105. package/md_cg/test_n230_dirty_replay.py +375 -375
  106. package/md_cg/test_n238_body_text_parity.py +156 -0
  107. package/md_cg/test_n276_probe_hoist.py +199 -0
  108. package/md_cg/test_p4_freshness.py +95 -1
  109. package/md_cg/test_policy_required_ccg.py +3 -1
  110. package/md_cg/test_readcache_prodpath.py +66 -0
  111. package/md_cg/test_sleep.py +58 -1
  112. package/md_cg/test_sleep_p1.py +218 -2
  113. package/md_cg/test_state_events.py +178 -0
  114. package/md_cg/test_time_core_lint.py +8 -1
  115. package/md_cg/test_writepipe.py +8 -5
  116. package/md_cg/tokens.py +11 -2
  117. package/md_cg/writepipe.py +76 -0
  118. package/package.json +2 -2
  119. package/skills/plugin.json +1 -1
  120. package/src/bridge.ts +9 -0
  121. package/src/hooks.ts +576 -517
  122. package/src/lib/hook_audit.ts +243 -0
  123. package/src/lib/mutual.ts +5 -1
  124. package/src/lib/roleplay_web.ts +17 -2
  125. package/zcode/AGENTS.md +9 -9
@@ -0,0 +1,243 @@
1
+ /**
2
+ * hook_audit.ts · 自动记忆钩子的**有界落盘审计**(issue #56,诊断面)
3
+ *
4
+ * 病灶(报告人 jiamajie,v0.7.1 实测):src/hooks.ts 的三处过滤分支(子代理会话 /
5
+ * 中继消息 / `source.kind !== 'user'`)只调 `ctx.logger.info`——宿主 logger 不落盘,
6
+ * 盘面完全无痕。其它插件按设计注入的 `role:"user"` 消息(如 @michengai/dsh-pua 的
7
+ * PUA_RUNTIME_V1 提示)不进记忆库时,从记忆侧只能看到「少了一条」,无法区分
8
+ * 「被设计滤除」与「写入失败/漏记」。
9
+ *
10
+ * 本模块 = **诊断面**,不参与任何写入/过滤判定(红线:哪些消息进记忆一字不动):
11
+ * ① 维护一份小 JSON 状态文件(缺省 `~/.dsh/logs/dsh-memory-hook-audit.json`,
12
+ * 与 `lingshu-bridge-debug.log` / `dsh-memory-apply-error.log` 同目录同惯例,
13
+ * 见 README.md:110);
14
+ * ② 每事件只写计数与「最近条目」摘要(action/reason/kind/role),**绝不记录
15
+ * 消息内容**(内容可能含隐私/凭据,审计文件是可被随手打开的诊断件);
16
+ * ③ 原子写:同目录临时文件 + rename(读者永远不会看到半截 JSON);
17
+ * ④ 有界:last 截断到 MAX_LAST=20(无追加式增长);kinds 键数上限 MAX_KINDS,
18
+ * 超限归并到 "other"(kind 值域理论上由宿主决定,防异常值把文件撑大);
19
+ * ⑤ 静默降级:公共方法全部 try/catch 包死——审计失败绝不冒泡进记忆路径
20
+ * (审计是增益,记忆写入必须照常)。
21
+ *
22
+ * 为什么落盘采用**同步** fs:`session/event` 回调是同步的,异步落盘会引入
23
+ * 「多事件并发写同一文件」的乱序与竞态;状态文件只有几 KB,一次同步读(仅首次)
24
+ * 加一次全量重写的代价远小于它守护的诊断价值。
25
+ *
26
+ * 与 ctx.logger.info 的关系:hook 内三行 info 日志**保留不动**(in-memory 实时
27
+ * 视图),本审计是**盘面**视图(宿主 logger 不落盘时仍可复盘)。
28
+ *
29
+ * 边界(如实):
30
+ * · 状态是「进程内缓存 + 每次全量重写」,多进程同时写同一路径会互相覆盖
31
+ * (插件单实例部署下不构成问题;审计非关键路径,不做跨进程锁);
32
+ * · 审计文件损坏/手改致形态不符时按空态重建(数值归零、last 逐条过滤),
33
+ * 不抛错、不保留非法内容;
34
+ * · `written` 记的是「**成功发起**」的写入(fire-and-forget 的发起时刻),
35
+ * 发起后失败另记 `skipped.failed`——与 hooks.ts 的 memorize 两段式对应。
36
+ */
37
+ import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'
38
+ import { homedir } from 'node:os'
39
+ import { dirname, join } from 'node:path'
40
+
41
+ /** 三处过滤分支的成因(与 hooks.ts 一一对应:子代理会话 / 中继消息 / kind 非 user)。 */
42
+ export type HookFilterReason = 'subagent' | 'relay' | 'kind'
43
+ /** 写入路径的角色(与 md_cg 对 DSH 会话事件的 role 约定一致)。 */
44
+ export type HookWriteRole = 'user' | 'assistant' | 'tool'
45
+ /** 跳过写入的成因。 */
46
+ export type HookSkipReason = 'sanitized' | 'not_ready' | 'failed'
47
+
48
+ /** last 的一条摘要(**绝不含消息内容**——只有动作、成因、kind 与 role)。 */
49
+ export interface HookAuditEntry {
50
+ ts: string
51
+ action: 'filtered' | 'written' | 'skipped'
52
+ reason?: HookFilterReason | HookSkipReason
53
+ kind?: string
54
+ role?: HookWriteRole
55
+ }
56
+
57
+ /** 审计状态(= 状态文件的结构)。 */
58
+ export interface HookAuditState {
59
+ counts: {
60
+ /** 三处过滤分支的计数。 */
61
+ filtered: Record<HookFilterReason, number>
62
+ /** 成功发起的写入计数(按 role 分解)。 */
63
+ written: Record<HookWriteRole, number>
64
+ /** 跳过写入的计数(脱敏置空 / 认知图未就绪 / 写入失败)。 */
65
+ skipped: Record<HookSkipReason, number>
66
+ }
67
+ /** 进入消息级处理的 `source.kind` 分布(缺字段/空串/非字符串记 "undefined")。 */
68
+ kinds: Record<string, number>
69
+ /** 最近 ≤MAX_LAST 条摘要。 */
70
+ last: HookAuditEntry[]
71
+ updated_at: string
72
+ }
73
+
74
+ /** 缺省审计文件:与桥探针 / apply 探针同目录同惯例(README.md:110)。 */
75
+ export const DEFAULT_HOOK_AUDIT_PATH = join(homedir(), '.dsh', 'logs', 'dsh-memory-hook-audit.json')
76
+
77
+ /** last 条数上限(有界性单点——没有它 audit 文件会随会话无限增长)。 */
78
+ const MAX_LAST = 20
79
+ /** kinds 键数上限(超限归并到 OTHER_KIND,防宿主 kind 值域异常撑大文件)。 */
80
+ const MAX_KINDS = 32
81
+ /** kinds 归并桶。 */
82
+ const OTHER_KIND = 'other'
83
+
84
+ const FILTER_REASONS: HookFilterReason[] = ['subagent', 'relay', 'kind']
85
+ const WRITE_ROLES: HookWriteRole[] = ['user', 'assistant', 'tool']
86
+ const SKIP_REASONS: HookSkipReason[] = ['sanitized', 'not_ready', 'failed']
87
+
88
+ /** `source.kind` 的规范化取值:非字符串(含缺字段)或空串一律记 "undefined"
89
+ * (不可辨认即无值——空串与缺字段在排障上同义)。 */
90
+ export function kindOf(source: unknown): string {
91
+ const k = (source as { kind?: unknown } | null | undefined)?.kind
92
+ return typeof k === 'string' && k.length > 0 ? k : 'undefined'
93
+ }
94
+
95
+ function nowIso(): string {
96
+ return new Date().toISOString()
97
+ }
98
+
99
+ function emptyState(): HookAuditState {
100
+ return {
101
+ counts: {
102
+ filtered: { subagent: 0, relay: 0, kind: 0 },
103
+ written: { user: 0, assistant: 0, tool: 0 },
104
+ skipped: { sanitized: 0, not_ready: 0, failed: 0 },
105
+ },
106
+ kinds: {},
107
+ last: [],
108
+ updated_at: nowIso(),
109
+ }
110
+ }
111
+
112
+ /** 计数位:只认有限非负整数,其余归零(防手改/坏文件把计数变成 NaN)。 */
113
+ function countOf(v: unknown): number {
114
+ return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? Math.floor(v) : 0
115
+ }
116
+
117
+ /** 盘面内容 → 状态:逐字段归并,形态不符的字段一律丢弃(不信任盘面内容)。 */
118
+ function mergeState(raw: unknown): HookAuditState {
119
+ const s = emptyState()
120
+ if (!raw || typeof raw !== 'object') return s
121
+ const r = raw as { counts?: unknown; kinds?: unknown; last?: unknown }
122
+ if (r.counts && typeof r.counts === 'object') {
123
+ const c = r.counts as { filtered?: unknown; written?: unknown; skipped?: unknown }
124
+ const buckets: Array<[Record<string, number>, string[], unknown]> = [
125
+ [s.counts.filtered as Record<string, number>, FILTER_REASONS, c.filtered],
126
+ [s.counts.written as Record<string, number>, WRITE_ROLES, c.written],
127
+ [s.counts.skipped as Record<string, number>, SKIP_REASONS, c.skipped],
128
+ ]
129
+ for (const [dst, keys, src] of buckets) {
130
+ if (!src || typeof src !== 'object') continue
131
+ for (const key of keys) dst[key] = countOf((src as Record<string, unknown>)[key])
132
+ }
133
+ }
134
+ if (r.kinds && typeof r.kinds === 'object') {
135
+ for (const [k, v] of Object.entries(r.kinds as Record<string, unknown>)) {
136
+ if (typeof v === 'number' && Number.isFinite(v) && v > 0) s.kinds[k] = Math.floor(v)
137
+ }
138
+ }
139
+ if (Array.isArray(r.last)) {
140
+ for (const e of r.last) {
141
+ if (!e || typeof e !== 'object') continue
142
+ const entry = e as Partial<HookAuditEntry>
143
+ if (typeof entry.ts !== 'string') continue
144
+ if (entry.action !== 'filtered' && entry.action !== 'written' && entry.action !== 'skipped') continue
145
+ s.last.push(entry as HookAuditEntry)
146
+ }
147
+ if (s.last.length > MAX_LAST) s.last = s.last.slice(-MAX_LAST)
148
+ }
149
+ return s
150
+ }
151
+
152
+ /**
153
+ * 有界落盘审计记录器(**静默降级**:任何公共方法都不会抛错)。
154
+ *
155
+ * 用法(hooks.ts 内):`new HookAuditRecorder(opts.auditPath)`,随后在
156
+ * 三处过滤分支与写入/跳过路径各调一次对应方法。审计失败只影响审计自身。
157
+ */
158
+ export class HookAuditRecorder {
159
+ /** 实际生效的审计文件路径(构造时定死,供日志/测试核对)。 */
160
+ readonly path: string
161
+ /** 进程内状态缓存(首次记录时从盘面载入,此后只增量改内存 + 全量重写)。 */
162
+ #state: HookAuditState | null = null
163
+
164
+ constructor(path?: string) {
165
+ this.path = typeof path === 'string' && path.trim() ? path.trim() : DEFAULT_HOOK_AUDIT_PATH
166
+ }
167
+
168
+ /** 一条**被设计滤除**的消息(三处过滤分支各调一次)。 */
169
+ filtered(reason: HookFilterReason, kind?: string): void {
170
+ this.#mutate((s) => {
171
+ s.counts.filtered[reason] += 1
172
+ s.last.push(kind === undefined
173
+ ? { ts: nowIso(), action: 'filtered', reason }
174
+ : { ts: nowIso(), action: 'filtered', reason, kind })
175
+ })
176
+ }
177
+
178
+ /** `source.kind` 分布(每条**进入消息级处理**的事件调一次,含被滤与放行)。 */
179
+ observeKind(kind: string): void {
180
+ this.#mutate((s) => {
181
+ const key = this.#boundedKindKey(s, kind)
182
+ s.kinds[key] = (s.kinds[key] ?? 0) + 1
183
+ })
184
+ }
185
+
186
+ /** 成功**发起**的写入(按 role;发起后失败另记 skipped.failed)。 */
187
+ written(role: HookWriteRole): void {
188
+ this.#mutate((s) => {
189
+ s.counts.written[role] += 1
190
+ s.last.push({ ts: nowIso(), action: 'written', role })
191
+ })
192
+ }
193
+
194
+ /** 跳过写入(脱敏置空 / 认知图未就绪 / 写入失败)。 */
195
+ skipped(reason: HookSkipReason, role?: HookWriteRole): void {
196
+ this.#mutate((s) => {
197
+ s.counts.skipped[reason] += 1
198
+ s.last.push(role === undefined
199
+ ? { ts: nowIso(), action: 'skipped', reason }
200
+ : { ts: nowIso(), action: 'skipped', reason, role })
201
+ })
202
+ }
203
+
204
+ /** 键数有界:已见键原样用;新键在未达上限时收录,达限后统一归并到 "other"。 */
205
+ #boundedKindKey(s: HookAuditState, kind: string): string {
206
+ if (Object.prototype.hasOwnProperty.call(s.kinds, kind)) return kind
207
+ return Object.keys(s.kinds).length >= MAX_KINDS ? OTHER_KIND : kind
208
+ }
209
+
210
+ /** 读改写 + 原子落盘;整段 try/catch——审计失败静默降级(绝不冒泡进记忆路径)。 */
211
+ #mutate(fn: (s: HookAuditState) => void): void {
212
+ try {
213
+ const s = this.#state ?? (this.#state = this.#load())
214
+ fn(s)
215
+ if (s.last.length > MAX_LAST) s.last = s.last.slice(-MAX_LAST)
216
+ s.updated_at = nowIso()
217
+ this.#save(s)
218
+ } catch { /* 审计失败静默降级:记忆路径必须照常 */ }
219
+ }
220
+
221
+ /** 盘面 → 状态;不存在/不可读/坏 JSON/形态不符 → 空态重建(不抛)。 */
222
+ #load(): HookAuditState {
223
+ try {
224
+ return mergeState(JSON.parse(readFileSync(this.path, 'utf8')) as unknown)
225
+ } catch {
226
+ return emptyState()
227
+ }
228
+ }
229
+
230
+ /** 原子写:同目录临时文件 + rename(临时文件在 rename 失败时清理)。 */
231
+ #save(s: HookAuditState): void {
232
+ const dir = dirname(this.path)
233
+ mkdirSync(dir, { recursive: true })
234
+ const tmp = `${this.path}.${process.pid.toString(36)}.${Date.now().toString(36)}.tmp`
235
+ writeFileSync(tmp, `${JSON.stringify(s, null, 2)}\n`, 'utf8')
236
+ try {
237
+ renameSync(tmp, this.path)
238
+ } catch (err) {
239
+ try { unlinkSync(tmp) } catch { /* 清理失败忽略:残留 .tmp 不影响读路径 */ }
240
+ throw err
241
+ }
242
+ }
243
+ }
package/src/lib/mutual.ts CHANGED
@@ -259,8 +259,12 @@ export function combineVerdict(w: VerifyResult['whitebox'],
259
259
  l: VerifyResult['llm_review']): VerifyResult['verdict'] {
260
260
  const c = l.conclusion || ''
261
261
  // P1 修复(GPT 审查):白箱通道异常 → fail-closed(不因复核「同意」而 pass)
262
+ // N268:**复核通道**异常的标记由 llmReview 写在 `l.conclusion`(`llm_error: …`)——
263
+ // 此前只查 w.judgment(该字段只可能由 whiteboxVerify 写 whitebox_error)⇒ 该分支
264
+ // 是死条件,复核抛异常时白箱「采纳」径直判 pass(复核不可用反成「通过」)。
265
+ // 两字段都查:l 侧是真正的生产者口径,w 侧保留同形防御(防上游把异常塞进白箱字段)。
262
266
  if (w.judgment.startsWith('whitebox_error') || w.judgment.startsWith('llm_error')
263
- || w.d_norm < 0) {
267
+ || c.startsWith('llm_error') || w.d_norm < 0) {
264
268
  return 'needs_revision'
265
269
  }
266
270
  if (w.judgment.startsWith('采纳')) {
@@ -847,6 +847,17 @@ export async function installRoleplayWeb(ctx, capability, config, disposers, mdc
847
847
  res.end(JSON.stringify({ ok: false, error: '非法角色 id(白名单 ^[A-Za-z0-9_.-]{1,64}$)' }));
848
848
  return;
849
849
  }
850
+ // N266:入口类型闸——message 必须是字符串。此前这段直接
851
+ // `p.message.includes(w)`:message 为数组时静默走
852
+ // Array.prototype.includes(逐元素**全等**比较),把敏感词拆进
853
+ // 不同元素即整条绕过硬拦截;数字/对象/缺省则抛 TypeError 落 500
854
+ // (内部异常文本随响应外泄)。与 role_id 同口径:非法形态在入口
855
+ // 一律 400 拒——不转发引擎、不落转录。
856
+ if (typeof p.message !== 'string') {
857
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
858
+ res.end(JSON.stringify({ ok: false, route: 'refused', refused: 'bad_message', error: 'message 必须是字符串(非字符串形态不得绕过内容硬拦截,N266)' }));
859
+ return;
860
+ }
850
861
  // P1 完善(会话隔离):按客户端实例隔离 session 与转录
851
862
  const cid = clientIdOf(req);
852
863
  // —— 服务端内容分级硬拦截(不依赖前端,法律与协议保护)——
@@ -860,13 +871,17 @@ export async function installRoleplayWeb(ctx, capability, config, disposers, mdc
860
871
  res.end(JSON.stringify({ reply: '⛔ 已拒绝:涉及未成年人的性内容违反国家法律与灵枢协议(未成年人保护)。灵枢不提供任何涉及未成年人的性扮演内容。', route: 'refused', refused: 'minor_nsfw' }));
861
872
  return;
862
873
  }
863
- appendTranscript(role, { time: Date.now(), role: 'user', text: p.message }, cid);
864
- void toGraph('user-turn', (g) => g.writeTranscript(role, cid, 'user', p.message));
865
874
  if (!capReady()) {
866
875
  res.writeHead(503, { 'Content-Type': 'application/json; charset=utf-8' });
867
876
  res.end(JSON.stringify({ reply: '', route: 'unavailable', error: CAP_ABSENT_MSG }));
868
877
  return;
869
878
  }
879
+ // N266:落盘移到内容门控与就绪闸**之后**——被内容门控拒绝的、以及
880
+ // 「能力未接入」(503)的回合都从未发生,此前这两笔写在校验与
881
+ // capReady 之前,未经门控的 message 原文会被留进本地转录
882
+ //(读同一文件的历史接口随即原样回放)。
883
+ appendTranscript(role, { time: Date.now(), role: 'user', text: p.message }, cid);
884
+ void toGraph('user-turn', (g) => g.writeTranscript(role, cid, 'user', p.message));
870
885
  const r = await capability.callTool('roleplay_chat', {
871
886
  message: p.message, role_id: role, session_id: sessionFor(role, cid), data_dir: roleDataDir,
872
887
  });
package/zcode/AGENTS.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  > 本文件由 `scripts/render_discipline.py` 从真源
4
4
  > `docs/工作纪律_认知图条目_v1.1.json` 渲染生成,**请勿手改**;改真源后重跑渲染。
5
- > 真源指纹(SHA256 前16位):e1e9112a409894aa
5
+ > 真源指纹(SHA256 前16位):e8c979edba52de41
6
6
 
7
7
  ## 概述
8
8
 
9
- 工作纪律集: 4条方法论(理论先行/全面处理/白箱方法/根因) + 3条执行(验证/双副本/兜底) + 1条思考(中文思考/协作模式分级) + 1条保密(敏感信息隔离) + 3条图像线(选源护栏/历史查询优先/算法权威唯一) + 1条协作(访谈澄清:先问清再动手) + 1条合规(内容政策合规:公开产物过内容政策+隐私双清单) + 2条执行调度与运行环境(命令执行统一走python/UTF-8: 显式encoding+PYTHONUTF8=1, 规避GBK, 全仓文本与路径一律UTF-8(无BOM, 中文可进路径), 不做控制台兼容(取用走python); 任务派发统一走蜂巢: 任何执行性任务经蜂巢spawn/submit执行并留痕, agent本体只做编排, L1出口=产物落点为只读判定(不改仓库/外部状态)可直跑须留痕) + 1条记忆闭环(任务收尾归档:每次任务执行完只将核心修改(内容/原因/位置/验证结论)按CCG六要素(功能名/生效条件/子功能/执行/验证方式/不适用条件)成文存入灵枢记忆, 不写中间过程; text 类缺要素即被写入闸门拒并当场返回缺失清单) + 1条信息获取(工作区索引优先: 查工作区文件先读 WORKSPACE_INDEX.md(仓根, 由 scripts/workspace_index.py 从 git 追踪面管线生成), 尚无则先跑 --write 生成再读, 不以重复全盘浏览代替; 索引是会陈化的手工件, 故须管线生成+机械守卫, 否则退化为误导源)
9
+ 工作纪律集: 4条方法论(理论先行/全面处理/白箱方法/根因) + 3条执行(验证/双副本/兜底) + 1条思考(中文思考/协作模式分级) + 1条保密(敏感信息隔离) + 3条图像线(选源护栏/历史查询优先/算法权威唯一) + 1条协作(访谈澄清:先问清再动手) + 1条合规(内容政策合规:公开产物过内容政策+隐私双清单) + 2条执行调度与运行环境(命令执行统一走python/UTF-8: 显式encoding+PYTHONUTF8=1, 规避GBK, 全仓文本与路径一律UTF-8(无BOM, 中文可进路径), 不做控制台兼容(取用走python), 公开面路径写相对路径(守 check_local_paths); 任务派发统一走蜂巢(zcode端的工作流工具为等效蜂巢工具——使用者2026-10-04裁定; 蜂巢功能完全完善之前可优先使用zcode工作流): 任何执行性任务经蜂巢spawn/submit执行并留痕, agent本体只做编排, L1出口=产物落点为只读判定(不改仓库/外部状态)可直跑须留痕) + 1条记忆闭环(任务收尾归档:每次任务执行完只将核心修改(内容/原因/位置/验证结论)按CCG六要素(功能名/生效条件/子功能/执行/验证方式/不适用条件)成文存入灵枢记忆, 不写中间过程; text 类缺要素即被写入闸门拒并当场返回缺失清单) + 1条信息获取(工作区索引优先: 查工作区文件先读 WORKSPACE_INDEX.md(仓根, 由 scripts/workspace_index.py 从 git 追踪面管线生成), 尚无则先跑 --write 生成再读, 不以重复全盘浏览代替; 索引是会陈化的手工件, 故须管线生成+机械守卫, 否则退化为误导源)
10
10
 
11
11
  ## 执行公约
12
12
 
@@ -120,10 +120,10 @@
120
120
 
121
121
  ### 15. 命令执行统一走python
122
122
 
123
- - **触发**:执行命令|运行测试/脚本|跨进程读写文本
124
- - **动作**:构造 argv 列表→subprocess.run(capture_output=True, text=True, encoding='utf-8', errors='replace')→env 带 PYTHONUTF8=1→shell=False;边界判据=本次调用是否产出非UTF-8字节流(纯MSYS工具与不起进程的内置工具豁免;原生Windows程序不豁免,须python包装或显式解码)
123
+ - **触发**:执行命令|运行测试/脚本|跨进程读写文本|书写文档/引用路径
124
+ - **动作**:构造 argv 列表→subprocess.run(capture_output=True, text=True, encoding='utf-8', errors='replace')→env 带 PYTHONUTF8=1→shell=False;边界判据=本次调用是否产出非UTF-8字节流(纯MSYS工具与不起进程的内置工具豁免;原生Windows程序不豁免,须python包装或显式解码);仓内引用与文档路径一律写相对路径(本机绝对路径以中性占位代), 判据 scripts/check_local_paths.py
125
125
  - **不适用**:IDE内置工具直调(判据=本次调用是否产出非UTF-8字节流;不起进程的内置工具(Read/Write/Edit/Grep/Glob/WebFetch)与纯MSYS工具(grep/ls/sed)归入本豁免;拉起shell且执行原生Windows程序者(taskkill/wmic/npm.cmd/cargo/where)不属本豁免,仍须python包装或对输出显式解码;2026-09-28使用者裁定)|无跨进程的纯内存计算
126
- - **声明**:按工作纪律第15条: 命令执行统一走python——argv列表+显式UTF-8+PYTHONUTF8=1, 不经Windows shell, 规避GBK解码异常。 另: 全仓文本与路径/文件名一律UTF-8(中文可进路径), 不做控制台兼容(取用走python, 控制台乱码属显示层)。
126
+ - **声明**:按工作纪律第15条: 命令执行统一走python——argv列表+显式UTF-8+PYTHONUTF8=1, 不经Windows shell, 规避GBK解码异常。 另: 全仓文本与路径/文件名一律UTF-8(中文可进路径), 不做控制台兼容(取用走python, 控制台乱码属显示层)。 另: 公开面路径写相对路径——本机绝对路径以中性占位代(仓内 `<仓根>`/家目录 `~/`/临时 `%TEMP%`), 门禁 check_local_paths。
127
127
 
128
128
  ### 16. 任务收尾归档(记忆闭环)
129
129
 
@@ -135,9 +135,9 @@
135
135
  ### 17. 任务派发统一走蜂巢
136
136
 
137
137
  - **触发**:执行性任务|多智能体协作|批量/并行工作|探索盘点|运行测试/脚本
138
- - **动作**:任务→按产物落点判据定层级(不改仓库/外部状态的只读判定=L1直跑并按任务留痕一条(复核者=使用者+GitHub使用者); 改状态=必派发, 不得自我豁免)→拆spec(LLM委托型=默认exec.py: model+user_prompt+context_files; 确定性执行=自定义worker, 契约「读spec.json写result.json」)→hive_spawn/CLI submit→poll→汇总; 兜底情形须声明「蜂巢不可用, 兜底降级」
138
+ - **动作**:任务→按产物落点判据定层级(不改仓库/外部状态的只读判定=L1直跑并按任务留痕一条(复核者=使用者+GitHub使用者); 改状态=必派发, 不得自我豁免)→拆spec(LLM委托型=默认exec.py: model+user_prompt+context_files; 确定性执行=自定义worker, 契约「读spec.json写result.json」)→hive_spawn/CLI submit(zcode端: 工作流工具为等效蜂巢工具——使用者2026-10-04裁定; 蜂巢功能完全完善之前, zcode端可优先使用zcode工作流处理; 留痕以工作流脚本与运行记录为凭)→poll→汇总; 兜底情形须声明「蜂巢不可用, 兜底降级」
139
139
  - **不适用**:纯会话内对话/问答(无执行动作)|蜂巢不可用且任务必须立即执行(兜底须声明)|L1只读判定(产物落点=不改仓库/不改外部状态的本地只读命令, 秒级·可逆·高频, 如跑门禁/测试/回归/lint看结论; 判据是产物落点而非「任务简单」的主观感受, 直跑须输出一行「L1 直跑:<命令> — 风险/频次/可逆性」留痕, 留痕粒度按任务(同一任务的多条只读命令合并为一条留痕, 不逐命令逐行); 层级归属的复核者=使用者+GitHub使用者)|情感交互|闲聊
140
- - **声明**:按工作纪律第17条: 任务派发统一走蜂巢——任何执行性任务经蜂巢 spawn/submit 执行并留痕(spec/status/result), agent本体只做编排; 宿主自带subagent/team不是等价通道, 兜底须声明。L1只读判定(产物落点=不改仓库/外部状态)可直跑, 须输出「L1 直跑:<命令> — 风险/频次/可逆性」留痕。
140
+ - **声明**:按工作纪律第17条: 任务派发统一走蜂巢——任何执行性任务经蜂巢 spawn/submit 执行并留痕(spec/status/result), agent本体只做编排; 宿主自带subagent/team不是等价通道(zcode端的工作流工具为等效蜂巢工具——使用者2026-10-04裁定; 蜂巢功能完全完善之前, zcode端可优先使用zcode工作流处理), 兜底须声明。L1只读判定(产物落点=不改仓库/外部状态)可直跑, 须输出「L1 直跑:<命令> — 风险/频次/可逆性」留痕。
141
141
 
142
142
  ### 18. 工作区索引优先
143
143
 
@@ -164,9 +164,9 @@
164
164
  | 12 | 按工作纪律第12条: 算法权威唯一(线稿=darkline.py/色块=contour_semantics)——选算法查文档, 勿临时切换/漂移; 换算法须记录+更新文档。 |
165
165
  | 13 | 按工作纪律第13条: 访谈澄清——先提问确认要做什么,逐轮问到完全清楚才动手;事实自查,决策归使用者,不完全清楚不固化。 |
166
166
  | 14 | 按工作纪律第14条: 内容政策合规——对外公开产物必须过「内容政策+隐私」双清单,过滤在生成阶段做;已发布发现违规要删条+重写git历史+通知平台清缓存。 |
167
- | 15 | 按工作纪律第15条: 命令执行统一走python——argv列表+显式UTF-8+PYTHONUTF8=1, 不经Windows shell, 规避GBK解码异常。 另: 全仓文本与路径/文件名一律UTF-8(中文可进路径), 不做控制台兼容(取用走python, 控制台乱码属显示层)。 |
167
+ | 15 | 按工作纪律第15条: 命令执行统一走python——argv列表+显式UTF-8+PYTHONUTF8=1, 不经Windows shell, 规避GBK解码异常。 另: 全仓文本与路径/文件名一律UTF-8(中文可进路径), 不做控制台兼容(取用走python, 控制台乱码属显示层)。 另: 公开面路径写相对路径——本机绝对路径以中性占位代(仓内 `<仓根>`/家目录 `~/`/临时 `%TEMP%`), 门禁 check_local_paths。 |
168
168
  | 16 | 按工作纪律第16条: 任务收尾归档——每次任务执行完只提炼核心修改(内容/原因/位置/验证结论)并按 CCG 六要素(功能名/生效条件/子功能/执行/验证方式/不适用条件)成文存入灵枢记忆(text 类六要素缺失会被写入闸门当场拒绝并返回缺失清单——补齐后重写即可, 不必等读回确认才发现), 写入后发起一次读取查询确认写入成功且可检索, 禁写中间过程/试错/调试等无效信息, 与第2条形成「查记忆→执行→写记忆→读回确认」闭环。 |
169
- | 17 | 按工作纪律第17条: 任务派发统一走蜂巢——任何执行性任务经蜂巢 spawn/submit 执行并留痕(spec/status/result), agent本体只做编排; 宿主自带subagent/team不是等价通道, 兜底须声明。L1只读判定(产物落点=不改仓库/外部状态)可直跑, 须输出「L1 直跑:<命令> — 风险/频次/可逆性」留痕。 |
169
+ | 17 | 按工作纪律第17条: 任务派发统一走蜂巢——任何执行性任务经蜂巢 spawn/submit 执行并留痕(spec/status/result), agent本体只做编排; 宿主自带subagent/team不是等价通道(zcode端的工作流工具为等效蜂巢工具——使用者2026-10-04裁定; 蜂巢功能完全完善之前, zcode端可优先使用zcode工作流处理), 兜底须声明。L1只读判定(产物落点=不改仓库/外部状态)可直跑, 须输出「L1 直跑:<命令> — 风险/频次/可逆性」留痕。 |
170
170
  | 18 | 按工作纪律第18条: 工作区索引优先——查工作区文件先读 WORKSPACE_INDEX.md(仓根, 管线生成); 无则先跑 scripts/workspace_index.py --write 生成再读, 不以重复全盘浏览代替。 |
171
171
 
172
172
  ## 记忆接口速查(`cg` / `stg`)