@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,491 +1,491 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { runSleep } from "../src/dream/sleep.js";
|
|
4
|
-
import { createDreamScheduler } from "../src/dream.js";
|
|
5
|
-
import { createStore } from "../src/store.js";
|
|
6
|
-
import { createService } from "../src/service.js";
|
|
7
|
-
import { createVectorIndex } from "../src/vector-index.js";
|
|
8
|
-
|
|
9
|
-
// v0.8.1(issue #170 第 2 步):sleep 冲突阶段的跨 scope 分流。
|
|
10
|
-
// 同一内容落在两个归属下(任一维标注键不等,含「一侧未标注=全局」)时,
|
|
11
|
-
// 不允许自动裁决(freeze 或 LLM)——归档败者可能销毁某归属下的唯一副本,
|
|
12
|
-
// 归属裁决是用户的决定。跨 scope 对一律停车到 conflict_pending(专属 reason),
|
|
13
|
-
// 同 scope 对维持既有路径。夹具为中性主题(构建笔记/编辑器主题)。
|
|
14
|
-
|
|
15
|
-
const embedder = {
|
|
16
|
-
embedSingle: async () => [1, 0, 0],
|
|
17
|
-
// 契约形状:按输入条数返回等长向量数组。update 会失效缓存并调度重嵌入,
|
|
18
|
-
// sleep 的回填走 embed()——返回单向量会让回填静默失败(no usable vectors)。
|
|
19
|
-
embed: async (texts) => (Array.isArray(texts) ? texts : [texts]).map(() => [1, 0, 0]),
|
|
20
|
-
schedule: () => {},
|
|
21
|
-
modelHash: "mock#1",
|
|
22
|
-
dimension: 3
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
function setup() {
|
|
26
|
-
const store = createStore(":memory:");
|
|
27
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
28
|
-
const vectorIndex = createVectorIndex({ store });
|
|
29
|
-
service.setEmbedder(embedder);
|
|
30
|
-
service.setVectorIndex(vectorIndex);
|
|
31
|
-
return { store, service, vectorIndex };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function baseConfig(overrides = {}) {
|
|
35
|
-
return {
|
|
36
|
-
sleepModeEnabled: true,
|
|
37
|
-
sleepIdleMinutes: 5,
|
|
38
|
-
sleepMinIntervalHours: 8,
|
|
39
|
-
sleepConflictStrictness: "normal",
|
|
40
|
-
sleepArchiveDays: 30,
|
|
41
|
-
sleepCompressDays: 90,
|
|
42
|
-
sleepPatternMinMemories: 10,
|
|
43
|
-
sleepMaxPatternPerRun: 3,
|
|
44
|
-
...overrides
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function llmCtx(captured = [], selection = { provider: "mock", model: "sleep-model" }) {
|
|
49
|
-
return {
|
|
50
|
-
logger: { warn: () => {}, info: () => {} },
|
|
51
|
-
agentDefaultModel: { currentSelection: () => selection },
|
|
52
|
-
llm: {
|
|
53
|
-
async *stream(options) {
|
|
54
|
-
captured.push(options);
|
|
55
|
-
const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
|
|
56
|
-
yield { type: "text-delta", index: 0, text: options.purpose === "sleep-conflict" ? "[]" : "[]" };
|
|
57
|
-
yield { type: "finish", reason: { kind: "stop" } };
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function noRouteCtx() {
|
|
64
|
-
return { logger: { warn: () => {}, info: () => {} } };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** 保存一对高相似记忆并预填向量。返回 [新, 旧](updated_at DESC 序)。 */
|
|
68
|
-
function savePair(service, vectorIndex, titleA, titleB, scopeA, scopeB) {
|
|
69
|
-
const a = service.saveWithDedupe({
|
|
70
|
-
type: "project", title: titleA, content: `${titleA} 的正文`,
|
|
71
|
-
...(scopeA?.agent ? { agent_scope: scopeA.agent } : {}),
|
|
72
|
-
...(scopeA?.workspace ? { workspace_scope: scopeA.workspace } : {})
|
|
73
|
-
}).memory;
|
|
74
|
-
const b = service.saveWithDedupe({
|
|
75
|
-
type: "project", title: titleB, content: `${titleB} 的正文`,
|
|
76
|
-
...(scopeB?.agent ? { agent_scope: scopeB.agent } : {}),
|
|
77
|
-
...(scopeB?.workspace ? { workspace_scope: scopeB.workspace } : {})
|
|
78
|
-
}).memory;
|
|
79
|
-
vectorIndex.saveEmbedding(a.id, [1, 0, 0]);
|
|
80
|
-
vectorIndex.saveEmbedding(b.id, [1, 0, 0]);
|
|
81
|
-
return [a, b];
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
test("freeze mode: cross-scope pair parks with the scope-candidate reason; same-scope pair keeps the plain reason", async () => {
|
|
85
|
-
const { store, service, vectorIndex } = setup();
|
|
86
|
-
// updated_at DESC 序决定贪心配对:后保存的对先配。x 对=跨 scope,y 对=同 scope。
|
|
87
|
-
const [yNew, yOld] = savePair(service, vectorIndex, "构建笔记A", "构建笔记B", { agent: "coder" }, { agent: "coder" });
|
|
88
|
-
const [xNew, xOld] = savePair(service, vectorIndex, "编辑器主题A", "编辑器主题B", { agent: "coder" }, { agent: "writer" });
|
|
89
|
-
|
|
90
|
-
const result = await runSleep(llmCtx(), service, baseConfig({ conflictFreezeEnabled: true }), { warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null);
|
|
91
|
-
assert.equal(result.status, "ok");
|
|
92
|
-
|
|
93
|
-
const queue = store.listConflictPending();
|
|
94
|
-
assert.equal(queue.length, 2, "both pairs parked exactly once");
|
|
95
|
-
const crossRow = queue.find((q) => [q.memory_a, q.memory_b].includes(xNew.id));
|
|
96
|
-
const sameRow = queue.find((q) => [q.memory_a, q.memory_b].includes(yNew.id));
|
|
97
|
-
assert.ok(crossRow, "cross-scope pair must be parked");
|
|
98
|
-
assert.match(crossRow.reason, /跨作用域|cross-scope/, "cross pair carries the dedicated reason");
|
|
99
|
-
assert.ok(sameRow, "same-scope pair still parked in freeze mode");
|
|
100
|
-
assert.doesNotMatch(sameRow.reason, /跨作用域|cross-scope/, "same-scope pair keeps the plain similarity reason");
|
|
101
|
-
|
|
102
|
-
// 再跑一轮:同一对不去重不重复入队。
|
|
103
|
-
await runSleep(llmCtx(), service, baseConfig({ conflictFreezeEnabled: true }), { warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null);
|
|
104
|
-
assert.equal(store.countConflictPending(), 2, "pair-level dedupe survives reruns");
|
|
105
|
-
store.close();
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("NULL (unlabeled) vs labeled counts as a cross-scope pair", async () => {
|
|
109
|
-
const { store, service, vectorIndex } = setup();
|
|
110
|
-
savePair(service, vectorIndex, "部署清单A", "部署清单B", {}, { agent: "coder" });
|
|
111
|
-
const result = await runSleep(llmCtx(), service, baseConfig({ conflictFreezeEnabled: true }), { warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null);
|
|
112
|
-
assert.equal(result.status, "ok");
|
|
113
|
-
const queue = store.listConflictPending();
|
|
114
|
-
assert.equal(queue.length, 1);
|
|
115
|
-
assert.match(queue[0].reason, /跨作用域|cross-scope/, "global(unlabeled) copy vs scoped copy is an ownership decision");
|
|
116
|
-
store.close();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test("non-freeze without an LLM route: cross-scope pair still parks; nothing is auto-adjudicated", async () => {
|
|
120
|
-
const { store, service, vectorIndex } = setup();
|
|
121
|
-
const [newer, older] = savePair(service, vectorIndex, "发布流程A", "发布流程B", { agent: "coder" }, { agent: "writer" });
|
|
122
|
-
const result = await runSleep(noRouteCtx(), service, baseConfig(), { warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null);
|
|
123
|
-
|
|
124
|
-
const queue = store.listConflictPending();
|
|
125
|
-
assert.equal(queue.length, 1, "cross-scope pair parks even with no LLM route");
|
|
126
|
-
assert.match(queue[0].reason, /跨作用域|cross-scope/);
|
|
127
|
-
assert.equal(store.getById(newer.id).archived, false, "no side is auto-archived");
|
|
128
|
-
assert.equal(store.getById(older.id).archived, false);
|
|
129
|
-
assert.ok(queue[0].resolved_at === undefined);
|
|
130
|
-
store.close();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("reviewed pair does not re-queue until one of its sides changes (review item 4)", async () => {
|
|
134
|
-
const { store, service, vectorIndex } = setup();
|
|
135
|
-
const [newer, older] = savePair(service, vectorIndex, "备份策略A", "备份策略B", { agent: "coder" }, { agent: "writer" });
|
|
136
|
-
await runSleep(llmCtx(), service, baseConfig({ conflictFreezeEnabled: true }), { warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null);
|
|
137
|
-
assert.equal(store.countConflictPending(), 1);
|
|
138
|
-
|
|
139
|
-
// 人工「保留双方」(winner=null)后,内容未变 → 下一轮不再入队。
|
|
140
|
-
const row = store.listConflictPending()[0];
|
|
141
|
-
store.resolveConflictPending(row.id, { winner: null });
|
|
142
|
-
assert.equal(store.countConflictPending(), 0);
|
|
143
|
-
const rerun = await runSleep(llmCtx(), service, baseConfig({ conflictFreezeEnabled: true }), { warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null);
|
|
144
|
-
assert.equal(store.countConflictPending(), 0, "unchanged reviewed pair must stay dismissed");
|
|
145
|
-
assert.equal(rerun.status, "noop", "suppressed parks count as noop");
|
|
146
|
-
|
|
147
|
-
// 任一侧被改过(updated_at 晚于裁决)→ 重新入队(新决策)。
|
|
148
|
-
// 留出真实时间间隔:updated_at/resolved_at 都是毫秒精度,同毫秒内
|
|
149
|
-
// 「裁决后立即修改」会算作未变(生产中人为修改不可能同毫秒)。
|
|
150
|
-
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
151
|
-
service.update(newer.id, { content: "备份策略A 的正文(改)" });
|
|
152
|
-
await runSleep(llmCtx(), service, baseConfig({ conflictFreezeEnabled: true }), { warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null);
|
|
153
|
-
assert.equal(store.countConflictPending(), 1, "changed pair is worth a fresh review");
|
|
154
|
-
store.close();
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("dream (non-freeze): cross-scope conflict parks for review instead of being adjudicated (review item 3)", async () => {
|
|
158
|
-
const { store, service } = setup();
|
|
159
|
-
const w = service.saveWithDedupe({ type: "decision", title: "发布时间", content: "8月20日", importance: 4, agent_scope: "coder", agent_scope_source: "explicit" }).memory;
|
|
160
|
-
const l = service.saveWithDedupe({ type: "decision", title: "发布时间旧", content: "8月15日", importance: 4, agent_scope: "writer", agent_scope_source: "explicit" }).memory;
|
|
161
|
-
const ctx = {
|
|
162
|
-
llm: { stream: async function* () {
|
|
163
|
-
yield { type: "text-delta", text: JSON.stringify([{ action: "conflict", winner: w.id, loser: l.id, reason: "日期更新" }]) };
|
|
164
|
-
yield { type: "finish", reason: { kind: "ok" } };
|
|
165
|
-
} },
|
|
166
|
-
logger: { warn: () => {}, info: () => {} }
|
|
167
|
-
};
|
|
168
|
-
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
169
|
-
const result = await dream.runDream(ctx, service, { dreamProvider: "mock", dreamModel: "mock-chat" });
|
|
170
|
-
assert.equal(result.ok, true);
|
|
171
|
-
assert.equal(result.applied, 0, "cross-scope conflict is NOT auto-adjudicated");
|
|
172
|
-
assert.equal(result.frozen, 1, "parked pair shows up in the run summary");
|
|
173
|
-
const pending = store.listConflictPending();
|
|
174
|
-
assert.equal(pending.length, 1);
|
|
175
|
-
assert.match(pending[0].reason, /跨作用域|cross-scope/, "dedicated reason");
|
|
176
|
-
assert.equal(store.getById(l.id).archived, false, "loser NOT archived");
|
|
177
|
-
assert.ok(!store.getById(w.id).content.includes("已否决旧信息"), "winner untouched");
|
|
178
|
-
store.close();
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test("dream (non-freeze): same-scope conflict is still auto-adjudicated (regression)", async () => {
|
|
182
|
-
const { store, service } = setup();
|
|
183
|
-
const w = service.saveWithDedupe({ type: "decision", title: "发布时间", content: "8月20日", importance: 4, agent_scope: "coder", agent_scope_source: "explicit" }).memory;
|
|
184
|
-
const l = service.saveWithDedupe({ type: "decision", title: "发布时间旧", content: "8月15日", importance: 4, agent_scope: "coder", agent_scope_source: "explicit" }).memory;
|
|
185
|
-
const ctx = {
|
|
186
|
-
llm: { stream: async function* () {
|
|
187
|
-
yield { type: "text-delta", text: JSON.stringify([{ action: "conflict", winner: w.id, loser: l.id, reason: "日期更新" }]) };
|
|
188
|
-
yield { type: "finish", reason: { kind: "ok" } };
|
|
189
|
-
} },
|
|
190
|
-
logger: { warn: () => {}, info: () => {} }
|
|
191
|
-
};
|
|
192
|
-
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
193
|
-
const result = await dream.runDream(ctx, service, { dreamProvider: "mock", dreamModel: "mock-chat" });
|
|
194
|
-
assert.equal(result.ok, true);
|
|
195
|
-
assert.equal(result.applied, 1, "same-scope conflict adjudicated as before");
|
|
196
|
-
assert.equal(result.frozen, 0);
|
|
197
|
-
assert.equal(store.listConflictPending().length, 0);
|
|
198
|
-
assert.equal(store.getById(l.id).archived, true);
|
|
199
|
-
store.close();
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
// Issue #603 第 1 层:freeze 分支此前绕过全部三个上限,且把「已在队列里的对」
|
|
203
|
-
// 当成新停车报进 frozen。下面三条锁住修完之后的三个性质。
|
|
204
|
-
test("#603 freeze: a pair already in the queue is not re-reported as a new freeze", async () => {
|
|
205
|
-
const { store, service, vectorIndex } = setup();
|
|
206
|
-
savePair(service, vectorIndex, "索引策略A", "索引策略B", { agent: "coder" }, { agent: "coder" });
|
|
207
|
-
const logger = { warn: () => {}, info: () => {} };
|
|
208
|
-
|
|
209
|
-
const first = await runSleep(llmCtx(), service, baseConfig({ conflictFreezeEnabled: true }), logger, { embedder, vectorIndex }, null);
|
|
210
|
-
assert.equal(first.phases.conflicts.frozen, 1, "第一次是真插入");
|
|
211
|
-
assert.equal(store.countConflictPending(), 1);
|
|
212
|
-
|
|
213
|
-
const second = await runSleep(llmCtx(), service, baseConfig({ conflictFreezeEnabled: true }), logger, { embedder, vectorIndex }, null);
|
|
214
|
-
assert.equal(second.phases.conflicts.frozen, 0, "第二次检出的是已入队的对,不算新停车");
|
|
215
|
-
assert.equal(second.status, "noop", "没有新停车就是 noop,不是 ok");
|
|
216
|
-
assert.equal(store.countConflictPending(), 1, "队列没有被重复写入");
|
|
217
|
-
store.close();
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
test("#603 freeze: cap is applied after filtering already-parked pairs, so the queue keeps growing", async () => {
|
|
221
|
-
const { store, service, vectorIndex } = setup();
|
|
222
|
-
// 三对互不相交的高相似记忆:贪心配对每轮都能检出三对。
|
|
223
|
-
savePair(service, vectorIndex, "队列A1", "队列A2", { agent: "coder" }, { agent: "coder" });
|
|
224
|
-
savePair(service, vectorIndex, "队列B1", "队列B2", { agent: "coder" }, { agent: "coder" });
|
|
225
|
-
savePair(service, vectorIndex, "队列C1", "队列C2", { agent: "coder" }, { agent: "coder" });
|
|
226
|
-
const logger = { warn: () => {}, info: () => {} };
|
|
227
|
-
// 每轮只处理一对,且远小于候选数——裸 slice 会让第一对永远占住这个名额。
|
|
228
|
-
const cfg = baseConfig({ conflictFreezeEnabled: true, sleepConflictMaxPairsPerRun: 1 });
|
|
229
|
-
|
|
230
|
-
await runSleep(llmCtx(), service, cfg, logger, { embedder, vectorIndex }, null);
|
|
231
|
-
assert.equal(store.countConflictPending(), 1);
|
|
232
|
-
await runSleep(llmCtx(), service, cfg, logger, { embedder, vectorIndex }, null);
|
|
233
|
-
assert.equal(store.countConflictPending(), 2, "第二对必须能轮到,不能被第一对饿死");
|
|
234
|
-
await runSleep(llmCtx(), service, cfg, logger, { embedder, vectorIndex }, null);
|
|
235
|
-
assert.equal(store.countConflictPending(), 3, "第三对同理");
|
|
236
|
-
|
|
237
|
-
const done = await runSleep(llmCtx(), service, cfg, logger, { embedder, vectorIndex }, null);
|
|
238
|
-
assert.equal(done.phases.conflicts.frozen, 0, "全部入队后不再虚报");
|
|
239
|
-
assert.equal(done.phases.conflicts.perRunSkipped, 0, "没有新对可处理,不算被截断");
|
|
240
|
-
store.close();
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
test("#603 freeze: the pending queue budget stops parking and says so", async () => {
|
|
244
|
-
const { store, service, vectorIndex } = setup();
|
|
245
|
-
savePair(service, vectorIndex, "预算A1", "预算A2", { agent: "coder" }, { agent: "coder" });
|
|
246
|
-
savePair(service, vectorIndex, "预算B1", "预算B2", { agent: "coder" }, { agent: "coder" });
|
|
247
|
-
savePair(service, vectorIndex, "预算C1", "预算C2", { agent: "coder" }, { agent: "coder" });
|
|
248
|
-
const warnings = [];
|
|
249
|
-
const logger = { warn: (m) => warnings.push(String(m)), info: () => {} };
|
|
250
|
-
|
|
251
|
-
const result = await runSleep(
|
|
252
|
-
llmCtx(), service,
|
|
253
|
-
baseConfig({ conflictFreezeEnabled: true, conflictFreezeMaxPending: 2 }),
|
|
254
|
-
logger, { embedder, vectorIndex }, null
|
|
255
|
-
);
|
|
256
|
-
assert.equal(store.countConflictPending(), 2, "只装到队列上限为止");
|
|
257
|
-
assert.equal(result.phases.conflicts.frozen, 2);
|
|
258
|
-
assert.equal(result.phases.conflicts.pendingSkipped, 1, "被队列上限挡下的对要如实报出来");
|
|
259
|
-
assert.ok(warnings.some((w) => /freeze queue full/.test(w)), "要留一条 warn,不能静默丢弃");
|
|
260
|
-
|
|
261
|
-
// 队列已满:再跑一轮不会新增,也不会把「满了」报成可续排的 backlog。
|
|
262
|
-
const again = await runSleep(
|
|
263
|
-
llmCtx(), service,
|
|
264
|
-
baseConfig({ conflictFreezeEnabled: true, conflictFreezeMaxPending: 2 }),
|
|
265
|
-
logger, { embedder, vectorIndex }, null
|
|
266
|
-
);
|
|
267
|
-
assert.equal(store.countConflictPending(), 2);
|
|
268
|
-
assert.equal(again.phases.conflicts.frozen, 0);
|
|
269
|
-
assert.equal(again.phases.conflicts.perRunSkipped, 0, "队列满不是 per-run 截断,不该触发续排");
|
|
270
|
-
store.close();
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
// Issue #603 第 2 层:sleep 停车时必须把候选对的相似度一起落库。此前这个数字只
|
|
274
|
-
// 活在 reason 文本里(「相似度 0.87」),面板靠正则回收、排序无从下手——同一轮
|
|
275
|
-
// 产出的对 created_at 全同,队列顺序等于随机。
|
|
276
|
-
test("#603 freeze: the parked row carries the similarity the detector measured", async () => {
|
|
277
|
-
const { store, service, vectorIndex } = setup();
|
|
278
|
-
savePair(service, vectorIndex, "排序A1", "排序A2", { agent: "coder" }, { agent: "coder" });
|
|
279
|
-
|
|
280
|
-
await runSleep(llmCtx(), service, baseConfig({ conflictFreezeEnabled: true }), { warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null);
|
|
281
|
-
|
|
282
|
-
const [row] = store.listConflictPending();
|
|
283
|
-
assert.ok(row, "the pair must be parked");
|
|
284
|
-
// 两条记忆的向量都是 [1,0,0],余弦为 1。断言「是数字且等于 1」而不是「非空」:
|
|
285
|
-
// 前者能抓住「传了 undefined / 传了字符串」这类静默降级。
|
|
286
|
-
assert.equal(typeof row.similarity, "number", "similarity must be a number, not a string or undefined");
|
|
287
|
-
assert.equal(row.similarity, 1, "identical vectors are similarity 1");
|
|
288
|
-
store.close();
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
// 与上一条互补:拿不到相似度时必须是「未知」而不是 0。0 会被排到最后,等于替
|
|
292
|
-
// 用户断言「这两条毫不相似」——那是编造。
|
|
293
|
-
test("#603 freeze: a pair parked without a measured similarity stays unknown", async () => {
|
|
294
|
-
const { store, service } = setup();
|
|
295
|
-
// 直接走 service 的停车口(没有向量索引参与 = 没测过相似度)。
|
|
296
|
-
const a = service.saveWithDedupe({ type: "project", title: "未知A", content: "正文A" }).memory;
|
|
297
|
-
const b = service.saveWithDedupe({ type: "project", title: "未知B", content: "正文B" }).memory;
|
|
298
|
-
service.saveConflictPending({ run_id: "r1", memory_a: a.id, memory_b: b.id, reason: "人工停车" });
|
|
299
|
-
|
|
300
|
-
const [row] = store.listConflictPending();
|
|
301
|
-
assert.equal(row.similarity, undefined, "no measurement must read back as unknown, not 0");
|
|
302
|
-
store.close();
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
// Issue #603 第 4 层:自动裁决档。相似度极高的**同 scope** 对直接保留新条、归档
|
|
306
|
-
// 旧条,不进人工队列;跨 scope 对永不自动(归属是用户的决定)。
|
|
307
|
-
test("#603 auto-resolve: a near-duplicate same-scope pair is superseded without entering the queue", async () => {
|
|
308
|
-
const { store, service, vectorIndex } = setup();
|
|
309
|
-
const [first, second] = savePair(service, vectorIndex, "缓存策略A", "缓存策略B", { agent: "coder" }, { agent: "coder" });
|
|
310
|
-
|
|
311
|
-
const result = await runSleep(
|
|
312
|
-
llmCtx(), service,
|
|
313
|
-
baseConfig({ conflictFreezeEnabled: true, conflictAutoResolveEnabled: true, conflictAutoResolveMinSimilarity: 0.95 }),
|
|
314
|
-
{ warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null
|
|
315
|
-
);
|
|
316
|
-
assert.equal(result.status, "ok");
|
|
317
|
-
|
|
318
|
-
// 队列必须是空的:这一对根本没进人工队列。
|
|
319
|
-
assert.equal(store.countConflictPending(), 0, "auto-resolved pairs must not reach the manual queue");
|
|
320
|
-
// 不猜 id 顺序,用归档状态反推哪一侧被取代。
|
|
321
|
-
const archivedIds = [first.id, second.id].filter((id) => store.getById(id).archived);
|
|
322
|
-
assert.equal(archivedIds.length, 1, "exactly one side is archived");
|
|
323
|
-
const loser = archivedIds[0];
|
|
324
|
-
const survivor = [first.id, second.id].find((id) => !store.getById(id).archived);
|
|
325
|
-
// 留下的那条正文一字不动(supersede 把注记写在败者侧,不污染幸存者)。
|
|
326
|
-
assert.doesNotMatch(store.getById(survivor).content, /已被取代|superseded by/,
|
|
327
|
-
"the survivor's body must stay clean");
|
|
328
|
-
// 败者侧带可追溯注记(#126 的原设计)。
|
|
329
|
-
assert.match(store.getById(loser).content, /已被取代|superseded by/,
|
|
330
|
-
"traceability lives on the archived side");
|
|
331
|
-
// 机器台账也要有一条:自动动作必须能事后回答「什么时候、被谁判的」。
|
|
332
|
-
const reviewed = typeof store.listConflictReviewed === "function" ? store.listConflictReviewed({ limit: 10 }) : [];
|
|
333
|
-
assert.ok(reviewed.length >= 1, "auto-resolve must leave a ledger row");
|
|
334
|
-
assert.equal(reviewed[0].outcome, "supersede");
|
|
335
|
-
store.close();
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
test("#603 auto-resolve: a cross-scope pair is never auto-adjudicated, even at similarity 1", async () => {
|
|
339
|
-
const { store, service, vectorIndex } = setup();
|
|
340
|
-
const [xNew, xOld] = savePair(service, vectorIndex, "部署清单A", "部署清单B", { agent: "coder" }, { agent: "writer" });
|
|
341
|
-
|
|
342
|
-
const result = await runSleep(
|
|
343
|
-
llmCtx(), service,
|
|
344
|
-
baseConfig({ conflictFreezeEnabled: true, conflictAutoResolveEnabled: true, conflictAutoResolveMinSimilarity: 0.95 }),
|
|
345
|
-
{ warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null
|
|
346
|
-
);
|
|
347
|
-
assert.equal(result.status, "ok");
|
|
348
|
-
|
|
349
|
-
// 向量完全相同(余弦 1),但归属不同 → 只能停车等人判。
|
|
350
|
-
assert.equal(store.countConflictPending(), 1, "cross-scope pair still goes to the manual queue");
|
|
351
|
-
assert.equal(store.getById(xOld.id).archived, false, "cross-scope loser is never auto-archived");
|
|
352
|
-
assert.equal(store.getById(xNew.id).archived, false, "cross-scope winner is untouched too");
|
|
353
|
-
store.close();
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
test("#603 auto-resolve: with the switch off nothing is auto-adjudicated", async () => {
|
|
357
|
-
const { store, service, vectorIndex } = setup();
|
|
358
|
-
savePair(service, vectorIndex, "主题A", "主题B", { agent: "coder" }, { agent: "coder" });
|
|
359
|
-
|
|
360
|
-
const result = await runSleep(
|
|
361
|
-
llmCtx(), service,
|
|
362
|
-
baseConfig({ conflictFreezeEnabled: true, conflictAutoResolveEnabled: false }),
|
|
363
|
-
{ warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null
|
|
364
|
-
);
|
|
365
|
-
assert.equal(result.status, "ok");
|
|
366
|
-
assert.equal(store.countConflictPending(), 1, "with the switch off, nothing is auto-adjudicated");
|
|
367
|
-
store.close();
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
// 自动裁决保留的是**信息量更大**的那条,不是更新的那条。
|
|
371
|
-
// 反例来自 live 库:>=0.95 的 20 对里 11 对(55%)新条比旧条短,平均短 132 字——
|
|
372
|
-
// 蒸馏压缩过的一版反而更新时间更晚,按「留新的」会把更完整的那条归档掉。
|
|
373
|
-
test("#603 auto-resolve: the longer copy survives even when the shorter one is newer", async () => {
|
|
374
|
-
const { store, service, vectorIndex } = setup();
|
|
375
|
-
// 先存长的那条(更旧),再存短的那条(更新):新条更短,正是实测里的多数情况。
|
|
376
|
-
const longBody = "缓存策略要按写入频率分三档:热数据走内存、温数据走 SSD、冷数据归档到对象存储。"
|
|
377
|
-
+ "切换阈值由每小时访问次数决定,低于一次就降档,连续三天高于十次再升档。";
|
|
378
|
-
const shortBody = "缓存策略按写入频率分档。";
|
|
379
|
-
const older = service.saveWithDedupe({
|
|
380
|
-
type: "project", title: "缓存分层", content: longBody
|
|
381
|
-
}).memory;
|
|
382
|
-
const newer = service.saveWithDedupe({
|
|
383
|
-
type: "project", title: "缓存分层摘要", content: shortBody
|
|
384
|
-
}).memory;
|
|
385
|
-
vectorIndex.saveEmbedding(older.id, [1, 0, 0]);
|
|
386
|
-
vectorIndex.saveEmbedding(newer.id, [1, 0, 0]);
|
|
387
|
-
assert.ok(older.content.length > newer.content.length, "fixture: the older copy must be the longer one");
|
|
388
|
-
|
|
389
|
-
const result = await runSleep(
|
|
390
|
-
llmCtx(), service,
|
|
391
|
-
baseConfig({ conflictFreezeEnabled: true, conflictAutoResolveEnabled: true, conflictAutoResolveMinSimilarity: 0.95 }),
|
|
392
|
-
{ warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null
|
|
393
|
-
);
|
|
394
|
-
assert.equal(result.status, "ok");
|
|
395
|
-
assert.equal(result.phases.conflicts.autoResolved, 1, "the pair is auto-adjudicated");
|
|
396
|
-
|
|
397
|
-
// 幸存者必须是长的那条:留新的会把三档策略削成一句话。
|
|
398
|
-
assert.equal(store.getById(older.id).archived, false, "the longer, older copy must survive");
|
|
399
|
-
assert.equal(store.getById(newer.id).archived, true, "the shorter, newer copy is the one archived");
|
|
400
|
-
store.close();
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
// 长度相同才是「信息量相当」,此时退回更新时间:新条胜出,且结论确定可重放。
|
|
404
|
-
test("#603 auto-resolve: equal-length copies fall back to the newer one, deterministically", async () => {
|
|
405
|
-
const { store, service, vectorIndex } = setup();
|
|
406
|
-
const body = "部署脚本先跑 migrate 再跑 smoke,任一步失败就回滚到上一个 tag。";
|
|
407
|
-
const older = service.saveWithDedupe({ type: "project", title: "部署甲", content: body }).memory;
|
|
408
|
-
const newer = service.saveWithDedupe({ type: "project", title: "部署乙", content: body }).memory;
|
|
409
|
-
vectorIndex.saveEmbedding(older.id, [1, 0, 0]);
|
|
410
|
-
vectorIndex.saveEmbedding(newer.id, [1, 0, 0]);
|
|
411
|
-
|
|
412
|
-
const result = await runSleep(
|
|
413
|
-
llmCtx(), service,
|
|
414
|
-
baseConfig({ conflictFreezeEnabled: true, conflictAutoResolveEnabled: true, conflictAutoResolveMinSimilarity: 0.95 }),
|
|
415
|
-
{ warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null
|
|
416
|
-
);
|
|
417
|
-
assert.equal(result.status, "ok");
|
|
418
|
-
assert.equal(result.phases.conflicts.autoResolved, 1);
|
|
419
|
-
// 等长 → 比 updated_at:后存的赢。长度相等时不至于两边都"更完整",留新的是安全的。
|
|
420
|
-
assert.equal(store.getById(newer.id).archived, false, "equal length: the newer copy survives");
|
|
421
|
-
assert.equal(store.getById(older.id).archived, true);
|
|
422
|
-
store.close();
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
test("#603 auto-resolve: a failed adjudication falls back to the manual queue instead of vanishing", async () => {
|
|
426
|
-
const { store, service, vectorIndex } = setup();
|
|
427
|
-
savePair(service, vectorIndex, "回退A", "回退B", { agent: "coder" }, { agent: "coder" });
|
|
428
|
-
|
|
429
|
-
// 让 supersede 一定失败:把 applyDecisions 依赖的 update 打断。
|
|
430
|
-
const realUpdate = service.update.bind(service);
|
|
431
|
-
let calls = 0;
|
|
432
|
-
service.update = (...args) => {
|
|
433
|
-
calls++;
|
|
434
|
-
throw new Error("write refused");
|
|
435
|
-
};
|
|
436
|
-
|
|
437
|
-
const result = await runSleep(
|
|
438
|
-
llmCtx(), service,
|
|
439
|
-
baseConfig({ conflictFreezeEnabled: true, conflictAutoResolveEnabled: true, conflictAutoResolveMinSimilarity: 0.95 }),
|
|
440
|
-
{ warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null
|
|
441
|
-
);
|
|
442
|
-
service.update = realUpdate;
|
|
443
|
-
|
|
444
|
-
assert.ok(calls > 0, "the auto path did try to apply");
|
|
445
|
-
// 自动档没落地 → 这一对必须退回人工队列。丢掉它才是真正的静默丢失。
|
|
446
|
-
assert.equal(store.countConflictPending(), 1, "a failed auto-resolve must land in the manual queue");
|
|
447
|
-
assert.equal(result.status, "ok", "the run itself is not a failure");
|
|
448
|
-
store.close();
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
test("non-freeze with an LLM: cross-scope pair bypasses adjudication; same-scope pair is adjudicated", async () => {
|
|
452
|
-
const { store, service, vectorIndex } = setup();
|
|
453
|
-
const [yNew, yOld] = savePair(service, vectorIndex, "缓存策略A", "缓存策略B", { agent: "coder" }, { agent: "coder" });
|
|
454
|
-
const [xNew, xOld] = savePair(service, vectorIndex, "编辑器主题A", "编辑器主题B", { agent: "coder" }, { agent: "writer" });
|
|
455
|
-
|
|
456
|
-
const captured = [];
|
|
457
|
-
const ctx = llmCtx(captured);
|
|
458
|
-
// 同 scope 对由 LLM 裁决:保留 yNew、归档 yOld。跨 scope 对绝不进 prompt。
|
|
459
|
-
ctx.llm.stream = async function* (options) {
|
|
460
|
-
captured.push(options);
|
|
461
|
-
const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
|
|
462
|
-
const isConflictPass = options.purpose === "sleep-conflict";
|
|
463
|
-
const text = isConflictPass
|
|
464
|
-
? JSON.stringify([{ action: "conflict", winner: yNew.id, loser: yOld.id, reason: "同归属重复" }])
|
|
465
|
-
: "[]";
|
|
466
|
-
yield { type: "text-delta", index: 0, text };
|
|
467
|
-
yield { type: "finish", reason: { kind: "stop" } };
|
|
468
|
-
};
|
|
469
|
-
|
|
470
|
-
const result = await runSleep(ctx, service, baseConfig(), ctx.logger, { embedder, vectorIndex }, null);
|
|
471
|
-
assert.equal(result.status, "ok");
|
|
472
|
-
|
|
473
|
-
// 认「哪一次是冲突裁决」改按 purpose,不再看 user 段首行:#493 追加的围栏让 user 段以
|
|
474
|
-
// 角色声明开头,而败者行的溯源注释里也会出现「候选冲突」这个词——用 includes 会把模式
|
|
475
|
-
// 阶段误判成冲突阶段。purpose 与 prompt 形态彻底解耦。
|
|
476
|
-
const conflictPassTexts = captured
|
|
477
|
-
.filter((o) => o.purpose === "sleep-conflict")
|
|
478
|
-
.map((o) => o.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "");
|
|
479
|
-
assert.ok(conflictPassTexts.length >= 1, "conflict pass ran for the same-scope pair");
|
|
480
|
-
for (const t of conflictPassTexts) {
|
|
481
|
-
assert.ok(!t.includes(xNew.id) && !t.includes(xOld.id), "cross-scope pair must never enter the LLM prompt");
|
|
482
|
-
assert.ok(t.includes(yNew.id) && t.includes(yOld.id), "same-scope pair is adjudicated");
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
const queue = store.listConflictPending();
|
|
486
|
-
assert.equal(queue.length, 1);
|
|
487
|
-
assert.match(queue[0].reason, /跨作用域|cross-scope/);
|
|
488
|
-
assert.equal(store.getById(xOld.id).archived, false, "cross-scope loser is NOT archived by the LLM");
|
|
489
|
-
assert.equal(store.getById(yOld.id).archived, true, "same-scope loser is adjudicated as usual");
|
|
490
|
-
store.close();
|
|
491
|
-
});
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { runSleep } from "../src/dream/sleep.js";
|
|
4
|
+
import { createDreamScheduler } from "../src/dream.js";
|
|
5
|
+
import { createStore } from "../src/store.js";
|
|
6
|
+
import { createService } from "../src/service.js";
|
|
7
|
+
import { createVectorIndex } from "../src/vector-index.js";
|
|
8
|
+
|
|
9
|
+
// v0.8.1(issue #170 第 2 步):sleep 冲突阶段的跨 scope 分流。
|
|
10
|
+
// 同一内容落在两个归属下(任一维标注键不等,含「一侧未标注=全局」)时,
|
|
11
|
+
// 不允许自动裁决(freeze 或 LLM)——归档败者可能销毁某归属下的唯一副本,
|
|
12
|
+
// 归属裁决是用户的决定。跨 scope 对一律停车到 conflict_pending(专属 reason),
|
|
13
|
+
// 同 scope 对维持既有路径。夹具为中性主题(构建笔记/编辑器主题)。
|
|
14
|
+
|
|
15
|
+
const embedder = {
|
|
16
|
+
embedSingle: async () => [1, 0, 0],
|
|
17
|
+
// 契约形状:按输入条数返回等长向量数组。update 会失效缓存并调度重嵌入,
|
|
18
|
+
// sleep 的回填走 embed()——返回单向量会让回填静默失败(no usable vectors)。
|
|
19
|
+
embed: async (texts) => (Array.isArray(texts) ? texts : [texts]).map(() => [1, 0, 0]),
|
|
20
|
+
schedule: () => {},
|
|
21
|
+
modelHash: "mock#1",
|
|
22
|
+
dimension: 3
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function setup() {
|
|
26
|
+
const store = createStore(":memory:");
|
|
27
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
28
|
+
const vectorIndex = createVectorIndex({ store });
|
|
29
|
+
service.setEmbedder(embedder);
|
|
30
|
+
service.setVectorIndex(vectorIndex);
|
|
31
|
+
return { store, service, vectorIndex };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function baseConfig(overrides = {}) {
|
|
35
|
+
return {
|
|
36
|
+
sleepModeEnabled: true,
|
|
37
|
+
sleepIdleMinutes: 5,
|
|
38
|
+
sleepMinIntervalHours: 8,
|
|
39
|
+
sleepConflictStrictness: "normal",
|
|
40
|
+
sleepArchiveDays: 30,
|
|
41
|
+
sleepCompressDays: 90,
|
|
42
|
+
sleepPatternMinMemories: 10,
|
|
43
|
+
sleepMaxPatternPerRun: 3,
|
|
44
|
+
...overrides
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function llmCtx(captured = [], selection = { provider: "mock", model: "sleep-model" }) {
|
|
49
|
+
return {
|
|
50
|
+
logger: { warn: () => {}, info: () => {} },
|
|
51
|
+
agentDefaultModel: { currentSelection: () => selection },
|
|
52
|
+
llm: {
|
|
53
|
+
async *stream(options) {
|
|
54
|
+
captured.push(options);
|
|
55
|
+
const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
|
|
56
|
+
yield { type: "text-delta", index: 0, text: options.purpose === "sleep-conflict" ? "[]" : "[]" };
|
|
57
|
+
yield { type: "finish", reason: { kind: "stop" } };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function noRouteCtx() {
|
|
64
|
+
return { logger: { warn: () => {}, info: () => {} } };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 保存一对高相似记忆并预填向量。返回 [新, 旧](updated_at DESC 序)。 */
|
|
68
|
+
function savePair(service, vectorIndex, titleA, titleB, scopeA, scopeB) {
|
|
69
|
+
const a = service.saveWithDedupe({
|
|
70
|
+
type: "project", title: titleA, content: `${titleA} 的正文`,
|
|
71
|
+
...(scopeA?.agent ? { agent_scope: scopeA.agent } : {}),
|
|
72
|
+
...(scopeA?.workspace ? { workspace_scope: scopeA.workspace } : {})
|
|
73
|
+
}).memory;
|
|
74
|
+
const b = service.saveWithDedupe({
|
|
75
|
+
type: "project", title: titleB, content: `${titleB} 的正文`,
|
|
76
|
+
...(scopeB?.agent ? { agent_scope: scopeB.agent } : {}),
|
|
77
|
+
...(scopeB?.workspace ? { workspace_scope: scopeB.workspace } : {})
|
|
78
|
+
}).memory;
|
|
79
|
+
vectorIndex.saveEmbedding(a.id, [1, 0, 0]);
|
|
80
|
+
vectorIndex.saveEmbedding(b.id, [1, 0, 0]);
|
|
81
|
+
return [a, b];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
test("freeze mode: cross-scope pair parks with the scope-candidate reason; same-scope pair keeps the plain reason", async () => {
|
|
85
|
+
const { store, service, vectorIndex } = setup();
|
|
86
|
+
// updated_at DESC 序决定贪心配对:后保存的对先配。x 对=跨 scope,y 对=同 scope。
|
|
87
|
+
const [yNew, yOld] = savePair(service, vectorIndex, "构建笔记A", "构建笔记B", { agent: "coder" }, { agent: "coder" });
|
|
88
|
+
const [xNew, xOld] = savePair(service, vectorIndex, "编辑器主题A", "编辑器主题B", { agent: "coder" }, { agent: "writer" });
|
|
89
|
+
|
|
90
|
+
const result = await runSleep(llmCtx(), service, baseConfig({ conflictFreezeEnabled: true }), { warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null);
|
|
91
|
+
assert.equal(result.status, "ok");
|
|
92
|
+
|
|
93
|
+
const queue = store.listConflictPending();
|
|
94
|
+
assert.equal(queue.length, 2, "both pairs parked exactly once");
|
|
95
|
+
const crossRow = queue.find((q) => [q.memory_a, q.memory_b].includes(xNew.id));
|
|
96
|
+
const sameRow = queue.find((q) => [q.memory_a, q.memory_b].includes(yNew.id));
|
|
97
|
+
assert.ok(crossRow, "cross-scope pair must be parked");
|
|
98
|
+
assert.match(crossRow.reason, /跨作用域|cross-scope/, "cross pair carries the dedicated reason");
|
|
99
|
+
assert.ok(sameRow, "same-scope pair still parked in freeze mode");
|
|
100
|
+
assert.doesNotMatch(sameRow.reason, /跨作用域|cross-scope/, "same-scope pair keeps the plain similarity reason");
|
|
101
|
+
|
|
102
|
+
// 再跑一轮:同一对不去重不重复入队。
|
|
103
|
+
await runSleep(llmCtx(), service, baseConfig({ conflictFreezeEnabled: true }), { warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null);
|
|
104
|
+
assert.equal(store.countConflictPending(), 2, "pair-level dedupe survives reruns");
|
|
105
|
+
store.close();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("NULL (unlabeled) vs labeled counts as a cross-scope pair", async () => {
|
|
109
|
+
const { store, service, vectorIndex } = setup();
|
|
110
|
+
savePair(service, vectorIndex, "部署清单A", "部署清单B", {}, { agent: "coder" });
|
|
111
|
+
const result = await runSleep(llmCtx(), service, baseConfig({ conflictFreezeEnabled: true }), { warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null);
|
|
112
|
+
assert.equal(result.status, "ok");
|
|
113
|
+
const queue = store.listConflictPending();
|
|
114
|
+
assert.equal(queue.length, 1);
|
|
115
|
+
assert.match(queue[0].reason, /跨作用域|cross-scope/, "global(unlabeled) copy vs scoped copy is an ownership decision");
|
|
116
|
+
store.close();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("non-freeze without an LLM route: cross-scope pair still parks; nothing is auto-adjudicated", async () => {
|
|
120
|
+
const { store, service, vectorIndex } = setup();
|
|
121
|
+
const [newer, older] = savePair(service, vectorIndex, "发布流程A", "发布流程B", { agent: "coder" }, { agent: "writer" });
|
|
122
|
+
const result = await runSleep(noRouteCtx(), service, baseConfig(), { warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null);
|
|
123
|
+
|
|
124
|
+
const queue = store.listConflictPending();
|
|
125
|
+
assert.equal(queue.length, 1, "cross-scope pair parks even with no LLM route");
|
|
126
|
+
assert.match(queue[0].reason, /跨作用域|cross-scope/);
|
|
127
|
+
assert.equal(store.getById(newer.id).archived, false, "no side is auto-archived");
|
|
128
|
+
assert.equal(store.getById(older.id).archived, false);
|
|
129
|
+
assert.ok(queue[0].resolved_at === undefined);
|
|
130
|
+
store.close();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("reviewed pair does not re-queue until one of its sides changes (review item 4)", async () => {
|
|
134
|
+
const { store, service, vectorIndex } = setup();
|
|
135
|
+
const [newer, older] = savePair(service, vectorIndex, "备份策略A", "备份策略B", { agent: "coder" }, { agent: "writer" });
|
|
136
|
+
await runSleep(llmCtx(), service, baseConfig({ conflictFreezeEnabled: true }), { warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null);
|
|
137
|
+
assert.equal(store.countConflictPending(), 1);
|
|
138
|
+
|
|
139
|
+
// 人工「保留双方」(winner=null)后,内容未变 → 下一轮不再入队。
|
|
140
|
+
const row = store.listConflictPending()[0];
|
|
141
|
+
store.resolveConflictPending(row.id, { winner: null });
|
|
142
|
+
assert.equal(store.countConflictPending(), 0);
|
|
143
|
+
const rerun = await runSleep(llmCtx(), service, baseConfig({ conflictFreezeEnabled: true }), { warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null);
|
|
144
|
+
assert.equal(store.countConflictPending(), 0, "unchanged reviewed pair must stay dismissed");
|
|
145
|
+
assert.equal(rerun.status, "noop", "suppressed parks count as noop");
|
|
146
|
+
|
|
147
|
+
// 任一侧被改过(updated_at 晚于裁决)→ 重新入队(新决策)。
|
|
148
|
+
// 留出真实时间间隔:updated_at/resolved_at 都是毫秒精度,同毫秒内
|
|
149
|
+
// 「裁决后立即修改」会算作未变(生产中人为修改不可能同毫秒)。
|
|
150
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
151
|
+
service.update(newer.id, { content: "备份策略A 的正文(改)" });
|
|
152
|
+
await runSleep(llmCtx(), service, baseConfig({ conflictFreezeEnabled: true }), { warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null);
|
|
153
|
+
assert.equal(store.countConflictPending(), 1, "changed pair is worth a fresh review");
|
|
154
|
+
store.close();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("dream (non-freeze): cross-scope conflict parks for review instead of being adjudicated (review item 3)", async () => {
|
|
158
|
+
const { store, service } = setup();
|
|
159
|
+
const w = service.saveWithDedupe({ type: "decision", title: "发布时间", content: "8月20日", importance: 4, agent_scope: "coder", agent_scope_source: "explicit" }).memory;
|
|
160
|
+
const l = service.saveWithDedupe({ type: "decision", title: "发布时间旧", content: "8月15日", importance: 4, agent_scope: "writer", agent_scope_source: "explicit" }).memory;
|
|
161
|
+
const ctx = {
|
|
162
|
+
llm: { stream: async function* () {
|
|
163
|
+
yield { type: "text-delta", text: JSON.stringify([{ action: "conflict", winner: w.id, loser: l.id, reason: "日期更新" }]) };
|
|
164
|
+
yield { type: "finish", reason: { kind: "ok" } };
|
|
165
|
+
} },
|
|
166
|
+
logger: { warn: () => {}, info: () => {} }
|
|
167
|
+
};
|
|
168
|
+
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
169
|
+
const result = await dream.runDream(ctx, service, { dreamProvider: "mock", dreamModel: "mock-chat" });
|
|
170
|
+
assert.equal(result.ok, true);
|
|
171
|
+
assert.equal(result.applied, 0, "cross-scope conflict is NOT auto-adjudicated");
|
|
172
|
+
assert.equal(result.frozen, 1, "parked pair shows up in the run summary");
|
|
173
|
+
const pending = store.listConflictPending();
|
|
174
|
+
assert.equal(pending.length, 1);
|
|
175
|
+
assert.match(pending[0].reason, /跨作用域|cross-scope/, "dedicated reason");
|
|
176
|
+
assert.equal(store.getById(l.id).archived, false, "loser NOT archived");
|
|
177
|
+
assert.ok(!store.getById(w.id).content.includes("已否决旧信息"), "winner untouched");
|
|
178
|
+
store.close();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("dream (non-freeze): same-scope conflict is still auto-adjudicated (regression)", async () => {
|
|
182
|
+
const { store, service } = setup();
|
|
183
|
+
const w = service.saveWithDedupe({ type: "decision", title: "发布时间", content: "8月20日", importance: 4, agent_scope: "coder", agent_scope_source: "explicit" }).memory;
|
|
184
|
+
const l = service.saveWithDedupe({ type: "decision", title: "发布时间旧", content: "8月15日", importance: 4, agent_scope: "coder", agent_scope_source: "explicit" }).memory;
|
|
185
|
+
const ctx = {
|
|
186
|
+
llm: { stream: async function* () {
|
|
187
|
+
yield { type: "text-delta", text: JSON.stringify([{ action: "conflict", winner: w.id, loser: l.id, reason: "日期更新" }]) };
|
|
188
|
+
yield { type: "finish", reason: { kind: "ok" } };
|
|
189
|
+
} },
|
|
190
|
+
logger: { warn: () => {}, info: () => {} }
|
|
191
|
+
};
|
|
192
|
+
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
193
|
+
const result = await dream.runDream(ctx, service, { dreamProvider: "mock", dreamModel: "mock-chat" });
|
|
194
|
+
assert.equal(result.ok, true);
|
|
195
|
+
assert.equal(result.applied, 1, "same-scope conflict adjudicated as before");
|
|
196
|
+
assert.equal(result.frozen, 0);
|
|
197
|
+
assert.equal(store.listConflictPending().length, 0);
|
|
198
|
+
assert.equal(store.getById(l.id).archived, true);
|
|
199
|
+
store.close();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Issue #603 第 1 层:freeze 分支此前绕过全部三个上限,且把「已在队列里的对」
|
|
203
|
+
// 当成新停车报进 frozen。下面三条锁住修完之后的三个性质。
|
|
204
|
+
test("#603 freeze: a pair already in the queue is not re-reported as a new freeze", async () => {
|
|
205
|
+
const { store, service, vectorIndex } = setup();
|
|
206
|
+
savePair(service, vectorIndex, "索引策略A", "索引策略B", { agent: "coder" }, { agent: "coder" });
|
|
207
|
+
const logger = { warn: () => {}, info: () => {} };
|
|
208
|
+
|
|
209
|
+
const first = await runSleep(llmCtx(), service, baseConfig({ conflictFreezeEnabled: true }), logger, { embedder, vectorIndex }, null);
|
|
210
|
+
assert.equal(first.phases.conflicts.frozen, 1, "第一次是真插入");
|
|
211
|
+
assert.equal(store.countConflictPending(), 1);
|
|
212
|
+
|
|
213
|
+
const second = await runSleep(llmCtx(), service, baseConfig({ conflictFreezeEnabled: true }), logger, { embedder, vectorIndex }, null);
|
|
214
|
+
assert.equal(second.phases.conflicts.frozen, 0, "第二次检出的是已入队的对,不算新停车");
|
|
215
|
+
assert.equal(second.status, "noop", "没有新停车就是 noop,不是 ok");
|
|
216
|
+
assert.equal(store.countConflictPending(), 1, "队列没有被重复写入");
|
|
217
|
+
store.close();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("#603 freeze: cap is applied after filtering already-parked pairs, so the queue keeps growing", async () => {
|
|
221
|
+
const { store, service, vectorIndex } = setup();
|
|
222
|
+
// 三对互不相交的高相似记忆:贪心配对每轮都能检出三对。
|
|
223
|
+
savePair(service, vectorIndex, "队列A1", "队列A2", { agent: "coder" }, { agent: "coder" });
|
|
224
|
+
savePair(service, vectorIndex, "队列B1", "队列B2", { agent: "coder" }, { agent: "coder" });
|
|
225
|
+
savePair(service, vectorIndex, "队列C1", "队列C2", { agent: "coder" }, { agent: "coder" });
|
|
226
|
+
const logger = { warn: () => {}, info: () => {} };
|
|
227
|
+
// 每轮只处理一对,且远小于候选数——裸 slice 会让第一对永远占住这个名额。
|
|
228
|
+
const cfg = baseConfig({ conflictFreezeEnabled: true, sleepConflictMaxPairsPerRun: 1 });
|
|
229
|
+
|
|
230
|
+
await runSleep(llmCtx(), service, cfg, logger, { embedder, vectorIndex }, null);
|
|
231
|
+
assert.equal(store.countConflictPending(), 1);
|
|
232
|
+
await runSleep(llmCtx(), service, cfg, logger, { embedder, vectorIndex }, null);
|
|
233
|
+
assert.equal(store.countConflictPending(), 2, "第二对必须能轮到,不能被第一对饿死");
|
|
234
|
+
await runSleep(llmCtx(), service, cfg, logger, { embedder, vectorIndex }, null);
|
|
235
|
+
assert.equal(store.countConflictPending(), 3, "第三对同理");
|
|
236
|
+
|
|
237
|
+
const done = await runSleep(llmCtx(), service, cfg, logger, { embedder, vectorIndex }, null);
|
|
238
|
+
assert.equal(done.phases.conflicts.frozen, 0, "全部入队后不再虚报");
|
|
239
|
+
assert.equal(done.phases.conflicts.perRunSkipped, 0, "没有新对可处理,不算被截断");
|
|
240
|
+
store.close();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("#603 freeze: the pending queue budget stops parking and says so", async () => {
|
|
244
|
+
const { store, service, vectorIndex } = setup();
|
|
245
|
+
savePair(service, vectorIndex, "预算A1", "预算A2", { agent: "coder" }, { agent: "coder" });
|
|
246
|
+
savePair(service, vectorIndex, "预算B1", "预算B2", { agent: "coder" }, { agent: "coder" });
|
|
247
|
+
savePair(service, vectorIndex, "预算C1", "预算C2", { agent: "coder" }, { agent: "coder" });
|
|
248
|
+
const warnings = [];
|
|
249
|
+
const logger = { warn: (m) => warnings.push(String(m)), info: () => {} };
|
|
250
|
+
|
|
251
|
+
const result = await runSleep(
|
|
252
|
+
llmCtx(), service,
|
|
253
|
+
baseConfig({ conflictFreezeEnabled: true, conflictFreezeMaxPending: 2 }),
|
|
254
|
+
logger, { embedder, vectorIndex }, null
|
|
255
|
+
);
|
|
256
|
+
assert.equal(store.countConflictPending(), 2, "只装到队列上限为止");
|
|
257
|
+
assert.equal(result.phases.conflicts.frozen, 2);
|
|
258
|
+
assert.equal(result.phases.conflicts.pendingSkipped, 1, "被队列上限挡下的对要如实报出来");
|
|
259
|
+
assert.ok(warnings.some((w) => /freeze queue full/.test(w)), "要留一条 warn,不能静默丢弃");
|
|
260
|
+
|
|
261
|
+
// 队列已满:再跑一轮不会新增,也不会把「满了」报成可续排的 backlog。
|
|
262
|
+
const again = await runSleep(
|
|
263
|
+
llmCtx(), service,
|
|
264
|
+
baseConfig({ conflictFreezeEnabled: true, conflictFreezeMaxPending: 2 }),
|
|
265
|
+
logger, { embedder, vectorIndex }, null
|
|
266
|
+
);
|
|
267
|
+
assert.equal(store.countConflictPending(), 2);
|
|
268
|
+
assert.equal(again.phases.conflicts.frozen, 0);
|
|
269
|
+
assert.equal(again.phases.conflicts.perRunSkipped, 0, "队列满不是 per-run 截断,不该触发续排");
|
|
270
|
+
store.close();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Issue #603 第 2 层:sleep 停车时必须把候选对的相似度一起落库。此前这个数字只
|
|
274
|
+
// 活在 reason 文本里(「相似度 0.87」),面板靠正则回收、排序无从下手——同一轮
|
|
275
|
+
// 产出的对 created_at 全同,队列顺序等于随机。
|
|
276
|
+
test("#603 freeze: the parked row carries the similarity the detector measured", async () => {
|
|
277
|
+
const { store, service, vectorIndex } = setup();
|
|
278
|
+
savePair(service, vectorIndex, "排序A1", "排序A2", { agent: "coder" }, { agent: "coder" });
|
|
279
|
+
|
|
280
|
+
await runSleep(llmCtx(), service, baseConfig({ conflictFreezeEnabled: true }), { warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null);
|
|
281
|
+
|
|
282
|
+
const [row] = store.listConflictPending();
|
|
283
|
+
assert.ok(row, "the pair must be parked");
|
|
284
|
+
// 两条记忆的向量都是 [1,0,0],余弦为 1。断言「是数字且等于 1」而不是「非空」:
|
|
285
|
+
// 前者能抓住「传了 undefined / 传了字符串」这类静默降级。
|
|
286
|
+
assert.equal(typeof row.similarity, "number", "similarity must be a number, not a string or undefined");
|
|
287
|
+
assert.equal(row.similarity, 1, "identical vectors are similarity 1");
|
|
288
|
+
store.close();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// 与上一条互补:拿不到相似度时必须是「未知」而不是 0。0 会被排到最后,等于替
|
|
292
|
+
// 用户断言「这两条毫不相似」——那是编造。
|
|
293
|
+
test("#603 freeze: a pair parked without a measured similarity stays unknown", async () => {
|
|
294
|
+
const { store, service } = setup();
|
|
295
|
+
// 直接走 service 的停车口(没有向量索引参与 = 没测过相似度)。
|
|
296
|
+
const a = service.saveWithDedupe({ type: "project", title: "未知A", content: "正文A" }).memory;
|
|
297
|
+
const b = service.saveWithDedupe({ type: "project", title: "未知B", content: "正文B" }).memory;
|
|
298
|
+
service.saveConflictPending({ run_id: "r1", memory_a: a.id, memory_b: b.id, reason: "人工停车" });
|
|
299
|
+
|
|
300
|
+
const [row] = store.listConflictPending();
|
|
301
|
+
assert.equal(row.similarity, undefined, "no measurement must read back as unknown, not 0");
|
|
302
|
+
store.close();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Issue #603 第 4 层:自动裁决档。相似度极高的**同 scope** 对直接保留新条、归档
|
|
306
|
+
// 旧条,不进人工队列;跨 scope 对永不自动(归属是用户的决定)。
|
|
307
|
+
test("#603 auto-resolve: a near-duplicate same-scope pair is superseded without entering the queue", async () => {
|
|
308
|
+
const { store, service, vectorIndex } = setup();
|
|
309
|
+
const [first, second] = savePair(service, vectorIndex, "缓存策略A", "缓存策略B", { agent: "coder" }, { agent: "coder" });
|
|
310
|
+
|
|
311
|
+
const result = await runSleep(
|
|
312
|
+
llmCtx(), service,
|
|
313
|
+
baseConfig({ conflictFreezeEnabled: true, conflictAutoResolveEnabled: true, conflictAutoResolveMinSimilarity: 0.95 }),
|
|
314
|
+
{ warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null
|
|
315
|
+
);
|
|
316
|
+
assert.equal(result.status, "ok");
|
|
317
|
+
|
|
318
|
+
// 队列必须是空的:这一对根本没进人工队列。
|
|
319
|
+
assert.equal(store.countConflictPending(), 0, "auto-resolved pairs must not reach the manual queue");
|
|
320
|
+
// 不猜 id 顺序,用归档状态反推哪一侧被取代。
|
|
321
|
+
const archivedIds = [first.id, second.id].filter((id) => store.getById(id).archived);
|
|
322
|
+
assert.equal(archivedIds.length, 1, "exactly one side is archived");
|
|
323
|
+
const loser = archivedIds[0];
|
|
324
|
+
const survivor = [first.id, second.id].find((id) => !store.getById(id).archived);
|
|
325
|
+
// 留下的那条正文一字不动(supersede 把注记写在败者侧,不污染幸存者)。
|
|
326
|
+
assert.doesNotMatch(store.getById(survivor).content, /已被取代|superseded by/,
|
|
327
|
+
"the survivor's body must stay clean");
|
|
328
|
+
// 败者侧带可追溯注记(#126 的原设计)。
|
|
329
|
+
assert.match(store.getById(loser).content, /已被取代|superseded by/,
|
|
330
|
+
"traceability lives on the archived side");
|
|
331
|
+
// 机器台账也要有一条:自动动作必须能事后回答「什么时候、被谁判的」。
|
|
332
|
+
const reviewed = typeof store.listConflictReviewed === "function" ? store.listConflictReviewed({ limit: 10 }) : [];
|
|
333
|
+
assert.ok(reviewed.length >= 1, "auto-resolve must leave a ledger row");
|
|
334
|
+
assert.equal(reviewed[0].outcome, "supersede");
|
|
335
|
+
store.close();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("#603 auto-resolve: a cross-scope pair is never auto-adjudicated, even at similarity 1", async () => {
|
|
339
|
+
const { store, service, vectorIndex } = setup();
|
|
340
|
+
const [xNew, xOld] = savePair(service, vectorIndex, "部署清单A", "部署清单B", { agent: "coder" }, { agent: "writer" });
|
|
341
|
+
|
|
342
|
+
const result = await runSleep(
|
|
343
|
+
llmCtx(), service,
|
|
344
|
+
baseConfig({ conflictFreezeEnabled: true, conflictAutoResolveEnabled: true, conflictAutoResolveMinSimilarity: 0.95 }),
|
|
345
|
+
{ warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null
|
|
346
|
+
);
|
|
347
|
+
assert.equal(result.status, "ok");
|
|
348
|
+
|
|
349
|
+
// 向量完全相同(余弦 1),但归属不同 → 只能停车等人判。
|
|
350
|
+
assert.equal(store.countConflictPending(), 1, "cross-scope pair still goes to the manual queue");
|
|
351
|
+
assert.equal(store.getById(xOld.id).archived, false, "cross-scope loser is never auto-archived");
|
|
352
|
+
assert.equal(store.getById(xNew.id).archived, false, "cross-scope winner is untouched too");
|
|
353
|
+
store.close();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("#603 auto-resolve: with the switch off nothing is auto-adjudicated", async () => {
|
|
357
|
+
const { store, service, vectorIndex } = setup();
|
|
358
|
+
savePair(service, vectorIndex, "主题A", "主题B", { agent: "coder" }, { agent: "coder" });
|
|
359
|
+
|
|
360
|
+
const result = await runSleep(
|
|
361
|
+
llmCtx(), service,
|
|
362
|
+
baseConfig({ conflictFreezeEnabled: true, conflictAutoResolveEnabled: false }),
|
|
363
|
+
{ warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null
|
|
364
|
+
);
|
|
365
|
+
assert.equal(result.status, "ok");
|
|
366
|
+
assert.equal(store.countConflictPending(), 1, "with the switch off, nothing is auto-adjudicated");
|
|
367
|
+
store.close();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// 自动裁决保留的是**信息量更大**的那条,不是更新的那条。
|
|
371
|
+
// 反例来自 live 库:>=0.95 的 20 对里 11 对(55%)新条比旧条短,平均短 132 字——
|
|
372
|
+
// 蒸馏压缩过的一版反而更新时间更晚,按「留新的」会把更完整的那条归档掉。
|
|
373
|
+
test("#603 auto-resolve: the longer copy survives even when the shorter one is newer", async () => {
|
|
374
|
+
const { store, service, vectorIndex } = setup();
|
|
375
|
+
// 先存长的那条(更旧),再存短的那条(更新):新条更短,正是实测里的多数情况。
|
|
376
|
+
const longBody = "缓存策略要按写入频率分三档:热数据走内存、温数据走 SSD、冷数据归档到对象存储。"
|
|
377
|
+
+ "切换阈值由每小时访问次数决定,低于一次就降档,连续三天高于十次再升档。";
|
|
378
|
+
const shortBody = "缓存策略按写入频率分档。";
|
|
379
|
+
const older = service.saveWithDedupe({
|
|
380
|
+
type: "project", title: "缓存分层", content: longBody
|
|
381
|
+
}).memory;
|
|
382
|
+
const newer = service.saveWithDedupe({
|
|
383
|
+
type: "project", title: "缓存分层摘要", content: shortBody
|
|
384
|
+
}).memory;
|
|
385
|
+
vectorIndex.saveEmbedding(older.id, [1, 0, 0]);
|
|
386
|
+
vectorIndex.saveEmbedding(newer.id, [1, 0, 0]);
|
|
387
|
+
assert.ok(older.content.length > newer.content.length, "fixture: the older copy must be the longer one");
|
|
388
|
+
|
|
389
|
+
const result = await runSleep(
|
|
390
|
+
llmCtx(), service,
|
|
391
|
+
baseConfig({ conflictFreezeEnabled: true, conflictAutoResolveEnabled: true, conflictAutoResolveMinSimilarity: 0.95 }),
|
|
392
|
+
{ warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null
|
|
393
|
+
);
|
|
394
|
+
assert.equal(result.status, "ok");
|
|
395
|
+
assert.equal(result.phases.conflicts.autoResolved, 1, "the pair is auto-adjudicated");
|
|
396
|
+
|
|
397
|
+
// 幸存者必须是长的那条:留新的会把三档策略削成一句话。
|
|
398
|
+
assert.equal(store.getById(older.id).archived, false, "the longer, older copy must survive");
|
|
399
|
+
assert.equal(store.getById(newer.id).archived, true, "the shorter, newer copy is the one archived");
|
|
400
|
+
store.close();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// 长度相同才是「信息量相当」,此时退回更新时间:新条胜出,且结论确定可重放。
|
|
404
|
+
test("#603 auto-resolve: equal-length copies fall back to the newer one, deterministically", async () => {
|
|
405
|
+
const { store, service, vectorIndex } = setup();
|
|
406
|
+
const body = "部署脚本先跑 migrate 再跑 smoke,任一步失败就回滚到上一个 tag。";
|
|
407
|
+
const older = service.saveWithDedupe({ type: "project", title: "部署甲", content: body }).memory;
|
|
408
|
+
const newer = service.saveWithDedupe({ type: "project", title: "部署乙", content: body }).memory;
|
|
409
|
+
vectorIndex.saveEmbedding(older.id, [1, 0, 0]);
|
|
410
|
+
vectorIndex.saveEmbedding(newer.id, [1, 0, 0]);
|
|
411
|
+
|
|
412
|
+
const result = await runSleep(
|
|
413
|
+
llmCtx(), service,
|
|
414
|
+
baseConfig({ conflictFreezeEnabled: true, conflictAutoResolveEnabled: true, conflictAutoResolveMinSimilarity: 0.95 }),
|
|
415
|
+
{ warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null
|
|
416
|
+
);
|
|
417
|
+
assert.equal(result.status, "ok");
|
|
418
|
+
assert.equal(result.phases.conflicts.autoResolved, 1);
|
|
419
|
+
// 等长 → 比 updated_at:后存的赢。长度相等时不至于两边都"更完整",留新的是安全的。
|
|
420
|
+
assert.equal(store.getById(newer.id).archived, false, "equal length: the newer copy survives");
|
|
421
|
+
assert.equal(store.getById(older.id).archived, true);
|
|
422
|
+
store.close();
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("#603 auto-resolve: a failed adjudication falls back to the manual queue instead of vanishing", async () => {
|
|
426
|
+
const { store, service, vectorIndex } = setup();
|
|
427
|
+
savePair(service, vectorIndex, "回退A", "回退B", { agent: "coder" }, { agent: "coder" });
|
|
428
|
+
|
|
429
|
+
// 让 supersede 一定失败:把 applyDecisions 依赖的 update 打断。
|
|
430
|
+
const realUpdate = service.update.bind(service);
|
|
431
|
+
let calls = 0;
|
|
432
|
+
service.update = (...args) => {
|
|
433
|
+
calls++;
|
|
434
|
+
throw new Error("write refused");
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const result = await runSleep(
|
|
438
|
+
llmCtx(), service,
|
|
439
|
+
baseConfig({ conflictFreezeEnabled: true, conflictAutoResolveEnabled: true, conflictAutoResolveMinSimilarity: 0.95 }),
|
|
440
|
+
{ warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null
|
|
441
|
+
);
|
|
442
|
+
service.update = realUpdate;
|
|
443
|
+
|
|
444
|
+
assert.ok(calls > 0, "the auto path did try to apply");
|
|
445
|
+
// 自动档没落地 → 这一对必须退回人工队列。丢掉它才是真正的静默丢失。
|
|
446
|
+
assert.equal(store.countConflictPending(), 1, "a failed auto-resolve must land in the manual queue");
|
|
447
|
+
assert.equal(result.status, "ok", "the run itself is not a failure");
|
|
448
|
+
store.close();
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("non-freeze with an LLM: cross-scope pair bypasses adjudication; same-scope pair is adjudicated", async () => {
|
|
452
|
+
const { store, service, vectorIndex } = setup();
|
|
453
|
+
const [yNew, yOld] = savePair(service, vectorIndex, "缓存策略A", "缓存策略B", { agent: "coder" }, { agent: "coder" });
|
|
454
|
+
const [xNew, xOld] = savePair(service, vectorIndex, "编辑器主题A", "编辑器主题B", { agent: "coder" }, { agent: "writer" });
|
|
455
|
+
|
|
456
|
+
const captured = [];
|
|
457
|
+
const ctx = llmCtx(captured);
|
|
458
|
+
// 同 scope 对由 LLM 裁决:保留 yNew、归档 yOld。跨 scope 对绝不进 prompt。
|
|
459
|
+
ctx.llm.stream = async function* (options) {
|
|
460
|
+
captured.push(options);
|
|
461
|
+
const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
|
|
462
|
+
const isConflictPass = options.purpose === "sleep-conflict";
|
|
463
|
+
const text = isConflictPass
|
|
464
|
+
? JSON.stringify([{ action: "conflict", winner: yNew.id, loser: yOld.id, reason: "同归属重复" }])
|
|
465
|
+
: "[]";
|
|
466
|
+
yield { type: "text-delta", index: 0, text };
|
|
467
|
+
yield { type: "finish", reason: { kind: "stop" } };
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const result = await runSleep(ctx, service, baseConfig(), ctx.logger, { embedder, vectorIndex }, null);
|
|
471
|
+
assert.equal(result.status, "ok");
|
|
472
|
+
|
|
473
|
+
// 认「哪一次是冲突裁决」改按 purpose,不再看 user 段首行:#493 追加的围栏让 user 段以
|
|
474
|
+
// 角色声明开头,而败者行的溯源注释里也会出现「候选冲突」这个词——用 includes 会把模式
|
|
475
|
+
// 阶段误判成冲突阶段。purpose 与 prompt 形态彻底解耦。
|
|
476
|
+
const conflictPassTexts = captured
|
|
477
|
+
.filter((o) => o.purpose === "sleep-conflict")
|
|
478
|
+
.map((o) => o.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "");
|
|
479
|
+
assert.ok(conflictPassTexts.length >= 1, "conflict pass ran for the same-scope pair");
|
|
480
|
+
for (const t of conflictPassTexts) {
|
|
481
|
+
assert.ok(!t.includes(xNew.id) && !t.includes(xOld.id), "cross-scope pair must never enter the LLM prompt");
|
|
482
|
+
assert.ok(t.includes(yNew.id) && t.includes(yOld.id), "same-scope pair is adjudicated");
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const queue = store.listConflictPending();
|
|
486
|
+
assert.equal(queue.length, 1);
|
|
487
|
+
assert.match(queue[0].reason, /跨作用域|cross-scope/);
|
|
488
|
+
assert.equal(store.getById(xOld.id).archived, false, "cross-scope loser is NOT archived by the LLM");
|
|
489
|
+
assert.equal(store.getById(yOld.id).archived, true, "same-scope loser is adjudicated as usual");
|
|
490
|
+
store.close();
|
|
491
|
+
});
|