@modusensus/dsh-mneme 0.13.0 → 0.14.0-beta.2

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 (543) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +830 -828
  3. package/assets/icon.svg +3 -3
  4. package/bin/cli.mjs +964 -964
  5. package/bin/dsh-mneme-choro.mjs +262 -254
  6. package/bin/dsh-mneme-mcp.mjs +510 -510
  7. package/bin/dsh-mneme-serve.mjs +151 -151
  8. package/cordis.patch.yml +21 -21
  9. package/docs/CONFIGURATION.md +407 -402
  10. package/docs/CONTINUITY.md +139 -139
  11. package/docs/DAEMON.md +57 -57
  12. package/docs/DIRECTORIES.md +114 -114
  13. package/docs/DISTILL.md +397 -371
  14. package/docs/DSH-GO.md +97 -97
  15. package/docs/ENTITIES.md +247 -247
  16. package/docs/FORGET.md +77 -77
  17. package/docs/KB.md +110 -110
  18. package/docs/LOCAL_MODEL.md +222 -222
  19. package/docs/SEMANTIC.md +256 -256
  20. package/docs/SHARE.md +552 -552
  21. package/docs/SLEEP.md +202 -202
  22. package/docs/STORAGE.md +122 -122
  23. package/docs/WORKFLOWS.md +366 -366
  24. package/lib/api-standalone.js +1007 -1007
  25. package/lib/api.js +3600 -3394
  26. package/lib/attachment-text.js +122 -122
  27. package/lib/bootstrap-http.js +105 -105
  28. package/lib/bootstrap.js +215 -215
  29. package/lib/candidates.js +454 -454
  30. package/lib/capabilities.js +819 -819
  31. package/lib/choro-command.js +111 -111
  32. package/lib/client.js +14420 -13436
  33. package/lib/commands.js +95 -95
  34. package/lib/config-keys.js +144 -144
  35. package/lib/config.js +1361 -1317
  36. package/lib/content-hash.js +54 -54
  37. package/lib/continuity.js +258 -258
  38. package/lib/date-hint.js +32 -0
  39. package/lib/diary/auto.js +190 -190
  40. package/lib/diary/compose.js +440 -440
  41. package/lib/diary/dir.js +59 -59
  42. package/lib/diary/export.js +97 -97
  43. package/lib/diary/index.js +26 -26
  44. package/lib/diary/library.js +189 -189
  45. package/lib/diary/qq-inbox.js +142 -142
  46. package/lib/diary/rollover.js +61 -61
  47. package/lib/diary/session-provider.js +122 -122
  48. package/lib/document-index.js +94 -94
  49. package/lib/document.js +322 -322
  50. package/lib/dream/clustering.js +123 -123
  51. package/lib/dream/decisions.js +945 -907
  52. package/lib/dream/narratives.js +44 -44
  53. package/lib/dream/sleep.js +2027 -1887
  54. package/lib/dream/triage.js +139 -0
  55. package/lib/dream.js +1879 -1764
  56. package/lib/dsh-go/host.js +162 -162
  57. package/lib/dsh-go/panel.js +2091 -2091
  58. package/lib/dsh-go/text.js +342 -342
  59. package/lib/dsh-go/views/common.js +68 -68
  60. package/lib/dsh-go/views/entity.js +48 -48
  61. package/lib/dsh-go/views/memory.js +198 -198
  62. package/lib/dsh-go/views/root.js +30 -30
  63. package/lib/dsh-go/views/settings.js +398 -398
  64. package/lib/dsh-go/views/status.js +51 -51
  65. package/lib/embedding.js +342 -342
  66. package/lib/entities/extractor.js +285 -285
  67. package/lib/fence.js +24 -24
  68. package/lib/graph/anchoring.js +184 -184
  69. package/lib/guide.js +74 -74
  70. package/lib/heat.js +144 -144
  71. package/lib/hot-memory.js +119 -107
  72. package/lib/image-describe.js +158 -0
  73. package/lib/index.js +852 -822
  74. package/lib/inject.js +656 -639
  75. package/lib/kb/index.js +309 -309
  76. package/lib/kb/routes.js +219 -219
  77. package/lib/kb/tools.js +148 -148
  78. package/lib/lang.js +607 -584
  79. package/lib/local-embedder.js +358 -358
  80. package/lib/maintenance.js +162 -162
  81. package/lib/memory-source.js +9 -9
  82. package/lib/mirror.js +330 -330
  83. package/lib/organize.js +456 -456
  84. package/lib/peak-hours.js +172 -172
  85. package/lib/pipeline-activities.js +178 -178
  86. package/lib/pipeline-attribution.js +165 -165
  87. package/lib/pipeline-command.js +279 -279
  88. package/lib/pipeline-confirm.js +142 -142
  89. package/lib/pipeline-exec.js +174 -174
  90. package/lib/pipeline-nudge-auto.js +129 -129
  91. package/lib/pipeline-nudge.js +1623 -1623
  92. package/lib/pipeline-observations.js +181 -181
  93. package/lib/pipeline-observe.js +133 -133
  94. package/lib/pipeline-runner.js +361 -361
  95. package/lib/pipeline-runs.js +442 -442
  96. package/lib/pipeline.js +716 -716
  97. package/lib/placement.js +13 -13
  98. package/lib/pre-inject-gate.js +354 -354
  99. package/lib/profile-extract.js +164 -164
  100. package/lib/profile-facts.js +356 -356
  101. package/lib/provenance-http.js +251 -251
  102. package/lib/quality-filter.js +162 -162
  103. package/lib/recall-stats.js +190 -190
  104. package/lib/reclaim-http.js +150 -150
  105. package/lib/reranker.js +269 -269
  106. package/lib/runtime/adopt.js +176 -176
  107. package/lib/runtime/closure.js +172 -172
  108. package/lib/runtime/download.js +247 -247
  109. package/lib/runtime/layout.js +265 -265
  110. package/lib/runtime/loader.js +252 -252
  111. package/lib/runtime/model-fetch.js +361 -361
  112. package/lib/runtime/provision.js +237 -237
  113. package/lib/runtime/tarball.js +130 -130
  114. package/lib/runtime/verify.js +223 -223
  115. package/lib/safe-slice.js +18 -18
  116. package/lib/scope-state.js +277 -277
  117. package/lib/scope.js +187 -187
  118. package/lib/search/adaptive.js +60 -60
  119. package/lib/search/bm25.js +96 -96
  120. package/lib/semantic.js +572 -572
  121. package/lib/sensitive-scan.js +209 -209
  122. package/lib/serve.js +228 -228
  123. package/lib/service.js +3268 -3196
  124. package/lib/session-gate.js +91 -91
  125. package/lib/session-usage.js +350 -350
  126. package/lib/settings.js +827 -802
  127. package/lib/share-http.js +281 -281
  128. package/lib/share-import.js +234 -234
  129. package/lib/share.js +335 -335
  130. package/lib/store.js +5895 -5662
  131. package/lib/summarize.js +1388 -1343
  132. package/lib/tools.js +1370 -1356
  133. package/lib/user-settings.js +230 -230
  134. package/lib/vector-index.js +180 -180
  135. package/lib/version-check.js +122 -122
  136. package/lib/wiki/claims.js +267 -0
  137. package/lib/wiki/cli-hint.js +90 -90
  138. package/lib/wiki/inject.js +164 -164
  139. package/lib/wiki/partition.js +200 -200
  140. package/lib/wiki/plan.js +214 -214
  141. package/lib/wiki/queries.js +640 -474
  142. package/lib/wiki/render.js +261 -187
  143. package/lib/wiki/scan.js +371 -371
  144. package/lib/wiki/t0.js +500 -463
  145. package/lib/wiki/t1-jobs.js +232 -232
  146. package/lib/wiki/t1.js +609 -502
  147. package/lib/write-admission.js +404 -404
  148. package/locale/en.json +6 -6
  149. package/locale/zh.json +6 -6
  150. package/package.json +15 -1
  151. package/runtime-manifest.json +526 -526
  152. package/scripts/benchmark-embed.js +201 -201
  153. package/scripts/benchmark-recall.js +249 -249
  154. package/scripts/benchmark-rerank.js +166 -166
  155. package/scripts/build-runtime-manifest.mjs +224 -224
  156. package/scripts/changelog-prep.mjs +91 -91
  157. package/scripts/check-sync.js +44 -44
  158. package/scripts/e2e-dsh.js +220 -220
  159. package/scripts/mneme-runtime.mjs +277 -277
  160. package/scripts/publish-gate.mjs +197 -197
  161. package/scripts/release-docs.mjs +210 -204
  162. package/scripts/release-notes.mjs +164 -161
  163. package/scripts/stress-dsh.js +255 -255
  164. package/scripts/sync-lib.js +47 -47
  165. package/scripts/sync-test-badge.mjs +69 -69
  166. package/scripts/test-count-sync.mjs +48 -48
  167. package/src/api-standalone.js +1007 -1007
  168. package/src/api.js +3600 -3394
  169. package/src/attachment-text.js +122 -122
  170. package/src/bootstrap-http.js +105 -105
  171. package/src/bootstrap.js +215 -215
  172. package/src/candidates.js +454 -454
  173. package/src/capabilities.js +819 -819
  174. package/src/choro-command.js +111 -111
  175. package/src/commands.js +95 -95
  176. package/src/config-keys.js +144 -144
  177. package/src/config.js +1361 -1317
  178. package/src/content-hash.js +54 -54
  179. package/src/continuity.js +258 -258
  180. package/src/date-hint.js +32 -0
  181. package/src/diary/auto.js +190 -190
  182. package/src/diary/compose.js +440 -440
  183. package/src/diary/dir.js +59 -59
  184. package/src/diary/export.js +97 -97
  185. package/src/diary/index.js +26 -26
  186. package/src/diary/library.js +189 -189
  187. package/src/diary/qq-inbox.js +142 -142
  188. package/src/diary/rollover.js +61 -61
  189. package/src/diary/session-provider.js +122 -122
  190. package/src/document-index.js +94 -94
  191. package/src/document.js +322 -322
  192. package/src/dream/clustering.js +123 -123
  193. package/src/dream/decisions.js +945 -907
  194. package/src/dream/narratives.js +44 -44
  195. package/src/dream/sleep.js +2027 -1887
  196. package/src/dream/triage.js +139 -0
  197. package/src/dream.js +1879 -1764
  198. package/src/dsh-go/host.js +162 -162
  199. package/src/dsh-go/panel.js +2091 -2091
  200. package/src/dsh-go/text.js +342 -342
  201. package/src/dsh-go/views/common.js +68 -68
  202. package/src/dsh-go/views/entity.js +48 -48
  203. package/src/dsh-go/views/memory.js +198 -198
  204. package/src/dsh-go/views/root.js +30 -30
  205. package/src/dsh-go/views/settings.js +398 -398
  206. package/src/dsh-go/views/status.js +51 -51
  207. package/src/embedding.js +342 -342
  208. package/src/entities/extractor.js +285 -285
  209. package/src/fence.js +24 -24
  210. package/src/graph/anchoring.js +184 -184
  211. package/src/guide.js +74 -74
  212. package/src/heat.js +144 -144
  213. package/src/hot-memory.js +119 -107
  214. package/src/image-describe.js +158 -0
  215. package/src/index.js +852 -822
  216. package/src/inject.js +656 -639
  217. package/src/kb/index.js +309 -309
  218. package/src/kb/routes.js +219 -219
  219. package/src/kb/tools.js +148 -148
  220. package/src/lang.js +607 -584
  221. package/src/local-embedder.js +358 -358
  222. package/src/maintenance.js +162 -162
  223. package/src/memory-source.js +9 -9
  224. package/src/mirror.js +330 -330
  225. package/src/organize.js +456 -456
  226. package/src/peak-hours.js +172 -172
  227. package/src/pipeline-activities.js +178 -178
  228. package/src/pipeline-attribution.js +165 -165
  229. package/src/pipeline-command.js +279 -279
  230. package/src/pipeline-confirm.js +142 -142
  231. package/src/pipeline-exec.js +174 -174
  232. package/src/pipeline-nudge-auto.js +129 -129
  233. package/src/pipeline-nudge.js +1623 -1623
  234. package/src/pipeline-observations.js +181 -181
  235. package/src/pipeline-observe.js +133 -133
  236. package/src/pipeline-runner.js +361 -361
  237. package/src/pipeline-runs.js +442 -442
  238. package/src/pipeline.js +716 -716
  239. package/src/placement.js +13 -13
  240. package/src/pre-inject-gate.js +354 -354
  241. package/src/profile-extract.js +164 -164
  242. package/src/profile-facts.js +356 -356
  243. package/src/provenance-http.js +251 -251
  244. package/src/quality-filter.js +162 -162
  245. package/src/recall-stats.js +190 -190
  246. package/src/reclaim-http.js +150 -150
  247. package/src/reranker.js +269 -269
  248. package/src/runtime/adopt.js +176 -176
  249. package/src/runtime/closure.js +172 -172
  250. package/src/runtime/download.js +247 -247
  251. package/src/runtime/layout.js +265 -265
  252. package/src/runtime/loader.js +252 -252
  253. package/src/runtime/model-fetch.js +361 -361
  254. package/src/runtime/provision.js +237 -237
  255. package/src/runtime/tarball.js +130 -130
  256. package/src/runtime/verify.js +223 -223
  257. package/src/safe-slice.js +18 -18
  258. package/src/scope-state.js +277 -277
  259. package/src/scope.js +187 -187
  260. package/src/search/adaptive.js +60 -60
  261. package/src/search/bm25.js +96 -96
  262. package/src/semantic.js +572 -572
  263. package/src/sensitive-scan.js +209 -209
  264. package/src/serve.js +228 -228
  265. package/src/service.js +3268 -3196
  266. package/src/session-gate.js +91 -91
  267. package/src/session-usage.js +350 -350
  268. package/src/settings.js +827 -802
  269. package/src/share-http.js +281 -281
  270. package/src/share-import.js +234 -234
  271. package/src/share.js +335 -335
  272. package/src/store.js +5895 -5662
  273. package/src/summarize.js +1388 -1343
  274. package/src/tools.js +1370 -1356
  275. package/src/user-settings.js +230 -230
  276. package/src/vector-index.js +180 -180
  277. package/src/version-check.js +122 -122
  278. package/src/wiki/claims.js +267 -0
  279. package/src/wiki/cli-hint.js +90 -90
  280. package/src/wiki/inject.js +164 -164
  281. package/src/wiki/partition.js +200 -200
  282. package/src/wiki/plan.js +214 -214
  283. package/src/wiki/queries.js +640 -474
  284. package/src/wiki/render.js +261 -187
  285. package/src/wiki/scan.js +371 -371
  286. package/src/wiki/t0.js +500 -463
  287. package/src/wiki/t1-jobs.js +232 -232
  288. package/src/wiki/t1.js +609 -502
  289. package/src/write-admission.js +404 -404
  290. package/test/adaptive-inject-budget.test.js +85 -85
  291. package/test/api-hardening.test.js +173 -173
  292. package/test/api-nudge-route.test.js +259 -259
  293. package/test/api-routes-gaps.test.js +1092 -1092
  294. package/test/api.test.js +3007 -2988
  295. package/test/attachment-text.test.js +139 -139
  296. package/test/audit.test.js +448 -448
  297. package/test/baseline-persist.test.js +248 -248
  298. package/test/benchmark.test.js +94 -94
  299. package/test/bm25-corpus-cache.test.js +145 -145
  300. package/test/bootstrap.test.js +228 -228
  301. package/test/candidates.test.js +345 -345
  302. package/test/capabilities.test.js +577 -577
  303. package/test/capacity-warning.test.js +262 -262
  304. package/test/changelog-prep.test.js +125 -125
  305. package/test/choro-command.test.js +218 -218
  306. package/test/cli-enums.test.js +37 -37
  307. package/test/cli-flag-value.test.js +86 -86
  308. package/test/cli-receive.test.js +390 -390
  309. package/test/cli-share.test.js +437 -437
  310. package/test/client-share.test.js +225 -225
  311. package/test/client.test.js +4595 -4184
  312. package/test/clustering.test.js +100 -100
  313. package/test/commands.test.js +106 -106
  314. package/test/config-keys-routes.test.js +233 -233
  315. package/test/config.test.js +153 -153
  316. package/test/conflict-freeze.test.js +344 -344
  317. package/test/conflict-ledger-enforce.test.js +452 -0
  318. package/test/conflict-ledger.test.js +242 -0
  319. package/test/conflict-reviewed.test.js +79 -79
  320. package/test/content-hash.test.js +204 -204
  321. package/test/continuity.test.js +452 -452
  322. package/test/contrast.test.js +432 -320
  323. package/test/date-hint.test.js +156 -0
  324. package/test/dedupe-candidate-window.test.js +180 -180
  325. package/test/dependency-overrides.test.js +78 -78
  326. package/test/diary-library.test.js +187 -187
  327. package/test/diary.test.js +576 -576
  328. package/test/differentiate-feedback.test.js +248 -248
  329. package/test/distill-tx-embed-gap.test.js +110 -110
  330. package/test/docs-config-keys.test.js +65 -65
  331. package/test/document-dir.test.js +233 -233
  332. package/test/document.test.js +703 -703
  333. package/test/dream-archive-guardrail.test.js +116 -116
  334. package/test/dream-baseline-clamp.test.js +174 -0
  335. package/test/dream-candidate-hybrid.test.js +281 -281
  336. package/test/dream-coverage-degrade.test.js +86 -86
  337. package/test/dream-decision-evidence.test.js +314 -314
  338. package/test/dream-failure-backoff.test.js +173 -173
  339. package/test/dream-fence.test.js +207 -207
  340. package/test/dream-maxtokens-fallback.test.js +132 -132
  341. package/test/dream-merge-guardrail.test.js +219 -219
  342. package/test/dream-peak-hours.test.js +194 -194
  343. package/test/dream.test.js +1528 -1528
  344. package/test/dsh-go-host.test.js +134 -134
  345. package/test/dsh-go-panel-actions.test.js +854 -854
  346. package/test/dsh-go-panel.test.js +251 -251
  347. package/test/embed-ready-poll.test.js +94 -94
  348. package/test/embed-text-reasons.test.js +112 -112
  349. package/test/embedding-cache-f32.test.js +276 -276
  350. package/test/embedding-ready.test.js +226 -226
  351. package/test/entities.test.js +711 -711
  352. package/test/entity-recall.test.js +179 -179
  353. package/test/epistemic.test.js +325 -325
  354. package/test/failure-query.test.js +147 -147
  355. package/test/failure-retention.test.js +105 -105
  356. package/test/fnew-0112.test.js +311 -311
  357. package/test/fnew-03.test.js +422 -422
  358. package/test/forget-corrections.test.js +70 -70
  359. package/test/graph-anchoring.test.js +184 -184
  360. package/test/graph-api.test.js +333 -209
  361. package/test/graph-boosted-relations.test.js +189 -189
  362. package/test/graph-inject-budget.test.js +103 -103
  363. package/test/graph-multipath.test.js +269 -269
  364. package/test/graph-passive-confirm.test.js +91 -91
  365. package/test/graph-weight-evolution.test.js +104 -104
  366. package/test/heat.test.js +175 -175
  367. package/test/helpers/dream-mock.js +86 -86
  368. package/test/helpers/peer-worker.mjs +25 -25
  369. package/test/helpers/runtime-source.js +60 -60
  370. package/test/helpers/tar-builder.js +80 -80
  371. package/test/helpers/write-admission-samples.js +312 -312
  372. package/test/hot-memory.test.js +259 -211
  373. package/test/image-description.test.js +271 -0
  374. package/test/image-memory.test.js +170 -170
  375. package/test/inject-content-cap.test.js +91 -91
  376. package/test/inject-first-round-bm25.test.js +89 -89
  377. package/test/inject-heat.test.js +77 -77
  378. package/test/inject-parent-gate.test.js +134 -106
  379. package/test/inject-pin-explicit.test.js +300 -300
  380. package/test/inject-pin-vector-crowd.test.js +225 -225
  381. package/test/inject-pools.test.js +195 -195
  382. package/test/inject-recall.test.js +172 -172
  383. package/test/inject-rotation.test.js +161 -161
  384. package/test/inject.test.js +553 -494
  385. package/test/issue-617-step1-query-and-fusion.test.js +361 -361
  386. package/test/kb-api-wiring.test.js +128 -128
  387. package/test/kb-collections.test.js +272 -272
  388. package/test/kb-routes.test.js +379 -379
  389. package/test/lang.test.js +98 -98
  390. package/test/lastrunat-persist.test.js +101 -101
  391. package/test/lib-smoke.test.js +116 -116
  392. package/test/llm-audit.test.js +815 -815
  393. package/test/llm-message-source.test.js +207 -207
  394. package/test/local-embedder.test.js +292 -292
  395. package/test/maintenance.test.js +501 -501
  396. package/test/mcp-stdio.test.js +385 -385
  397. package/test/mint-source.test.js +311 -311
  398. package/test/mirror-dirty.test.js +424 -424
  399. package/test/mirror-edit-digest.test.js +187 -187
  400. package/test/mirror-generation.test.js +499 -499
  401. package/test/mirror-types.test.js +208 -208
  402. package/test/mirror.test.js +249 -249
  403. package/test/narrative-bars.test.js +162 -162
  404. package/test/organize-tool.test.js +232 -232
  405. package/test/organize.test.js +418 -418
  406. package/test/panel-chart-scale.test.js +90 -0
  407. package/test/panel-tokens.test.js +245 -0
  408. package/test/peak-hours-observability.test.js +70 -70
  409. package/test/peer-blockers.test.js +193 -193
  410. package/test/persona-addressing.test.js +83 -83
  411. package/test/persona-api.test.js +172 -172
  412. package/test/persona-by-session.test.js +568 -568
  413. package/test/persona-flag.test.js +36 -36
  414. package/test/persona-inject.test.js +145 -145
  415. package/test/persona-settings.test.js +198 -198
  416. package/test/pipeline-activities.test.js +155 -155
  417. package/test/pipeline-attribution.test.js +175 -175
  418. package/test/pipeline-command.test.js +243 -243
  419. package/test/pipeline-confirm.test.js +148 -148
  420. package/test/pipeline-exec.test.js +172 -172
  421. package/test/pipeline-nudge-auto.test.js +248 -248
  422. package/test/pipeline-nudge.test.js +1278 -1278
  423. package/test/pipeline-observations.test.js +157 -157
  424. package/test/pipeline-observe.test.js +127 -127
  425. package/test/pipeline-r1r2.test.js +278 -278
  426. package/test/pipeline-runner.test.js +468 -468
  427. package/test/pipeline-runs.test.js +767 -767
  428. package/test/pipeline.test.js +636 -636
  429. package/test/placement.test.js +36 -36
  430. package/test/policy-epoch.test.js +259 -259
  431. package/test/pre-inject-gate.test.js +444 -444
  432. package/test/profile-extract.test.js +269 -269
  433. package/test/profile-facts.test.js +492 -492
  434. package/test/prompt-stance.test.js +128 -128
  435. package/test/provenance-http.test.js +367 -367
  436. package/test/publish-gate.test.js +272 -272
  437. package/test/quality-backfill.test.js +290 -290
  438. package/test/quality-filter.test.js +295 -295
  439. package/test/readme-claims.test.js +113 -113
  440. package/test/reasoning-effort.test.js +752 -752
  441. package/test/recall-anti-update.test.js +82 -82
  442. package/test/recall-evals.test.js +253 -253
  443. package/test/recall-layer.test.js +326 -326
  444. package/test/recall-retention.test.js +128 -128
  445. package/test/recall-stats.test.js +274 -274
  446. package/test/receipt-chain.test.js +451 -451
  447. package/test/reclaim-http.test.js +209 -209
  448. package/test/reflection-update-gate.test.js +129 -129
  449. package/test/reflection.test.js +226 -226
  450. package/test/reindex-backfill.test.js +475 -475
  451. package/test/reindex-scheduler.test.js +369 -369
  452. package/test/release-docs.test.js +156 -136
  453. package/test/release-notes.test.js +265 -240
  454. package/test/release-workflow.test.js +93 -93
  455. package/test/reranker-single-logit.test.js +73 -73
  456. package/test/reranker.test.js +197 -197
  457. package/test/resident-status.test.js +83 -83
  458. package/test/route-policy.test.js +113 -113
  459. package/test/runtime-adopt.test.js +206 -206
  460. package/test/runtime-cli.test.js +89 -89
  461. package/test/runtime-closure.test.js +193 -193
  462. package/test/runtime-download.test.js +302 -302
  463. package/test/runtime-layout.test.js +163 -163
  464. package/test/runtime-loader.test.js +255 -255
  465. package/test/runtime-manifest.test.js +159 -159
  466. package/test/runtime-model-fetch.test.js +348 -348
  467. package/test/runtime-provision.test.js +101 -101
  468. package/test/runtime-tarball.test.js +81 -81
  469. package/test/runtime-verify.test.js +319 -319
  470. package/test/scope-candidates.test.js +491 -491
  471. package/test/scope-distill-stamp.test.js +134 -134
  472. package/test/scope-explicit.test.js +452 -452
  473. package/test/scope-inject-soft.test.js +106 -106
  474. package/test/scope-optional-service.test.js +110 -110
  475. package/test/scope-retrieval.test.js +197 -197
  476. package/test/scope-state.test.js +610 -610
  477. package/test/scope-storage.test.js +255 -255
  478. package/test/scope-strict.test.js +367 -367
  479. package/test/search-fusion.test.js +90 -90
  480. package/test/security-doc.test.js +188 -178
  481. package/test/semantic.test.js +124 -124
  482. package/test/sensitive-scan.test.js +251 -251
  483. package/test/serve-bin.test.js +121 -121
  484. package/test/serve.test.js +258 -258
  485. package/test/service-search.test.js +174 -174
  486. package/test/service.test.js +410 -410
  487. package/test/session-gate.test.js +264 -264
  488. package/test/session-usage.test.js +317 -317
  489. package/test/settings.test.js +273 -273
  490. package/test/share-foreign-rows.test.js +144 -144
  491. package/test/share-http.test.js +144 -144
  492. package/test/share-import.test.js +577 -577
  493. package/test/share-imported-guard.test.js +311 -311
  494. package/test/share-imported-identity.test.js +111 -111
  495. package/test/share-pack.test.js +511 -511
  496. package/test/share-receive-http.test.js +756 -756
  497. package/test/share-routes.test.js +337 -337
  498. package/test/share-store.test.js +133 -133
  499. package/test/share-validate.test.js +101 -101
  500. package/test/skip-invalid-restore.test.js +262 -262
  501. package/test/sleep-action-set.test.js +441 -441
  502. package/test/sleep-catchup.test.js +315 -315
  503. package/test/sleep-failure-backoff.test.js +156 -156
  504. package/test/sleep-heat.test.js +174 -174
  505. package/test/sleep-scheduler-rearm.test.js +89 -89
  506. package/test/sleep.test.js +1455 -1418
  507. package/test/standalone-api.test.js +1344 -1344
  508. package/test/store-vector-cache.test.js +98 -98
  509. package/test/store-write-path-columns.test.js +191 -191
  510. package/test/store.test.js +744 -744
  511. package/test/stress.test.js +209 -209
  512. package/test/summarize-occurred-at.test.js +127 -127
  513. package/test/summarize-parse-diagnostics.test.js +75 -75
  514. package/test/summarize-reasoning-effort.test.js +189 -189
  515. package/test/summarize.test.js +2251 -2251
  516. package/test/supersede-state-tracking.test.js +394 -394
  517. package/test/test-count-sync.test.js +71 -71
  518. package/test/tool-state-ignore.test.js +65 -65
  519. package/test/tools-dto-schema-matrix.test.js +398 -398
  520. package/test/tools-memory-get-schema.test.js +65 -65
  521. package/test/tools.test.js +585 -585
  522. package/test/updated-at-semantics.test.js +113 -113
  523. package/test/user-settings-preview.test.js +127 -127
  524. package/test/user-settings-segments.test.js +527 -527
  525. package/test/vector-index.test.js +334 -334
  526. package/test/version-check.test.js +152 -152
  527. package/test/webServer-optional.test.js +82 -82
  528. package/test/wiki-api.test.js +734 -627
  529. package/test/wiki-claims.test.js +194 -0
  530. package/test/wiki-cli-hint.test.js +118 -118
  531. package/test/wiki-inject.test.js +265 -265
  532. package/test/wiki-partition.test.js +112 -112
  533. package/test/wiki-plan.test.js +80 -80
  534. package/test/wiki-provenance.test.js +202 -0
  535. package/test/wiki-queries.test.js +398 -277
  536. package/test/wiki-scan.test.js +208 -208
  537. package/test/wiki-store.test.js +190 -190
  538. package/test/wiki-t0.test.js +456 -380
  539. package/test/wiki-t1-jobs.test.js +125 -125
  540. package/test/wiki-t1.test.js +460 -235
  541. package/test/wiki-tools.test.js +176 -176
  542. package/test/workflow-guards.test.js +155 -155
  543. package/test/write-admission.test.js +668 -668
@@ -1,568 +1,568 @@
1
- // 绑会话的人格卡(#606)。为什么需要这一维:人格卡原本只有 agentPreset 一个寻址键,
2
- // 而 DS Bot 这类宿主把它写成常量——实测 bot 工作区 50 个会话里 43 个是 "standard",
3
- // 四条主 bot(Amarre / Ephor / Loom / Persis)全部相同,但它们的 session.id 各不相同。
4
- // 所以「一个 bot 一张卡」只能用会话 id 当键。
5
- //
6
- // 本文件锁四件事:
7
- // 1. 纯加法:没有 bySession 的老数据读回仍是空对象,注入文本逐字节不变。
8
- // 2. 优先级:bySession > byAgent > default(会话比预设具体)。
9
- // 3. 严格寻址的两维合判:只建了 bySession 卡时,按 byAgent 寻址的会话也不许被兜底。
10
- // 4. scenario / mesExample:未设置零输出,设置后排在 replyStyle 之后。
11
- import test from "node:test";
12
- import assert from "node:assert/strict";
13
- import { EventEmitter } from "node:events";
14
- import { createStore } from "../src/store.js";
15
- import { createService } from "../src/service.js";
16
- import { createInjector } from "../src/inject.js";
17
- import { createApi } from "../src/api.js";
18
- import { createSettings } from "../src/settings.js";
19
- import { resolvePersonaCard, renderUserSettingsBlock } from "../src/user-settings.js";
20
-
21
- class FakeRes extends EventEmitter {
22
- constructor() { super(); this.statusCode = 200; this.body = ""; }
23
- writeHead(code, headers) { this.statusCode = code; this.headers = headers; return this; }
24
- end(text) { this.body = text ?? ""; this.emit("end"); return this; }
25
- }
26
-
27
- function req(path, method = "GET") {
28
- return { url: path, method, headers: {}, on() {}, emit() {} };
29
- }
30
-
31
- // 注入器 + API 共用同一份 settings/config(同源纪律),两边都能按 {agentPreset, sessionId} 寻址。
32
- function setup(config = {}) {
33
- const store = createStore(":memory:");
34
- const service = createService({ store, mirror: null, config });
35
- const settings = createSettings(store.db);
36
- const contexts = [];
37
- const ctx = { systemPrompt: { context(def) { contexts.push(def); return () => {}; } } };
38
- createInjector(ctx, service, settings, config);
39
- const settingsCtx = contexts.find((c) => c.name === "user-settings");
40
- // 造 ctx:只给要测的那几个键,缺省不给(模拟宿主解析不到)。
41
- const makeCtx = ({ agentPreset = null, sessionId = null, parentSession = null } = {}) => {
42
- if (!agentPreset && !sessionId && !parentSession) return {};
43
- const session = {};
44
- if (sessionId) session.id = sessionId;
45
- if (agentPreset || parentSession) {
46
- session.header = {};
47
- if (agentPreset) session.header.agentPreset = agentPreset;
48
- if (parentSession) session.header.parentSession = parentSession;
49
- }
50
- return { agent: { session } };
51
- };
52
- const injectorText = (opts) => settingsCtx.text(makeCtx(opts));
53
- const routes = [];
54
- createApi(
55
- { webServer: { register: (route) => { routes.push(route); return () => {}; } } },
56
- service,
57
- settings,
58
- { add: () => {}, remove: () => {}, list: () => [] },
59
- undefined,
60
- undefined,
61
- "",
62
- config
63
- );
64
- const route = routes.find((r) => r.path === "/api/dsh-mneme/persona");
65
- const preview = async ({ agent = null, session = null, parent = null } = {}) => {
66
- const qs = [];
67
- if (agent) qs.push(`agent=${encodeURIComponent(agent)}`);
68
- if (session) qs.push(`session=${encodeURIComponent(session)}`);
69
- if (parent) qs.push(`parent=${encodeURIComponent(parent)}`);
70
- const res = new FakeRes();
71
- await route.handler(req(`/api/dsh-mneme/persona${qs.length ? `?${qs.join("&")}` : ""}`), res);
72
- return JSON.parse(res.body);
73
- };
74
- return { store, settings, settingsCtx, injectorText, preview };
75
- }
76
-
77
- function seed(settings) {
78
- settings.setProfile("用户是规划背景");
79
- settings.setPersona({ name: "Amarre", personality: "轻松幽默", enabled: true }); // default
80
- settings.setPersonaFor("standard", { name: "标准卡", personality: "通用", enabled: true }); // byAgent
81
- }
82
-
83
- // ---- 1. 纯加法:老数据零变化 -------------------------------------------------
84
-
85
- test("老数据(只有 {default, byAgent})读回 bySession 为空对象,注入文本逐字节不变", () => {
86
- const { store, settings } = setup({ personaEnabled: true });
87
- // 直写 v1.2 形(无 bySession 键)模拟存量库
88
- store.db.prepare("INSERT INTO user_settings (key, value) VALUES ('persona', ?)")
89
- .run(JSON.stringify({
90
- default: { name: "Amarre", enabled: true },
91
- byAgent: { standard: { name: "标准卡", enabled: true } }
92
- }));
93
- const got = settings.getPersona();
94
- assert.deepEqual(got.bySession, {}, "缺 bySession 键必须读成空对象,不是 undefined");
95
- assert.equal(got.default.name, "Amarre");
96
- assert.equal(got.byAgent.standard.name, "标准卡");
97
- // 寻址结果与改动前一致:按 preset 命中 byAgent,不因多了这一维而变
98
- assert.equal(resolvePersonaCard(settings, "standard").source, "byAgent");
99
- assert.equal(resolvePersonaCard(settings, "unknown").source, "default");
100
- store.close();
101
- });
102
-
103
- test("v1 单对象老数据:读作 default,bySession 为 {}(零迁移)", () => {
104
- const { store, settings } = setup();
105
- store.db.prepare("INSERT INTO user_settings (key, value) VALUES ('persona', ?)")
106
- .run(JSON.stringify({ name: "Amarre", personality: "轻松幽默", enabled: true }));
107
- const got = settings.getPersona();
108
- assert.equal(got.default.name, "Amarre");
109
- assert.deepEqual(got.byAgent, {});
110
- assert.deepEqual(got.bySession, {});
111
- store.close();
112
- });
113
-
114
- test("没设 scenario / mesExample 时,注入文本与历史版本逐字节一致", () => {
115
- const { store, settings, injectorText } = setup({ personaEnabled: true });
116
- settings.setProfile("用户是规划背景");
117
- settings.setRules(["不提及用户名"]);
118
- settings.setPersona({ name: "Amarre", personality: "轻松幽默", replyStyle: "先结论后展开", enabled: true });
119
- const text = injectorText({});
120
- assert.equal(text, [
121
- "[用户设置] 来自 dsh-mneme 的用户画像与规则:",
122
- "- 你的人格名字:Amarre",
123
- "- 人格性格设定:轻松幽默",
124
- "- 回复要求:先结论后展开",
125
- "- 用户画像:用户是规划背景",
126
- "- 规则:不提及用户名"
127
- ].join("\n"), "两个新字段都没设时不许留下空标签或空行");
128
- store.close();
129
- });
130
-
131
- // ---- 2. 存储往返 -------------------------------------------------------------
132
-
133
- test("bySession 存取往返:写入 → 读回 → 列出 → 删除", () => {
134
- const { store, settings } = setup();
135
- const card = { name: "Ephor", personality: "审计口径", enabled: true };
136
- assert.equal(settings.setPersonaForSession("session-389faaa7", card), true);
137
- assert.deepEqual(resolvePersonaCard(settings, null, { sessionId: "session-389faaa7" }).card, card);
138
- assert.deepEqual(settings.listPersonaSessions(), ["session-389faaa7"]);
139
- // 与 byAgent 互不干扰
140
- settings.setPersonaFor("standard", { name: "标准卡", enabled: true });
141
- assert.deepEqual(settings.listPersonas(), ["standard"]);
142
- assert.deepEqual(settings.listPersonaSessions(), ["session-389faaa7"]);
143
- // 删
144
- assert.equal(settings.removePersonaForSession("session-389faaa7"), true);
145
- assert.deepEqual(settings.listPersonaSessions(), []);
146
- assert.equal(settings.removePersonaForSession("session-389faaa7"), false, "删不存在的卡返回 false");
147
- store.close();
148
- });
149
-
150
- test("setPersonaForSession 拒绝空 id 与 default(兜底位不可被顶替)", () => {
151
- const { store, settings } = setup();
152
- assert.equal(settings.setPersonaForSession("", { name: "x" }), false);
153
- assert.equal(settings.setPersonaForSession(" ", { name: "x" }), false);
154
- assert.equal(settings.setPersonaForSession(null, { name: "x" }), false);
155
- assert.equal(settings.setPersonaForSession("default", { name: "x" }), false);
156
- assert.deepEqual(settings.listPersonaSessions(), []);
157
- store.close();
158
- });
159
-
160
- test("写空卡等于删除该会话卡(与 setPersonaFor 同形)", () => {
161
- const { store, settings } = setup();
162
- settings.setPersonaForSession("s1", { name: "甲", enabled: true });
163
- assert.deepEqual(settings.listPersonaSessions(), ["s1"]);
164
- settings.setPersonaForSession("s1", {});
165
- assert.deepEqual(settings.listPersonaSessions(), [], "空卡不留空壳键");
166
- store.close();
167
- });
168
-
169
- test("setPersona 单对象形只改 default,bySession 保留", () => {
170
- const { store, settings } = setup();
171
- settings.setPersonaForSession("s1", { name: "会话卡", enabled: true });
172
- settings.setPersona({ name: "Amarre-新", enabled: true }); // 旧面板写法
173
- const got = settings.getPersona();
174
- assert.equal(got.default.name, "Amarre-新");
175
- assert.equal(got.bySession.s1.name, "会话卡", "bySession 必须活过一次 legacy 形写入");
176
- store.close();
177
- });
178
-
179
- test("多卡形全量替换:带 bySession 键即整份替换", () => {
180
- const { store, settings } = setup();
181
- settings.setPersonaForSession("old", { name: "旧卡", enabled: true });
182
- settings.setPersona({ default: { name: "新默认", enabled: true }, byAgent: {}, bySession: { new: { name: "新卡", enabled: true } } });
183
- assert.deepEqual(settings.listPersonaSessions(), ["new"]);
184
- store.close();
185
- });
186
-
187
- // ---- 3. 寻址优先级 -----------------------------------------------------------
188
-
189
- test("优先级:bySession 盖过 byAgent(会话比预设具体)", () => {
190
- const { store, settings, injectorText } = setup({ personaEnabled: true });
191
- seed(settings); // default=Amarre, byAgent.standard=标准卡
192
- settings.setPersonaForSession("session-abc", { name: "会话专属", personality: "只在这次会话", enabled: true });
193
- const text = injectorText({ agentPreset: "standard", sessionId: "session-abc" });
194
- assert.ok(text.includes("你的人格名字:会话专属"), "会话卡必须赢");
195
- assert.ok(!text.includes("标准卡"), "byAgent 卡不该同时出现");
196
- assert.ok(!text.includes("Amarre"), "default 更不该出现");
197
- store.close();
198
- });
199
-
200
- test("优先级:会话卡被关掉时落回 byAgent(不是直接跳 default)", () => {
201
- const { store, settings, injectorText } = setup({ personaEnabled: true });
202
- seed(settings);
203
- settings.setPersonaForSession("session-abc", { name: "会话专属", enabled: false });
204
- const text = injectorText({ agentPreset: "standard", sessionId: "session-abc" });
205
- assert.ok(text.includes("你的人格名字:标准卡"), "关掉的会话卡让位给身份卡");
206
- assert.ok(!text.includes("会话专属"));
207
- store.close();
208
- });
209
-
210
- test("优先级:没有会话卡时按 byAgent 走(老行为不变)", () => {
211
- const { store, settings } = setup({ personaEnabled: true });
212
- seed(settings);
213
- const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-unknown" });
214
- assert.equal(hit.source, "byAgent");
215
- assert.equal(hit.card.name, "标准卡");
216
- store.close();
217
- });
218
-
219
- test("两个键都没有:落 default", () => {
220
- const { store, settings } = setup({ personaEnabled: true });
221
- seed(settings);
222
- const hit = resolvePersonaCard(settings, null, { sessionId: null });
223
- assert.equal(hit.source, "default");
224
- assert.equal(hit.card.name, "Amarre");
225
- store.close();
226
- });
227
-
228
- test("只给 sessionId、不给 agentId 也能命中会话卡(宿主可能只有会话 id)", () => {
229
- const { store, settings, injectorText } = setup({ personaEnabled: true });
230
- seed(settings);
231
- settings.setPersonaForSession("session-only", { name: "仅会话卡", enabled: true });
232
- const text = injectorText({ sessionId: "session-only" });
233
- assert.ok(text.includes("你的人格名字:仅会话卡"));
234
- store.close();
235
- });
236
-
237
- // ---- 4. 严格寻址:两维合判 ---------------------------------------------------
238
-
239
- test("strict:只建了 bySession 卡时,按 byAgent 寻址的会话也不许被兜底", () => {
240
- // #587 的例外是「一张卡都没建」才回落 default。用户建了会话卡就已经做过分区,
241
- // 此时一个没建卡的 preset 会话再拿 default 去兜,等于把分区抹掉一半。
242
- const { store, settings } = setup();
243
- settings.setPersona({ name: "Amarre", enabled: true });
244
- assert.deepEqual(settings.getPersona().byAgent, {}, "前提:byAgent 为空");
245
- assert.deepEqual(settings.getPersona().bySession, {}, "前提:bySession 也为空");
246
- // 两维都空 → 仍回落 default(#587 例外保持)
247
- assert.equal(resolvePersonaCard(settings, "standard", { strict: true }).source, "default");
248
- // 只建会话卡之后,例外立即失效
249
- settings.setPersonaForSession("session-x", { name: "会话卡", enabled: true });
250
- assert.equal(resolvePersonaCard(settings, "standard", { strict: true }).source, "none");
251
- // 而那张会话卡自己仍然命中
252
- assert.equal(resolvePersonaCard(settings, null, { strict: true, sessionId: "session-x" }).source, "bySession");
253
- store.close();
254
- });
255
-
256
- test("strict:只建了 byAgent 卡时,按会话寻址的会话也不许被兜底", () => {
257
- const { store, settings } = setup();
258
- settings.setPersona({ name: "Amarre", enabled: true });
259
- settings.setPersonaFor("standard", { name: "标准卡", enabled: true });
260
- // 有会话 id 但没建会话卡 → none(用户已经做过 byAgent 分区)
261
- assert.equal(resolvePersonaCard(settings, null, { strict: true, sessionId: "session-y" }).source, "none");
262
- store.close();
263
- });
264
-
265
- test("strict:两个键都解析不到时仍用 default(无名会话是 default 的受众)", () => {
266
- const { store, settings } = setup();
267
- settings.setPersona({ name: "Amarre", enabled: true });
268
- settings.setPersonaForSession("session-x", { name: "会话卡", enabled: true });
269
- assert.equal(resolvePersonaCard(settings, null, { strict: true }).source, "default");
270
- store.close();
271
- });
272
-
273
- // ---- 5. scenario / mesExample ------------------------------------------------
274
-
275
- test("scenario / mesExample 设置在 replyStyle 之后渲染", () => {
276
- const { store, settings, injectorText } = setup({ personaEnabled: true });
277
- settings.setPersona({
278
- name: "Amarre",
279
- personality: "轻松幽默",
280
- replyStyle: "先结论后展开",
281
- scenario: "深夜陪你赶论文",
282
- mesExample: "桉桉:好累。Amarre:先睡,剩下的我盯着。",
283
- enabled: true
284
- });
285
- const text = injectorText({});
286
- const idxReply = text.indexOf("回复要求:先结论后展开");
287
- const idxScene = text.indexOf("场景设定:深夜陪你赶论文");
288
- const idxExample = text.indexOf("对话示例:桉桉:好累。");
289
- assert.ok(idxReply > -1 && idxScene > -1 && idxExample > -1, "三段都在");
290
- assert.ok(idxScene > idxReply, "场景在语气之后:身份 → 语气 → 场景 → 示例");
291
- assert.ok(idxExample > idxScene, "示例在场景之后");
292
- store.close();
293
- });
294
-
295
- test("scenario 只设一个字段时只出一行,不牵连另一个", () => {
296
- const { store, settings, injectorText } = setup({ personaEnabled: true });
297
- settings.setPersona({ name: "Amarre", scenario: "只在夜里出现", enabled: true });
298
- const text = injectorText({});
299
- assert.ok(text.includes("场景设定:只在夜里出现"));
300
- assert.ok(!text.includes("对话示例"), "没设 mesExample 就不出那一行");
301
- store.close();
302
- });
303
-
304
- test("卡里存着空白 scenario / mesExample 时不出空标签(未设置零输出)", () => {
305
- // 未设置的字段被 parsePersona 白名单挡在外面,但「存了空白」是另一条路:
306
- // 卡里确实有这个键、值是空白串。这一行必须整行不出现,不能渲染成「场景设定:」。
307
- const { store, settings, injectorText } = setup({ personaEnabled: true });
308
- settings.setPersona({ name: "Amarre", scenario: " ", mesExample: "\t ", enabled: true });
309
- const card = settings.getPersona().default;
310
- assert.equal(card.scenario, " ", "空白串仍被存下(白名单不负责裁空白)");
311
- const text = injectorText({});
312
- assert.ok(!text.includes("场景设定"), "空白 scenario 不许渲染出标签");
313
- assert.ok(!text.includes("对话示例"), "空白 mesExample 不许渲染出标签");
314
- assert.ok(text.includes("你的人格名字:Amarre"), "其余段落照常");
315
- store.close();
316
- });
317
-
318
- test("scenario / mesExample 走同一个转义(花括号不许漏出去)", () => {
319
- const { store, settings, injectorText } = setup({ personaEnabled: true });
320
- settings.setPersona({ name: "Amarre", scenario: "{{evil}}", mesExample: "{{x}}", enabled: true });
321
- const text = injectorText({});
322
- assert.ok(!text.includes("{{"), "no raw {{ in user-settings block");
323
- assert.ok(!text.includes("}}"), "no raw }} in user-settings block");
324
- store.close();
325
- });
326
-
327
- test("scenario / mesExample 只在卡 enabled 时渲染(关掉的卡一个字都不出)", () => {
328
- const { store, settings, injectorText } = setup({ personaEnabled: true });
329
- settings.setPersona({ name: "Amarre", scenario: "深夜", enabled: false });
330
- settings.setProfile("有画像");
331
- const text = injectorText({});
332
- assert.ok(!text.includes("场景设定"), "卡关着时新字段同样不渲染");
333
- assert.ok(text.includes("用户画像:"));
334
- store.close();
335
- });
336
-
337
- test("scenario / mesExample 过字段白名单(未知字段仍被丢弃)", () => {
338
- const { store, settings } = setup();
339
- settings.setPersona({ name: "Amarre", scenario: "场景", mesExample: "示例", sneaky: "x" });
340
- const card = settings.getPersona().default;
341
- assert.equal(card.scenario, "场景");
342
- assert.equal(card.mesExample, "示例");
343
- assert.equal("sneaky" in card, false, "白名单没放宽");
344
- store.close();
345
- });
346
-
347
- // ---- 6. 端到端:注入链路 + 预览路由 ------------------------------------------
348
-
349
- test("注入链路端到端:同一个 agentPreset、不同 sessionId 拿到不同人格", () => {
350
- // 这就是 #606 要解决的场景本身:DS Bot 里四个 bot 的 agentPreset 全是 standard。
351
- const { store, settings, injectorText } = setup({ personaEnabled: true });
352
- settings.setPersona({ name: "Amarre", enabled: true });
353
- settings.setPersonaForSession("session-d530e132", { name: "Amarre", personality: "主控", enabled: true });
354
- settings.setPersonaForSession("session-389faaa7", { name: "Ephor", personality: "审计", enabled: true });
355
- const a = injectorText({ agentPreset: "standard", sessionId: "session-d530e132" });
356
- const b = injectorText({ agentPreset: "standard", sessionId: "session-389faaa7" });
357
- assert.ok(a.includes("你的人格名字:Amarre"));
358
- assert.ok(b.includes("你的人格名字:Ephor"), "同一个 standard 预设下,会话卡必须分得开");
359
- assert.ok(!b.includes("你的人格名字:Amarre"));
360
- store.close();
361
- });
362
-
363
- test("预览路由支持 &session=,且与注入器逐字节同源", async () => {
364
- const { store, settings, injectorText, preview } = setup({ personaEnabled: true });
365
- seed(settings);
366
- settings.setPersonaForSession("session-abc", { name: "会话专属", personality: "只在这里", enabled: true });
367
- const fromInjector = injectorText({ agentPreset: "standard", sessionId: "session-abc" });
368
- const body = await preview({ agent: "standard", session: "session-abc" });
369
- assert.equal(body.preview.text, fromInjector, "预览必须等于注入器这一帧的输出");
370
- assert.equal(body.preview.source, "bySession");
371
- assert.equal(body.preview.session, "session-abc", "面板要靠它说清用的是哪张会话卡");
372
- assert.ok(body.preview.text.includes("你的人格名字:会话专属"));
373
- store.close();
374
- });
375
-
376
- test("预览只给 session 不给 agent 也走会话卡", async () => {
377
- const { store, settings, preview } = setup({ personaEnabled: true });
378
- seed(settings);
379
- settings.setPersonaForSession("session-only", { name: "仅会话卡", enabled: true });
380
- const body = await preview({ session: "session-only" });
381
- assert.equal(body.preview.source, "bySession");
382
- assert.equal(body.preview.agent, null);
383
- assert.equal(body.preview.session, "session-only");
384
- store.close();
385
- });
386
-
387
- test("预览的 session 参数在 strict 下同样生效(不许比注入更乐观)", async () => {
388
- const { store, settings, preview } = setup({ personaEnabled: true, personaStrictAddressing: true });
389
- seed(settings);
390
- const miss = await preview({ session: "session-unknown" });
391
- assert.equal(miss.preview.source, "none", "建过卡、这次会话没建 → 不兜底");
392
- assert.ok(!miss.preview.text.includes("Amarre"));
393
- settings.setPersonaForSession("session-hit", { name: "命中卡", enabled: true });
394
- const hit = await preview({ session: "session-hit" });
395
- assert.equal(hit.preview.source, "bySession");
396
- store.close();
397
- });
398
-
399
- // ---- 7. 健壮性 ---------------------------------------------------------------
400
-
401
- test("session.id 取不到(宿主 accessor 抛错)时不影响 agentPreset 寻址", () => {
402
- const { store, settings, settingsCtx } = setup({ personaEnabled: true });
403
- seed(settings);
404
- // session.id 是抛错的 getter,header 正常——两个键各自护住,不许互相拖累
405
- const hostile = {
406
- agent: {
407
- session: {
408
- get id() { throw new Error("no id accessor"); },
409
- header: { agentPreset: "standard" }
410
- }
411
- }
412
- };
413
- const text = settingsCtx.text(hostile);
414
- assert.ok(text.includes("你的人格名字:标准卡"), "id 抛错不该让 preset 也丢掉");
415
- store.close();
416
- });
417
-
418
- test("bySession 里的损坏卡(非对象)被丢弃,不抛错", () => {
419
- const { store, settings } = setup();
420
- store.db.prepare("INSERT INTO user_settings (key, value) VALUES ('persona', ?)")
421
- .run(JSON.stringify({ default: {}, byAgent: {}, bySession: { bad: "not a card", good: { name: "好卡", enabled: true } } }));
422
- const got = settings.getPersona();
423
- assert.equal("bad" in got.bySession, false, "坏卡读成空对象后不收(与 byAgent 同口径)");
424
- assert.equal(got.bySession.good.name, "好卡");
425
- store.close();
426
- });
427
-
428
- test("renderUserSettingsBlock 返回值带 session 字段(面板定位用)", () => {
429
- const { store, settings } = setup({ personaEnabled: true });
430
- settings.setPersona({ name: "Amarre", enabled: true });
431
- const out = renderUserSettingsBlock({ settings, config: { personaEnabled: true }, agentId: "standard", sessionId: "s1" });
432
- assert.equal(out.session, "s1");
433
- assert.equal(out.agent, "standard");
434
- assert.equal(out.source, "default");
435
- store.close();
436
- });
437
-
438
- // ---- 8. 子 agent 继承父会话人格卡 --------------------------------------------
439
- //
440
- // 子 agent 是新会话(header 带 parentSession / origin:"subagent"),用户不可能给每个
441
- // 派生会话建卡。strict 一开,「有身份、没这张身份的卡」= 不注入人格,子 agent 会从
442
- // 「自称父会话的人格」变成「不知道自己是谁」。所以自己没卡时继承父会话的卡。
443
-
444
- test("子 agent 没自己的卡时继承父会话卡(source 是 parentSession)", () => {
445
- const { store, settings, injectorText } = setup({ personaEnabled: true });
446
- settings.setPersona({ name: "兜底卡", enabled: true });
447
- settings.setPersonaForSession("session-parent", { name: "主控人格", personality: "轻松诙谐", enabled: true });
448
- const text = injectorText({ agentPreset: "standard", sessionId: "session-child", parentSession: "session-parent" });
449
- assert.ok(text.includes("你的人格名字:主控人格"), "子 agent 必须拿到父会话那张卡");
450
- assert.ok(!text.includes("兜底卡"), "不许回落到 default");
451
- const out = renderUserSettingsBlock({
452
- settings,
453
- config: { personaEnabled: true },
454
- agentId: "standard",
455
- sessionId: "session-child",
456
- parentSessionId: "session-parent"
457
- });
458
- assert.equal(out.source, "parentSession");
459
- store.close();
460
- });
461
-
462
- test("子 agent 自己有卡时自己的卡优先(继承不覆盖专门指定的卡)", () => {
463
- const { store, settings } = setup({ personaEnabled: true });
464
- settings.setPersonaForSession("session-parent", { name: "父卡", enabled: true });
465
- settings.setPersonaForSession("session-child", { name: "子卡", enabled: true });
466
- const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-child", parentSessionId: "session-parent" });
467
- assert.equal(hit.source, "bySession");
468
- assert.equal(hit.card.name, "子卡");
469
- store.close();
470
- });
471
-
472
- test("父卡优先于 byAgent(同一段对话比同一预设更能说明用谁的人格)", () => {
473
- const { store, settings } = setup({ personaEnabled: true });
474
- settings.setPersonaFor("standard", { name: "预设卡", enabled: true });
475
- settings.setPersonaForSession("session-parent", { name: "父卡", enabled: true });
476
- const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-child", parentSessionId: "session-parent" });
477
- assert.equal(hit.source, "parentSession");
478
- assert.equal(hit.card.name, "父卡");
479
- store.close();
480
- });
481
-
482
- test("父卡被显式关掉时不继承,落回 byAgent(关掉就是关掉)", () => {
483
- const { store, settings } = setup({ personaEnabled: true });
484
- settings.setPersonaFor("standard", { name: "预设卡", enabled: true });
485
- settings.setPersonaForSession("session-parent", { name: "父卡", enabled: false });
486
- const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-child", parentSessionId: "session-parent" });
487
- assert.equal(hit.source, "byAgent");
488
- assert.equal(hit.card.name, "预设卡");
489
- store.close();
490
- });
491
-
492
- test("strict:子 agent 只解析到父会话 id 时也不许被 default 兜底", () => {
493
- const { store, settings } = setup({ personaEnabled: true, personaStrictAddressing: true });
494
- settings.setPersonaForSession("session-other", { name: "别人的卡", enabled: true });
495
- // 子会话与父会话都没有卡;anyCard=true 且确实有身份 → none
496
- const miss = resolvePersonaCard(settings, "standard", {
497
- strict: true,
498
- sessionId: "session-child",
499
- parentSessionId: "session-nobody"
500
- });
501
- assert.equal(miss.source, "none", "有身份但没卡,strict 下不兜底");
502
- store.close();
503
- });
504
-
505
- test("strict 关时子 agent 没父卡照旧落 default(零行为变化)", () => {
506
- const { store, settings } = setup({ personaEnabled: true });
507
- settings.setPersona({ name: "Amarre", enabled: true });
508
- settings.setPersonaForSession("session-other", { name: "别人的卡", enabled: true });
509
- const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-child", parentSessionId: "session-nope" });
510
- assert.equal(hit.source, "default");
511
- assert.equal(hit.card.name, "Amarre");
512
- store.close();
513
- });
514
-
515
- test("parentSession 与自身 id 相同(脏数据)时不重复查同一个键", () => {
516
- const { store, settings } = setup({ personaEnabled: true });
517
- settings.setPersonaForSession("session-same", { name: "同一张", enabled: true });
518
- const hit = resolvePersonaCard(settings, "standard", {
519
- sessionId: "session-same",
520
- parentSessionId: "session-same"
521
- });
522
- assert.equal(hit.source, "bySession", "自身维度先命中,不会误标成继承");
523
- store.close();
524
- });
525
-
526
- test("父会话 id 是空白/非字符串时不影响其余寻址", () => {
527
- const { store, settings } = setup({ personaEnabled: true });
528
- settings.setPersonaFor("standard", { name: "预设卡", enabled: true });
529
- for (const bad of ["", " ", null, undefined, 42, {}]) {
530
- const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-child", parentSessionId: bad });
531
- assert.equal(hit.source, "byAgent", `parentSessionId=${JSON.stringify(bad)} 应被忽略`);
532
- }
533
- store.close();
534
- });
535
-
536
- test("session.header 取不到时不影响 agentPreset 寻址(新加的父会话读取也各自兜错)", () => {
537
- const { store, settings, settingsCtx } = setup({ personaEnabled: true });
538
- seed(settings);
539
- const hostile = {
540
- agent: {
541
- session: {
542
- id: "session-x",
543
- get header() { throw new Error("no header accessor"); }
544
- }
545
- }
546
- };
547
- const text = settingsCtx.text(hostile);
548
- // header 整体取不到 → agentId/parentSessionId 皆 null,只剩 sessionId,落 default
549
- assert.ok(text.includes("你的人格名字:Amarre"), "会话卡字段可用时不该整块崩掉");
550
- store.close();
551
- });
552
-
553
- test("注入链路端到端:子 agent 拿到父会话人格,且与预览同源", async () => {
554
- const { store, settings, injectorText, preview } = setup({ personaEnabled: true });
555
- settings.setPersona({ name: "兜底", enabled: true });
556
- settings.setPersonaForSession("session-parent", { name: "父会话人格", personality: "父的性格", enabled: true });
557
- const fromInjector = injectorText({
558
- agentPreset: "standard",
559
- sessionId: "session-child",
560
- parentSession: "session-parent"
561
- });
562
- assert.ok(fromInjector.includes("你的人格名字:父会话人格"));
563
- const body = await preview({ agent: "standard", session: "session-child", parent: "session-parent" });
564
- assert.equal(body.preview.text, fromInjector, "预览必须与注入器逐字节同源");
565
- assert.equal(body.preview.source, "parentSession");
566
- assert.equal(body.preview.parent, "session-parent");
567
- store.close();
568
- });
1
+ // 绑会话的人格卡(#606)。为什么需要这一维:人格卡原本只有 agentPreset 一个寻址键,
2
+ // 而 DS Bot 这类宿主把它写成常量——实测 bot 工作区 50 个会话里 43 个是 "standard",
3
+ // 四条主 bot(Amarre / Ephor / Loom / Persis)全部相同,但它们的 session.id 各不相同。
4
+ // 所以「一个 bot 一张卡」只能用会话 id 当键。
5
+ //
6
+ // 本文件锁四件事:
7
+ // 1. 纯加法:没有 bySession 的老数据读回仍是空对象,注入文本逐字节不变。
8
+ // 2. 优先级:bySession > byAgent > default(会话比预设具体)。
9
+ // 3. 严格寻址的两维合判:只建了 bySession 卡时,按 byAgent 寻址的会话也不许被兜底。
10
+ // 4. scenario / mesExample:未设置零输出,设置后排在 replyStyle 之后。
11
+ import test from "node:test";
12
+ import assert from "node:assert/strict";
13
+ import { EventEmitter } from "node:events";
14
+ import { createStore } from "../src/store.js";
15
+ import { createService } from "../src/service.js";
16
+ import { createInjector } from "../src/inject.js";
17
+ import { createApi } from "../src/api.js";
18
+ import { createSettings } from "../src/settings.js";
19
+ import { resolvePersonaCard, renderUserSettingsBlock } from "../src/user-settings.js";
20
+
21
+ class FakeRes extends EventEmitter {
22
+ constructor() { super(); this.statusCode = 200; this.body = ""; }
23
+ writeHead(code, headers) { this.statusCode = code; this.headers = headers; return this; }
24
+ end(text) { this.body = text ?? ""; this.emit("end"); return this; }
25
+ }
26
+
27
+ function req(path, method = "GET") {
28
+ return { url: path, method, headers: {}, on() {}, emit() {} };
29
+ }
30
+
31
+ // 注入器 + API 共用同一份 settings/config(同源纪律),两边都能按 {agentPreset, sessionId} 寻址。
32
+ function setup(config = {}) {
33
+ const store = createStore(":memory:");
34
+ const service = createService({ store, mirror: null, config });
35
+ const settings = createSettings(store.db);
36
+ const contexts = [];
37
+ const ctx = { systemPrompt: { context(def) { contexts.push(def); return () => {}; } } };
38
+ createInjector(ctx, service, settings, config);
39
+ const settingsCtx = contexts.find((c) => c.name === "user-settings");
40
+ // 造 ctx:只给要测的那几个键,缺省不给(模拟宿主解析不到)。
41
+ const makeCtx = ({ agentPreset = null, sessionId = null, parentSession = null } = {}) => {
42
+ if (!agentPreset && !sessionId && !parentSession) return {};
43
+ const session = {};
44
+ if (sessionId) session.id = sessionId;
45
+ if (agentPreset || parentSession) {
46
+ session.header = {};
47
+ if (agentPreset) session.header.agentPreset = agentPreset;
48
+ if (parentSession) session.header.parentSession = parentSession;
49
+ }
50
+ return { agent: { session } };
51
+ };
52
+ const injectorText = (opts) => settingsCtx.text(makeCtx(opts));
53
+ const routes = [];
54
+ createApi(
55
+ { webServer: { register: (route) => { routes.push(route); return () => {}; } } },
56
+ service,
57
+ settings,
58
+ { add: () => {}, remove: () => {}, list: () => [] },
59
+ undefined,
60
+ undefined,
61
+ "",
62
+ config
63
+ );
64
+ const route = routes.find((r) => r.path === "/api/dsh-mneme/persona");
65
+ const preview = async ({ agent = null, session = null, parent = null } = {}) => {
66
+ const qs = [];
67
+ if (agent) qs.push(`agent=${encodeURIComponent(agent)}`);
68
+ if (session) qs.push(`session=${encodeURIComponent(session)}`);
69
+ if (parent) qs.push(`parent=${encodeURIComponent(parent)}`);
70
+ const res = new FakeRes();
71
+ await route.handler(req(`/api/dsh-mneme/persona${qs.length ? `?${qs.join("&")}` : ""}`), res);
72
+ return JSON.parse(res.body);
73
+ };
74
+ return { store, settings, settingsCtx, injectorText, preview };
75
+ }
76
+
77
+ function seed(settings) {
78
+ settings.setProfile("用户是规划背景");
79
+ settings.setPersona({ name: "Amarre", personality: "轻松幽默", enabled: true }); // default
80
+ settings.setPersonaFor("standard", { name: "标准卡", personality: "通用", enabled: true }); // byAgent
81
+ }
82
+
83
+ // ---- 1. 纯加法:老数据零变化 -------------------------------------------------
84
+
85
+ test("老数据(只有 {default, byAgent})读回 bySession 为空对象,注入文本逐字节不变", () => {
86
+ const { store, settings } = setup({ personaEnabled: true });
87
+ // 直写 v1.2 形(无 bySession 键)模拟存量库
88
+ store.db.prepare("INSERT INTO user_settings (key, value) VALUES ('persona', ?)")
89
+ .run(JSON.stringify({
90
+ default: { name: "Amarre", enabled: true },
91
+ byAgent: { standard: { name: "标准卡", enabled: true } }
92
+ }));
93
+ const got = settings.getPersona();
94
+ assert.deepEqual(got.bySession, {}, "缺 bySession 键必须读成空对象,不是 undefined");
95
+ assert.equal(got.default.name, "Amarre");
96
+ assert.equal(got.byAgent.standard.name, "标准卡");
97
+ // 寻址结果与改动前一致:按 preset 命中 byAgent,不因多了这一维而变
98
+ assert.equal(resolvePersonaCard(settings, "standard").source, "byAgent");
99
+ assert.equal(resolvePersonaCard(settings, "unknown").source, "default");
100
+ store.close();
101
+ });
102
+
103
+ test("v1 单对象老数据:读作 default,bySession 为 {}(零迁移)", () => {
104
+ const { store, settings } = setup();
105
+ store.db.prepare("INSERT INTO user_settings (key, value) VALUES ('persona', ?)")
106
+ .run(JSON.stringify({ name: "Amarre", personality: "轻松幽默", enabled: true }));
107
+ const got = settings.getPersona();
108
+ assert.equal(got.default.name, "Amarre");
109
+ assert.deepEqual(got.byAgent, {});
110
+ assert.deepEqual(got.bySession, {});
111
+ store.close();
112
+ });
113
+
114
+ test("没设 scenario / mesExample 时,注入文本与历史版本逐字节一致", () => {
115
+ const { store, settings, injectorText } = setup({ personaEnabled: true });
116
+ settings.setProfile("用户是规划背景");
117
+ settings.setRules(["不提及用户名"]);
118
+ settings.setPersona({ name: "Amarre", personality: "轻松幽默", replyStyle: "先结论后展开", enabled: true });
119
+ const text = injectorText({});
120
+ assert.equal(text, [
121
+ "[用户设置] 来自 dsh-mneme 的用户画像与规则:",
122
+ "- 你的人格名字:Amarre",
123
+ "- 人格性格设定:轻松幽默",
124
+ "- 回复要求:先结论后展开",
125
+ "- 用户画像:用户是规划背景",
126
+ "- 规则:不提及用户名"
127
+ ].join("\n"), "两个新字段都没设时不许留下空标签或空行");
128
+ store.close();
129
+ });
130
+
131
+ // ---- 2. 存储往返 -------------------------------------------------------------
132
+
133
+ test("bySession 存取往返:写入 → 读回 → 列出 → 删除", () => {
134
+ const { store, settings } = setup();
135
+ const card = { name: "Ephor", personality: "审计口径", enabled: true };
136
+ assert.equal(settings.setPersonaForSession("session-389faaa7", card), true);
137
+ assert.deepEqual(resolvePersonaCard(settings, null, { sessionId: "session-389faaa7" }).card, card);
138
+ assert.deepEqual(settings.listPersonaSessions(), ["session-389faaa7"]);
139
+ // 与 byAgent 互不干扰
140
+ settings.setPersonaFor("standard", { name: "标准卡", enabled: true });
141
+ assert.deepEqual(settings.listPersonas(), ["standard"]);
142
+ assert.deepEqual(settings.listPersonaSessions(), ["session-389faaa7"]);
143
+ // 删
144
+ assert.equal(settings.removePersonaForSession("session-389faaa7"), true);
145
+ assert.deepEqual(settings.listPersonaSessions(), []);
146
+ assert.equal(settings.removePersonaForSession("session-389faaa7"), false, "删不存在的卡返回 false");
147
+ store.close();
148
+ });
149
+
150
+ test("setPersonaForSession 拒绝空 id 与 default(兜底位不可被顶替)", () => {
151
+ const { store, settings } = setup();
152
+ assert.equal(settings.setPersonaForSession("", { name: "x" }), false);
153
+ assert.equal(settings.setPersonaForSession(" ", { name: "x" }), false);
154
+ assert.equal(settings.setPersonaForSession(null, { name: "x" }), false);
155
+ assert.equal(settings.setPersonaForSession("default", { name: "x" }), false);
156
+ assert.deepEqual(settings.listPersonaSessions(), []);
157
+ store.close();
158
+ });
159
+
160
+ test("写空卡等于删除该会话卡(与 setPersonaFor 同形)", () => {
161
+ const { store, settings } = setup();
162
+ settings.setPersonaForSession("s1", { name: "甲", enabled: true });
163
+ assert.deepEqual(settings.listPersonaSessions(), ["s1"]);
164
+ settings.setPersonaForSession("s1", {});
165
+ assert.deepEqual(settings.listPersonaSessions(), [], "空卡不留空壳键");
166
+ store.close();
167
+ });
168
+
169
+ test("setPersona 单对象形只改 default,bySession 保留", () => {
170
+ const { store, settings } = setup();
171
+ settings.setPersonaForSession("s1", { name: "会话卡", enabled: true });
172
+ settings.setPersona({ name: "Amarre-新", enabled: true }); // 旧面板写法
173
+ const got = settings.getPersona();
174
+ assert.equal(got.default.name, "Amarre-新");
175
+ assert.equal(got.bySession.s1.name, "会话卡", "bySession 必须活过一次 legacy 形写入");
176
+ store.close();
177
+ });
178
+
179
+ test("多卡形全量替换:带 bySession 键即整份替换", () => {
180
+ const { store, settings } = setup();
181
+ settings.setPersonaForSession("old", { name: "旧卡", enabled: true });
182
+ settings.setPersona({ default: { name: "新默认", enabled: true }, byAgent: {}, bySession: { new: { name: "新卡", enabled: true } } });
183
+ assert.deepEqual(settings.listPersonaSessions(), ["new"]);
184
+ store.close();
185
+ });
186
+
187
+ // ---- 3. 寻址优先级 -----------------------------------------------------------
188
+
189
+ test("优先级:bySession 盖过 byAgent(会话比预设具体)", () => {
190
+ const { store, settings, injectorText } = setup({ personaEnabled: true });
191
+ seed(settings); // default=Amarre, byAgent.standard=标准卡
192
+ settings.setPersonaForSession("session-abc", { name: "会话专属", personality: "只在这次会话", enabled: true });
193
+ const text = injectorText({ agentPreset: "standard", sessionId: "session-abc" });
194
+ assert.ok(text.includes("你的人格名字:会话专属"), "会话卡必须赢");
195
+ assert.ok(!text.includes("标准卡"), "byAgent 卡不该同时出现");
196
+ assert.ok(!text.includes("Amarre"), "default 更不该出现");
197
+ store.close();
198
+ });
199
+
200
+ test("优先级:会话卡被关掉时落回 byAgent(不是直接跳 default)", () => {
201
+ const { store, settings, injectorText } = setup({ personaEnabled: true });
202
+ seed(settings);
203
+ settings.setPersonaForSession("session-abc", { name: "会话专属", enabled: false });
204
+ const text = injectorText({ agentPreset: "standard", sessionId: "session-abc" });
205
+ assert.ok(text.includes("你的人格名字:标准卡"), "关掉的会话卡让位给身份卡");
206
+ assert.ok(!text.includes("会话专属"));
207
+ store.close();
208
+ });
209
+
210
+ test("优先级:没有会话卡时按 byAgent 走(老行为不变)", () => {
211
+ const { store, settings } = setup({ personaEnabled: true });
212
+ seed(settings);
213
+ const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-unknown" });
214
+ assert.equal(hit.source, "byAgent");
215
+ assert.equal(hit.card.name, "标准卡");
216
+ store.close();
217
+ });
218
+
219
+ test("两个键都没有:落 default", () => {
220
+ const { store, settings } = setup({ personaEnabled: true });
221
+ seed(settings);
222
+ const hit = resolvePersonaCard(settings, null, { sessionId: null });
223
+ assert.equal(hit.source, "default");
224
+ assert.equal(hit.card.name, "Amarre");
225
+ store.close();
226
+ });
227
+
228
+ test("只给 sessionId、不给 agentId 也能命中会话卡(宿主可能只有会话 id)", () => {
229
+ const { store, settings, injectorText } = setup({ personaEnabled: true });
230
+ seed(settings);
231
+ settings.setPersonaForSession("session-only", { name: "仅会话卡", enabled: true });
232
+ const text = injectorText({ sessionId: "session-only" });
233
+ assert.ok(text.includes("你的人格名字:仅会话卡"));
234
+ store.close();
235
+ });
236
+
237
+ // ---- 4. 严格寻址:两维合判 ---------------------------------------------------
238
+
239
+ test("strict:只建了 bySession 卡时,按 byAgent 寻址的会话也不许被兜底", () => {
240
+ // #587 的例外是「一张卡都没建」才回落 default。用户建了会话卡就已经做过分区,
241
+ // 此时一个没建卡的 preset 会话再拿 default 去兜,等于把分区抹掉一半。
242
+ const { store, settings } = setup();
243
+ settings.setPersona({ name: "Amarre", enabled: true });
244
+ assert.deepEqual(settings.getPersona().byAgent, {}, "前提:byAgent 为空");
245
+ assert.deepEqual(settings.getPersona().bySession, {}, "前提:bySession 也为空");
246
+ // 两维都空 → 仍回落 default(#587 例外保持)
247
+ assert.equal(resolvePersonaCard(settings, "standard", { strict: true }).source, "default");
248
+ // 只建会话卡之后,例外立即失效
249
+ settings.setPersonaForSession("session-x", { name: "会话卡", enabled: true });
250
+ assert.equal(resolvePersonaCard(settings, "standard", { strict: true }).source, "none");
251
+ // 而那张会话卡自己仍然命中
252
+ assert.equal(resolvePersonaCard(settings, null, { strict: true, sessionId: "session-x" }).source, "bySession");
253
+ store.close();
254
+ });
255
+
256
+ test("strict:只建了 byAgent 卡时,按会话寻址的会话也不许被兜底", () => {
257
+ const { store, settings } = setup();
258
+ settings.setPersona({ name: "Amarre", enabled: true });
259
+ settings.setPersonaFor("standard", { name: "标准卡", enabled: true });
260
+ // 有会话 id 但没建会话卡 → none(用户已经做过 byAgent 分区)
261
+ assert.equal(resolvePersonaCard(settings, null, { strict: true, sessionId: "session-y" }).source, "none");
262
+ store.close();
263
+ });
264
+
265
+ test("strict:两个键都解析不到时仍用 default(无名会话是 default 的受众)", () => {
266
+ const { store, settings } = setup();
267
+ settings.setPersona({ name: "Amarre", enabled: true });
268
+ settings.setPersonaForSession("session-x", { name: "会话卡", enabled: true });
269
+ assert.equal(resolvePersonaCard(settings, null, { strict: true }).source, "default");
270
+ store.close();
271
+ });
272
+
273
+ // ---- 5. scenario / mesExample ------------------------------------------------
274
+
275
+ test("scenario / mesExample 设置在 replyStyle 之后渲染", () => {
276
+ const { store, settings, injectorText } = setup({ personaEnabled: true });
277
+ settings.setPersona({
278
+ name: "Amarre",
279
+ personality: "轻松幽默",
280
+ replyStyle: "先结论后展开",
281
+ scenario: "深夜陪你赶论文",
282
+ mesExample: "桉桉:好累。Amarre:先睡,剩下的我盯着。",
283
+ enabled: true
284
+ });
285
+ const text = injectorText({});
286
+ const idxReply = text.indexOf("回复要求:先结论后展开");
287
+ const idxScene = text.indexOf("场景设定:深夜陪你赶论文");
288
+ const idxExample = text.indexOf("对话示例:桉桉:好累。");
289
+ assert.ok(idxReply > -1 && idxScene > -1 && idxExample > -1, "三段都在");
290
+ assert.ok(idxScene > idxReply, "场景在语气之后:身份 → 语气 → 场景 → 示例");
291
+ assert.ok(idxExample > idxScene, "示例在场景之后");
292
+ store.close();
293
+ });
294
+
295
+ test("scenario 只设一个字段时只出一行,不牵连另一个", () => {
296
+ const { store, settings, injectorText } = setup({ personaEnabled: true });
297
+ settings.setPersona({ name: "Amarre", scenario: "只在夜里出现", enabled: true });
298
+ const text = injectorText({});
299
+ assert.ok(text.includes("场景设定:只在夜里出现"));
300
+ assert.ok(!text.includes("对话示例"), "没设 mesExample 就不出那一行");
301
+ store.close();
302
+ });
303
+
304
+ test("卡里存着空白 scenario / mesExample 时不出空标签(未设置零输出)", () => {
305
+ // 未设置的字段被 parsePersona 白名单挡在外面,但「存了空白」是另一条路:
306
+ // 卡里确实有这个键、值是空白串。这一行必须整行不出现,不能渲染成「场景设定:」。
307
+ const { store, settings, injectorText } = setup({ personaEnabled: true });
308
+ settings.setPersona({ name: "Amarre", scenario: " ", mesExample: "\t ", enabled: true });
309
+ const card = settings.getPersona().default;
310
+ assert.equal(card.scenario, " ", "空白串仍被存下(白名单不负责裁空白)");
311
+ const text = injectorText({});
312
+ assert.ok(!text.includes("场景设定"), "空白 scenario 不许渲染出标签");
313
+ assert.ok(!text.includes("对话示例"), "空白 mesExample 不许渲染出标签");
314
+ assert.ok(text.includes("你的人格名字:Amarre"), "其余段落照常");
315
+ store.close();
316
+ });
317
+
318
+ test("scenario / mesExample 走同一个转义(花括号不许漏出去)", () => {
319
+ const { store, settings, injectorText } = setup({ personaEnabled: true });
320
+ settings.setPersona({ name: "Amarre", scenario: "{{evil}}", mesExample: "{{x}}", enabled: true });
321
+ const text = injectorText({});
322
+ assert.ok(!text.includes("{{"), "no raw {{ in user-settings block");
323
+ assert.ok(!text.includes("}}"), "no raw }} in user-settings block");
324
+ store.close();
325
+ });
326
+
327
+ test("scenario / mesExample 只在卡 enabled 时渲染(关掉的卡一个字都不出)", () => {
328
+ const { store, settings, injectorText } = setup({ personaEnabled: true });
329
+ settings.setPersona({ name: "Amarre", scenario: "深夜", enabled: false });
330
+ settings.setProfile("有画像");
331
+ const text = injectorText({});
332
+ assert.ok(!text.includes("场景设定"), "卡关着时新字段同样不渲染");
333
+ assert.ok(text.includes("用户画像:"));
334
+ store.close();
335
+ });
336
+
337
+ test("scenario / mesExample 过字段白名单(未知字段仍被丢弃)", () => {
338
+ const { store, settings } = setup();
339
+ settings.setPersona({ name: "Amarre", scenario: "场景", mesExample: "示例", sneaky: "x" });
340
+ const card = settings.getPersona().default;
341
+ assert.equal(card.scenario, "场景");
342
+ assert.equal(card.mesExample, "示例");
343
+ assert.equal("sneaky" in card, false, "白名单没放宽");
344
+ store.close();
345
+ });
346
+
347
+ // ---- 6. 端到端:注入链路 + 预览路由 ------------------------------------------
348
+
349
+ test("注入链路端到端:同一个 agentPreset、不同 sessionId 拿到不同人格", () => {
350
+ // 这就是 #606 要解决的场景本身:DS Bot 里四个 bot 的 agentPreset 全是 standard。
351
+ const { store, settings, injectorText } = setup({ personaEnabled: true });
352
+ settings.setPersona({ name: "Amarre", enabled: true });
353
+ settings.setPersonaForSession("session-d530e132", { name: "Amarre", personality: "主控", enabled: true });
354
+ settings.setPersonaForSession("session-389faaa7", { name: "Ephor", personality: "审计", enabled: true });
355
+ const a = injectorText({ agentPreset: "standard", sessionId: "session-d530e132" });
356
+ const b = injectorText({ agentPreset: "standard", sessionId: "session-389faaa7" });
357
+ assert.ok(a.includes("你的人格名字:Amarre"));
358
+ assert.ok(b.includes("你的人格名字:Ephor"), "同一个 standard 预设下,会话卡必须分得开");
359
+ assert.ok(!b.includes("你的人格名字:Amarre"));
360
+ store.close();
361
+ });
362
+
363
+ test("预览路由支持 &session=,且与注入器逐字节同源", async () => {
364
+ const { store, settings, injectorText, preview } = setup({ personaEnabled: true });
365
+ seed(settings);
366
+ settings.setPersonaForSession("session-abc", { name: "会话专属", personality: "只在这里", enabled: true });
367
+ const fromInjector = injectorText({ agentPreset: "standard", sessionId: "session-abc" });
368
+ const body = await preview({ agent: "standard", session: "session-abc" });
369
+ assert.equal(body.preview.text, fromInjector, "预览必须等于注入器这一帧的输出");
370
+ assert.equal(body.preview.source, "bySession");
371
+ assert.equal(body.preview.session, "session-abc", "面板要靠它说清用的是哪张会话卡");
372
+ assert.ok(body.preview.text.includes("你的人格名字:会话专属"));
373
+ store.close();
374
+ });
375
+
376
+ test("预览只给 session 不给 agent 也走会话卡", async () => {
377
+ const { store, settings, preview } = setup({ personaEnabled: true });
378
+ seed(settings);
379
+ settings.setPersonaForSession("session-only", { name: "仅会话卡", enabled: true });
380
+ const body = await preview({ session: "session-only" });
381
+ assert.equal(body.preview.source, "bySession");
382
+ assert.equal(body.preview.agent, null);
383
+ assert.equal(body.preview.session, "session-only");
384
+ store.close();
385
+ });
386
+
387
+ test("预览的 session 参数在 strict 下同样生效(不许比注入更乐观)", async () => {
388
+ const { store, settings, preview } = setup({ personaEnabled: true, personaStrictAddressing: true });
389
+ seed(settings);
390
+ const miss = await preview({ session: "session-unknown" });
391
+ assert.equal(miss.preview.source, "none", "建过卡、这次会话没建 → 不兜底");
392
+ assert.ok(!miss.preview.text.includes("Amarre"));
393
+ settings.setPersonaForSession("session-hit", { name: "命中卡", enabled: true });
394
+ const hit = await preview({ session: "session-hit" });
395
+ assert.equal(hit.preview.source, "bySession");
396
+ store.close();
397
+ });
398
+
399
+ // ---- 7. 健壮性 ---------------------------------------------------------------
400
+
401
+ test("session.id 取不到(宿主 accessor 抛错)时不影响 agentPreset 寻址", () => {
402
+ const { store, settings, settingsCtx } = setup({ personaEnabled: true });
403
+ seed(settings);
404
+ // session.id 是抛错的 getter,header 正常——两个键各自护住,不许互相拖累
405
+ const hostile = {
406
+ agent: {
407
+ session: {
408
+ get id() { throw new Error("no id accessor"); },
409
+ header: { agentPreset: "standard" }
410
+ }
411
+ }
412
+ };
413
+ const text = settingsCtx.text(hostile);
414
+ assert.ok(text.includes("你的人格名字:标准卡"), "id 抛错不该让 preset 也丢掉");
415
+ store.close();
416
+ });
417
+
418
+ test("bySession 里的损坏卡(非对象)被丢弃,不抛错", () => {
419
+ const { store, settings } = setup();
420
+ store.db.prepare("INSERT INTO user_settings (key, value) VALUES ('persona', ?)")
421
+ .run(JSON.stringify({ default: {}, byAgent: {}, bySession: { bad: "not a card", good: { name: "好卡", enabled: true } } }));
422
+ const got = settings.getPersona();
423
+ assert.equal("bad" in got.bySession, false, "坏卡读成空对象后不收(与 byAgent 同口径)");
424
+ assert.equal(got.bySession.good.name, "好卡");
425
+ store.close();
426
+ });
427
+
428
+ test("renderUserSettingsBlock 返回值带 session 字段(面板定位用)", () => {
429
+ const { store, settings } = setup({ personaEnabled: true });
430
+ settings.setPersona({ name: "Amarre", enabled: true });
431
+ const out = renderUserSettingsBlock({ settings, config: { personaEnabled: true }, agentId: "standard", sessionId: "s1" });
432
+ assert.equal(out.session, "s1");
433
+ assert.equal(out.agent, "standard");
434
+ assert.equal(out.source, "default");
435
+ store.close();
436
+ });
437
+
438
+ // ---- 8. 子 agent 继承父会话人格卡 --------------------------------------------
439
+ //
440
+ // 子 agent 是新会话(header 带 parentSession / origin:"subagent"),用户不可能给每个
441
+ // 派生会话建卡。strict 一开,「有身份、没这张身份的卡」= 不注入人格,子 agent 会从
442
+ // 「自称父会话的人格」变成「不知道自己是谁」。所以自己没卡时继承父会话的卡。
443
+
444
+ test("子 agent 没自己的卡时继承父会话卡(source 是 parentSession)", () => {
445
+ const { store, settings, injectorText } = setup({ personaEnabled: true });
446
+ settings.setPersona({ name: "兜底卡", enabled: true });
447
+ settings.setPersonaForSession("session-parent", { name: "主控人格", personality: "轻松诙谐", enabled: true });
448
+ const text = injectorText({ agentPreset: "standard", sessionId: "session-child", parentSession: "session-parent" });
449
+ assert.ok(text.includes("你的人格名字:主控人格"), "子 agent 必须拿到父会话那张卡");
450
+ assert.ok(!text.includes("兜底卡"), "不许回落到 default");
451
+ const out = renderUserSettingsBlock({
452
+ settings,
453
+ config: { personaEnabled: true },
454
+ agentId: "standard",
455
+ sessionId: "session-child",
456
+ parentSessionId: "session-parent"
457
+ });
458
+ assert.equal(out.source, "parentSession");
459
+ store.close();
460
+ });
461
+
462
+ test("子 agent 自己有卡时自己的卡优先(继承不覆盖专门指定的卡)", () => {
463
+ const { store, settings } = setup({ personaEnabled: true });
464
+ settings.setPersonaForSession("session-parent", { name: "父卡", enabled: true });
465
+ settings.setPersonaForSession("session-child", { name: "子卡", enabled: true });
466
+ const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-child", parentSessionId: "session-parent" });
467
+ assert.equal(hit.source, "bySession");
468
+ assert.equal(hit.card.name, "子卡");
469
+ store.close();
470
+ });
471
+
472
+ test("父卡优先于 byAgent(同一段对话比同一预设更能说明用谁的人格)", () => {
473
+ const { store, settings } = setup({ personaEnabled: true });
474
+ settings.setPersonaFor("standard", { name: "预设卡", enabled: true });
475
+ settings.setPersonaForSession("session-parent", { name: "父卡", enabled: true });
476
+ const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-child", parentSessionId: "session-parent" });
477
+ assert.equal(hit.source, "parentSession");
478
+ assert.equal(hit.card.name, "父卡");
479
+ store.close();
480
+ });
481
+
482
+ test("父卡被显式关掉时不继承,落回 byAgent(关掉就是关掉)", () => {
483
+ const { store, settings } = setup({ personaEnabled: true });
484
+ settings.setPersonaFor("standard", { name: "预设卡", enabled: true });
485
+ settings.setPersonaForSession("session-parent", { name: "父卡", enabled: false });
486
+ const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-child", parentSessionId: "session-parent" });
487
+ assert.equal(hit.source, "byAgent");
488
+ assert.equal(hit.card.name, "预设卡");
489
+ store.close();
490
+ });
491
+
492
+ test("strict:子 agent 只解析到父会话 id 时也不许被 default 兜底", () => {
493
+ const { store, settings } = setup({ personaEnabled: true, personaStrictAddressing: true });
494
+ settings.setPersonaForSession("session-other", { name: "别人的卡", enabled: true });
495
+ // 子会话与父会话都没有卡;anyCard=true 且确实有身份 → none
496
+ const miss = resolvePersonaCard(settings, "standard", {
497
+ strict: true,
498
+ sessionId: "session-child",
499
+ parentSessionId: "session-nobody"
500
+ });
501
+ assert.equal(miss.source, "none", "有身份但没卡,strict 下不兜底");
502
+ store.close();
503
+ });
504
+
505
+ test("strict 关时子 agent 没父卡照旧落 default(零行为变化)", () => {
506
+ const { store, settings } = setup({ personaEnabled: true });
507
+ settings.setPersona({ name: "Amarre", enabled: true });
508
+ settings.setPersonaForSession("session-other", { name: "别人的卡", enabled: true });
509
+ const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-child", parentSessionId: "session-nope" });
510
+ assert.equal(hit.source, "default");
511
+ assert.equal(hit.card.name, "Amarre");
512
+ store.close();
513
+ });
514
+
515
+ test("parentSession 与自身 id 相同(脏数据)时不重复查同一个键", () => {
516
+ const { store, settings } = setup({ personaEnabled: true });
517
+ settings.setPersonaForSession("session-same", { name: "同一张", enabled: true });
518
+ const hit = resolvePersonaCard(settings, "standard", {
519
+ sessionId: "session-same",
520
+ parentSessionId: "session-same"
521
+ });
522
+ assert.equal(hit.source, "bySession", "自身维度先命中,不会误标成继承");
523
+ store.close();
524
+ });
525
+
526
+ test("父会话 id 是空白/非字符串时不影响其余寻址", () => {
527
+ const { store, settings } = setup({ personaEnabled: true });
528
+ settings.setPersonaFor("standard", { name: "预设卡", enabled: true });
529
+ for (const bad of ["", " ", null, undefined, 42, {}]) {
530
+ const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-child", parentSessionId: bad });
531
+ assert.equal(hit.source, "byAgent", `parentSessionId=${JSON.stringify(bad)} 应被忽略`);
532
+ }
533
+ store.close();
534
+ });
535
+
536
+ test("session.header 取不到时不影响 agentPreset 寻址(新加的父会话读取也各自兜错)", () => {
537
+ const { store, settings, settingsCtx } = setup({ personaEnabled: true });
538
+ seed(settings);
539
+ const hostile = {
540
+ agent: {
541
+ session: {
542
+ id: "session-x",
543
+ get header() { throw new Error("no header accessor"); }
544
+ }
545
+ }
546
+ };
547
+ const text = settingsCtx.text(hostile);
548
+ // header 整体取不到 → agentId/parentSessionId 皆 null,只剩 sessionId,落 default
549
+ assert.ok(text.includes("你的人格名字:Amarre"), "会话卡字段可用时不该整块崩掉");
550
+ store.close();
551
+ });
552
+
553
+ test("注入链路端到端:子 agent 拿到父会话人格,且与预览同源", async () => {
554
+ const { store, settings, injectorText, preview } = setup({ personaEnabled: true });
555
+ settings.setPersona({ name: "兜底", enabled: true });
556
+ settings.setPersonaForSession("session-parent", { name: "父会话人格", personality: "父的性格", enabled: true });
557
+ const fromInjector = injectorText({
558
+ agentPreset: "standard",
559
+ sessionId: "session-child",
560
+ parentSession: "session-parent"
561
+ });
562
+ assert.ok(fromInjector.includes("你的人格名字:父会话人格"));
563
+ const body = await preview({ agent: "standard", session: "session-child", parent: "session-parent" });
564
+ assert.equal(body.preview.text, fromInjector, "预览必须与注入器逐字节同源");
565
+ assert.equal(body.preview.source, "parentSession");
566
+ assert.equal(body.preview.parent, "session-parent");
567
+ store.close();
568
+ });