@jjh32456/dsh-longlive 1.6.8 → 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 (288) hide show
  1. package/CHANGELOG.md +69 -1539
  2. package/LICENSE +1 -1
  3. package/QUICKSTART.md +33 -12
  4. package/README.en.md +17 -7
  5. package/README.md +29 -10
  6. package/SPEC.md +5 -5
  7. package/cordis.patch.yml +17 -12
  8. package/docs/ANSWER-GUARD.md +92 -0
  9. package/docs/ARCHITECTURE.md +3 -3
  10. package/docs/AUTO-RECALL-WIRING.md +1 -1
  11. package/docs/AUTO-RECALL.md +15 -9
  12. package/docs/CONFIG.md +143 -37
  13. package/docs/ERRORS.md +17 -5
  14. package/docs/FAQ.md +7 -7
  15. package/docs/FEATURES.md +66 -29
  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 +3 -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 +42 -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 +129 -18
  41. package/src/activity/model.js +11 -1
  42. package/src/activity/store.js +22 -3
  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 +145 -48
  58. package/src/deps.js +6 -4
  59. package/src/features.js +158 -8
  60. package/src/identity/index.js +13 -5
  61. package/src/identity/inject-position.js +2 -2
  62. package/src/index.js +97 -7
  63. package/src/lifecycle/gap-clocks.js +328 -0
  64. package/src/lifecycle/index.js +269 -0
  65. package/src/memory/activity-capture.js +229 -0
  66. package/src/memory/anchors.js +194 -0
  67. package/src/memory/audit-records.js +222 -0
  68. package/src/memory/auto-cards.js +401 -0
  69. package/src/memory/capture.js +442 -0
  70. package/src/memory/card-anchors.js +203 -0
  71. package/src/memory/conflicts.js +106 -0
  72. package/src/memory/decay.js +311 -0
  73. package/src/memory/distill.js +471 -0
  74. package/src/memory/event-index.js +917 -0
  75. package/src/memory/evidence-chain.js +295 -0
  76. package/src/memory/evidence.js +145 -0
  77. package/src/memory/filter-runner.js +1 -1
  78. package/src/memory/filter.js +5 -1
  79. package/src/memory/forget-v2.js +410 -0
  80. package/src/memory/forget.js +71 -0
  81. package/src/memory/frames-activation.js +247 -0
  82. package/src/memory/fts-query.js +174 -0
  83. package/src/memory/governance.js +604 -0
  84. package/src/memory/index.js +1 -0
  85. package/src/memory/l2-gate.js +169 -0
  86. package/src/memory/lock.js +121 -0
  87. package/src/memory/migrate.js +139 -0
  88. package/src/memory/recall-v2.js +223 -0
  89. package/src/memory/recall.js +65 -2
  90. package/src/memory/records.js +425 -0
  91. package/src/memory/retention.js +128 -0
  92. package/src/memory/review.js +124 -0
  93. package/src/memory/trust.js +88 -0
  94. package/src/memory/version-chain.js +264 -0
  95. package/src/memory/wal.js +137 -0
  96. package/src/memory/working-frame.js +516 -0
  97. package/src/metrics/classify.mjs +5 -3
  98. package/src/metrics/index.mjs +21 -1
  99. package/src/metrics/prestep-latency.mjs +300 -0
  100. package/src/metrics/store.mjs +25 -0
  101. package/src/policy-profile.js +18 -3
  102. package/src/recall/anaphora.mjs +87 -0
  103. package/src/recall/cards.mjs +7 -0
  104. package/src/recall/embedding.mjs +8 -8
  105. package/src/recall/entities.mjs +112 -0
  106. package/src/recall/index.js +235 -19
  107. package/src/recall/inject-order.mjs +10 -8
  108. package/src/recall/privacy.mjs +1 -1
  109. package/src/recall/recall-core.mjs +116 -10
  110. package/src/recall/shared-visibility.js +302 -0
  111. package/src/recall/time-normalizer.mjs +196 -0
  112. package/src/role/activation.js +191 -0
  113. package/src/role/attention.js +318 -0
  114. package/src/role/base.js +36 -0
  115. package/src/role/channels.js +423 -0
  116. package/src/role/index.js +425 -0
  117. package/src/role/interfaces.js +115 -0
  118. package/src/role/personality.js +370 -0
  119. package/src/role/readers.js +151 -0
  120. package/src/role/relationships.js +477 -0
  121. package/src/role/store.js +202 -0
  122. package/src/role/writeprotect.js +373 -0
  123. package/src/router/aliases.json +176 -25
  124. package/src/router/background/agent-runner.mjs +1 -1
  125. package/src/router/background/anchor.mjs +1 -1
  126. package/src/router/background/audit.mjs +2 -2
  127. package/src/router/background/budget-ledger.mjs +2 -2
  128. package/src/router/background/host-sim.mjs +1 -1
  129. package/src/router/background/memory.mjs +4 -4
  130. package/src/router/background/queue.mjs +1 -1
  131. package/src/router/background/retry.mjs +1 -1
  132. package/src/router/background/router.mjs +1 -1
  133. package/src/router/background/stream.mjs +1 -1
  134. package/src/router/background/tool-policy.mjs +1 -1
  135. package/src/router/design-headings.json +5 -5
  136. package/src/router/face/config.mjs +1 -1
  137. package/src/router/face/identity.mjs +3 -3
  138. package/src/router/judge.mjs +8 -7
  139. package/src/router/policies/generic.json +21 -4
  140. package/src/router/route.js +109 -22
  141. package/src/router/router.mjs +10 -6
  142. package/src/router/routing-rules.json +260 -445
  143. package/src/router/run-sim.mjs +8 -8
  144. package/src/router/runtime.js +23 -12
  145. package/src/router/service/config.mjs +5 -5
  146. package/src/router/service/context.mjs +1 -1
  147. package/src/router/service/queue.mjs +1 -1
  148. package/src/router/shadow.mjs +4 -3
  149. package/src/surgery-recovery.js +2 -2
  150. package/src/tool-output.js +2 -2
  151. package/src/tools.js +39 -7
  152. package/src/truncate-visible.js +2 -2
  153. package/src/ui/actions.js +1312 -0
  154. package/src/ui/panel-api.js +367 -0
  155. package/src/ui/panel-auth.js +251 -0
  156. package/src/ui/panel-integrity.js +243 -0
  157. package/src/ui/routes-readonly.js +695 -0
  158. package/src/ui/routes.js +3 -0
  159. package/docs/DEV.md +0 -279
  160. package/docs/PRESENCE-BUS-WIRING.md +0 -491
  161. package/docs/PRESENCE-BUS.md +0 -694
  162. package/docs/SURGERY.md +0 -186
  163. package/scripts/mark-core-cards.mjs +0 -289
  164. package/scripts/pack.mjs +0 -392
  165. package/scripts/router-proposals.mjs +0 -386
  166. package/src/router/fixtures/M1.json +0 -16
  167. package/src/router/fixtures/M2.json +0 -16
  168. package/src/router/fixtures/M3.json +0 -20
  169. package/src/router/fixtures/M4.json +0 -20
  170. package/src/router/fixtures/M5.json +0 -45
  171. package/src/router/fixtures/M6.json +0 -53
  172. package/src/router/fixtures/M7.json +0 -46
  173. package/src/router/fixtures/S1.json +0 -20
  174. package/src/router/fixtures/S2.json +0 -23
  175. package/src/router/fixtures/S3.json +0 -20
  176. package/src/router/fixtures/S4.json +0 -34
  177. package/src/router/fixtures/S5.json +0 -43
  178. package/src/router/fixtures/S6.json +0 -47
  179. package/src/router/fixtures/T1.json +0 -23
  180. package/src/router/fixtures/T2.json +0 -16
  181. package/src/router/fixtures/T3.json +0 -19
  182. package/src/router/fixtures/T4.json +0 -16
  183. package/src/router/fixtures/X1.json +0 -17
  184. package/src/router/fixtures/X2.json +0 -20
  185. package/src/router/gen-design-headings.mjs +0 -107
  186. package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
  187. package/test/activity-realtime.test.mjs +0 -959
  188. package/test/activity-stream.test.mjs +0 -512
  189. package/test/auto-recall.test.mjs +0 -474
  190. package/test/auto-truncate.test.mjs +0 -931
  191. package/test/autobind.test.mjs +0 -399
  192. package/test/bus.test.mjs +0 -1572
  193. package/test/cli-guard.test.mjs +0 -198
  194. package/test/conclusion.test.mjs +0 -408
  195. package/test/config-schema.test.mjs +0 -210
  196. package/test/d6-output.test.mjs +0 -194
  197. package/test/d6.test.mjs +0 -395
  198. package/test/deps.test.mjs +0 -342
  199. package/test/doctor-setup.test.mjs +0 -297
  200. package/test/embedding-recall.test.mjs +0 -347
  201. package/test/feature-flags.test.mjs +0 -585
  202. package/test/filter-shadow.test.mjs +0 -170
  203. package/test/fixtures/activity-append-child.mjs +0 -29
  204. package/test/fixtures/baseline-0.8.0-registration.json +0 -556
  205. package/test/fixtures/d6-header-tools.json +0 -1
  206. package/test/fixtures/lock-child.mjs +0 -67
  207. package/test/fixtures/pre-step-container.mjs +0 -100
  208. package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
  209. package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
  210. package/test/fixtures/profile-session-longlive-config.json +0 -20
  211. package/test/fixtures/proposal-cas-child.mjs +0 -66
  212. package/test/host-engine-status.test.mjs +0 -307
  213. package/test/hot-truncate.test.mjs +0 -1349
  214. package/test/identity-card-marker.test.mjs +0 -307
  215. package/test/identity-router/run.mjs +0 -721
  216. package/test/integration-pre-step.test.mjs +0 -553
  217. package/test/live.test.mjs +0 -1023
  218. package/test/maintenance-schedule.test.mjs +0 -538
  219. package/test/memory.test.mjs +0 -2828
  220. package/test/metrics.test.mjs +0 -478
  221. package/test/orchestrator.test.mjs +0 -266
  222. package/test/p1_probe.mjs +0 -97
  223. package/test/packaging.test.mjs +0 -184
  224. package/test/portability.test.mjs +0 -343
  225. package/test/rc4-regressions.test.mjs +0 -562
  226. package/test/rc5-regressions.test.mjs +0 -197
  227. package/test/rc6-chinese.test.mjs +0 -394
  228. package/test/router-1100/apply-off.test.mjs +0 -175
  229. package/test/router-1100/budget-reconcile.test.mjs +0 -215
  230. package/test/router-1100/config-context.test.mjs +0 -211
  231. package/test/router-1100/helpers.mjs +0 -129
  232. package/test/router-1100/hint-envelope.test.mjs +0 -150
  233. package/test/router-1100/lock-crossproc.test.mjs +0 -206
  234. package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
  235. package/test/router-1100/router-apply.test.mjs +0 -372
  236. package/test/router-1100/router-fixb.test.mjs +0 -602
  237. package/test/router-1100/router-proposals.test.mjs +0 -405
  238. package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
  239. package/test/router-1100/router-security-cr.test.mjs +0 -333
  240. package/test/router-1100/service-lifecycle.test.mjs +0 -220
  241. package/test/router-1100/tools-contract.test.mjs +0 -164
  242. package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
  243. package/test/router-1100/v6-contract.test.mjs +0 -158
  244. package/test/router-1100/v6b-contract.test.mjs +0 -293
  245. package/test/router-1100/v6c-contract.test.mjs +0 -249
  246. package/test/router-1100/wal-rotation.test.mjs +0 -375
  247. package/test/router-1100/wp1-config.test.mjs +0 -170
  248. package/test/router-1100/wp1-default-off.test.mjs +0 -77
  249. package/test/router-1100/wp1-model-status.test.mjs +0 -90
  250. package/test/router-1100/wp1-status.test.mjs +0 -220
  251. package/test/router-1100/wp1-tools.test.mjs +0 -146
  252. package/test/router-1100/wp5-cli.test.mjs +0 -240
  253. package/test/router-1100/wp5-model-auto.test.mjs +0 -418
  254. package/test/router-1100/wp6-merge.test.mjs +0 -386
  255. package/test/router-1100/wp6-wake.test.mjs +0 -502
  256. package/test/router-background/audit.test.mjs +0 -37
  257. package/test/router-background/budget-ledger.test.mjs +0 -330
  258. package/test/router-background/cross-process.test.mjs +0 -222
  259. package/test/router-background/fixtures/cross-child.mjs +0 -85
  260. package/test/router-background/fixtures/real-shapes.mjs +0 -239
  261. package/test/router-background/router.test.mjs +0 -1076
  262. package/test/router-background/stream-retry.test.mjs +0 -358
  263. package/test/router-background/tool-agent.test.mjs +0 -384
  264. package/test/router-face/bridge.test.mjs +0 -169
  265. package/test/router-face/face.test.mjs +0 -316
  266. package/test/router-failclosed.test.mjs +0 -466
  267. package/test/router-identity.test.mjs +0 -384
  268. package/test/router-l1-l2.contract.test.mjs +0 -1075
  269. package/test/router-memory-tools.test.mjs +0 -262
  270. package/test/router-policy.test.mjs +0 -225
  271. package/test/router-runtime.test.mjs +0 -438
  272. package/test/router-shadow.test.mjs +0 -287
  273. package/test/router.test.mjs +0 -903
  274. package/test/run-rc.mjs +0 -315
  275. package/test/run.mjs +0 -2469
  276. package/test/safety-defaults.test.mjs +0 -181
  277. package/test/seeded-load.test.mjs +0 -436
  278. package/test/session-original.test.mjs +0 -338
  279. package/test/state-truth.test.mjs +0 -937
  280. package/test/surgery-recovery.test.mjs +0 -936
  281. package/test/t7_drive.mjs +0 -151
  282. package/test/tool-output.test.mjs +0 -393
  283. package/test/tool-schema.test.mjs +0 -177
  284. package/test/tools_contract.test.mjs +0 -716
  285. package/test/truncate-visible.test.mjs +0 -1150
  286. package/test/ui-flags.test.mjs +0 -477
  287. package/test/v4-hardening.test.mjs +0 -427
  288. package/test/version-consistency.test.mjs +0 -286
@@ -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
- });