@jjh32456/dsh-longlive 1.6.7 → 1.8.0

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 (289) hide show
  1. package/CHANGELOG.md +70 -1508
  2. package/LICENSE +1 -1
  3. package/QUICKSTART.md +34 -13
  4. package/README.en.md +18 -7
  5. package/README.md +34 -9
  6. package/SPEC.md +5 -5
  7. package/cordis.patch.yml +23 -9
  8. package/docs/ANSWER-GUARD.md +92 -0
  9. package/docs/ARCHITECTURE.md +4 -3
  10. package/docs/AUTO-RECALL-WIRING.md +1 -1
  11. package/docs/AUTO-RECALL.md +15 -9
  12. package/docs/CONFIG.md +168 -35
  13. package/docs/ERRORS.md +17 -5
  14. package/docs/FAQ.md +7 -7
  15. package/docs/FEATURES.md +96 -28
  16. package/docs/MIGRATION-1.6.0.md +2 -2
  17. package/docs/OPERATIONS.md +6 -6
  18. package/docs/PANEL-INTEGRATION.md +6 -0
  19. package/docs/PRIVACY.md +88 -0
  20. package/docs/QUICKSTART.md +5 -5
  21. package/docs/README.en.md +4 -2
  22. package/docs/RESIDENT-WATCHER.md +1 -1
  23. package/docs/SESSION-WATCH.md +6 -6
  24. package/docs/TROUBLESHOOTING.md +4 -4
  25. package/package.json +47 -117
  26. package/runbook.md +2 -2
  27. package/scripts/backfill-auto-cards.mjs +98 -0
  28. package/scripts/install-surgery-task.ps1 +5 -2
  29. package/scripts/lib/session-watch-fixture.mjs +1 -1
  30. package/scripts/longlive-doctor.mjs +6 -0
  31. package/scripts/migrate-180.mjs +163 -0
  32. package/scripts/recall-index-build.mjs +128 -0
  33. package/scripts/session-watch-drill.mjs +1 -1
  34. package/scripts/surgery-run.cmd +1 -1
  35. package/scripts/unattended-surgery.ps1 +5 -2
  36. package/scripts/verify-card-anchors.mjs +138 -0
  37. package/src/actions/audit.js +125 -0
  38. package/src/actions/evidence.js +122 -0
  39. package/src/actions/fs.js +162 -0
  40. package/src/activity/index.js +760 -0
  41. package/src/activity/model.js +351 -0
  42. package/src/activity/store.js +644 -0
  43. package/src/activity/thinking.js +176 -0
  44. package/src/adapters/constants.js +11 -0
  45. package/src/adapters/engram/data/aliases.json +51 -0
  46. package/src/adapters/engram/data/policy.json +8 -0
  47. package/src/adapters/engram/data/routing-rules.json +546 -0
  48. package/src/adapters/engram/index.js +235 -0
  49. package/src/adapters/registry.js +315 -0
  50. package/src/answer/audit.js +193 -0
  51. package/src/answer/evidence.js +30 -0
  52. package/src/answer/guard.js +1370 -0
  53. package/src/answer/index.js +1067 -0
  54. package/src/answer/query.js +111 -0
  55. package/src/answer/second-hop.mjs +101 -0
  56. package/src/auto-truncate.js +1 -1
  57. package/src/config.js +171 -47
  58. package/src/deps.js +6 -4
  59. package/src/features.js +182 -6
  60. package/src/heartbeat.js +156 -23
  61. package/src/identity/handover.js +52 -1
  62. package/src/identity/index.js +13 -5
  63. package/src/identity/inject-position.js +2 -2
  64. package/src/index.js +97 -7
  65. package/src/lifecycle/gap-clocks.js +328 -0
  66. package/src/lifecycle/index.js +269 -0
  67. package/src/live.js +22 -2
  68. package/src/memory/activity-capture.js +229 -0
  69. package/src/memory/anchors.js +194 -0
  70. package/src/memory/audit-records.js +222 -0
  71. package/src/memory/auto-cards.js +401 -0
  72. package/src/memory/capture.js +442 -0
  73. package/src/memory/card-anchors.js +203 -0
  74. package/src/memory/conflicts.js +106 -0
  75. package/src/memory/decay.js +311 -0
  76. package/src/memory/distill.js +471 -0
  77. package/src/memory/event-index.js +917 -0
  78. package/src/memory/evidence-chain.js +295 -0
  79. package/src/memory/evidence.js +145 -0
  80. package/src/memory/filter-runner.js +1 -1
  81. package/src/memory/filter.js +5 -1
  82. package/src/memory/forget-v2.js +410 -0
  83. package/src/memory/forget.js +71 -0
  84. package/src/memory/frames-activation.js +247 -0
  85. package/src/memory/fts-query.js +174 -0
  86. package/src/memory/governance.js +604 -0
  87. package/src/memory/index.js +1 -0
  88. package/src/memory/l2-gate.js +169 -0
  89. package/src/memory/lock.js +121 -0
  90. package/src/memory/migrate.js +139 -0
  91. package/src/memory/recall-v2.js +223 -0
  92. package/src/memory/recall.js +65 -2
  93. package/src/memory/records.js +425 -0
  94. package/src/memory/retention.js +128 -0
  95. package/src/memory/review.js +124 -0
  96. package/src/memory/trust.js +88 -0
  97. package/src/memory/version-chain.js +264 -0
  98. package/src/memory/wal.js +137 -0
  99. package/src/memory/working-frame.js +516 -0
  100. package/src/metrics/classify.mjs +5 -3
  101. package/src/metrics/index.mjs +21 -1
  102. package/src/metrics/prestep-latency.mjs +300 -0
  103. package/src/metrics/store.mjs +25 -0
  104. package/src/policy-profile.js +18 -3
  105. package/src/recall/anaphora.mjs +87 -0
  106. package/src/recall/cards.mjs +7 -0
  107. package/src/recall/embedding.mjs +8 -8
  108. package/src/recall/entities.mjs +112 -0
  109. package/src/recall/index.js +235 -19
  110. package/src/recall/inject-order.mjs +10 -8
  111. package/src/recall/privacy.mjs +105 -16
  112. package/src/recall/recall-core.mjs +116 -10
  113. package/src/recall/shared-visibility.js +302 -0
  114. package/src/recall/time-normalizer.mjs +196 -0
  115. package/src/role/activation.js +191 -0
  116. package/src/role/attention.js +318 -0
  117. package/src/role/base.js +36 -0
  118. package/src/role/channels.js +423 -0
  119. package/src/role/index.js +425 -0
  120. package/src/role/interfaces.js +115 -0
  121. package/src/role/personality.js +370 -0
  122. package/src/role/readers.js +151 -0
  123. package/src/role/relationships.js +477 -0
  124. package/src/role/store.js +202 -0
  125. package/src/role/writeprotect.js +373 -0
  126. package/src/router/aliases.json +176 -25
  127. package/src/router/background/agent-runner.mjs +1 -1
  128. package/src/router/background/anchor.mjs +1 -1
  129. package/src/router/background/audit.mjs +2 -2
  130. package/src/router/background/budget-ledger.mjs +2 -2
  131. package/src/router/background/host-sim.mjs +1 -1
  132. package/src/router/background/memory.mjs +4 -4
  133. package/src/router/background/queue.mjs +1 -1
  134. package/src/router/background/retry.mjs +1 -1
  135. package/src/router/background/router.mjs +1 -1
  136. package/src/router/background/stream.mjs +1 -1
  137. package/src/router/background/tool-policy.mjs +1 -1
  138. package/src/router/design-headings.json +5 -5
  139. package/src/router/face/config.mjs +1 -1
  140. package/src/router/face/identity.mjs +3 -3
  141. package/src/router/judge.mjs +8 -7
  142. package/src/router/policies/generic.json +21 -4
  143. package/src/router/route.js +109 -22
  144. package/src/router/router.mjs +10 -6
  145. package/src/router/routing-rules.json +260 -445
  146. package/src/router/run-sim.mjs +8 -8
  147. package/src/router/runtime.js +23 -12
  148. package/src/router/service/board.mjs +95 -15
  149. package/src/router/service/config.mjs +5 -5
  150. package/src/router/service/context.mjs +1 -1
  151. package/src/router/service/hint.mjs +14 -0
  152. package/src/router/service/queue.mjs +1 -1
  153. package/src/router/service/service.mjs +27 -11
  154. package/src/router/shadow.mjs +4 -3
  155. package/src/surgery-recovery.js +2 -2
  156. package/src/tool-output.js +2 -2
  157. package/src/tools.js +39 -7
  158. package/src/truncate-visible.js +2 -2
  159. package/src/ui/actions.js +1312 -0
  160. package/src/ui/panel-api.js +367 -0
  161. package/src/ui/panel-auth.js +251 -0
  162. package/src/ui/panel-integrity.js +243 -0
  163. package/src/ui/routes-readonly.js +695 -0
  164. package/src/ui/routes.js +3 -0
  165. package/docs/DEV.md +0 -279
  166. package/docs/PRESENCE-BUS-WIRING.md +0 -491
  167. package/docs/PRESENCE-BUS.md +0 -694
  168. package/docs/SURGERY.md +0 -186
  169. package/scripts/mark-core-cards.mjs +0 -289
  170. package/scripts/pack.mjs +0 -392
  171. package/scripts/router-proposals.mjs +0 -386
  172. package/src/router/fixtures/M1.json +0 -16
  173. package/src/router/fixtures/M2.json +0 -16
  174. package/src/router/fixtures/M3.json +0 -20
  175. package/src/router/fixtures/M4.json +0 -20
  176. package/src/router/fixtures/M5.json +0 -45
  177. package/src/router/fixtures/M6.json +0 -53
  178. package/src/router/fixtures/M7.json +0 -46
  179. package/src/router/fixtures/S1.json +0 -20
  180. package/src/router/fixtures/S2.json +0 -23
  181. package/src/router/fixtures/S3.json +0 -20
  182. package/src/router/fixtures/S4.json +0 -34
  183. package/src/router/fixtures/S5.json +0 -43
  184. package/src/router/fixtures/S6.json +0 -47
  185. package/src/router/fixtures/T1.json +0 -23
  186. package/src/router/fixtures/T2.json +0 -16
  187. package/src/router/fixtures/T3.json +0 -19
  188. package/src/router/fixtures/T4.json +0 -16
  189. package/src/router/fixtures/X1.json +0 -17
  190. package/src/router/fixtures/X2.json +0 -20
  191. package/src/router/gen-design-headings.mjs +0 -107
  192. package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
  193. package/test/auto-recall.test.mjs +0 -474
  194. package/test/auto-truncate.test.mjs +0 -931
  195. package/test/autobind.test.mjs +0 -385
  196. package/test/bus.test.mjs +0 -1572
  197. package/test/cli-guard.test.mjs +0 -198
  198. package/test/conclusion.test.mjs +0 -408
  199. package/test/config-schema.test.mjs +0 -204
  200. package/test/d6-output.test.mjs +0 -194
  201. package/test/d6.test.mjs +0 -395
  202. package/test/deps.test.mjs +0 -342
  203. package/test/doctor-setup.test.mjs +0 -297
  204. package/test/embedding-recall.test.mjs +0 -347
  205. package/test/feature-flags.test.mjs +0 -568
  206. package/test/filter-shadow.test.mjs +0 -170
  207. package/test/fixtures/baseline-0.8.0-registration.json +0 -556
  208. package/test/fixtures/d6-header-tools.json +0 -1
  209. package/test/fixtures/lock-child.mjs +0 -67
  210. package/test/fixtures/pre-step-container.mjs +0 -100
  211. package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
  212. package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
  213. package/test/fixtures/profile-session-longlive-config.json +0 -20
  214. package/test/fixtures/proposal-cas-child.mjs +0 -66
  215. package/test/host-engine-status.test.mjs +0 -307
  216. package/test/hot-truncate.test.mjs +0 -1349
  217. package/test/identity-card-marker.test.mjs +0 -307
  218. package/test/identity-router/run.mjs +0 -721
  219. package/test/integration-pre-step.test.mjs +0 -553
  220. package/test/live.test.mjs +0 -1020
  221. package/test/maintenance-schedule.test.mjs +0 -538
  222. package/test/memory.test.mjs +0 -2828
  223. package/test/metrics.test.mjs +0 -478
  224. package/test/orchestrator.test.mjs +0 -266
  225. package/test/p1_probe.mjs +0 -97
  226. package/test/packaging.test.mjs +0 -182
  227. package/test/portability.test.mjs +0 -343
  228. package/test/rc4-regressions.test.mjs +0 -562
  229. package/test/rc5-regressions.test.mjs +0 -197
  230. package/test/rc6-chinese.test.mjs +0 -394
  231. package/test/router-1100/apply-off.test.mjs +0 -175
  232. package/test/router-1100/budget-reconcile.test.mjs +0 -215
  233. package/test/router-1100/config-context.test.mjs +0 -211
  234. package/test/router-1100/helpers.mjs +0 -129
  235. package/test/router-1100/hint-envelope.test.mjs +0 -150
  236. package/test/router-1100/lock-crossproc.test.mjs +0 -206
  237. package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
  238. package/test/router-1100/router-apply.test.mjs +0 -372
  239. package/test/router-1100/router-fixb.test.mjs +0 -602
  240. package/test/router-1100/router-proposals.test.mjs +0 -405
  241. package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
  242. package/test/router-1100/router-security-cr.test.mjs +0 -333
  243. package/test/router-1100/service-lifecycle.test.mjs +0 -220
  244. package/test/router-1100/tools-contract.test.mjs +0 -164
  245. package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
  246. package/test/router-1100/v6-contract.test.mjs +0 -158
  247. package/test/router-1100/v6b-contract.test.mjs +0 -293
  248. package/test/router-1100/v6c-contract.test.mjs +0 -249
  249. package/test/router-1100/wal-rotation.test.mjs +0 -375
  250. package/test/router-1100/wp1-config.test.mjs +0 -170
  251. package/test/router-1100/wp1-default-off.test.mjs +0 -77
  252. package/test/router-1100/wp1-model-status.test.mjs +0 -90
  253. package/test/router-1100/wp1-status.test.mjs +0 -220
  254. package/test/router-1100/wp1-tools.test.mjs +0 -146
  255. package/test/router-1100/wp5-cli.test.mjs +0 -240
  256. package/test/router-1100/wp5-model-auto.test.mjs +0 -418
  257. package/test/router-1100/wp6-merge.test.mjs +0 -386
  258. package/test/router-1100/wp6-wake.test.mjs +0 -502
  259. package/test/router-background/audit.test.mjs +0 -37
  260. package/test/router-background/budget-ledger.test.mjs +0 -330
  261. package/test/router-background/cross-process.test.mjs +0 -222
  262. package/test/router-background/fixtures/cross-child.mjs +0 -85
  263. package/test/router-background/fixtures/real-shapes.mjs +0 -239
  264. package/test/router-background/router.test.mjs +0 -1044
  265. package/test/router-background/stream-retry.test.mjs +0 -358
  266. package/test/router-background/tool-agent.test.mjs +0 -384
  267. package/test/router-face/bridge.test.mjs +0 -169
  268. package/test/router-face/face.test.mjs +0 -316
  269. package/test/router-failclosed.test.mjs +0 -466
  270. package/test/router-identity.test.mjs +0 -384
  271. package/test/router-l1-l2.contract.test.mjs +0 -1075
  272. package/test/router-memory-tools.test.mjs +0 -262
  273. package/test/router-policy.test.mjs +0 -225
  274. package/test/router-runtime.test.mjs +0 -438
  275. package/test/router-shadow.test.mjs +0 -287
  276. package/test/router.test.mjs +0 -903
  277. package/test/run-rc.mjs +0 -299
  278. package/test/run.mjs +0 -2469
  279. package/test/safety-defaults.test.mjs +0 -179
  280. package/test/seeded-load.test.mjs +0 -436
  281. package/test/session-original.test.mjs +0 -338
  282. package/test/surgery-recovery.test.mjs +0 -936
  283. package/test/t7_drive.mjs +0 -151
  284. package/test/tool-output.test.mjs +0 -393
  285. package/test/tool-schema.test.mjs +0 -177
  286. package/test/tools_contract.test.mjs +0 -716
  287. package/test/truncate-visible.test.mjs +0 -1150
  288. package/test/ui-flags.test.mjs +0 -477
  289. package/test/v4-hardening.test.mjs +0 -427
@@ -1,553 +0,0 @@
1
- /**
2
- * 真·集成测试:两个插件的 apply() 跑在复刻的 Cordis pre-step 容器里。
3
- *
4
- * 红队第 5 条:旧测试 0 次调用 apply()、顺序靠手拼数组。这里所有顺序断言
5
- * 都来自 PreStepBus 的 waterfall 合成(见 fixtures/pre-step-container.mjs),
6
- * 不出现 `[a, b, c]` 式的人工排序。
7
- *
8
- * 运行:node --test test/
9
- */
10
-
11
- import test from 'node:test';
12
- import assert from 'node:assert/strict';
13
- import fs from 'node:fs';
14
- import os from 'node:os';
15
- import path from 'node:path';
16
-
17
- import { apply as applyAuto } from '../src/recall/index.js';
18
- import { apply as applyIdentity } from '../src/identity/index.js';
19
- import { loadCards } from '../src/recall/cards.mjs';
20
- import { PRIVACY_HIDDEN_PATH, buildQueryText, chooseRecall, renderItemHeader } from '../src/recall/recall-core.mjs';
21
- import { isAutoMessage, isIdentityMessage } from '../src/recall/inject-order.mjs';
22
- import { PreStepBus, makeUserMessageFactory, noopLogger, textMessage } from './fixtures/pre-step-container.mjs';
23
-
24
- const BOOT_MARK = '【开机卡】';
25
- const AUTO_MARK = '【自动想起】';
26
-
27
- /** 宿主 MessageSourceMap 的完整成员(dsh-llm/lib/typert.host.js:475-477)。 */
28
- const HOST_SOURCE_KINDS = new Set([
29
- 'user', 'model', 'tool', 'system-prompt', 'model-selection', 'runtime-context',
30
- 'working-directory', 'user-approval', 'ptc-mode', 'tool-registry', 'user-rpc',
31
- 'agent-message', 'subagent-settled', 'skill-invocation', 'compact-checkpoint',
32
- 'cordis-host-runner', 'team-message', 'user-question-reply', 'goal', 'plan-mode',
33
- 'schedule', 'session-reference', 'webhook',
34
- ]);
35
-
36
- const AGENT = { session: { id: 'sess-integration', header: { origin: 'main' } } };
37
-
38
- function writeFile(root, rel, text) {
39
- const full = path.join(root, rel);
40
- fs.mkdirSync(path.dirname(full), { recursive: true });
41
- fs.writeFileSync(full, text, 'utf8');
42
- }
43
-
44
- /**
45
- * 临时记忆根:topics + handover + STATE.md。测试结束自动删。
46
- * @param {import('node:test').TestContext} t
47
- * @param {{topics?:Record<string,string>, handover?:Record<string,string>, state?:string}} [input]
48
- */
49
- function makeMemory(t, { topics = {}, handover = {}, state = null } = {}) {
50
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'auto-recall-it-'));
51
- t.after(() => fs.rmSync(root, { recursive: true, force: true }));
52
- writeFile(root, 'STATE.md', state ?? [
53
- '## 我在哪',
54
- '自动想起集成测试',
55
- '## 在等什么',
56
- '红队验收',
57
- '## 最近三件',
58
- '- 修顺序',
59
- '- 修打包',
60
- '- 修测试',
61
- ].join('\n'));
62
- for (const [name, text] of Object.entries(topics)) writeFile(root, `topics/${name}`, text);
63
- for (const [name, text] of Object.entries(handover)) writeFile(root, `handover/${name}`, text);
64
- return root;
65
- }
66
-
67
- const TOPIC_CARD = [
68
- '---',
69
- 'title: 听觉训练',
70
- 'scope_in: ["音程辨认"]',
71
- '---',
72
- '# 听觉训练',
73
- '- 每天练音程辨认,先听根音再听音程。',
74
- ].join('\n');
75
-
76
- const HANDOVER_CARD = [
77
- '---',
78
- 'title: 测试交接卡一',
79
- 'scope_in: ["测试交接卡一"]',
80
- '---',
81
- '# 测试交接卡一',
82
- '- 上次停在 auto-recall 的红队修复。',
83
- ].join('\n');
84
-
85
- function makeStack({ memoryRoot, identity = true, auto = true }) {
86
- const bus = new PreStepBus();
87
- const ctx = { on: bus.on.bind(bus), logger: noopLogger() };
88
- const factory = makeUserMessageFactory();
89
- if (identity) {
90
- applyIdentity(ctx, {
91
- memoryRoot,
92
- createUserMessage: factory,
93
- writeOnTurnEnd: false,
94
- writeOnSessionEnd: false,
95
- });
96
- }
97
- if (auto) {
98
- applyAuto(ctx, {
99
- memoryRoot,
100
- createUserMessage: factory,
101
- dirs: ['topics', 'handover'],
102
- log: false,
103
- });
104
- }
105
- return { bus, factory };
106
- }
107
-
108
- function textOf(message) {
109
- return (Array.isArray(message?.content) ? message.content : [])
110
- .filter((b) => b?.type === 'text')
111
- .map((b) => b.text)
112
- .join('\n');
113
- }
114
-
115
- function roleOf(message) {
116
- const text = textOf(message);
117
- if (text.startsWith(BOOT_MARK)) return 'boot';
118
- if (text.startsWith(AUTO_MARK)) return 'auto';
119
- if (message?.source?.kind === 'runtime-context') return 'runtime';
120
- return 'user';
121
- }
122
-
123
- function runtimeMessage(text = 'runtime context snapshot') {
124
- return textMessage(text, { role: 'user', source: { kind: 'runtime-context' } });
125
- }
126
-
127
- function legacyBootMessage(text = `${BOOT_MARK}旧版 identity 在 next() 之后追加`) {
128
- return textMessage(text, { id: 'legacy-boot' });
129
- }
130
-
131
- /* ================================================================== *
132
- * A:两插件都在 → [user, boot, runtime, auto](v3:boot 在 user 之后)
133
- * ================================================================== */
134
-
135
- test('apply() 集成 A:identity + auto 合成后 user 在首、boot 紧随其后、auto 在尾', async (t) => {
136
- const memoryRoot = makeMemory(t, { topics: { 'ear.md': TOPIC_CARD }, handover: { '2026-11-04-boot.md': HANDOVER_CARD } });
137
- const { bus } = makeStack({ memoryRoot });
138
- const user = textMessage('今天继续练音程辨认,顺便看测试交接卡一', { id: 'user-1' });
139
-
140
- const { decision, claimed } = await bus.compose({
141
- messages: [user], agent: AGENT, turn: 1, step: 1, context: runtimeMessage(),
142
- });
143
-
144
- // 顺序只由注册语义 + 插件代码合成,断言不手工拼期望数组。
145
- const order = decision.messages.map(roleOf);
146
- assert.deepEqual(order, ['user', 'boot', 'runtime', 'auto'], `实际:${order.join(',')}`);
147
- // 宿主共享的 claimed 里也只允许 [user, boot]:boot 插在真实 user 之后,不是最前。
148
- assert.deepEqual(claimed.map(roleOf), ['user', 'boot'], 'boot 必须紧跟真实 user,不能抢首位');
149
- assert.equal(decision.messages.filter((m) => roleOf(m) === 'boot').length, 1);
150
- assert.equal(decision.messages.filter((m) => roleOf(m) === 'auto').length, 1);
151
- });
152
-
153
- /* ================================================================== *
154
- * TurnNavigator 等价:宿主取该轮第一个可见 kind:'user' 节点做 prompt
155
- * (dsh-client-ui-chat/lib/client.js:8371-8383;tooltip :460-467)
156
- * ================================================================== */
157
-
158
- /**
159
- * 复刻宿主投影:user/message 且 source.kind === 'user' → kind:'user' 可见节点,
160
- * 其余(session-reference / runtime-context 等)→ context 节点(client.js:9887-9893)。
161
- * @param {object[]} messages
162
- * @returns {{kind:string, data:object}[]}
163
- */
164
- function chatNodes(messages) {
165
- return (Array.isArray(messages) ? messages : []).map((message) => ({
166
- kind: message?.source?.kind === 'user' ? 'user' : 'context',
167
- data: message,
168
- }));
169
- }
170
-
171
- /** 复刻 turnNavigationItem 的 prompt 提取:loaded.find(kind === 'user')。 */
172
- function firstVisibleUserPrompt(messages) {
173
- const user = chatNodes(messages).filter((node) => node.kind === 'user')[0];
174
- return user === undefined ? '' : textOf(user.data);
175
- }
176
-
177
- test('TurnNavigator 等价:第 1 轮 prompt 取首个非注入 user,不是开机卡', async (t) => {
178
- const memoryRoot = makeMemory(t, { topics: { 'ear.md': TOPIC_CARD }, handover: { '2026-11-04-boot.md': HANDOVER_CARD } });
179
- const { bus } = makeStack({ memoryRoot });
180
- const user = textMessage('今天继续练音程辨认,顺便看测试交接卡一', { id: 'user-1' });
181
-
182
- const { decision } = await bus.compose({
183
- messages: [user], agent: AGENT, turn: 1, step: 1, context: runtimeMessage(),
184
- });
185
-
186
- const prompt = firstVisibleUserPrompt(decision.messages);
187
- assert.ok(prompt.startsWith('今天继续练音程辨认'), `tooltip 必须是用户提问,实际:${prompt.slice(0, 40)}`);
188
- assert.doesNotMatch(prompt, /【开机卡】/, 'tooltip 不能显示开机卡');
189
- assert.equal(prompt, textOf(user), '首个可见 user 节点必须就是用户原话');
190
-
191
- // 反证:把 boot 的 source.kind 改回 v2 的 'user',同一个提取函数就会拿到开机卡
192
- // (v4 的 session-reference 不会再投影成 user 节点)。
193
- const boot = decision.messages.find((m) => roleOf(m) === 'boot');
194
- const legacyBoot = { ...boot, source: { ...boot.source, kind: 'user' } };
195
- const legacyOrder = [legacyBoot, user];
196
- assert.match(firstVisibleUserPrompt(legacyOrder), /【开机卡】/);
197
- });
198
-
199
- /* ================================================================== *
200
- * B:只有 auto → [user, runtime, auto]
201
- * ================================================================== */
202
-
203
- test('apply() 集成 B:没有 identity 时 auto 追加在 runtime 之后,不伪造 boot', async (t) => {
204
- const memoryRoot = makeMemory(t, { topics: { 'ear.md': TOPIC_CARD } });
205
- const { bus } = makeStack({ memoryRoot, identity: false });
206
- const user = textMessage('今天继续练音程辨认', { id: 'user-1' });
207
-
208
- const { decision } = await bus.compose({
209
- messages: [user], agent: AGENT, turn: 1, step: 1, context: runtimeMessage(),
210
- });
211
- assert.deepEqual(decision.messages.map(roleOf), ['user', 'runtime', 'auto']);
212
- assert.equal(decision.messages.some((m) => roleOf(m) === 'boot'), false);
213
- });
214
-
215
- /* ================================================================== *
216
- * 回归夹具:旧 identity 在 next() 之后 append → before 顺序
217
- * ================================================================== */
218
-
219
- test('旧 identity 语义(next 后 append)确实复现 [user, runtime, auto, boot]', async (t) => {
220
- const memoryRoot = makeMemory(t, { topics: { 'ear.md': TOPIC_CARD } });
221
- const { bus } = makeStack({ memoryRoot, identity: false });
222
- const user = textMessage('今天继续练音程辨认', { id: 'user-1' });
223
-
224
- // 只模拟旧实现的注入点(outer prepend + next 后 append),不模拟最终数组。
225
- bus.on('agent/pre-step', async (payload, next) => {
226
- const decision = await next();
227
- if (!decision || decision.kind === 'reject' || Number(payload?.step) !== 1) return decision;
228
- return { ...decision, messages: [...decision.messages, legacyBootMessage()] };
229
- }, { prepend: true });
230
-
231
- const { decision } = await bus.compose({
232
- messages: [user], agent: AGENT, turn: 1, step: 1, context: runtimeMessage(),
233
- });
234
- assert.deepEqual(decision.messages.map(roleOf), ['user', 'runtime', 'auto', 'boot']);
235
- const bootIndex = decision.messages.findIndex((m) => roleOf(m) === 'boot');
236
- assert.equal(bootIndex, decision.messages.length - 1, '旧语义下 boot 被追加到末尾');
237
- });
238
-
239
- /* ================================================================== *
240
- * mustFix #1:firstUserText 污染 —— v2 红 / v3 绿
241
- * ================================================================== */
242
-
243
- function firstText(message) {
244
- for (const block of Array.isArray(message?.content) ? message.content : []) {
245
- if (block?.type === 'text' && typeof block.text === 'string') return block.text;
246
- }
247
- return '';
248
- }
249
-
250
- /** v2 的 session/event 实现:只按 type 取第一条 user/message,不区分注入。 */
251
- function legacyFirstUserText(events) {
252
- const first = (Array.isArray(events) ? events : []).find((e) => e?.type === 'user/message');
253
- return first ? firstText(first.data) : '';
254
- }
255
-
256
- function userMessageEvent(message, seq) {
257
- return { type: 'user/message', data: message, seq };
258
- }
259
-
260
- const BOOT_TOKEN = '开机卡专属词';
261
- const USER_TOKEN = '真实问题词';
262
-
263
- /** 污染回归共用夹具:STATE.md 与一张 topic 都含开机卡专属词,另一张只含真实问题词。 */
264
- function makePollutionMemory(t) {
265
- const state = [
266
- '## 我在哪',
267
- `- ${BOOT_TOKEN}:这句只该出现在 STATE.md 和开机卡里。`,
268
- '## 在等什么',
269
- '- 红队验收',
270
- '## 最近三件',
271
- '- 修污染',
272
- ].join('\n');
273
- const card = (title) => [
274
- '---',
275
- `title: ${title}`,
276
- `scope_in: ["${title}"]`,
277
- '---',
278
- `# ${title}`,
279
- `- ${title}:这是测试卡。`,
280
- ].join('\n');
281
- return makeMemory(t, {
282
- state,
283
- topics: { 'boot-token.md': card(BOOT_TOKEN), 'user-token.md': card(USER_TOKEN) },
284
- });
285
- }
286
-
287
- test('污染回归:v2 把【开机卡】当 firstUserText(红),v3 跳过注入消息(绿)', async (t) => {
288
- const memoryRoot = makePollutionMemory(t);
289
- const { bus } = makeStack({ memoryRoot });
290
- const user = textMessage('今天先随便聊聊,不开工。', { id: 'user-1' });
291
-
292
- const first = await bus.compose({
293
- messages: [user], agent: AGENT, turn: 1, step: 1, context: runtimeMessage(),
294
- });
295
- const boot = first.decision.messages.find((m) => roleOf(m) === 'boot');
296
- assert.ok(boot, '第一轮应注入开机卡');
297
- assert.match(textOf(boot), new RegExp(BOOT_TOKEN), '开机卡正文带着专属词(污染源)');
298
-
299
- // v2 的现实:boot 在最前 → 事件流里第一条 user/message 就是开机卡。
300
- const events = [
301
- userMessageEvent(boot, 1),
302
- userMessageEvent(user, 2),
303
- userMessageEvent(runtimeMessage(), 3),
304
- ];
305
- const legacyText = legacyFirstUserText(events);
306
- assert.equal(legacyText, textOf(boot), '红:v2 把开机卡当首条用户消息');
307
- const cards = loadCards(memoryRoot, { dirs: ['topics'] });
308
- const legacyQuery = buildQueryText({
309
- firstUserText: legacyText,
310
- recentTexts: [`${USER_TOKEN}是什么`],
311
- cwd: process.cwd(),
312
- });
313
- const legacyHits = chooseRecall({ queryText: legacyQuery, cards, options: { minScore: 0.7 } });
314
- const legacyPaths = legacyHits.items.map((item) => item.path);
315
- assert.ok(
316
- legacyPaths.some((p) => p.includes('boot-token')),
317
- `红:污染查询会召回开机卡专属词卡,实际 ${legacyPaths.join(',')}`,
318
- );
319
-
320
- // v3:把同一段「boot 在前」的事件流喂给真实 handler;注入消息必须被跳过。
321
- for (const event of events) await bus.emit('session/event', { id: AGENT.session.id }, event);
322
- await bus.emit('session/event', { id: AGENT.session.id }, { type: 'turn/end', data: { turn: 1 } });
323
- await bus.emit('session/event', { id: AGENT.session.id }, { type: 'compaction/end' });
324
-
325
- const turn2 = textMessage(`${USER_TOKEN}是什么`, { id: 'user-2' });
326
- const second = await bus.compose({
327
- messages: [turn2], agent: AGENT, turn: 2, step: 1, context: runtimeMessage(),
328
- });
329
- const auto = second.decision.messages.find((m) => roleOf(m) === 'auto');
330
- assert.ok(auto, '第二轮应召回真实问题词卡');
331
- assert.match(textOf(auto), new RegExp(USER_TOKEN));
332
- assert.doesNotMatch(textOf(auto), new RegExp(BOOT_TOKEN), '绿:开机卡专属词不能再借 firstUserText 混进第二轮');
333
- });
334
-
335
- test('实测:v3 真实两轮事件流(user→boot→runtime)不污染第二轮查询', async (t) => {
336
- const memoryRoot = makePollutionMemory(t);
337
- const { bus } = makeStack({ memoryRoot });
338
- const turn1 = textMessage('今天先随便聊聊,不开工。', { id: 'user-1' });
339
-
340
- const first = await bus.compose({
341
- messages: [turn1], agent: AGENT, turn: 1, step: 1, context: runtimeMessage(),
342
- });
343
- assert.deepEqual(first.decision.messages.map(roleOf), ['user', 'boot', 'runtime']);
344
-
345
- // 严格按 decision.messages 顺序发事件,镜像宿主 step() 的 session 写入。
346
- let seq = 1;
347
- for (const message of first.decision.messages) {
348
- await bus.emit('session/event', { id: AGENT.session.id }, userMessageEvent(message, seq));
349
- seq += 1;
350
- }
351
- await bus.emit('session/event', { id: AGENT.session.id }, { type: 'turn/end', data: { turn: 1 } });
352
- await bus.emit('session/event', { id: AGENT.session.id }, { type: 'compaction/end' });
353
-
354
- const turn2 = textMessage(`${USER_TOKEN}是什么`, { id: 'user-2' });
355
- const second = await bus.compose({
356
- messages: [turn2], agent: AGENT, turn: 2, step: 1, context: runtimeMessage(),
357
- });
358
- const auto = second.decision.messages.find((m) => roleOf(m) === 'auto');
359
- assert.ok(auto, '第二轮应召回真实问题词卡');
360
- assert.match(textOf(auto), new RegExp(USER_TOKEN));
361
- assert.doesNotMatch(textOf(auto), new RegExp(BOOT_TOKEN));
362
-
363
- // 同一时刻 TurnNavigator 拿到的仍是用户原话。
364
- assert.equal(firstVisibleUserPrompt(first.decision.messages), textOf(turn1));
365
- });
366
-
367
- /* ================================================================== *
368
- * boot 在 user 之后 + 路径去重:auto 仍看得到,且不重复注入同一张 handover
369
- * ================================================================== */
370
-
371
- test('apply() 集成:boot 在 user 之后时 auto 仍看得到,且不重复注入同一张 handover', async (t) => {
372
- const memoryRoot = makeMemory(t, { topics: { 'ear.md': TOPIC_CARD }, handover: { '2026-11-04-boot.md': HANDOVER_CARD } });
373
- const { bus } = makeStack({ memoryRoot });
374
- const user = textMessage('测试交接卡一 和 音程辨认 都看一下', { id: 'user-1' });
375
-
376
- const { decision } = await bus.compose({
377
- messages: [user], agent: AGENT, turn: 1, step: 1, context: runtimeMessage(),
378
- });
379
- const auto = decision.messages.find((m) => roleOf(m) === 'auto');
380
- assert.ok(auto, 'auto 应该有注入');
381
- const autoText = textOf(auto);
382
- assert.match(autoText, /听觉训练/);
383
- assert.doesNotMatch(autoText, /测试交接卡一|handover\//, 'identity 已注入的 handover 卡不能再由 auto 注入');
384
- });
385
-
386
- /* ================================================================== *
387
- * 首轮 handover 不该被无条件吞掉(P2 handover-suppressed)
388
- * ================================================================== */
389
-
390
- test('apply() 集成:没装 identity 时首轮 handover 仍可被 auto 召回', async (t) => {
391
- const memoryRoot = makeMemory(t, { handover: { '2026-11-04-boot.md': HANDOVER_CARD } });
392
- const { bus } = makeStack({ memoryRoot, identity: false });
393
- const user = textMessage('测试交接卡一 接着上次的做', { id: 'user-1' });
394
-
395
- const { decision } = await bus.compose({
396
- messages: [user], agent: AGENT, turn: 1, step: 1, context: runtimeMessage(),
397
- });
398
- const auto = decision.messages.find((m) => roleOf(m) === 'auto');
399
- assert.ok(auto, '首轮 handover 应该能被召回,而不是被 sessionStartExcludedPaths 吞掉');
400
- assert.match(textOf(auto), /测试交接卡一/);
401
- });
402
-
403
- /* ================================================================== *
404
- * PII 路径:整卡 drop;render 层再兜一层 <hidden>
405
- * ================================================================== */
406
-
407
- test('apply() 集成:路径带手机号的卡整卡 drop,注入文本不含号码', async (t) => {
408
- const piiCard = [
409
- '---',
410
- 'title: 订单备注',
411
- 'scope_in: ["订单备注"]',
412
- '---',
413
- '- 订单备注:按常规口径归档。',
414
- ].join('\n');
415
- const memoryRoot = makeMemory(t, { topics: { '13900002222-订单.md': piiCard } });
416
- const { bus } = makeStack({ memoryRoot, identity: false });
417
- const user = textMessage('订单备注 该怎么归档', { id: 'user-1' });
418
-
419
- const { decision } = await bus.compose({
420
- messages: [user], agent: AGENT, turn: 1, step: 1, context: runtimeMessage(),
421
- });
422
- assert.deepEqual(decision.messages.map(roleOf), ['user', 'runtime']);
423
- assert.doesNotMatch(decision.messages.map(textOf).join('\n'), /13900002222/);
424
- });
425
-
426
- test('chooseRecall 路径隐私:命中文件名手机号时整卡 drop 并计入 droppedByPrivacy', () => {
427
- const result = chooseRecall({
428
- queryText: '订单备注 该怎么归档',
429
- cards: [{
430
- path: 'topics/13900002222-订单.md',
431
- title: '订单备注',
432
- body: '- 订单备注:按常规口径归档。',
433
- scopeIn: ['订单备注'],
434
- aliases: [],
435
- }],
436
- options: { minScore: 0 },
437
- });
438
- assert.equal(result.items.length, 0);
439
- assert.equal(result.hits.droppedByPrivacy, 1);
440
- });
441
-
442
- test('renderItemHeader 对路径兜底:命中 PII 只显示 <hidden>', () => {
443
- const header = renderItemHeader({
444
- path: 'topics/13900002222-订单.md',
445
- title: '订单备注',
446
- score: 1.5,
447
- });
448
- assert.ok(header.includes(PRIVACY_HIDDEN_PATH));
449
- assert.doesNotMatch(header, /13900002222/);
450
-
451
- const clean = renderItemHeader({ path: 'topics/ear.md', title: '听觉训练', score: 1.5 });
452
- assert.match(clean, /topics\/ear\.md/);
453
- });
454
-
455
- /* ================================================================== *
456
- * 零命中沉默
457
- * ================================================================== */
458
-
459
- test('apply() 集成:零命中时一个字段都不改(decision 原样返回)', async (t) => {
460
- const memoryRoot = makeMemory(t, { topics: { 'ear.md': TOPIC_CARD } });
461
- const { bus } = makeStack({ memoryRoot, identity: false });
462
- const user = textMessage('今天晚饭吃什么', { id: 'user-1' });
463
-
464
- const { decision } = await bus.compose({
465
- messages: [user], agent: AGENT, turn: 1, step: 1, context: runtimeMessage(),
466
- });
467
- assert.deepEqual(decision.messages.map(roleOf), ['user', 'runtime']);
468
- assert.equal(decision.messages.some((m) => isAutoMessage(m)), false);
469
- });
470
-
471
- /* ================================================================== *
472
- * source.kind 契约
473
- * ================================================================== */
474
-
475
- test('apply() 集成:注入消息 source.kind 是 session-reference,形状 form/version/references 完整', async (t) => {
476
- const memoryRoot = makeMemory(t, { topics: { 'ear.md': TOPIC_CARD }, handover: { '2026-11-04-boot.md': HANDOVER_CARD } });
477
- const { bus } = makeStack({ memoryRoot });
478
- const user = textMessage('今天继续练音程辨认', { id: 'user-1' });
479
-
480
- const { decision } = await bus.compose({
481
- messages: [user], agent: AGENT, turn: 1, step: 1, context: runtimeMessage(),
482
- });
483
- const injected = decision.messages.filter((m) => isAutoMessage(m) || isIdentityMessage(m));
484
- assert.equal(injected.length, 2, '应各有一条 auto / identity 注入');
485
- for (const message of injected) {
486
- assert.equal(message.source?.kind, 'session-reference');
487
- assert.ok(HOST_SOURCE_KINDS.has(message.source.kind), `kind=${message.source.kind} 不在封闭 union`);
488
- assert.equal(message.source.form, 'recall');
489
- assert.equal(message.source.version, 1);
490
- assert.deepEqual(message.source.references, []);
491
- assert.equal(message.source.sections, undefined);
492
- assert.equal(message.source.cardPaths, undefined);
493
- }
494
- });
495
-
496
- /* ================================================================== *
497
- * reject 时撤回 boot(不在 claimed 里留半个注入)
498
- * ================================================================== */
499
-
500
- test('apply() 集成:下游 reject 时 identity 撤回已 prepend 的 boot', async (t) => {
501
- const memoryRoot = makeMemory(t, { handover: { '2026-11-04-boot.md': HANDOVER_CARD } });
502
- const { bus } = makeStack({ memoryRoot, identity: true, auto: false });
503
- bus.on('agent/pre-step', async () => ({ kind: 'reject', reason: 'test-reject' }), { prepend: false });
504
- const user = textMessage('任意消息', { id: 'user-1' });
505
-
506
- const { decision, claimed } = await bus.compose({
507
- messages: [user], agent: AGENT, turn: 1, step: 1, context: runtimeMessage(),
508
- });
509
- assert.equal(decision.kind, 'reject');
510
- assert.deepEqual(claimed.map(roleOf), ['user'], 'reject 后 claimed 里不能残留 boot');
511
- });
512
-
513
- /* ================================================================== *
514
- * 类型契约:直接读宿主 MessageSourceMap 的声明,不靠手抄列表
515
- * ================================================================== */
516
-
517
- function declaredString(source, name) {
518
- const at = source.indexOf(`"name": "${name}"`);
519
- if (at === -1) return null;
520
- const key = '"declaration": "';
521
- const start = source.indexOf(key, at);
522
- if (start === -1) return null;
523
- let i = start + key.length;
524
- let escaped = false;
525
- let raw = '';
526
- for (; i < source.length; i += 1) {
527
- const ch = source[i];
528
- if (escaped) { raw += `\\${ch}`; escaped = false; continue; }
529
- if (ch === '\\') { escaped = true; continue; }
530
- if (ch === '"') break;
531
- raw += ch;
532
- }
533
- try {
534
- return JSON.parse(`"${raw}"`);
535
- } catch {
536
- return null;
537
- }
538
- }
539
-
540
- test('宿主类型:MessageSourceMap 是封闭 union,包含 user、不含 plugin:*', (t) => {
541
- const candidates = [
542
- process.env.DSH_LLM_TYPERT,
543
- 'C:/work/redteam2/asar-extract/dsh/node_modules/@deepseek-ai/dsh-llm/lib/typert.host.js',
544
- ].filter(Boolean);
545
- const file = candidates.find((p) => fs.existsSync(p));
546
- if (!file) return t.skip('没有宿主类型文件(设置 DSH_LLM_TYPERT 可补跑)');
547
-
548
- const declaration = declaredString(fs.readFileSync(file, 'utf8'), 'MessageSourceMap');
549
- assert.ok(declaration, '没能解析出 MessageSourceMap 声明');
550
- assert.match(declaration, /user:\s*\{\s*kind:\s*'user';\s*\}/);
551
- assert.doesNotMatch(declaration, /plugin:/, '宿主 union 里不应有 plugin:*');
552
- // 本测试与 apply 集成里的 HOST_SOURCE_KINDS 互相独立:一个读声明,一个读运行时消息。
553
- });