@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.
- package/LICENSE +21 -21
- package/README.md +830 -828
- package/assets/icon.svg +3 -3
- package/bin/cli.mjs +964 -964
- package/bin/dsh-mneme-choro.mjs +262 -254
- package/bin/dsh-mneme-mcp.mjs +510 -510
- package/bin/dsh-mneme-serve.mjs +151 -151
- package/cordis.patch.yml +21 -21
- package/docs/CONFIGURATION.md +407 -402
- package/docs/CONTINUITY.md +139 -139
- package/docs/DAEMON.md +57 -57
- package/docs/DIRECTORIES.md +114 -114
- package/docs/DISTILL.md +397 -371
- package/docs/DSH-GO.md +97 -97
- package/docs/ENTITIES.md +247 -247
- package/docs/FORGET.md +77 -77
- package/docs/KB.md +110 -110
- package/docs/LOCAL_MODEL.md +222 -222
- package/docs/SEMANTIC.md +256 -256
- package/docs/SHARE.md +552 -552
- package/docs/SLEEP.md +202 -202
- package/docs/STORAGE.md +122 -122
- package/docs/WORKFLOWS.md +366 -366
- package/lib/api-standalone.js +1007 -1007
- package/lib/api.js +3600 -3394
- package/lib/attachment-text.js +122 -122
- package/lib/bootstrap-http.js +105 -105
- package/lib/bootstrap.js +215 -215
- package/lib/candidates.js +454 -454
- package/lib/capabilities.js +819 -819
- package/lib/choro-command.js +111 -111
- package/lib/client.js +14420 -13436
- package/lib/commands.js +95 -95
- package/lib/config-keys.js +144 -144
- package/lib/config.js +1361 -1317
- package/lib/content-hash.js +54 -54
- package/lib/continuity.js +258 -258
- package/lib/date-hint.js +32 -0
- package/lib/diary/auto.js +190 -190
- package/lib/diary/compose.js +440 -440
- package/lib/diary/dir.js +59 -59
- package/lib/diary/export.js +97 -97
- package/lib/diary/index.js +26 -26
- package/lib/diary/library.js +189 -189
- package/lib/diary/qq-inbox.js +142 -142
- package/lib/diary/rollover.js +61 -61
- package/lib/diary/session-provider.js +122 -122
- package/lib/document-index.js +94 -94
- package/lib/document.js +322 -322
- package/lib/dream/clustering.js +123 -123
- package/lib/dream/decisions.js +945 -907
- package/lib/dream/narratives.js +44 -44
- package/lib/dream/sleep.js +2027 -1887
- package/lib/dream/triage.js +139 -0
- package/lib/dream.js +1879 -1764
- package/lib/dsh-go/host.js +162 -162
- package/lib/dsh-go/panel.js +2091 -2091
- package/lib/dsh-go/text.js +342 -342
- package/lib/dsh-go/views/common.js +68 -68
- package/lib/dsh-go/views/entity.js +48 -48
- package/lib/dsh-go/views/memory.js +198 -198
- package/lib/dsh-go/views/root.js +30 -30
- package/lib/dsh-go/views/settings.js +398 -398
- package/lib/dsh-go/views/status.js +51 -51
- package/lib/embedding.js +342 -342
- package/lib/entities/extractor.js +285 -285
- package/lib/fence.js +24 -24
- package/lib/graph/anchoring.js +184 -184
- package/lib/guide.js +74 -74
- package/lib/heat.js +144 -144
- package/lib/hot-memory.js +119 -107
- package/lib/image-describe.js +158 -0
- package/lib/index.js +852 -822
- package/lib/inject.js +656 -639
- package/lib/kb/index.js +309 -309
- package/lib/kb/routes.js +219 -219
- package/lib/kb/tools.js +148 -148
- package/lib/lang.js +607 -584
- package/lib/local-embedder.js +358 -358
- package/lib/maintenance.js +162 -162
- package/lib/memory-source.js +9 -9
- package/lib/mirror.js +330 -330
- package/lib/organize.js +456 -456
- package/lib/peak-hours.js +172 -172
- package/lib/pipeline-activities.js +178 -178
- package/lib/pipeline-attribution.js +165 -165
- package/lib/pipeline-command.js +279 -279
- package/lib/pipeline-confirm.js +142 -142
- package/lib/pipeline-exec.js +174 -174
- package/lib/pipeline-nudge-auto.js +129 -129
- package/lib/pipeline-nudge.js +1623 -1623
- package/lib/pipeline-observations.js +181 -181
- package/lib/pipeline-observe.js +133 -133
- package/lib/pipeline-runner.js +361 -361
- package/lib/pipeline-runs.js +442 -442
- package/lib/pipeline.js +716 -716
- package/lib/placement.js +13 -13
- package/lib/pre-inject-gate.js +354 -354
- package/lib/profile-extract.js +164 -164
- package/lib/profile-facts.js +356 -356
- package/lib/provenance-http.js +251 -251
- package/lib/quality-filter.js +162 -162
- package/lib/recall-stats.js +190 -190
- package/lib/reclaim-http.js +150 -150
- package/lib/reranker.js +269 -269
- package/lib/runtime/adopt.js +176 -176
- package/lib/runtime/closure.js +172 -172
- package/lib/runtime/download.js +247 -247
- package/lib/runtime/layout.js +265 -265
- package/lib/runtime/loader.js +252 -252
- package/lib/runtime/model-fetch.js +361 -361
- package/lib/runtime/provision.js +237 -237
- package/lib/runtime/tarball.js +130 -130
- package/lib/runtime/verify.js +223 -223
- package/lib/safe-slice.js +18 -18
- package/lib/scope-state.js +277 -277
- package/lib/scope.js +187 -187
- package/lib/search/adaptive.js +60 -60
- package/lib/search/bm25.js +96 -96
- package/lib/semantic.js +572 -572
- package/lib/sensitive-scan.js +209 -209
- package/lib/serve.js +228 -228
- package/lib/service.js +3268 -3196
- package/lib/session-gate.js +91 -91
- package/lib/session-usage.js +350 -350
- package/lib/settings.js +827 -802
- package/lib/share-http.js +281 -281
- package/lib/share-import.js +234 -234
- package/lib/share.js +335 -335
- package/lib/store.js +5895 -5662
- package/lib/summarize.js +1388 -1343
- package/lib/tools.js +1370 -1356
- package/lib/user-settings.js +230 -230
- package/lib/vector-index.js +180 -180
- package/lib/version-check.js +122 -122
- package/lib/wiki/claims.js +267 -0
- package/lib/wiki/cli-hint.js +90 -90
- package/lib/wiki/inject.js +164 -164
- package/lib/wiki/partition.js +200 -200
- package/lib/wiki/plan.js +214 -214
- package/lib/wiki/queries.js +640 -474
- package/lib/wiki/render.js +261 -187
- package/lib/wiki/scan.js +371 -371
- package/lib/wiki/t0.js +500 -463
- package/lib/wiki/t1-jobs.js +232 -232
- package/lib/wiki/t1.js +609 -502
- package/lib/write-admission.js +404 -404
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +15 -1
- package/runtime-manifest.json +526 -526
- package/scripts/benchmark-embed.js +201 -201
- package/scripts/benchmark-recall.js +249 -249
- package/scripts/benchmark-rerank.js +166 -166
- package/scripts/build-runtime-manifest.mjs +224 -224
- package/scripts/changelog-prep.mjs +91 -91
- package/scripts/check-sync.js +44 -44
- package/scripts/e2e-dsh.js +220 -220
- package/scripts/mneme-runtime.mjs +277 -277
- package/scripts/publish-gate.mjs +197 -197
- package/scripts/release-docs.mjs +210 -204
- package/scripts/release-notes.mjs +164 -161
- package/scripts/stress-dsh.js +255 -255
- package/scripts/sync-lib.js +47 -47
- package/scripts/sync-test-badge.mjs +69 -69
- package/scripts/test-count-sync.mjs +48 -48
- package/src/api-standalone.js +1007 -1007
- package/src/api.js +3600 -3394
- package/src/attachment-text.js +122 -122
- package/src/bootstrap-http.js +105 -105
- package/src/bootstrap.js +215 -215
- package/src/candidates.js +454 -454
- package/src/capabilities.js +819 -819
- package/src/choro-command.js +111 -111
- package/src/commands.js +95 -95
- package/src/config-keys.js +144 -144
- package/src/config.js +1361 -1317
- package/src/content-hash.js +54 -54
- package/src/continuity.js +258 -258
- package/src/date-hint.js +32 -0
- package/src/diary/auto.js +190 -190
- package/src/diary/compose.js +440 -440
- package/src/diary/dir.js +59 -59
- package/src/diary/export.js +97 -97
- package/src/diary/index.js +26 -26
- package/src/diary/library.js +189 -189
- package/src/diary/qq-inbox.js +142 -142
- package/src/diary/rollover.js +61 -61
- package/src/diary/session-provider.js +122 -122
- package/src/document-index.js +94 -94
- package/src/document.js +322 -322
- package/src/dream/clustering.js +123 -123
- package/src/dream/decisions.js +945 -907
- package/src/dream/narratives.js +44 -44
- package/src/dream/sleep.js +2027 -1887
- package/src/dream/triage.js +139 -0
- package/src/dream.js +1879 -1764
- package/src/dsh-go/host.js +162 -162
- package/src/dsh-go/panel.js +2091 -2091
- package/src/dsh-go/text.js +342 -342
- package/src/dsh-go/views/common.js +68 -68
- package/src/dsh-go/views/entity.js +48 -48
- package/src/dsh-go/views/memory.js +198 -198
- package/src/dsh-go/views/root.js +30 -30
- package/src/dsh-go/views/settings.js +398 -398
- package/src/dsh-go/views/status.js +51 -51
- package/src/embedding.js +342 -342
- package/src/entities/extractor.js +285 -285
- package/src/fence.js +24 -24
- package/src/graph/anchoring.js +184 -184
- package/src/guide.js +74 -74
- package/src/heat.js +144 -144
- package/src/hot-memory.js +119 -107
- package/src/image-describe.js +158 -0
- package/src/index.js +852 -822
- package/src/inject.js +656 -639
- package/src/kb/index.js +309 -309
- package/src/kb/routes.js +219 -219
- package/src/kb/tools.js +148 -148
- package/src/lang.js +607 -584
- package/src/local-embedder.js +358 -358
- package/src/maintenance.js +162 -162
- package/src/memory-source.js +9 -9
- package/src/mirror.js +330 -330
- package/src/organize.js +456 -456
- package/src/peak-hours.js +172 -172
- package/src/pipeline-activities.js +178 -178
- package/src/pipeline-attribution.js +165 -165
- package/src/pipeline-command.js +279 -279
- package/src/pipeline-confirm.js +142 -142
- package/src/pipeline-exec.js +174 -174
- package/src/pipeline-nudge-auto.js +129 -129
- package/src/pipeline-nudge.js +1623 -1623
- package/src/pipeline-observations.js +181 -181
- package/src/pipeline-observe.js +133 -133
- package/src/pipeline-runner.js +361 -361
- package/src/pipeline-runs.js +442 -442
- package/src/pipeline.js +716 -716
- package/src/placement.js +13 -13
- package/src/pre-inject-gate.js +354 -354
- package/src/profile-extract.js +164 -164
- package/src/profile-facts.js +356 -356
- package/src/provenance-http.js +251 -251
- package/src/quality-filter.js +162 -162
- package/src/recall-stats.js +190 -190
- package/src/reclaim-http.js +150 -150
- package/src/reranker.js +269 -269
- package/src/runtime/adopt.js +176 -176
- package/src/runtime/closure.js +172 -172
- package/src/runtime/download.js +247 -247
- package/src/runtime/layout.js +265 -265
- package/src/runtime/loader.js +252 -252
- package/src/runtime/model-fetch.js +361 -361
- package/src/runtime/provision.js +237 -237
- package/src/runtime/tarball.js +130 -130
- package/src/runtime/verify.js +223 -223
- package/src/safe-slice.js +18 -18
- package/src/scope-state.js +277 -277
- package/src/scope.js +187 -187
- package/src/search/adaptive.js +60 -60
- package/src/search/bm25.js +96 -96
- package/src/semantic.js +572 -572
- package/src/sensitive-scan.js +209 -209
- package/src/serve.js +228 -228
- package/src/service.js +3268 -3196
- package/src/session-gate.js +91 -91
- package/src/session-usage.js +350 -350
- package/src/settings.js +827 -802
- package/src/share-http.js +281 -281
- package/src/share-import.js +234 -234
- package/src/share.js +335 -335
- package/src/store.js +5895 -5662
- package/src/summarize.js +1388 -1343
- package/src/tools.js +1370 -1356
- package/src/user-settings.js +230 -230
- package/src/vector-index.js +180 -180
- package/src/version-check.js +122 -122
- package/src/wiki/claims.js +267 -0
- package/src/wiki/cli-hint.js +90 -90
- package/src/wiki/inject.js +164 -164
- package/src/wiki/partition.js +200 -200
- package/src/wiki/plan.js +214 -214
- package/src/wiki/queries.js +640 -474
- package/src/wiki/render.js +261 -187
- package/src/wiki/scan.js +371 -371
- package/src/wiki/t0.js +500 -463
- package/src/wiki/t1-jobs.js +232 -232
- package/src/wiki/t1.js +609 -502
- package/src/write-admission.js +404 -404
- package/test/adaptive-inject-budget.test.js +85 -85
- package/test/api-hardening.test.js +173 -173
- package/test/api-nudge-route.test.js +259 -259
- package/test/api-routes-gaps.test.js +1092 -1092
- package/test/api.test.js +3007 -2988
- package/test/attachment-text.test.js +139 -139
- package/test/audit.test.js +448 -448
- package/test/baseline-persist.test.js +248 -248
- package/test/benchmark.test.js +94 -94
- package/test/bm25-corpus-cache.test.js +145 -145
- package/test/bootstrap.test.js +228 -228
- package/test/candidates.test.js +345 -345
- package/test/capabilities.test.js +577 -577
- package/test/capacity-warning.test.js +262 -262
- package/test/changelog-prep.test.js +125 -125
- package/test/choro-command.test.js +218 -218
- package/test/cli-enums.test.js +37 -37
- package/test/cli-flag-value.test.js +86 -86
- package/test/cli-receive.test.js +390 -390
- package/test/cli-share.test.js +437 -437
- package/test/client-share.test.js +225 -225
- package/test/client.test.js +4595 -4184
- package/test/clustering.test.js +100 -100
- package/test/commands.test.js +106 -106
- package/test/config-keys-routes.test.js +233 -233
- package/test/config.test.js +153 -153
- package/test/conflict-freeze.test.js +344 -344
- package/test/conflict-ledger-enforce.test.js +452 -0
- package/test/conflict-ledger.test.js +242 -0
- package/test/conflict-reviewed.test.js +79 -79
- package/test/content-hash.test.js +204 -204
- package/test/continuity.test.js +452 -452
- package/test/contrast.test.js +432 -320
- package/test/date-hint.test.js +156 -0
- package/test/dedupe-candidate-window.test.js +180 -180
- package/test/dependency-overrides.test.js +78 -78
- package/test/diary-library.test.js +187 -187
- package/test/diary.test.js +576 -576
- package/test/differentiate-feedback.test.js +248 -248
- package/test/distill-tx-embed-gap.test.js +110 -110
- package/test/docs-config-keys.test.js +65 -65
- package/test/document-dir.test.js +233 -233
- package/test/document.test.js +703 -703
- package/test/dream-archive-guardrail.test.js +116 -116
- package/test/dream-baseline-clamp.test.js +174 -0
- package/test/dream-candidate-hybrid.test.js +281 -281
- package/test/dream-coverage-degrade.test.js +86 -86
- package/test/dream-decision-evidence.test.js +314 -314
- package/test/dream-failure-backoff.test.js +173 -173
- package/test/dream-fence.test.js +207 -207
- package/test/dream-maxtokens-fallback.test.js +132 -132
- package/test/dream-merge-guardrail.test.js +219 -219
- package/test/dream-peak-hours.test.js +194 -194
- package/test/dream.test.js +1528 -1528
- package/test/dsh-go-host.test.js +134 -134
- package/test/dsh-go-panel-actions.test.js +854 -854
- package/test/dsh-go-panel.test.js +251 -251
- package/test/embed-ready-poll.test.js +94 -94
- package/test/embed-text-reasons.test.js +112 -112
- package/test/embedding-cache-f32.test.js +276 -276
- package/test/embedding-ready.test.js +226 -226
- package/test/entities.test.js +711 -711
- package/test/entity-recall.test.js +179 -179
- package/test/epistemic.test.js +325 -325
- package/test/failure-query.test.js +147 -147
- package/test/failure-retention.test.js +105 -105
- package/test/fnew-0112.test.js +311 -311
- package/test/fnew-03.test.js +422 -422
- package/test/forget-corrections.test.js +70 -70
- package/test/graph-anchoring.test.js +184 -184
- package/test/graph-api.test.js +333 -209
- package/test/graph-boosted-relations.test.js +189 -189
- package/test/graph-inject-budget.test.js +103 -103
- package/test/graph-multipath.test.js +269 -269
- package/test/graph-passive-confirm.test.js +91 -91
- package/test/graph-weight-evolution.test.js +104 -104
- package/test/heat.test.js +175 -175
- package/test/helpers/dream-mock.js +86 -86
- package/test/helpers/peer-worker.mjs +25 -25
- package/test/helpers/runtime-source.js +60 -60
- package/test/helpers/tar-builder.js +80 -80
- package/test/helpers/write-admission-samples.js +312 -312
- package/test/hot-memory.test.js +259 -211
- package/test/image-description.test.js +271 -0
- package/test/image-memory.test.js +170 -170
- package/test/inject-content-cap.test.js +91 -91
- package/test/inject-first-round-bm25.test.js +89 -89
- package/test/inject-heat.test.js +77 -77
- package/test/inject-parent-gate.test.js +134 -106
- package/test/inject-pin-explicit.test.js +300 -300
- package/test/inject-pin-vector-crowd.test.js +225 -225
- package/test/inject-pools.test.js +195 -195
- package/test/inject-recall.test.js +172 -172
- package/test/inject-rotation.test.js +161 -161
- package/test/inject.test.js +553 -494
- package/test/issue-617-step1-query-and-fusion.test.js +361 -361
- package/test/kb-api-wiring.test.js +128 -128
- package/test/kb-collections.test.js +272 -272
- package/test/kb-routes.test.js +379 -379
- package/test/lang.test.js +98 -98
- package/test/lastrunat-persist.test.js +101 -101
- package/test/lib-smoke.test.js +116 -116
- package/test/llm-audit.test.js +815 -815
- package/test/llm-message-source.test.js +207 -207
- package/test/local-embedder.test.js +292 -292
- package/test/maintenance.test.js +501 -501
- package/test/mcp-stdio.test.js +385 -385
- package/test/mint-source.test.js +311 -311
- package/test/mirror-dirty.test.js +424 -424
- package/test/mirror-edit-digest.test.js +187 -187
- package/test/mirror-generation.test.js +499 -499
- package/test/mirror-types.test.js +208 -208
- package/test/mirror.test.js +249 -249
- package/test/narrative-bars.test.js +162 -162
- package/test/organize-tool.test.js +232 -232
- package/test/organize.test.js +418 -418
- package/test/panel-chart-scale.test.js +90 -0
- package/test/panel-tokens.test.js +245 -0
- package/test/peak-hours-observability.test.js +70 -70
- package/test/peer-blockers.test.js +193 -193
- package/test/persona-addressing.test.js +83 -83
- package/test/persona-api.test.js +172 -172
- package/test/persona-by-session.test.js +568 -568
- package/test/persona-flag.test.js +36 -36
- package/test/persona-inject.test.js +145 -145
- package/test/persona-settings.test.js +198 -198
- package/test/pipeline-activities.test.js +155 -155
- package/test/pipeline-attribution.test.js +175 -175
- package/test/pipeline-command.test.js +243 -243
- package/test/pipeline-confirm.test.js +148 -148
- package/test/pipeline-exec.test.js +172 -172
- package/test/pipeline-nudge-auto.test.js +248 -248
- package/test/pipeline-nudge.test.js +1278 -1278
- package/test/pipeline-observations.test.js +157 -157
- package/test/pipeline-observe.test.js +127 -127
- package/test/pipeline-r1r2.test.js +278 -278
- package/test/pipeline-runner.test.js +468 -468
- package/test/pipeline-runs.test.js +767 -767
- package/test/pipeline.test.js +636 -636
- package/test/placement.test.js +36 -36
- package/test/policy-epoch.test.js +259 -259
- package/test/pre-inject-gate.test.js +444 -444
- package/test/profile-extract.test.js +269 -269
- package/test/profile-facts.test.js +492 -492
- package/test/prompt-stance.test.js +128 -128
- package/test/provenance-http.test.js +367 -367
- package/test/publish-gate.test.js +272 -272
- package/test/quality-backfill.test.js +290 -290
- package/test/quality-filter.test.js +295 -295
- package/test/readme-claims.test.js +113 -113
- package/test/reasoning-effort.test.js +752 -752
- package/test/recall-anti-update.test.js +82 -82
- package/test/recall-evals.test.js +253 -253
- package/test/recall-layer.test.js +326 -326
- package/test/recall-retention.test.js +128 -128
- package/test/recall-stats.test.js +274 -274
- package/test/receipt-chain.test.js +451 -451
- package/test/reclaim-http.test.js +209 -209
- package/test/reflection-update-gate.test.js +129 -129
- package/test/reflection.test.js +226 -226
- package/test/reindex-backfill.test.js +475 -475
- package/test/reindex-scheduler.test.js +369 -369
- package/test/release-docs.test.js +156 -136
- package/test/release-notes.test.js +265 -240
- package/test/release-workflow.test.js +93 -93
- package/test/reranker-single-logit.test.js +73 -73
- package/test/reranker.test.js +197 -197
- package/test/resident-status.test.js +83 -83
- package/test/route-policy.test.js +113 -113
- package/test/runtime-adopt.test.js +206 -206
- package/test/runtime-cli.test.js +89 -89
- package/test/runtime-closure.test.js +193 -193
- package/test/runtime-download.test.js +302 -302
- package/test/runtime-layout.test.js +163 -163
- package/test/runtime-loader.test.js +255 -255
- package/test/runtime-manifest.test.js +159 -159
- package/test/runtime-model-fetch.test.js +348 -348
- package/test/runtime-provision.test.js +101 -101
- package/test/runtime-tarball.test.js +81 -81
- package/test/runtime-verify.test.js +319 -319
- package/test/scope-candidates.test.js +491 -491
- package/test/scope-distill-stamp.test.js +134 -134
- package/test/scope-explicit.test.js +452 -452
- package/test/scope-inject-soft.test.js +106 -106
- package/test/scope-optional-service.test.js +110 -110
- package/test/scope-retrieval.test.js +197 -197
- package/test/scope-state.test.js +610 -610
- package/test/scope-storage.test.js +255 -255
- package/test/scope-strict.test.js +367 -367
- package/test/search-fusion.test.js +90 -90
- package/test/security-doc.test.js +188 -178
- package/test/semantic.test.js +124 -124
- package/test/sensitive-scan.test.js +251 -251
- package/test/serve-bin.test.js +121 -121
- package/test/serve.test.js +258 -258
- package/test/service-search.test.js +174 -174
- package/test/service.test.js +410 -410
- package/test/session-gate.test.js +264 -264
- package/test/session-usage.test.js +317 -317
- package/test/settings.test.js +273 -273
- package/test/share-foreign-rows.test.js +144 -144
- package/test/share-http.test.js +144 -144
- package/test/share-import.test.js +577 -577
- package/test/share-imported-guard.test.js +311 -311
- package/test/share-imported-identity.test.js +111 -111
- package/test/share-pack.test.js +511 -511
- package/test/share-receive-http.test.js +756 -756
- package/test/share-routes.test.js +337 -337
- package/test/share-store.test.js +133 -133
- package/test/share-validate.test.js +101 -101
- package/test/skip-invalid-restore.test.js +262 -262
- package/test/sleep-action-set.test.js +441 -441
- package/test/sleep-catchup.test.js +315 -315
- package/test/sleep-failure-backoff.test.js +156 -156
- package/test/sleep-heat.test.js +174 -174
- package/test/sleep-scheduler-rearm.test.js +89 -89
- package/test/sleep.test.js +1455 -1418
- package/test/standalone-api.test.js +1344 -1344
- package/test/store-vector-cache.test.js +98 -98
- package/test/store-write-path-columns.test.js +191 -191
- package/test/store.test.js +744 -744
- package/test/stress.test.js +209 -209
- package/test/summarize-occurred-at.test.js +127 -127
- package/test/summarize-parse-diagnostics.test.js +75 -75
- package/test/summarize-reasoning-effort.test.js +189 -189
- package/test/summarize.test.js +2251 -2251
- package/test/supersede-state-tracking.test.js +394 -394
- package/test/test-count-sync.test.js +71 -71
- package/test/tool-state-ignore.test.js +65 -65
- package/test/tools-dto-schema-matrix.test.js +398 -398
- package/test/tools-memory-get-schema.test.js +65 -65
- package/test/tools.test.js +585 -585
- package/test/updated-at-semantics.test.js +113 -113
- package/test/user-settings-preview.test.js +127 -127
- package/test/user-settings-segments.test.js +527 -527
- package/test/vector-index.test.js +334 -334
- package/test/version-check.test.js +152 -152
- package/test/webServer-optional.test.js +82 -82
- package/test/wiki-api.test.js +734 -627
- package/test/wiki-claims.test.js +194 -0
- package/test/wiki-cli-hint.test.js +118 -118
- package/test/wiki-inject.test.js +265 -265
- package/test/wiki-partition.test.js +112 -112
- package/test/wiki-plan.test.js +80 -80
- package/test/wiki-provenance.test.js +202 -0
- package/test/wiki-queries.test.js +398 -277
- package/test/wiki-scan.test.js +208 -208
- package/test/wiki-store.test.js +190 -190
- package/test/wiki-t0.test.js +456 -380
- package/test/wiki-t1-jobs.test.js +125 -125
- package/test/wiki-t1.test.js +460 -235
- package/test/wiki-tools.test.js +176 -176
- package/test/workflow-guards.test.js +155 -155
- package/test/write-admission.test.js +668 -668
|
@@ -1,361 +1,361 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { createStore } from "../src/store.js";
|
|
4
|
-
import { createService } from "../src/service.js";
|
|
5
|
-
import { createInjector } from "../src/inject.js";
|
|
6
|
-
import { createSettings } from "../src/settings.js";
|
|
7
|
-
import { createVectorIndex } from "../src/vector-index.js";
|
|
8
|
-
import { createSessionGate } from "../src/session-gate.js";
|
|
9
|
-
|
|
10
|
-
// #617 回归。三条缺陷各自独立成组,组内都用「一正一反」两把尺子:正向锁
|
|
11
|
-
// 「改对了」,反向(关掉对应开关 / 换成对照配方)锁「这条断言真的是在测那条
|
|
12
|
-
// 通路」,免得断言退化成常量真。
|
|
13
|
-
|
|
14
|
-
// --- 组 1:Step 1 的当轮查询(根因 A)-------------------------------------
|
|
15
|
-
//
|
|
16
|
-
// 宿主 agent-loop 的每个 step 是 claim() → assemble() → step(),而 user/message
|
|
17
|
-
// 要到 step() 末尾才 append。注入的 text 回调在 assemble 里求值,所以只看事件流
|
|
18
|
-
// 必然慢一拍:Step 1 拿到 "",第 N 轮的 Step 1 拿到第 N-1 轮的旧输入。
|
|
19
|
-
// 票据(agent/inbox/claimed)在 claim 那一刻记原文,渲染时事件流还没长就用它。
|
|
20
|
-
function setupInjector(over = {}) {
|
|
21
|
-
const store = createStore(":memory:");
|
|
22
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
23
|
-
const settings = createSettings(store.db);
|
|
24
|
-
const contexts = [];
|
|
25
|
-
const handlers = new Map();
|
|
26
|
-
const ctx = {
|
|
27
|
-
// 真实宿主 ctx 有 on();旧夹具没有,所以旧夹具永远测不到这条路径。
|
|
28
|
-
on(event, fn) {
|
|
29
|
-
handlers.set(event, fn);
|
|
30
|
-
return () => handlers.delete(event);
|
|
31
|
-
},
|
|
32
|
-
systemPrompt: {
|
|
33
|
-
context(def) {
|
|
34
|
-
contexts.push(def);
|
|
35
|
-
return () => {};
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
const config = { maxInjectedItems: 3, importanceThreshold: 3, ...over };
|
|
40
|
-
const injector = createInjector(ctx, service, settings, config);
|
|
41
|
-
// 查询词是注入链路里唯一能直接观测的输入:包一层记下每次渲染实际传了什么。
|
|
42
|
-
const queries = [];
|
|
43
|
-
const orig = service.injectCandidates.bind(service);
|
|
44
|
-
service.injectCandidates = (args) => {
|
|
45
|
-
queries.push(args.query);
|
|
46
|
-
return orig(args);
|
|
47
|
-
};
|
|
48
|
-
const fireClaim = (message, session) => handlers.get("agent/inbox/claimed")?.({ message, agent: { session } });
|
|
49
|
-
return { store, service, contexts, injector, queries, fireClaim, handlers };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const userMessage = (text) => ({ type: "user/message", data: { source: { kind: "user" }, content: [{ type: "text", text }] } });
|
|
53
|
-
const claimed = (text) => ({ content: [{ type: "text", text }], source: { kind: "user" }, id: "m1" });
|
|
54
|
-
|
|
55
|
-
test("#617-A: Step 1 在 user/message 落盘之前,用认领票据拿到当轮输入", () => {
|
|
56
|
-
const { contexts, queries, fireClaim } = setupInjector();
|
|
57
|
-
const session = { id: "s-617", events: [], snapshotEvents() { return this.events; } };
|
|
58
|
-
// claim 发生在 assemble 之前,此刻事件流里还没有本轮消息
|
|
59
|
-
fireClaim(claimed("夜航App令牌桶限流方案怎么设计"), session);
|
|
60
|
-
assert.equal(session.events.length, 0, "夹具前提:认领时 user/message 尚未落盘");
|
|
61
|
-
contexts[0].text({ agent: { session } });
|
|
62
|
-
assert.equal(queries.at(-1), "夜航App令牌桶限流方案怎么设计", "Step 1 必须拿到当轮输入,而不是空串");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("#617-A: claim 与 assemble 之间插入无关事件,票据不得提前退休", () => {
|
|
66
|
-
// 宿主 preStep 的顺序是 claim() → await tools.resolve() → assemble()。中间那个
|
|
67
|
-
// await 给了别的插件追加事件的机会;若按「事件条数变多」判票据失效,Step 1 就
|
|
68
|
-
// 又退回空串——正是本 issue 要修的那一拍。判据必须是文本,不是条数。
|
|
69
|
-
const { contexts, queries, fireClaim } = setupInjector();
|
|
70
|
-
const session = { id: "s-617i", events: [], snapshotEvents() { return this.events; } };
|
|
71
|
-
session.events.push(userMessage("上一轮的问题"));
|
|
72
|
-
fireClaim(claimed("这一轮的新问题"), session);
|
|
73
|
-
// tools.resolve 期间,别的插件追加了一个与用户消息无关的事件
|
|
74
|
-
session.events.push({ type: "system/message", data: { content: [{ type: "text", text: "插件上下文" }] } });
|
|
75
|
-
contexts[0].text({ agent: { session } });
|
|
76
|
-
assert.equal(queries.at(-1), "这一轮的新问题", "无关事件不得让票据提前失效");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("#617-A: 事件流长过票据记录后,票据退休、以事件流为准", () => {
|
|
80
|
-
const { contexts, queries, fireClaim } = setupInjector();
|
|
81
|
-
const session = { id: "s-617b", events: [], snapshotEvents() { return this.events; } };
|
|
82
|
-
fireClaim(claimed("第一个问题"), session);
|
|
83
|
-
contexts[0].text({ agent: { session } });
|
|
84
|
-
assert.equal(queries.at(-1), "第一个问题");
|
|
85
|
-
// step() 末尾 append → 事件流变长
|
|
86
|
-
session.events.push(userMessage("第一个问题"));
|
|
87
|
-
contexts[0].text({ agent: { session } });
|
|
88
|
-
assert.equal(queries.at(-1), "第一个问题", "落盘后两者一致(此轮无差异)");
|
|
89
|
-
// 下一轮:新的 claim 覆盖票据,事件流里仍是上一轮的文本
|
|
90
|
-
fireClaim(claimed("第二个问题"), session);
|
|
91
|
-
contexts[0].text({ agent: { session } });
|
|
92
|
-
assert.equal(queries.at(-1), "第二个问题", "新一轮的 Step 1 不得拿到上一轮的旧输入");
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("#617-A: 非人类消息(source.kind 非 user)不产生票据", () => {
|
|
96
|
-
const { contexts, queries, fireClaim } = setupInjector();
|
|
97
|
-
const session = { id: "s-617c", events: [], snapshotEvents() { return this.events; } };
|
|
98
|
-
fireClaim({ content: [{ type: "text", text: "插件注入的上下文" }], source: { kind: "system" } }, session);
|
|
99
|
-
contexts[0].text({ agent: { session } });
|
|
100
|
-
assert.equal(queries.at(-1), "", "插件自己的话不得被当成用户问题");
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("#617-A: 卸载后摘掉 claimed 监听,不再写票据", () => {
|
|
104
|
-
const { contexts, injector, queries, fireClaim, handlers } = setupInjector();
|
|
105
|
-
const session = { id: "s-617d", events: [], snapshotEvents() { return this.events; } };
|
|
106
|
-
assert.ok(handlers.has("agent/inbox/claimed"), "挂载时注册监听");
|
|
107
|
-
injector();
|
|
108
|
-
assert.equal(handlers.has("agent/inbox/claimed"), false, "卸载即摘,不留孤儿监听");
|
|
109
|
-
fireClaim(claimed("卸载后不该记"), session);
|
|
110
|
-
// 注入器已卸载,context 也不再生效;这里只确认没有票据泄漏到下一次装配
|
|
111
|
-
assert.equal(queries.length, 0);
|
|
112
|
-
void contexts;
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
// --- 组 2:prefetch 去重与失败冷却(根因 B)-------------------------------
|
|
116
|
-
//
|
|
117
|
-
// 旧实现用 lastPrefetched 字符串去重,且置位在 await 之前 —— 同一条查询第一次
|
|
118
|
-
// 失败(模型没下好、超时)之后永不重试,而那恰好是最需要重试的一次。
|
|
119
|
-
function setupPrefetch(embedImpl) {
|
|
120
|
-
const store = createStore(":memory:");
|
|
121
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
122
|
-
const settings = createSettings(store.db);
|
|
123
|
-
const contexts = [];
|
|
124
|
-
const ctx = { systemPrompt: { context(def) { contexts.push(def); return () => {}; } } };
|
|
125
|
-
const calls = [];
|
|
126
|
-
service.setEmbedder({
|
|
127
|
-
embedSingle: async (text) => {
|
|
128
|
-
calls.push(text);
|
|
129
|
-
return embedImpl(text);
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
const injector = createInjector(ctx, service, settings, { maxInjectedItems: 3, importanceThreshold: 3 });
|
|
133
|
-
return { store, service, contexts, injector, calls };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const renderWith = (contexts, session) => contexts[0].text({ agent: { session } });
|
|
137
|
-
const tick = () => new Promise((r) => setTimeout(r, 0));
|
|
138
|
-
|
|
139
|
-
test("#617-B: 同一步的两次求值(assemble + refreshContext)只嵌一次", async () => {
|
|
140
|
-
const { contexts, calls } = setupPrefetch(async () => [1, 0, 0]);
|
|
141
|
-
const session = { id: "p1", events: [userMessage("查询文本")], snapshotEvents() { return this.events; } };
|
|
142
|
-
renderWith(contexts, session); // 第一次求值:发起 inflight
|
|
143
|
-
renderWith(contexts, session); // 第二次求值:必须复用 inflight,不再嵌
|
|
144
|
-
await tick();
|
|
145
|
-
renderWith(contexts, session); // 第三次:命中缓存
|
|
146
|
-
assert.equal(calls.length, 1, "同一条查询只应嵌入一次");
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test("#617-B: 失败后进入冷却期,不在同一帧反复撞模型", async () => {
|
|
150
|
-
const { contexts, calls } = setupPrefetch(async () => { throw new Error("model not ready"); });
|
|
151
|
-
const session = { id: "p2", events: [userMessage("失败查询")], snapshotEvents() { return this.events; } };
|
|
152
|
-
renderWith(contexts, session);
|
|
153
|
-
await tick();
|
|
154
|
-
renderWith(contexts, session);
|
|
155
|
-
await tick();
|
|
156
|
-
renderWith(contexts, session);
|
|
157
|
-
assert.equal(calls.length, 1, "冷却期内不得重试(15s 内)");
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("#617-B: 冷却按查询分别计,别的查询照常嵌入", async () => {
|
|
161
|
-
const { contexts, calls } = setupPrefetch(async (text) => {
|
|
162
|
-
if (text.includes("失败")) throw new Error("boom");
|
|
163
|
-
return [1, 0, 0];
|
|
164
|
-
});
|
|
165
|
-
const s1 = { id: "p3", events: [userMessage("失败查询")], snapshotEvents() { return this.events; } };
|
|
166
|
-
renderWith(contexts, s1);
|
|
167
|
-
await tick();
|
|
168
|
-
renderWith(contexts, s1);
|
|
169
|
-
const s2 = { id: "p4", events: [userMessage("另一条查询")], snapshotEvents() { return this.events; } };
|
|
170
|
-
renderWith(contexts, s2);
|
|
171
|
-
await tick();
|
|
172
|
-
assert.equal(calls.length, 2, "一条查询的失败冷却不应连坐其它查询");
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// --- 组 3:向量路与 BM25 路并存(根因 C,前半)----------------------------
|
|
176
|
-
function seedStalePreferences(service, n = 8) {
|
|
177
|
-
const ids = [];
|
|
178
|
-
for (let i = 0; i < n; i++) {
|
|
179
|
-
ids.push(service.saveWithDedupe({
|
|
180
|
-
type: "preference",
|
|
181
|
-
title: `旧偏好条目${i}`,
|
|
182
|
-
content: `用户喜欢深色界面与紧凑排版,这是第${i}条历史偏好存档`,
|
|
183
|
-
importance: 5
|
|
184
|
-
}).memory.id);
|
|
185
|
-
}
|
|
186
|
-
return ids;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function setupDualPath(config = {}) {
|
|
190
|
-
const store = createStore(":memory:");
|
|
191
|
-
const service = createService({ store, mirror: null, config });
|
|
192
|
-
return { store, service };
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
test("#617-C: 有向量命中时,BM25 的词法精确命中不再整批消失", () => {
|
|
196
|
-
const { service, store } = setupDualPath();
|
|
197
|
-
seedStalePreferences(service, 8);
|
|
198
|
-
// 与查询词法完全不相交,但向量最近
|
|
199
|
-
const vecOnly = service.saveWithDedupe({
|
|
200
|
-
type: "project", title: "星图渲染重构", content: "绘制管线迁移与图层合并", importance: 3
|
|
201
|
-
}).memory;
|
|
202
|
-
// 词法精确命中,但没有向量(真实库里大量老行就是这样)
|
|
203
|
-
const lexHit = service.saveWithDedupe({
|
|
204
|
-
type: "decision", title: "夜航限流选型", content: "决定采用令牌桶做夜航App的限流方案", importance: 3
|
|
205
|
-
}).memory;
|
|
206
|
-
const vectorIndex = createVectorIndex({ store });
|
|
207
|
-
service.setVectorIndex(vectorIndex);
|
|
208
|
-
vectorIndex.saveEmbedding(vecOnly.id, [1, 0, 0]);
|
|
209
|
-
|
|
210
|
-
const picked = service.injectCandidates({
|
|
211
|
-
query: "夜航App令牌桶限流方案怎么设计的", maxItems: 5, threshold: 3, queryVector: [1, 0, 0]
|
|
212
|
-
});
|
|
213
|
-
assert.equal(picked[0].id, vecOnly.id, "纯余弦命中仍是首位");
|
|
214
|
-
assert.ok(picked.some((m) => m.id === lexHit.id), "BM25 命中不得被向量路挤掉(旧代码:有向量则 BM25 整段跳过)");
|
|
215
|
-
assert.ok(picked.length <= 5, "池子仍按 maxItems 封顶");
|
|
216
|
-
store.close();
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
test("#617-C: 关掉 BM25 后同一条词法命中确实消失(反向对照)", () => {
|
|
220
|
-
const { service, store } = setupDualPath({ bm25SearchEnabled: false });
|
|
221
|
-
seedStalePreferences(service, 8);
|
|
222
|
-
const vecOnly = service.saveWithDedupe({
|
|
223
|
-
type: "project", title: "星图渲染重构", content: "绘制管线迁移与图层合并", importance: 3
|
|
224
|
-
}).memory;
|
|
225
|
-
const lexHit = service.saveWithDedupe({
|
|
226
|
-
type: "decision", title: "夜航限流选型", content: "决定采用令牌桶做夜航App的限流方案", importance: 3
|
|
227
|
-
}).memory;
|
|
228
|
-
const vectorIndex = createVectorIndex({ store });
|
|
229
|
-
service.setVectorIndex(vectorIndex);
|
|
230
|
-
vectorIndex.saveEmbedding(vecOnly.id, [1, 0, 0]);
|
|
231
|
-
|
|
232
|
-
const picked = service.injectCandidates({
|
|
233
|
-
query: "夜航App令牌桶限流方案怎么设计的", maxItems: 5, threshold: 3, queryVector: [1, 0, 0]
|
|
234
|
-
});
|
|
235
|
-
assert.equal(picked[0].id, vecOnly.id);
|
|
236
|
-
assert.equal(picked.some((m) => m.id === lexHit.id), false, "BM25 关 = 词法路不参与,正向用例才不是在测别的东西");
|
|
237
|
-
store.close();
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
// --- 组 4:auto 不再被 keyword 覆盖(根因 C,后半)------------------------
|
|
241
|
-
//
|
|
242
|
-
// 旧代码在 fuseRecall 收尾处,只要 recipe !== "blend" 且 mode === "auto",就用
|
|
243
|
-
// keyword.slice(0,lim) 覆盖 merged —— 于是配了 rrf 却拿到 keyword 的顺序,配方
|
|
244
|
-
// 白选。auto 的「关键词领位」语义只属于 blend。
|
|
245
|
-
function setupFusion(recipe) {
|
|
246
|
-
const store = createStore(":memory:");
|
|
247
|
-
const service = createService({ store, mirror: null, config: { recallFusion: recipe } });
|
|
248
|
-
// 标题命中(keyword 第一),但没有向量
|
|
249
|
-
const kwTitle = service.saveWithDedupe({
|
|
250
|
-
type: "preference", title: "令牌桶限流笔记", content: "随手记的限流笔记", importance: 2
|
|
251
|
-
}).memory;
|
|
252
|
-
// 正文命中(keyword 第二)+ 向量第一 —— 两路都靠前
|
|
253
|
-
const both = service.saveWithDedupe({
|
|
254
|
-
type: "decision", title: "夜航限流选型", content: "决定采用令牌桶限流方案,阈值按等级分层", importance: 5
|
|
255
|
-
}).memory;
|
|
256
|
-
const vectorIndex = createVectorIndex({ store });
|
|
257
|
-
service.setVectorIndex(vectorIndex);
|
|
258
|
-
vectorIndex.saveEmbedding(both.id, [1, 0, 0]);
|
|
259
|
-
service.setEmbedder({ embedSingle: async () => [1, 0, 0] });
|
|
260
|
-
return { service, store, kwTitle, both };
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
test("#617-C: recallFusion=rrf 时 auto 用 RRF 自己的排序,不再被 keyword 覆盖", async () => {
|
|
264
|
-
const { service, store, kwTitle, both } = setupFusion("rrf");
|
|
265
|
-
const rows = await service.searchMemories("令牌桶限流", { mode: "auto", topK: 2, useRerank: false, recordRecall: false });
|
|
266
|
-
assert.equal(rows.length, 2);
|
|
267
|
-
assert.equal(rows[0].id, both.id, "两路都命中的行必须排第一(keyword 只命中标题的行不该领位)");
|
|
268
|
-
assert.equal(rows[1].id, kwTitle.id);
|
|
269
|
-
store.close();
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
test("#617-C: blend 的 auto 关键词领位契约保持不变(反向对照)", async () => {
|
|
273
|
-
const { service, store, kwTitle, both } = setupFusion("blend");
|
|
274
|
-
const rows = await service.searchMemories("令牌桶限流", { mode: "auto", topK: 2, useRerank: false, recordRecall: false });
|
|
275
|
-
assert.equal(rows.length, 2);
|
|
276
|
-
assert.equal(rows[0].id, kwTitle.id, "blend 下 keyword 领位是既有契约,本次改动不该碰它");
|
|
277
|
-
assert.equal(rows[1].id, both.id);
|
|
278
|
-
store.close();
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
// --- 组 5:无痕会话的认领路径不得外发(#539 × #617)-----------------------
|
|
282
|
-
//
|
|
283
|
-
// claim 在宿主 pre-step 里同步触发,早于 assemble,因此也早于渲染侧的闸门判断。
|
|
284
|
-
// 这条路径既写票据(票据会成为注入查询文本)又预取向量(embedQuery 直接 POST
|
|
285
|
-
// 外部 /embeddings),闸门命中时两者都是该会话数据的外流;嵌入层自身没有任何
|
|
286
|
-
// 会话维度判断(embedding.js / service.js 里 sessionGate 零命中),这里是唯一
|
|
287
|
-
// 拦截点。两条反向对照(未命中名单 / 总开关关闭)保证断言不是退化成「嵌入器
|
|
288
|
-
// 恰好没跑」。
|
|
289
|
-
function setupGated(over = {}) {
|
|
290
|
-
const store = createStore(":memory:");
|
|
291
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
292
|
-
const settings = createSettings(store.db);
|
|
293
|
-
const contexts = [];
|
|
294
|
-
const handlers = new Map();
|
|
295
|
-
const ctx = {
|
|
296
|
-
on(event, fn) {
|
|
297
|
-
handlers.set(event, fn);
|
|
298
|
-
return () => handlers.delete(event);
|
|
299
|
-
},
|
|
300
|
-
systemPrompt: { context(def) { contexts.push(def); return () => {}; } }
|
|
301
|
-
};
|
|
302
|
-
const calls = [];
|
|
303
|
-
service.setEmbedder({
|
|
304
|
-
embedSingle: async (text) => {
|
|
305
|
-
calls.push(text);
|
|
306
|
-
return [1, 0, 0];
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
const queries = [];
|
|
310
|
-
const orig = service.injectCandidates.bind(service);
|
|
311
|
-
service.injectCandidates = (args) => {
|
|
312
|
-
queries.push(args.query);
|
|
313
|
-
return orig(args);
|
|
314
|
-
};
|
|
315
|
-
const sessionGate = createSessionGate({
|
|
316
|
-
sessionGateEnabled: true,
|
|
317
|
-
sessionGateSessions: ["s-gated"],
|
|
318
|
-
sessionGateSubagents: true,
|
|
319
|
-
...over
|
|
320
|
-
});
|
|
321
|
-
const injector = createInjector(
|
|
322
|
-
ctx, service, settings, { maxInjectedItems: 3, importanceThreshold: 3 }, null, sessionGate
|
|
323
|
-
);
|
|
324
|
-
const fireClaim = (message, session) => handlers.get("agent/inbox/claimed")?.({ message, agent: { session } });
|
|
325
|
-
return { store, service, contexts, injector, calls, queries, fireClaim, sessionGate };
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
test("#617-D: 无痕会话的认领消息既不预取向量,也不留票据", async () => {
|
|
329
|
-
const { contexts, calls, queries, fireClaim, sessionGate } = setupGated();
|
|
330
|
-
const session = { id: "s-gated", events: [], snapshotEvents() { return this.events; } };
|
|
331
|
-
assert.equal(sessionGate.isOff(session), true, "夹具前提:闸门认得出这个会话");
|
|
332
|
-
fireClaim(claimed("桉桉的私人日记:我今天去了协和医院复查"), session);
|
|
333
|
-
await tick();
|
|
334
|
-
assert.equal(calls.length, 0, "闸门命中时不得把原文发往嵌入端点");
|
|
335
|
-
// 票据若被写下,渲染时事件流还没长,Step 1 就会拿到这段原文
|
|
336
|
-
contexts[0].text({ agent: { session } });
|
|
337
|
-
assert.deepEqual(queries, [], "闸门命中时连 injectCandidates 都不该被调到");
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
test("#617-D: 未命中名单的会话照常预取(反向对照)", async () => {
|
|
341
|
-
const { contexts, calls, queries, fireClaim, sessionGate } = setupGated();
|
|
342
|
-
const session = { id: "s-normal", events: [], snapshotEvents() { return this.events; } };
|
|
343
|
-
assert.equal(sessionGate.isOff(session), false);
|
|
344
|
-
fireClaim(claimed("夜航App令牌桶限流方案怎么设计"), session);
|
|
345
|
-
await tick();
|
|
346
|
-
assert.equal(calls.length, 1, "普通会话的预取行为不变");
|
|
347
|
-
assert.equal(calls[0], "夜航App令牌桶限流方案怎么设计");
|
|
348
|
-
// 票据确实写下了:渲染拿到的就是认领的原文
|
|
349
|
-
contexts[0].text({ agent: { session } });
|
|
350
|
-
assert.deepEqual(queries, ["夜航App令牌桶限流方案怎么设计"]);
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
test("#617-D: 总开关关闭时闸门不生效(反向对照)", async () => {
|
|
354
|
-
const { calls, fireClaim, sessionGate } = setupGated({ sessionGateEnabled: false });
|
|
355
|
-
const session = { id: "s-gated", events: [], snapshotEvents() { return this.events; } };
|
|
356
|
-
assert.equal(sessionGate.isEnabled(), false);
|
|
357
|
-
assert.equal(sessionGate.isOff(session), false, "总开关关时名单不得生效");
|
|
358
|
-
fireClaim(claimed("夜航App令牌桶限流方案怎么设计"), session);
|
|
359
|
-
await tick();
|
|
360
|
-
assert.equal(calls.length, 1, "未开启 #539 时不得凭空拦截(默认行为须与基线一致)");
|
|
361
|
-
});
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createStore } from "../src/store.js";
|
|
4
|
+
import { createService } from "../src/service.js";
|
|
5
|
+
import { createInjector } from "../src/inject.js";
|
|
6
|
+
import { createSettings } from "../src/settings.js";
|
|
7
|
+
import { createVectorIndex } from "../src/vector-index.js";
|
|
8
|
+
import { createSessionGate } from "../src/session-gate.js";
|
|
9
|
+
|
|
10
|
+
// #617 回归。三条缺陷各自独立成组,组内都用「一正一反」两把尺子:正向锁
|
|
11
|
+
// 「改对了」,反向(关掉对应开关 / 换成对照配方)锁「这条断言真的是在测那条
|
|
12
|
+
// 通路」,免得断言退化成常量真。
|
|
13
|
+
|
|
14
|
+
// --- 组 1:Step 1 的当轮查询(根因 A)-------------------------------------
|
|
15
|
+
//
|
|
16
|
+
// 宿主 agent-loop 的每个 step 是 claim() → assemble() → step(),而 user/message
|
|
17
|
+
// 要到 step() 末尾才 append。注入的 text 回调在 assemble 里求值,所以只看事件流
|
|
18
|
+
// 必然慢一拍:Step 1 拿到 "",第 N 轮的 Step 1 拿到第 N-1 轮的旧输入。
|
|
19
|
+
// 票据(agent/inbox/claimed)在 claim 那一刻记原文,渲染时事件流还没长就用它。
|
|
20
|
+
function setupInjector(over = {}) {
|
|
21
|
+
const store = createStore(":memory:");
|
|
22
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
23
|
+
const settings = createSettings(store.db);
|
|
24
|
+
const contexts = [];
|
|
25
|
+
const handlers = new Map();
|
|
26
|
+
const ctx = {
|
|
27
|
+
// 真实宿主 ctx 有 on();旧夹具没有,所以旧夹具永远测不到这条路径。
|
|
28
|
+
on(event, fn) {
|
|
29
|
+
handlers.set(event, fn);
|
|
30
|
+
return () => handlers.delete(event);
|
|
31
|
+
},
|
|
32
|
+
systemPrompt: {
|
|
33
|
+
context(def) {
|
|
34
|
+
contexts.push(def);
|
|
35
|
+
return () => {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const config = { maxInjectedItems: 3, importanceThreshold: 3, ...over };
|
|
40
|
+
const injector = createInjector(ctx, service, settings, config);
|
|
41
|
+
// 查询词是注入链路里唯一能直接观测的输入:包一层记下每次渲染实际传了什么。
|
|
42
|
+
const queries = [];
|
|
43
|
+
const orig = service.injectCandidates.bind(service);
|
|
44
|
+
service.injectCandidates = (args) => {
|
|
45
|
+
queries.push(args.query);
|
|
46
|
+
return orig(args);
|
|
47
|
+
};
|
|
48
|
+
const fireClaim = (message, session) => handlers.get("agent/inbox/claimed")?.({ message, agent: { session } });
|
|
49
|
+
return { store, service, contexts, injector, queries, fireClaim, handlers };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const userMessage = (text) => ({ type: "user/message", data: { source: { kind: "user" }, content: [{ type: "text", text }] } });
|
|
53
|
+
const claimed = (text) => ({ content: [{ type: "text", text }], source: { kind: "user" }, id: "m1" });
|
|
54
|
+
|
|
55
|
+
test("#617-A: Step 1 在 user/message 落盘之前,用认领票据拿到当轮输入", () => {
|
|
56
|
+
const { contexts, queries, fireClaim } = setupInjector();
|
|
57
|
+
const session = { id: "s-617", events: [], snapshotEvents() { return this.events; } };
|
|
58
|
+
// claim 发生在 assemble 之前,此刻事件流里还没有本轮消息
|
|
59
|
+
fireClaim(claimed("夜航App令牌桶限流方案怎么设计"), session);
|
|
60
|
+
assert.equal(session.events.length, 0, "夹具前提:认领时 user/message 尚未落盘");
|
|
61
|
+
contexts[0].text({ agent: { session } });
|
|
62
|
+
assert.equal(queries.at(-1), "夜航App令牌桶限流方案怎么设计", "Step 1 必须拿到当轮输入,而不是空串");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("#617-A: claim 与 assemble 之间插入无关事件,票据不得提前退休", () => {
|
|
66
|
+
// 宿主 preStep 的顺序是 claim() → await tools.resolve() → assemble()。中间那个
|
|
67
|
+
// await 给了别的插件追加事件的机会;若按「事件条数变多」判票据失效,Step 1 就
|
|
68
|
+
// 又退回空串——正是本 issue 要修的那一拍。判据必须是文本,不是条数。
|
|
69
|
+
const { contexts, queries, fireClaim } = setupInjector();
|
|
70
|
+
const session = { id: "s-617i", events: [], snapshotEvents() { return this.events; } };
|
|
71
|
+
session.events.push(userMessage("上一轮的问题"));
|
|
72
|
+
fireClaim(claimed("这一轮的新问题"), session);
|
|
73
|
+
// tools.resolve 期间,别的插件追加了一个与用户消息无关的事件
|
|
74
|
+
session.events.push({ type: "system/message", data: { content: [{ type: "text", text: "插件上下文" }] } });
|
|
75
|
+
contexts[0].text({ agent: { session } });
|
|
76
|
+
assert.equal(queries.at(-1), "这一轮的新问题", "无关事件不得让票据提前失效");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("#617-A: 事件流长过票据记录后,票据退休、以事件流为准", () => {
|
|
80
|
+
const { contexts, queries, fireClaim } = setupInjector();
|
|
81
|
+
const session = { id: "s-617b", events: [], snapshotEvents() { return this.events; } };
|
|
82
|
+
fireClaim(claimed("第一个问题"), session);
|
|
83
|
+
contexts[0].text({ agent: { session } });
|
|
84
|
+
assert.equal(queries.at(-1), "第一个问题");
|
|
85
|
+
// step() 末尾 append → 事件流变长
|
|
86
|
+
session.events.push(userMessage("第一个问题"));
|
|
87
|
+
contexts[0].text({ agent: { session } });
|
|
88
|
+
assert.equal(queries.at(-1), "第一个问题", "落盘后两者一致(此轮无差异)");
|
|
89
|
+
// 下一轮:新的 claim 覆盖票据,事件流里仍是上一轮的文本
|
|
90
|
+
fireClaim(claimed("第二个问题"), session);
|
|
91
|
+
contexts[0].text({ agent: { session } });
|
|
92
|
+
assert.equal(queries.at(-1), "第二个问题", "新一轮的 Step 1 不得拿到上一轮的旧输入");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("#617-A: 非人类消息(source.kind 非 user)不产生票据", () => {
|
|
96
|
+
const { contexts, queries, fireClaim } = setupInjector();
|
|
97
|
+
const session = { id: "s-617c", events: [], snapshotEvents() { return this.events; } };
|
|
98
|
+
fireClaim({ content: [{ type: "text", text: "插件注入的上下文" }], source: { kind: "system" } }, session);
|
|
99
|
+
contexts[0].text({ agent: { session } });
|
|
100
|
+
assert.equal(queries.at(-1), "", "插件自己的话不得被当成用户问题");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("#617-A: 卸载后摘掉 claimed 监听,不再写票据", () => {
|
|
104
|
+
const { contexts, injector, queries, fireClaim, handlers } = setupInjector();
|
|
105
|
+
const session = { id: "s-617d", events: [], snapshotEvents() { return this.events; } };
|
|
106
|
+
assert.ok(handlers.has("agent/inbox/claimed"), "挂载时注册监听");
|
|
107
|
+
injector();
|
|
108
|
+
assert.equal(handlers.has("agent/inbox/claimed"), false, "卸载即摘,不留孤儿监听");
|
|
109
|
+
fireClaim(claimed("卸载后不该记"), session);
|
|
110
|
+
// 注入器已卸载,context 也不再生效;这里只确认没有票据泄漏到下一次装配
|
|
111
|
+
assert.equal(queries.length, 0);
|
|
112
|
+
void contexts;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// --- 组 2:prefetch 去重与失败冷却(根因 B)-------------------------------
|
|
116
|
+
//
|
|
117
|
+
// 旧实现用 lastPrefetched 字符串去重,且置位在 await 之前 —— 同一条查询第一次
|
|
118
|
+
// 失败(模型没下好、超时)之后永不重试,而那恰好是最需要重试的一次。
|
|
119
|
+
function setupPrefetch(embedImpl) {
|
|
120
|
+
const store = createStore(":memory:");
|
|
121
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
122
|
+
const settings = createSettings(store.db);
|
|
123
|
+
const contexts = [];
|
|
124
|
+
const ctx = { systemPrompt: { context(def) { contexts.push(def); return () => {}; } } };
|
|
125
|
+
const calls = [];
|
|
126
|
+
service.setEmbedder({
|
|
127
|
+
embedSingle: async (text) => {
|
|
128
|
+
calls.push(text);
|
|
129
|
+
return embedImpl(text);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
const injector = createInjector(ctx, service, settings, { maxInjectedItems: 3, importanceThreshold: 3 });
|
|
133
|
+
return { store, service, contexts, injector, calls };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const renderWith = (contexts, session) => contexts[0].text({ agent: { session } });
|
|
137
|
+
const tick = () => new Promise((r) => setTimeout(r, 0));
|
|
138
|
+
|
|
139
|
+
test("#617-B: 同一步的两次求值(assemble + refreshContext)只嵌一次", async () => {
|
|
140
|
+
const { contexts, calls } = setupPrefetch(async () => [1, 0, 0]);
|
|
141
|
+
const session = { id: "p1", events: [userMessage("查询文本")], snapshotEvents() { return this.events; } };
|
|
142
|
+
renderWith(contexts, session); // 第一次求值:发起 inflight
|
|
143
|
+
renderWith(contexts, session); // 第二次求值:必须复用 inflight,不再嵌
|
|
144
|
+
await tick();
|
|
145
|
+
renderWith(contexts, session); // 第三次:命中缓存
|
|
146
|
+
assert.equal(calls.length, 1, "同一条查询只应嵌入一次");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("#617-B: 失败后进入冷却期,不在同一帧反复撞模型", async () => {
|
|
150
|
+
const { contexts, calls } = setupPrefetch(async () => { throw new Error("model not ready"); });
|
|
151
|
+
const session = { id: "p2", events: [userMessage("失败查询")], snapshotEvents() { return this.events; } };
|
|
152
|
+
renderWith(contexts, session);
|
|
153
|
+
await tick();
|
|
154
|
+
renderWith(contexts, session);
|
|
155
|
+
await tick();
|
|
156
|
+
renderWith(contexts, session);
|
|
157
|
+
assert.equal(calls.length, 1, "冷却期内不得重试(15s 内)");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("#617-B: 冷却按查询分别计,别的查询照常嵌入", async () => {
|
|
161
|
+
const { contexts, calls } = setupPrefetch(async (text) => {
|
|
162
|
+
if (text.includes("失败")) throw new Error("boom");
|
|
163
|
+
return [1, 0, 0];
|
|
164
|
+
});
|
|
165
|
+
const s1 = { id: "p3", events: [userMessage("失败查询")], snapshotEvents() { return this.events; } };
|
|
166
|
+
renderWith(contexts, s1);
|
|
167
|
+
await tick();
|
|
168
|
+
renderWith(contexts, s1);
|
|
169
|
+
const s2 = { id: "p4", events: [userMessage("另一条查询")], snapshotEvents() { return this.events; } };
|
|
170
|
+
renderWith(contexts, s2);
|
|
171
|
+
await tick();
|
|
172
|
+
assert.equal(calls.length, 2, "一条查询的失败冷却不应连坐其它查询");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// --- 组 3:向量路与 BM25 路并存(根因 C,前半)----------------------------
|
|
176
|
+
function seedStalePreferences(service, n = 8) {
|
|
177
|
+
const ids = [];
|
|
178
|
+
for (let i = 0; i < n; i++) {
|
|
179
|
+
ids.push(service.saveWithDedupe({
|
|
180
|
+
type: "preference",
|
|
181
|
+
title: `旧偏好条目${i}`,
|
|
182
|
+
content: `用户喜欢深色界面与紧凑排版,这是第${i}条历史偏好存档`,
|
|
183
|
+
importance: 5
|
|
184
|
+
}).memory.id);
|
|
185
|
+
}
|
|
186
|
+
return ids;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function setupDualPath(config = {}) {
|
|
190
|
+
const store = createStore(":memory:");
|
|
191
|
+
const service = createService({ store, mirror: null, config });
|
|
192
|
+
return { store, service };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
test("#617-C: 有向量命中时,BM25 的词法精确命中不再整批消失", () => {
|
|
196
|
+
const { service, store } = setupDualPath();
|
|
197
|
+
seedStalePreferences(service, 8);
|
|
198
|
+
// 与查询词法完全不相交,但向量最近
|
|
199
|
+
const vecOnly = service.saveWithDedupe({
|
|
200
|
+
type: "project", title: "星图渲染重构", content: "绘制管线迁移与图层合并", importance: 3
|
|
201
|
+
}).memory;
|
|
202
|
+
// 词法精确命中,但没有向量(真实库里大量老行就是这样)
|
|
203
|
+
const lexHit = service.saveWithDedupe({
|
|
204
|
+
type: "decision", title: "夜航限流选型", content: "决定采用令牌桶做夜航App的限流方案", importance: 3
|
|
205
|
+
}).memory;
|
|
206
|
+
const vectorIndex = createVectorIndex({ store });
|
|
207
|
+
service.setVectorIndex(vectorIndex);
|
|
208
|
+
vectorIndex.saveEmbedding(vecOnly.id, [1, 0, 0]);
|
|
209
|
+
|
|
210
|
+
const picked = service.injectCandidates({
|
|
211
|
+
query: "夜航App令牌桶限流方案怎么设计的", maxItems: 5, threshold: 3, queryVector: [1, 0, 0]
|
|
212
|
+
});
|
|
213
|
+
assert.equal(picked[0].id, vecOnly.id, "纯余弦命中仍是首位");
|
|
214
|
+
assert.ok(picked.some((m) => m.id === lexHit.id), "BM25 命中不得被向量路挤掉(旧代码:有向量则 BM25 整段跳过)");
|
|
215
|
+
assert.ok(picked.length <= 5, "池子仍按 maxItems 封顶");
|
|
216
|
+
store.close();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("#617-C: 关掉 BM25 后同一条词法命中确实消失(反向对照)", () => {
|
|
220
|
+
const { service, store } = setupDualPath({ bm25SearchEnabled: false });
|
|
221
|
+
seedStalePreferences(service, 8);
|
|
222
|
+
const vecOnly = service.saveWithDedupe({
|
|
223
|
+
type: "project", title: "星图渲染重构", content: "绘制管线迁移与图层合并", importance: 3
|
|
224
|
+
}).memory;
|
|
225
|
+
const lexHit = service.saveWithDedupe({
|
|
226
|
+
type: "decision", title: "夜航限流选型", content: "决定采用令牌桶做夜航App的限流方案", importance: 3
|
|
227
|
+
}).memory;
|
|
228
|
+
const vectorIndex = createVectorIndex({ store });
|
|
229
|
+
service.setVectorIndex(vectorIndex);
|
|
230
|
+
vectorIndex.saveEmbedding(vecOnly.id, [1, 0, 0]);
|
|
231
|
+
|
|
232
|
+
const picked = service.injectCandidates({
|
|
233
|
+
query: "夜航App令牌桶限流方案怎么设计的", maxItems: 5, threshold: 3, queryVector: [1, 0, 0]
|
|
234
|
+
});
|
|
235
|
+
assert.equal(picked[0].id, vecOnly.id);
|
|
236
|
+
assert.equal(picked.some((m) => m.id === lexHit.id), false, "BM25 关 = 词法路不参与,正向用例才不是在测别的东西");
|
|
237
|
+
store.close();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// --- 组 4:auto 不再被 keyword 覆盖(根因 C,后半)------------------------
|
|
241
|
+
//
|
|
242
|
+
// 旧代码在 fuseRecall 收尾处,只要 recipe !== "blend" 且 mode === "auto",就用
|
|
243
|
+
// keyword.slice(0,lim) 覆盖 merged —— 于是配了 rrf 却拿到 keyword 的顺序,配方
|
|
244
|
+
// 白选。auto 的「关键词领位」语义只属于 blend。
|
|
245
|
+
function setupFusion(recipe) {
|
|
246
|
+
const store = createStore(":memory:");
|
|
247
|
+
const service = createService({ store, mirror: null, config: { recallFusion: recipe } });
|
|
248
|
+
// 标题命中(keyword 第一),但没有向量
|
|
249
|
+
const kwTitle = service.saveWithDedupe({
|
|
250
|
+
type: "preference", title: "令牌桶限流笔记", content: "随手记的限流笔记", importance: 2
|
|
251
|
+
}).memory;
|
|
252
|
+
// 正文命中(keyword 第二)+ 向量第一 —— 两路都靠前
|
|
253
|
+
const both = service.saveWithDedupe({
|
|
254
|
+
type: "decision", title: "夜航限流选型", content: "决定采用令牌桶限流方案,阈值按等级分层", importance: 5
|
|
255
|
+
}).memory;
|
|
256
|
+
const vectorIndex = createVectorIndex({ store });
|
|
257
|
+
service.setVectorIndex(vectorIndex);
|
|
258
|
+
vectorIndex.saveEmbedding(both.id, [1, 0, 0]);
|
|
259
|
+
service.setEmbedder({ embedSingle: async () => [1, 0, 0] });
|
|
260
|
+
return { service, store, kwTitle, both };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
test("#617-C: recallFusion=rrf 时 auto 用 RRF 自己的排序,不再被 keyword 覆盖", async () => {
|
|
264
|
+
const { service, store, kwTitle, both } = setupFusion("rrf");
|
|
265
|
+
const rows = await service.searchMemories("令牌桶限流", { mode: "auto", topK: 2, useRerank: false, recordRecall: false });
|
|
266
|
+
assert.equal(rows.length, 2);
|
|
267
|
+
assert.equal(rows[0].id, both.id, "两路都命中的行必须排第一(keyword 只命中标题的行不该领位)");
|
|
268
|
+
assert.equal(rows[1].id, kwTitle.id);
|
|
269
|
+
store.close();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("#617-C: blend 的 auto 关键词领位契约保持不变(反向对照)", async () => {
|
|
273
|
+
const { service, store, kwTitle, both } = setupFusion("blend");
|
|
274
|
+
const rows = await service.searchMemories("令牌桶限流", { mode: "auto", topK: 2, useRerank: false, recordRecall: false });
|
|
275
|
+
assert.equal(rows.length, 2);
|
|
276
|
+
assert.equal(rows[0].id, kwTitle.id, "blend 下 keyword 领位是既有契约,本次改动不该碰它");
|
|
277
|
+
assert.equal(rows[1].id, both.id);
|
|
278
|
+
store.close();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// --- 组 5:无痕会话的认领路径不得外发(#539 × #617)-----------------------
|
|
282
|
+
//
|
|
283
|
+
// claim 在宿主 pre-step 里同步触发,早于 assemble,因此也早于渲染侧的闸门判断。
|
|
284
|
+
// 这条路径既写票据(票据会成为注入查询文本)又预取向量(embedQuery 直接 POST
|
|
285
|
+
// 外部 /embeddings),闸门命中时两者都是该会话数据的外流;嵌入层自身没有任何
|
|
286
|
+
// 会话维度判断(embedding.js / service.js 里 sessionGate 零命中),这里是唯一
|
|
287
|
+
// 拦截点。两条反向对照(未命中名单 / 总开关关闭)保证断言不是退化成「嵌入器
|
|
288
|
+
// 恰好没跑」。
|
|
289
|
+
function setupGated(over = {}) {
|
|
290
|
+
const store = createStore(":memory:");
|
|
291
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
292
|
+
const settings = createSettings(store.db);
|
|
293
|
+
const contexts = [];
|
|
294
|
+
const handlers = new Map();
|
|
295
|
+
const ctx = {
|
|
296
|
+
on(event, fn) {
|
|
297
|
+
handlers.set(event, fn);
|
|
298
|
+
return () => handlers.delete(event);
|
|
299
|
+
},
|
|
300
|
+
systemPrompt: { context(def) { contexts.push(def); return () => {}; } }
|
|
301
|
+
};
|
|
302
|
+
const calls = [];
|
|
303
|
+
service.setEmbedder({
|
|
304
|
+
embedSingle: async (text) => {
|
|
305
|
+
calls.push(text);
|
|
306
|
+
return [1, 0, 0];
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
const queries = [];
|
|
310
|
+
const orig = service.injectCandidates.bind(service);
|
|
311
|
+
service.injectCandidates = (args) => {
|
|
312
|
+
queries.push(args.query);
|
|
313
|
+
return orig(args);
|
|
314
|
+
};
|
|
315
|
+
const sessionGate = createSessionGate({
|
|
316
|
+
sessionGateEnabled: true,
|
|
317
|
+
sessionGateSessions: ["s-gated"],
|
|
318
|
+
sessionGateSubagents: true,
|
|
319
|
+
...over
|
|
320
|
+
});
|
|
321
|
+
const injector = createInjector(
|
|
322
|
+
ctx, service, settings, { maxInjectedItems: 3, importanceThreshold: 3 }, null, sessionGate
|
|
323
|
+
);
|
|
324
|
+
const fireClaim = (message, session) => handlers.get("agent/inbox/claimed")?.({ message, agent: { session } });
|
|
325
|
+
return { store, service, contexts, injector, calls, queries, fireClaim, sessionGate };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
test("#617-D: 无痕会话的认领消息既不预取向量,也不留票据", async () => {
|
|
329
|
+
const { contexts, calls, queries, fireClaim, sessionGate } = setupGated();
|
|
330
|
+
const session = { id: "s-gated", events: [], snapshotEvents() { return this.events; } };
|
|
331
|
+
assert.equal(sessionGate.isOff(session), true, "夹具前提:闸门认得出这个会话");
|
|
332
|
+
fireClaim(claimed("桉桉的私人日记:我今天去了协和医院复查"), session);
|
|
333
|
+
await tick();
|
|
334
|
+
assert.equal(calls.length, 0, "闸门命中时不得把原文发往嵌入端点");
|
|
335
|
+
// 票据若被写下,渲染时事件流还没长,Step 1 就会拿到这段原文
|
|
336
|
+
contexts[0].text({ agent: { session } });
|
|
337
|
+
assert.deepEqual(queries, [], "闸门命中时连 injectCandidates 都不该被调到");
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("#617-D: 未命中名单的会话照常预取(反向对照)", async () => {
|
|
341
|
+
const { contexts, calls, queries, fireClaim, sessionGate } = setupGated();
|
|
342
|
+
const session = { id: "s-normal", events: [], snapshotEvents() { return this.events; } };
|
|
343
|
+
assert.equal(sessionGate.isOff(session), false);
|
|
344
|
+
fireClaim(claimed("夜航App令牌桶限流方案怎么设计"), session);
|
|
345
|
+
await tick();
|
|
346
|
+
assert.equal(calls.length, 1, "普通会话的预取行为不变");
|
|
347
|
+
assert.equal(calls[0], "夜航App令牌桶限流方案怎么设计");
|
|
348
|
+
// 票据确实写下了:渲染拿到的就是认领的原文
|
|
349
|
+
contexts[0].text({ agent: { session } });
|
|
350
|
+
assert.deepEqual(queries, ["夜航App令牌桶限流方案怎么设计"]);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("#617-D: 总开关关闭时闸门不生效(反向对照)", async () => {
|
|
354
|
+
const { calls, fireClaim, sessionGate } = setupGated({ sessionGateEnabled: false });
|
|
355
|
+
const session = { id: "s-gated", events: [], snapshotEvents() { return this.events; } };
|
|
356
|
+
assert.equal(sessionGate.isEnabled(), false);
|
|
357
|
+
assert.equal(sessionGate.isOff(session), false, "总开关关时名单不得生效");
|
|
358
|
+
fireClaim(claimed("夜航App令牌桶限流方案怎么设计"), session);
|
|
359
|
+
await tick();
|
|
360
|
+
assert.equal(calls.length, 1, "未开启 #539 时不得凭空拦截(默认行为须与基线一致)");
|
|
361
|
+
});
|