@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
package/lib/dream.js
CHANGED
|
@@ -1,1764 +1,1879 @@
|
|
|
1
|
-
import { validateDecisions, applyDecisions } from "./dream/decisions.js";
|
|
2
|
-
import { clusterMemories, findPotentialConflicts, cosineSimilarity } from "./dream/clustering.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
cleaned =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
.
|
|
107
|
-
.
|
|
108
|
-
.join("\
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* the
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
byId[d.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
byId[d.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
* digest
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
.
|
|
194
|
-
.
|
|
195
|
-
.join("
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
if (chunk.type === "
|
|
300
|
-
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
// chunk
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
return code
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
const
|
|
349
|
-
const
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
//
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
let
|
|
426
|
-
let
|
|
427
|
-
let
|
|
428
|
-
let
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
if (Number.isFinite(
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
//
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
// "
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
//
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
*
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
if (
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
*
|
|
608
|
-
* →
|
|
609
|
-
*
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
{ role: "
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
681
|
-
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
if (
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
//
|
|
728
|
-
//
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
//
|
|
736
|
-
//
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
//
|
|
744
|
-
//
|
|
745
|
-
//
|
|
746
|
-
//
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
//
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
*
|
|
770
|
-
*
|
|
771
|
-
*
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
//
|
|
790
|
-
//
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
const
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
const
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
*
|
|
819
|
-
*
|
|
820
|
-
*
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
if (!parent.has(p.
|
|
832
|
-
|
|
833
|
-
const
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
byRoot.
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
*
|
|
858
|
-
*
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
const
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
const
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
*
|
|
889
|
-
*
|
|
890
|
-
*
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
const
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
.
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
*
|
|
919
|
-
*
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
//
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
//
|
|
943
|
-
//
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
const
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
//
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
//
|
|
974
|
-
//
|
|
975
|
-
|
|
976
|
-
let
|
|
977
|
-
let
|
|
978
|
-
|
|
979
|
-
//
|
|
980
|
-
//
|
|
981
|
-
//
|
|
982
|
-
//
|
|
983
|
-
//
|
|
984
|
-
|
|
985
|
-
let
|
|
986
|
-
|
|
987
|
-
//
|
|
988
|
-
//
|
|
989
|
-
//
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
//
|
|
993
|
-
//
|
|
994
|
-
//
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
//
|
|
999
|
-
//
|
|
1000
|
-
//
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
const
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
//
|
|
1017
|
-
//
|
|
1018
|
-
//
|
|
1019
|
-
//
|
|
1020
|
-
//
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
return
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
//
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
//
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
//
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
//
|
|
1255
|
-
//
|
|
1256
|
-
|
|
1257
|
-
//
|
|
1258
|
-
|
|
1259
|
-
//
|
|
1260
|
-
//
|
|
1261
|
-
// (
|
|
1262
|
-
//
|
|
1263
|
-
//
|
|
1264
|
-
|
|
1265
|
-
//
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
//
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
//
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
if (
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
//
|
|
1474
|
-
const
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
//
|
|
1483
|
-
//
|
|
1484
|
-
//
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
)
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
//
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
//
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
//
|
|
1733
|
-
//
|
|
1734
|
-
let
|
|
1735
|
-
let
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
});
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1
|
+
import { validateDecisions, applyDecisions, supersedeInfoFirstPair } from "./dream/decisions.js";
|
|
2
|
+
import { clusterMemories, findPotentialConflicts, cosineSimilarity } from "./dream/clustering.js";
|
|
3
|
+
import { triagePair } from "./dream/triage.js";
|
|
4
|
+
import { clusterByTag, intersectEvidence } from "./dream/narratives.js";
|
|
5
|
+
import { scopeKeyOf } from "./scope.js";
|
|
6
|
+
// Issue #239(第 4 项)镜像到巩固:错峰时段解析与「最近的高峰结束时刻」从独立的
|
|
7
|
+
// 零依赖模块 peak-hours.js 取(该模块从 summarize.js 抽出,PR #320 review)——
|
|
8
|
+
// 不另写一份解析器,两份实现漂移会让「同一个时段串在两处行为不同」,那比没有
|
|
9
|
+
// 这个功能更糟。此前直接 import summarize.js,#316 后 summarize 反向依赖 dream
|
|
10
|
+
// (withEffortFallback 复用),会成真循环,故抽模块。
|
|
11
|
+
import { isInPeakWindow, nextOffPeakAt } from "./peak-hours.js";
|
|
12
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
13
|
+
import { STR, langOf } from "./lang.js";
|
|
14
|
+
// 巩固读的是行池,而动行的写手不经汇流口:applyDecisions 归档败者直接走
|
|
15
|
+
// service.setArchived(dream/decisions.js),所以「不许改写外来行」这条契约要在
|
|
16
|
+
// 每个池子各挡一遍。清单与静态锁见 share-imported-guard.test.js。
|
|
17
|
+
import { isImportedRow } from "./memory-source.js";
|
|
18
|
+
// 断链是围栏的共同底子:三处围栏(巩固/睡眠清单、蒸馏转录、画像条目)共用这一份,
|
|
19
|
+
// 不许各写一条(#493 补充 §③)。
|
|
20
|
+
import { breakFenceMarkers } from "./fence.js";
|
|
21
|
+
export { validateDecisions, applyDecisions, withEffortFallback, withMaxTokensFallback, parseMaxTokensCap, describeStreamFailure, resolveDreamEffort, resolveRoute, streamText, runAuditedLlm };
|
|
22
|
+
|
|
23
|
+
// Extract the first JSON array from LLM output, tolerating markdown fences,
|
|
24
|
+
// leading/trailing prose, and common wrapper noise. Returns an array or null.
|
|
25
|
+
function extractJsonArray(text) {
|
|
26
|
+
if (typeof text !== "string" || text.trim().length === 0) return null;
|
|
27
|
+
|
|
28
|
+
// 1. Strip markdown code fences (```json ... ``` or ``` ... ```).
|
|
29
|
+
let cleaned = text.replace(/```(?:json)?\s*([\s\S]*?)```/gi, "$1");
|
|
30
|
+
cleaned = cleaned.trim();
|
|
31
|
+
|
|
32
|
+
// 2. Find the first '[' and the matching last ']' that yields valid JSON.
|
|
33
|
+
const start = cleaned.indexOf("[");
|
|
34
|
+
if (start === -1) return null;
|
|
35
|
+
for (let end = cleaned.lastIndexOf("]"); end > start; end = cleaned.lastIndexOf("]", end - 1)) {
|
|
36
|
+
const candidate = cleaned.slice(start, end + 1);
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(candidate);
|
|
39
|
+
} catch {
|
|
40
|
+
// Light repair: remove trailing commas before ] or }.
|
|
41
|
+
try {
|
|
42
|
+
const repaired = candidate.replace(/,(\s*[}\]])/g, "$1");
|
|
43
|
+
return JSON.parse(repaired);
|
|
44
|
+
} catch {
|
|
45
|
+
// keep searching backwards
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 3. Fallback: a broader regex extraction.
|
|
51
|
+
try {
|
|
52
|
+
const match = cleaned.match(/\[[\s\S]*\]/);
|
|
53
|
+
if (match) return JSON.parse(match[0]);
|
|
54
|
+
} catch {
|
|
55
|
+
// fall through
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
function totalChars(memories) {
|
|
60
|
+
return memories.reduce((sum, m) => sum + (m.title?.length ?? 0) + (m.content?.length ?? 0), 0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 阈值口径的唯一实现:活跃记忆(未归档、非 summary/document)的条数与正文字符数。
|
|
64
|
+
// 触发判定(shouldTrigger)与「成功轮把基线落库」(runDream 的 finish)都走这里——
|
|
65
|
+
// 两处若各写一份过滤条件,重启播种读到的基线就会和运行期判定口径漂移(一个把
|
|
66
|
+
// document 算进去、一个不算),阈值随之失真,且这种漂移只会在重启后才显形。
|
|
67
|
+
function activeStoreSize(service) {
|
|
68
|
+
const memories = withoutPaused(service, service.all().filter((m) => !isImportedRow(m) && !m.archived && m.type !== "summary" && m.type !== "document"));
|
|
69
|
+
return { count: memories.length, chars: totalChars(memories) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// issue #471 政策轴:被按下的作用域不参与巩固,也就不该再撑着阈值把它自己触发起来。
|
|
73
|
+
// 三个读入口(本函数、generateNarratives 的 inputs、runDream 的 memories)各自的附加
|
|
74
|
+
// 条件不同——有的排 forgotten、有的不排——但「政策轴这一层」对三处是一样的,故收成
|
|
75
|
+
// 一个助手而不是各写一遍过滤,理由与 activeStoreSize 开头记着的口径漂移同源:两处
|
|
76
|
+
// 各写一份过滤条件,漂移只会在重启后才显形。
|
|
77
|
+
//
|
|
78
|
+
// 走 service.filterPaused 而不是自己读状态表:判定只许有一份实现(见
|
|
79
|
+
// src/scope-state.js 的模块注释)。service 上取不到这个函数时(老装配、测试夹具)
|
|
80
|
+
// 原样返回,行为与改动前一致。
|
|
81
|
+
function withoutPaused(service, rows) {
|
|
82
|
+
if (typeof service?.filterPaused !== "function") return rows;
|
|
83
|
+
return service.filterPaused(rows);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 基线的唯一合法形状:两个非负整数;其余(缺字段、负数、非整数、整个对象缺失)
|
|
87
|
+
// 一律视为「没有基线」→ 调用方退回 {0,0}。播种值来自持久化列,可能是老库的 NULL
|
|
88
|
+
// 或手工改坏的库,判定必须比调用点更严格,而不是相信数据库里存的一定合法。
|
|
89
|
+
function toBaseline(raw) {
|
|
90
|
+
if (!raw || typeof raw !== "object") return null;
|
|
91
|
+
const count = Number.isInteger(raw.count) && raw.count >= 0 ? raw.count : null;
|
|
92
|
+
const chars = Number.isInteger(raw.chars) && raw.chars >= 0 ? raw.chars : null;
|
|
93
|
+
if (count === null || chars === null) return null;
|
|
94
|
+
return { count, chars };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------- audit
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Canonical digest of the consolidation input snapshot. Built from stable
|
|
101
|
+
* fields sorted by id, so identical inputs always yield the same hash — the
|
|
102
|
+
* basis for replaying/verifying a recorded decision (receipt check).
|
|
103
|
+
*/
|
|
104
|
+
export function hashSnapshot(memories) {
|
|
105
|
+
const canon = memories
|
|
106
|
+
.map((m) => [m.id, m.type, m.title, m.content, m.importance, m.updated_at])
|
|
107
|
+
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
|
108
|
+
.map((parts) => parts.map((p) => String(p ?? "")).join("\u0001"))
|
|
109
|
+
.join("\u0002");
|
|
110
|
+
return createHash("sha256").update(canon).digest("hex");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Compact machine-verifiable receipt for one autoDream run. Format:
|
|
115
|
+
* dsh-mneme:run:<runId>:<status>:<snapshotHash(12)>:<inputCount>:<applied>:<summaryFlag>
|
|
116
|
+
* Enough to correlate a run with its persisted audit row and to spot silent
|
|
117
|
+
* drift (same snapshot hash + same decisions must reproduce the same outcome).
|
|
118
|
+
*/
|
|
119
|
+
export function buildReceipt({ runId, status, snapshotHash, inputCount, applied, summaryStored }) {
|
|
120
|
+
return `dsh-mneme:run:${runId}:${status}:${snapshotHash.slice(0, 12)}:${inputCount}:${applied}:${summaryStored ? 1 : 0}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Parse a receipt back into fields; returns undefined for malformed input.
|
|
125
|
+
*/
|
|
126
|
+
export function parseReceipt(receipt) {
|
|
127
|
+
if (typeof receipt !== "string") return undefined;
|
|
128
|
+
const parts = receipt.split(":");
|
|
129
|
+
if (parts.length !== 8 || parts[0] !== "dsh-mneme" || parts[1] !== "run") return undefined;
|
|
130
|
+
const [, , runId, status, snapshotHash, inputCount, applied, summaryStored] = parts;
|
|
131
|
+
// reconcile = decisions validated but one or more did not commit (CAS
|
|
132
|
+
// conflict / transaction rollback) — the store diverges from the decision
|
|
133
|
+
// list and the run must be reconciled, never reported as a fake ok.
|
|
134
|
+
if (!runId || !/^(ok|noop|degraded|reconcile|failed)$/.test(status)) return undefined;
|
|
135
|
+
const count = Number(inputCount);
|
|
136
|
+
const appliedN = Number(applied);
|
|
137
|
+
if (!Number.isInteger(count) || !Number.isInteger(appliedN)) return undefined;
|
|
138
|
+
return { runId, status, snapshotHash, inputCount: count, applied: appliedN, summaryStored: summaryStored === "1" };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Derive the per-id disposition (keep / merge-keep / merge-archived /
|
|
143
|
+
* archived / conflict-winner / conflict-archived) from a validated decision
|
|
144
|
+
* list. Stored in the audit row so a run can be replayed without re-running
|
|
145
|
+
* the LLM.
|
|
146
|
+
*/
|
|
147
|
+
export function buildOutcome(decisions) {
|
|
148
|
+
const byId = {};
|
|
149
|
+
for (const d of decisions ?? []) {
|
|
150
|
+
if (d.action === "keep") {
|
|
151
|
+
for (const id of d.ids) byId[id] = "keep";
|
|
152
|
+
} else if (d.action === "archive") {
|
|
153
|
+
for (const id of d.ids) byId[id] = "archived";
|
|
154
|
+
} else if (d.action === "merge") {
|
|
155
|
+
for (const id of d.ids) byId[id] = id === d.keepSource ? "merge-keep" : "merge-archived";
|
|
156
|
+
} else if (d.action === "conflict") {
|
|
157
|
+
byId[d.winner] = "conflict-winner";
|
|
158
|
+
byId[d.loser] = "conflict-archived";
|
|
159
|
+
} else if (d.action === "update") {
|
|
160
|
+
for (const id of d.ids) byId[id] = "updated";
|
|
161
|
+
} else if (d.action === "supersede") {
|
|
162
|
+
// Issue #126 review(Copilot):新动作此前在 outcome.byId 里没有 disposition,
|
|
163
|
+
// 审计行产了 receipt 却看不出这些目标被如何处置。
|
|
164
|
+
byId[d.winner] = "supersede-winner";
|
|
165
|
+
byId[d.loser] = "superseded-archived";
|
|
166
|
+
} else if (d.action === "differentiate") {
|
|
167
|
+
for (const id of d.ids ?? []) byId[id] = "differentiated";
|
|
168
|
+
} else if (d.action === "create") {
|
|
169
|
+
// #619:create 铸出新行——记下「这一条是这一轮新生的」。它此前特意不派
|
|
170
|
+
// disposition,好让 receipt_chain 不为它产行(新行的改动无处可记,只有
|
|
171
|
+
// memory_mints 记出生)。这里补上的是审计面能读懂的那句话,不改 receipt_chain。
|
|
172
|
+
if (d.id) byId[d.id] = "created";
|
|
173
|
+
} else if (d.action === "create_merged") {
|
|
174
|
+
// #619 / Ephor 第七轮 ②:create 撞上同题活跃行时走原地刷新,没有新条目。
|
|
175
|
+
// 这条动作原本在 if 链里缺席,`outcome.byId` 就是空对象——一个只刷新不铸造
|
|
176
|
+
// 的轮,审计行看不出任何目标被处置过(#126 review 在 supersede 上踩过同一个
|
|
177
|
+
// 坑)。刷新有自己的名字,不冒充 created。
|
|
178
|
+
if (d.id) byId[d.id] = "created-refreshed";
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { byId };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Content-addressed digest of the memories a verdict was decided against
|
|
186
|
+
* (id + title + content + importance), sorted by id so identical inputs always
|
|
187
|
+
* hash the same. This is the per-record "判定依据" fingerprint: a receipt whose
|
|
188
|
+
* digest cannot be reproduced from the involved memories is a bare claim, and a
|
|
189
|
+
* digest match with a divergent outcome pinpoints drift to the exact record.
|
|
190
|
+
*/
|
|
191
|
+
export function hashDecisionInput(memories) {
|
|
192
|
+
const canon = (memories ?? [])
|
|
193
|
+
.map((m) => [m.id, m.title, m.content, m.importance])
|
|
194
|
+
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
|
195
|
+
.map((p) => p.map((x) => String(x ?? "")).join(""))
|
|
196
|
+
.join("");
|
|
197
|
+
return createHash("sha256").update(canon).digest("hex");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Build the per-record receipts for a run's actually-committed mutable verdicts
|
|
202
|
+
* (merge/conflict/update) — one row per verdict in the receipt_chain. Inputs
|
|
203
|
+
* are drawn from the run snapshot (what the LLM actually arbitrated against),
|
|
204
|
+
* and the idempotency counters count_before → count_after come from the
|
|
205
|
+
* committed sub-step, so replaying the same decision must reproduce the same
|
|
206
|
+
* numbers. verdict starts "live"; a later policy_epoch upgrade will batch-mark
|
|
207
|
+
* older verdicts "historical" (a receipt_chain rewrite driven by the store's
|
|
208
|
+
* getLatestPolicyEpoch — out of scope for this pass), while "revoked" is
|
|
209
|
+
* reserved for verdicts later overturned by an explicit human decision.
|
|
210
|
+
*/
|
|
211
|
+
function buildRecordReceipts({ runId, committed, snapshot, policyEpoch }) {
|
|
212
|
+
const at = (id) => snapshot?.get?.(id);
|
|
213
|
+
const receipts = [];
|
|
214
|
+
for (const c of committed ?? []) {
|
|
215
|
+
const base = {
|
|
216
|
+
run_id: runId,
|
|
217
|
+
verdict: "live",
|
|
218
|
+
count_before: c.count_before,
|
|
219
|
+
count_after: c.count_after,
|
|
220
|
+
policy_epoch: policyEpoch,
|
|
221
|
+
created_at: new Date().toISOString()
|
|
222
|
+
};
|
|
223
|
+
if (c.action === "merge") {
|
|
224
|
+
receipts.push({
|
|
225
|
+
...base,
|
|
226
|
+
receipt_id: randomUUID(),
|
|
227
|
+
record_id: c.keepSource,
|
|
228
|
+
kind: "merge",
|
|
229
|
+
input_digest: hashDecisionInput((c.ids ?? []).map(at).filter(Boolean)),
|
|
230
|
+
keep_source: c.keepSource,
|
|
231
|
+
sources: c.ids
|
|
232
|
+
});
|
|
233
|
+
} else if (c.action === "conflict") {
|
|
234
|
+
receipts.push({
|
|
235
|
+
...base,
|
|
236
|
+
receipt_id: randomUUID(),
|
|
237
|
+
record_id: c.winner,
|
|
238
|
+
kind: "conflict",
|
|
239
|
+
input_digest: hashDecisionInput([at(c.winner), at(c.loser)].filter(Boolean)),
|
|
240
|
+
winner_id: c.winner,
|
|
241
|
+
loser_id: c.loser
|
|
242
|
+
});
|
|
243
|
+
} else if (c.action === "update") {
|
|
244
|
+
receipts.push({
|
|
245
|
+
...base,
|
|
246
|
+
receipt_id: randomUUID(),
|
|
247
|
+
record_id: c.ids[0],
|
|
248
|
+
kind: "update",
|
|
249
|
+
input_digest: hashDecisionInput([at(c.ids[0])].filter(Boolean))
|
|
250
|
+
});
|
|
251
|
+
} else if (c.action === "supersede") {
|
|
252
|
+
// Issue #126:演进型裁决——record 记被取代的一方。winner 正文未被改动,
|
|
253
|
+
// 不需要自己的 receipt;loser 归档并追加"已被取代"注记。
|
|
254
|
+
receipts.push({
|
|
255
|
+
...base,
|
|
256
|
+
receipt_id: randomUUID(),
|
|
257
|
+
record_id: c.loser,
|
|
258
|
+
kind: "supersede",
|
|
259
|
+
input_digest: hashDecisionInput([at(c.winner), at(c.loser)].filter(Boolean)),
|
|
260
|
+
winner_id: c.winner,
|
|
261
|
+
loser_id: c.loser
|
|
262
|
+
});
|
|
263
|
+
} else if (c.action === "differentiate") {
|
|
264
|
+
// Issue #126:互补型裁决——两条都保留,各记一条 receipt。数量没有变化,
|
|
265
|
+
// count_before/after 固定 1/1。Issue #533 起正文不再追加差异注记、一个字节
|
|
266
|
+
// 都不动,这里记的是「这一对被裁定为互补」,配对关系另见 differentiated_pairs。
|
|
267
|
+
for (const id of c.ids ?? []) {
|
|
268
|
+
receipts.push({
|
|
269
|
+
...base,
|
|
270
|
+
receipt_id: randomUUID(),
|
|
271
|
+
record_id: id,
|
|
272
|
+
kind: "differentiate",
|
|
273
|
+
input_digest: hashDecisionInput([at(id)].filter(Boolean)),
|
|
274
|
+
count_before: 1,
|
|
275
|
+
count_after: 1
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return receipts;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Consume an LLM stream and return the accumulated text. Direct text-delta
|
|
285
|
+
* accumulation covers both the real protocol ({type:"text-delta", index, text})
|
|
286
|
+
* and looser test doubles ({type:"text-delta", text}); a terminal error/abort
|
|
287
|
+
* surfaces as undefined. The caller decides how to treat an empty result.
|
|
288
|
+
* `onUsage` (optional, Bug8) receives any usage chunk for token accounting.
|
|
289
|
+
*/
|
|
290
|
+
async function streamText(ctx, options, onUsage, onStreamError) {
|
|
291
|
+
let text = "";
|
|
292
|
+
for await (const chunk of ctx.llm.stream(options)) {
|
|
293
|
+
if (chunk.type === "text-delta" && typeof chunk.text === "string") text += chunk.text;
|
|
294
|
+
// DSH 的 StreamChunk 契约把用量嵌在 chunk.usage(TokenUsage:inputTokens /
|
|
295
|
+
// outputTokens / …),chunk 顶层没有 token 字段——把整个 chunk 当用量对象交给
|
|
296
|
+
// reporter,input/output 恒为 undefined,审计行落 0(v0.8.4 实测:7 天 49 次
|
|
297
|
+
// success 调用 token 全 0,面板「LLM 消耗」长期显示 0)。`?? chunk` 兜底把用量
|
|
298
|
+
// 平铺在顶层的替身。
|
|
299
|
+
if (chunk.type === "usage" && typeof onUsage === "function") onUsage(chunk.usage ?? chunk);
|
|
300
|
+
if (chunk.type === "finish" && (chunk.reason?.kind === "error" || chunk.reason?.kind === "aborted")) {
|
|
301
|
+
// dsh-llm rc.1 turns adapter-stage failures (unknown provider route,
|
|
302
|
+
// UNSUPPORTED_REASONING_EFFORT from resolveCallWithInfo, …) into a
|
|
303
|
+
// terminal finish chunk instead of a throw — the cause rides in
|
|
304
|
+
// chunk.reason.failure {message, code}. Surface it, never swallow it.
|
|
305
|
+
if (typeof onStreamError === "function") {
|
|
306
|
+
try { onStreamError(chunk.reason); } catch { /* diagnostics only */ }
|
|
307
|
+
}
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return text;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** One-line human-readable cause from a finish-chunk failure reason. */
|
|
315
|
+
function describeStreamFailure(reason) {
|
|
316
|
+
const failure = reason?.failure ?? reason ?? {};
|
|
317
|
+
const code = failure.code ? String(failure.code) : "";
|
|
318
|
+
const message = String(failure.message ?? failure.error ?? "");
|
|
319
|
+
if (code && message) return message.includes(code) ? message : `${code}: ${message}`;
|
|
320
|
+
return code || message;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Bug8: wrap a background LLM call so its token/time/status are recorded in the
|
|
325
|
+
* llm_audit_logs table. Best-effort bookkeeping: a failure to WRITE the audit
|
|
326
|
+
* row is swallowed (never blocks the LLM call), while a failure of the call
|
|
327
|
+
* itself is captured as status='error' and re-thrown so the caller keeps its
|
|
328
|
+
* existing error path. `spec` carries the static metadata (trigger_source,
|
|
329
|
+
* operation_type, model_id, related_memory_ids); `body(reportUsage)` performs
|
|
330
|
+
* the actual stream consumption and is handed a usage reporter for the chunks.
|
|
331
|
+
*/
|
|
332
|
+
/**
|
|
333
|
+
* 把喂给模型的清单包进围栏(#493 的另一半:dream / sleep 那 5 处 user 消息以前是裸放)。
|
|
334
|
+
* 三个细节各自有原因,别当装饰删:
|
|
335
|
+
* - 角色声明落在**开头**:判断「这段文本是什么」的是它所在的那条 user 消息,只写在 system
|
|
336
|
+
* 里实测救不回来(#493 的 A/C 臂:只加 system 约束仍 0/2 出 JSON);
|
|
337
|
+
* - 正文里的 `<<<` / `>>>` 用零宽空格断链——**只中和标记,不删内容**。不这么做时,一条写着
|
|
338
|
+
* 「<<<记忆清单结束>>>」的记忆就能提前闭合围栏,把后面的文字升格成指令;而记忆正文本来就
|
|
339
|
+
* 是模型的可影响输入,这是最省事的注入入口;
|
|
340
|
+
* - 纯函数、不带 nonce:同一批记忆在下一次巩固阈值到来时会被原样再喂一次,重试收到的 prompt
|
|
341
|
+
* 必须与首次逐字节相同,否则「同输入同错」这条诊断依据就没了(#493 的重试 20 次现场就靠它)。
|
|
342
|
+
* @param {"json"|"text"} [shape] 该次调用的**输出契约**:巩固/叙述/冲突/模式要 JSON 数组,
|
|
343
|
+
* 总览摘要要散文。角色声明里那句「只输出 JSON 数组」必须跟着契约走——对散文契约说它,
|
|
344
|
+
* 模型会回 `[]`,而 `summaryText.trim()` 判非空通过,`[]` 就被当成摘要存进库(评审实测)。
|
|
345
|
+
*/
|
|
346
|
+
export function fenceListBody(language, text, shape = "json") {
|
|
347
|
+
const open = STR.memoryFenceOpen[language] ?? STR.memoryFenceOpen.en;
|
|
348
|
+
const close = STR.memoryFenceClose[language] ?? STR.memoryFenceClose.en;
|
|
349
|
+
const table = shape === "text" ? STR.dreamTextRoleReminder : STR.dreamRoleReminder;
|
|
350
|
+
const reminder = table[language] ?? table.en;
|
|
351
|
+
const body = breakFenceMarkers(text);
|
|
352
|
+
return `${reminder}\n\n${open}\n${body}\n${close}`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* 解析失败行的审计读数(#493 同款,报告者提的第 2 条)。dream 的 warn 日志本来就带
|
|
357
|
+
* raw length + 前 300 字,缺的是**可查询**的那一面:日志要翻、按不了、也进不了面板的
|
|
358
|
+
* 「LLM 消耗」。`raw_has_bracket` 是区分「模型压根没按格式写」与「格式对但内容救不回来」
|
|
359
|
+
* 的唯一低成本判据。
|
|
360
|
+
*/
|
|
361
|
+
export function parseFailureMeta(text) {
|
|
362
|
+
const raw = typeof text === "string" ? text : "";
|
|
363
|
+
return {
|
|
364
|
+
parse_failure: true,
|
|
365
|
+
raw_len: raw.length,
|
|
366
|
+
raw_head: raw.slice(0, 300).replace(/\s+/g, " ").trim(),
|
|
367
|
+
raw_has_bracket: raw.includes("[")
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* #534 建议 3(分账读数):一轮巩固在「校验层」判了什么。
|
|
373
|
+
*
|
|
374
|
+
* 调用层的审计行在 LLM 返回时就写死了 status=success,而这一轮可能整单被校验
|
|
375
|
+
* 拒绝、也可能只跳过几条(degraded)。把这三个数字补进那一行的 metadata,
|
|
376
|
+
* 「调用成功率」与「整轮有效率」就有了同一份可对齐的分母,不必再拿两张表硬拼。
|
|
377
|
+
*
|
|
378
|
+
* 只记计数与动作名,不记原文——原文已经在 raw_output 列(失败轮)与 skipped
|
|
379
|
+
* (逐条 error)里了,这里再抄一遍等于同一份数据存三处。
|
|
380
|
+
*/
|
|
381
|
+
export function decisionValidationMeta({ verdict, errors, skipped, survivorCount } = {}) {
|
|
382
|
+
const skips = Array.isArray(skipped) ? skipped : [];
|
|
383
|
+
const actions = [...new Set(skips.map((s) => s?.action).filter((a) => typeof a === "string" && a.length > 0))];
|
|
384
|
+
// verdict 由调用方给:它知道这一轮是「没解析出数组」「整单拒绝」还是「合法子集」,
|
|
385
|
+
// 判据复用校验器的返回值而不是在这里再推一遍布尔组合。
|
|
386
|
+
return {
|
|
387
|
+
decision_validation: {
|
|
388
|
+
verdict: typeof verdict === "string" ? verdict : "unknown",
|
|
389
|
+
error_count: Array.isArray(errors) ? errors.length : 0,
|
|
390
|
+
skipped_count: skips.length,
|
|
391
|
+
survivor_count: Number.isFinite(survivorCount) ? survivorCount : 0,
|
|
392
|
+
invalid_actions: actions
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* #534 建议 2:失败轮把模型原文留在行上。此前失败只有一条 warn,日志一滚现场
|
|
399
|
+
* 就没了——作者实测 19 轮里 17 轮整轮无效,事后连模型是怎么坏的都复不了盘。
|
|
400
|
+
*
|
|
401
|
+
* 截断口径沿用 #502/#504(summarize.js 的 summarizeParseFailureAudit):头部
|
|
402
|
+
* 300 字,仅当原文 >500 字时补末 200 字。只留头尾是刻意的——模型跑飞时中段是
|
|
403
|
+
* 重复噪声,头尾才分别在「它怎么起手」「它怎么收尾」上给线索;整段原文既不必要
|
|
404
|
+
* 也不该进库(一轮可有数十 KB)。
|
|
405
|
+
*
|
|
406
|
+
* 空文本返回 undefined,调用方据此整键不写(与 saveDreamRun 的 undefined→NULL
|
|
407
|
+
* 约定一致,不落一个空壳对象冒充「有原文」)。
|
|
408
|
+
*/
|
|
409
|
+
export function rawOutputPayload(text) {
|
|
410
|
+
if (typeof text !== "string" || text.length === 0) return undefined;
|
|
411
|
+
const tail = text.length > 500 ? text.slice(-200) : "";
|
|
412
|
+
return {
|
|
413
|
+
raw_len: text.length,
|
|
414
|
+
raw_head: text.slice(0, 300),
|
|
415
|
+
...(tail ? { raw_tail: tail } : {}),
|
|
416
|
+
raw_has_bracket: text.includes("[")
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function runAuditedLlm(ctx, service, config, spec, body) {
|
|
421
|
+
const audit = config?.llmAudit;
|
|
422
|
+
if (audit?.enabled === false || typeof service?.saveLlmAudit !== "function") return body(() => {});
|
|
423
|
+
const startedAt = Date.now();
|
|
424
|
+
const timestamp = new Date(startedAt).toISOString();
|
|
425
|
+
let inputTokens = 0;
|
|
426
|
+
let outputTokens = 0;
|
|
427
|
+
let status = "success";
|
|
428
|
+
let errorMessage = null;
|
|
429
|
+
let result;
|
|
430
|
+
try {
|
|
431
|
+
result = await body((usage) => {
|
|
432
|
+
if (!usage) return;
|
|
433
|
+
const i = usage.input_tokens ?? usage.inputTokens ?? usage.prompt_tokens ?? usage.promptTokens;
|
|
434
|
+
const o = usage.output_tokens ?? usage.outputTokens ?? usage.completion_tokens ?? usage.completionTokens;
|
|
435
|
+
if (Number.isFinite(i)) inputTokens = i;
|
|
436
|
+
if (Number.isFinite(o)) outputTokens = o;
|
|
437
|
+
});
|
|
438
|
+
if (result === undefined) {
|
|
439
|
+
// stream aborted/errored: the caller treats undefined as a failed run;
|
|
440
|
+
// record it as error here so the audit shows the truth. spec.streamError
|
|
441
|
+
// (a getter) lets the caller attach the finish-chunk cause so the audit
|
|
442
|
+
// row names it instead of a bare "aborted".
|
|
443
|
+
status = "error";
|
|
444
|
+
const streamErr = typeof spec.streamError === "function" ? String(spec.streamError() ?? "") : "";
|
|
445
|
+
errorMessage = errorMessage ?? (streamErr ? `llm stream aborted or errored (${streamErr})` : "llm stream aborted or errored");
|
|
446
|
+
} else if (typeof spec.auditError === "function") {
|
|
447
|
+
// A stream that returned text but yields nothing usable is still a
|
|
448
|
+
// failed call — record it as error, not the default success, so the
|
|
449
|
+
// audit no longer contradicts a failed run (dream "no json array").
|
|
450
|
+
const message = spec.auditError(result);
|
|
451
|
+
if (message) {
|
|
452
|
+
status = "error";
|
|
453
|
+
errorMessage = message;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return result;
|
|
457
|
+
} catch (error) {
|
|
458
|
+
status = "error";
|
|
459
|
+
errorMessage = String(error?.message ?? error);
|
|
460
|
+
throw error;
|
|
461
|
+
} finally {
|
|
462
|
+
// spec.metadataFrom(result):调用方按**这一轮的原始输出**决定要不要带读数(解析成功就
|
|
463
|
+
// 返回 null → 整键不写)。判据函数必须是纯的:它读的是 result,不读控制流里的缓存,
|
|
464
|
+
// 否则「审计说了什么」会随调用方走到哪一行而变(sleep.js 同一条硬约定)。
|
|
465
|
+
let metadata;
|
|
466
|
+
if (result !== undefined && typeof spec.metadataFrom === "function") {
|
|
467
|
+
try {
|
|
468
|
+
metadata = spec.metadataFrom(result) ?? undefined;
|
|
469
|
+
} catch {
|
|
470
|
+
metadata = undefined; // 读数失败不许把审计行整个弄没
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
const row = service.saveLlmAudit({
|
|
475
|
+
timestamp,
|
|
476
|
+
trigger_source: spec.triggerSource,
|
|
477
|
+
operation_type: spec.operationType,
|
|
478
|
+
model_id: spec.modelId,
|
|
479
|
+
input_tokens: inputTokens,
|
|
480
|
+
output_tokens: outputTokens,
|
|
481
|
+
total_tokens: inputTokens + outputTokens,
|
|
482
|
+
cost_usd: 0,
|
|
483
|
+
duration_ms: Date.now() - startedAt,
|
|
484
|
+
status,
|
|
485
|
+
error_message: errorMessage,
|
|
486
|
+
related_memory_ids: spec.relatedMemoryIds ?? [],
|
|
487
|
+
...(metadata ? { metadata } : {})
|
|
488
|
+
});
|
|
489
|
+
// #534 建议 3:把刚落库这一行的 id 交回调用方。校验判决发生在调用**之后**,
|
|
490
|
+
// 只有拿到 id 才补得回去(见 store 的 annotateLlmAudit)。这里只做交接,
|
|
491
|
+
// 不改写任何调用层事实——status/token/耗时仍然是调用那一刻的真相。
|
|
492
|
+
if (spec.auditRef && typeof spec.auditRef === "object") spec.auditRef.id = row?.id;
|
|
493
|
+
} catch (auditError) {
|
|
494
|
+
ctx.logger?.warn?.(`dsh-mneme: llm audit write failed: ${String(auditError)}`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Reasoning-effort rejection fallback (v0.8.1): a configured dreamReasoningEffort
|
|
501
|
+
* / sleepReasoningEffort may be rejected by the provider (volcano-engine returns
|
|
502
|
+
* UNSUPPORTED_REASONING_EFFORT for values it does not accept — "off" is known
|
|
503
|
+
* rejected there). When that happens, retry once WITHOUT the reasoning field
|
|
504
|
+
* instead of hard-failing the run, so effort config is safe to experiment with:
|
|
505
|
+
* accepted → reasoning capped; rejected → provider default (old behavior),
|
|
506
|
+
* logged so the rejection is observable.
|
|
507
|
+
*/
|
|
508
|
+
// Issue #315:effort 拒收判别式的单一来源。summarize 的流失败以 aborted 结果
|
|
509
|
+
// 返回,需在折叠成 undefined 前用同一甄别(各处手写会漂移)。
|
|
510
|
+
export const EFFORT_REJECT_RE = /reasoning[\s_]*effort|UNSUPPORTED_REASONING_EFFORT/i;
|
|
511
|
+
async function withEffortFallback(ctx, effort, attempt, fallback, getStreamError) {
|
|
512
|
+
if (!effort || effort === "none") return attempt();
|
|
513
|
+
try {
|
|
514
|
+
const result = await attempt();
|
|
515
|
+
if (result === undefined) {
|
|
516
|
+
// dsh-llm rc.1 streams a provider effort-rejection as a terminal error
|
|
517
|
+
// finish chunk (adapterStream catches everything, never throws) — match
|
|
518
|
+
// on the chunk's failure reason here or the retry below is dead code
|
|
519
|
+
// for the stream path.
|
|
520
|
+
const reason = String(getStreamError?.() ?? "");
|
|
521
|
+
if (EFFORT_REJECT_RE.test(reason)) {
|
|
522
|
+
ctx.logger?.warn?.(`dsh-mneme dream: reasoningEffort "${effort}" rejected via stream (${reason}); retrying without it`);
|
|
523
|
+
return fallback();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return result;
|
|
527
|
+
} catch (error) {
|
|
528
|
+
// dispose/取消中止直接放行,绝不能被误判成 effort 拒收而触发 fallback
|
|
529
|
+
//(取消后重打一次不带 effort 的调用是浪费,且可能掩盖真实的取消意图)。
|
|
530
|
+
if (error?.name === "AbortError") throw error;
|
|
531
|
+
const message = String(error?.message ?? error);
|
|
532
|
+
// matches both "reasoning effort" (natural language) and the bare
|
|
533
|
+
// "UNSUPPORTED_REASONING_EFFORT" error code (underscore).
|
|
534
|
+
if (!EFFORT_REJECT_RE.test(message)) throw error;
|
|
535
|
+
ctx.logger?.warn?.(`dsh-mneme dream: reasoningEffort "${effort}" rejected (${message}); retrying without it`);
|
|
536
|
+
return fallback();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Issue #478:max_tokens 超服务商硬上限的拒收判别式与上限提取的单一来源。
|
|
541
|
+
// 实测失败样例(dream_runs 180 连败):"max_tokens 不能超过 65536"——131072 的
|
|
542
|
+
// 默认值原样发出,网关 400 直接拒,推理还没开始。与 EFFORT_REJECT_RE 同款思路:
|
|
543
|
+
// 各处手写会漂移,导出供测试与其它调用点复用。
|
|
544
|
+
export const MAX_TOKENS_REJECT_RE = /max[\s_-]*tokens?/i;
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* 从拒收报错里提取服务商声明的 max_tokens 上限。只认「显式给数」的说法
|
|
548
|
+
* (不能超过 / at most / maximum value is / <= 65536 …),认不出就返回 null
|
|
549
|
+
* ——宁可不降级重试,也不能把报错里那个被拒的请求值(131072)误当上限。
|
|
550
|
+
* @returns {number|null}
|
|
551
|
+
*/
|
|
552
|
+
function parseMaxTokensCap(message) {
|
|
553
|
+
const m = String(message ?? "").match(
|
|
554
|
+
/(?:不能超过|不得超过|超出上限|至多|最多(?:只能|仅)?|at\s+most|no\s+more\s+than|maximum(?:\s+value)?(?:\s+is)?|max(?:\.)?(?:\s+value)?(?:\s+is)?|less\s+than\s+or\s+equal\s+to|<=?)\s*[::]?\s*(\d{2,10})/i
|
|
555
|
+
);
|
|
556
|
+
const cap = m ? Number(m[1]) : null;
|
|
557
|
+
return cap && Number.isFinite(cap) && cap > 0 ? cap : null;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Issue #478:max_tokens 被服务商 400 拒收时,按报错里的上限自动降级重试一次。
|
|
562
|
+
* 与 withEffortFallback 同构:stream 路径的结果折叠成 undefined、真实原因在
|
|
563
|
+
* getStreamError() 里;throw 路径直接看 message。命中拒收且提取到比当前值小的
|
|
564
|
+
* 上限才重试(attempt 接收 tokens 值),否则原样返回/抛出。单次重试,与
|
|
565
|
+
* effort 的先例一致——重试后若仍被拒(同上限再报),cap >= tokens 的守卫让
|
|
566
|
+
* 它自然终止,不会循环。
|
|
567
|
+
*/
|
|
568
|
+
async function withMaxTokensFallback(ctx, maxTokens, attempt, getStreamError) {
|
|
569
|
+
const retry = (tokens, reason) => {
|
|
570
|
+
ctx.logger?.warn?.(`dsh-mneme dream: max_tokens ${maxTokens} rejected by provider (${reason}); retrying with ${tokens}`);
|
|
571
|
+
return attempt(tokens);
|
|
572
|
+
};
|
|
573
|
+
try {
|
|
574
|
+
const result = await attempt(maxTokens);
|
|
575
|
+
if (result === undefined) {
|
|
576
|
+
const reason = String(getStreamError?.() ?? "");
|
|
577
|
+
if (MAX_TOKENS_REJECT_RE.test(reason)) {
|
|
578
|
+
const cap = parseMaxTokensCap(reason);
|
|
579
|
+
if (cap && cap < maxTokens) return retry(cap, reason);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return result;
|
|
583
|
+
} catch (error) {
|
|
584
|
+
// AbortError 直接放行(取消意图绝不能被降级重试吞掉,与 effort 同款守卫)。
|
|
585
|
+
if (error?.name === "AbortError") throw error;
|
|
586
|
+
const message = String(error?.message ?? error);
|
|
587
|
+
if (!MAX_TOKENS_REJECT_RE.test(message)) throw error;
|
|
588
|
+
const cap = parseMaxTokensCap(message);
|
|
589
|
+
if (!cap || cap >= maxTokens) throw error;
|
|
590
|
+
return retry(cap, message);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** 向量索引同步(总览与叙述条共用):fail-safe,失败只 warn 不反噬主流程。 */
|
|
595
|
+
async function reEmbedMemory(semantic, memory, logger) {
|
|
596
|
+
if (!semantic?.embedder || !semantic?.vectorIndex || !memory) return;
|
|
597
|
+
try {
|
|
598
|
+
const v = await semantic.embedder.embedSingle([memory.title, memory.content].filter(Boolean).join("\n"));
|
|
599
|
+
if (v?.length) semantic.vectorIndex.saveEmbedding(memory.id, v);
|
|
600
|
+
if (semantic.embedder.modelHash) semantic.vectorIndex.markModel?.(semantic.embedder.modelHash, semantic.embedder.dimension);
|
|
601
|
+
} catch (error) {
|
|
602
|
+
logger?.warn?.(`dsh-mneme dream: re-embed failed: ${String(error)}`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* 叙述条阶段(#164 对齐,opt-in dreamNarrativeEnabled):共享 tag 主题聚类 →
|
|
608
|
+
* 单次 LLM 调用按簇合成叙述 → 证据求交(防捏造,dream/narratives.js 纯函数)
|
|
609
|
+
* → saveWithDedupe(source="narrative", _overwrite) 落库。按需检索不常驻注入。
|
|
610
|
+
* 任何失败降级为 0 条,绝不反噬 dream 主流程。
|
|
611
|
+
*/
|
|
612
|
+
async function generateNarratives({ ctx, service, config, route, language, effort, semantic, logger }) {
|
|
613
|
+
const inputs = withoutPaused(service, service.all().filter((m) => !isImportedRow(m) && !m.archived && !m.forgotten && m.type !== "summary" && m.type !== "document"));
|
|
614
|
+
const clusters = clusterByTag(inputs, { minCluster: config.dreamNarrativeMinCluster ?? 3 });
|
|
615
|
+
if (!clusters.length) return 0;
|
|
616
|
+
|
|
617
|
+
const listing = clusters
|
|
618
|
+
.map(({ tag, members }) =>
|
|
619
|
+
[`# ${tag}`, ...members.map((m) => `- id=${m.id} | title=${m.title} | ${m.content}`)].join("\n"))
|
|
620
|
+
.join("\n\n");
|
|
621
|
+
let streamFailure = "";
|
|
622
|
+
const runNarratives = (withEffort, tokens) => {
|
|
623
|
+
streamFailure = "";
|
|
624
|
+
return runAuditedLlm(ctx, service, config, {
|
|
625
|
+
triggerSource: "autoDream",
|
|
626
|
+
operationType: "dream_narrative",
|
|
627
|
+
modelId: `${route.provider}:${route.model}`,
|
|
628
|
+
relatedMemoryIds: clusters.flatMap((c) => c.members.map((m) => m.id)),
|
|
629
|
+
streamError: () => streamFailure
|
|
630
|
+
}, (reportUsage) => streamText(ctx, {
|
|
631
|
+
provider: route.provider,
|
|
632
|
+
model: route.model,
|
|
633
|
+
purpose: "compaction",
|
|
634
|
+
maxTokens: tokens,
|
|
635
|
+
...(withEffort && effort ? { reasoningEffort: effort } : {}),
|
|
636
|
+
messages: [
|
|
637
|
+
{ role: "system", content: [{ type: "text", text: STR.prompts.narrative[language] }], source: { kind: "plugin:dsh-mneme", plugin: "dsh-mneme" } },
|
|
638
|
+
{ role: "user", content: [{ type: "text", text: fenceListBody(language, listing) }], source: { kind: "plugin:dsh-mneme", plugin: "dsh-mneme" } }
|
|
639
|
+
]
|
|
640
|
+
}, reportUsage, (reason) => { streamFailure = describeStreamFailure(reason); }));
|
|
641
|
+
};
|
|
642
|
+
const text = await withMaxTokensFallback(ctx, config.dreamMaxTokens ?? 2048, (tokens) => withEffortFallback(ctx, effort, () => runNarratives(true, tokens), () => runNarratives(false, tokens), () => streamFailure), () => streamFailure);
|
|
643
|
+
|
|
644
|
+
const entries = extractJsonArray(text);
|
|
645
|
+
if (!Array.isArray(entries)) return 0;
|
|
646
|
+
const clusterByOfferedTag = new Map(clusters.map((c) => [c.tag, c]));
|
|
647
|
+
const now = new Date().toISOString();
|
|
648
|
+
let stored = 0;
|
|
649
|
+
for (const entry of entries) {
|
|
650
|
+
if (!entry || typeof entry !== "object") continue;
|
|
651
|
+
const cluster = clusterByOfferedTag.get(typeof entry.tag === "string" ? entry.tag : "");
|
|
652
|
+
const content = String(entry.content ?? "").trim();
|
|
653
|
+
if (!cluster || !content) continue; // 未提供的 tag / 空叙述 → 跳过
|
|
654
|
+
const evidenceIds = intersectEvidence(entry.evidence, cluster.members);
|
|
655
|
+
const { memory } = service.saveWithDedupe({
|
|
656
|
+
type: "summary",
|
|
657
|
+
title: STR.narrativeTitle[language](cluster.tag),
|
|
658
|
+
content,
|
|
659
|
+
importance: 3,
|
|
660
|
+
tags: [cluster.tag],
|
|
661
|
+
source: "narrative",
|
|
662
|
+
evidence: evidenceIds.map((id) => ({ memory_id: id, op: "support", at: now })),
|
|
663
|
+
_overwrite: true
|
|
664
|
+
});
|
|
665
|
+
if (!memory) continue;
|
|
666
|
+
stored++;
|
|
667
|
+
// 叙述条按需可检索:向量索引同步(与总览共用 reEmbedMemory,fail-safe)。
|
|
668
|
+
await reEmbedMemory(semantic, memory, logger);
|
|
669
|
+
}
|
|
670
|
+
return stored;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Resolve the reasoning effort to actually send for a dream/sleep route.
|
|
675
|
+
*
|
|
676
|
+
* Reasoning-effort config that the provider does not accept trips the harness's
|
|
677
|
+
* UNSUPPORTED_REASONING_EFFORT, and the defaultEffort trap makes retrying
|
|
678
|
+
* "without the field" useless: the harness substitutes `reasoning.defaultEffort`,
|
|
679
|
+
* which may itself be unsupported (DSH Desktop volcano-engine adapter declares
|
|
680
|
+
* defaultEffort=low that its model rejects). So instead of blind retries, ask
|
|
681
|
+
* the harness for the model's declared capability and pick a value that is
|
|
682
|
+
* actually accepted — or omit the field entirely when the model declares no
|
|
683
|
+
* reasoning capability at all.
|
|
684
|
+
*
|
|
685
|
+
* Unset vs explicit 'none' (Issue #135 建议 4/5): these are different intents.
|
|
686
|
+
* Explicit 'none' = the user asked to omit the field (provider default
|
|
687
|
+
* applies). Unset is the out-of-the-box state — dream/sleep calls must produce
|
|
688
|
+
* JSON, and omission lets the harness substitute the model's defaultEffort
|
|
689
|
+
* (thinking-type models often default to high), which drains the token budget
|
|
690
|
+
* and returns an empty body. So unset resolves to the LOWEST effort the model
|
|
691
|
+
* declares instead of omitting; when the capability query is unavailable the
|
|
692
|
+
* field stays omitted (fail-safe, retry guard unchanged).
|
|
693
|
+
*
|
|
694
|
+
* @returns a supported effort id, or null when no effort should be sent, or the
|
|
695
|
+
* configured value unchanged when the capability query is unavailable.
|
|
696
|
+
*/
|
|
697
|
+
|
|
698
|
+
// 常识档位排序(越靠前推理开销越低);未知档位排最后(宁可交给显式配置)。
|
|
699
|
+
const EFFORT_RANK = { minimal: 0, off: 0, low: 1, medium: 2, high: 3, max: 4 };
|
|
700
|
+
|
|
701
|
+
function lowestSupportedEffort(reasoning) {
|
|
702
|
+
const ids = (reasoning?.efforts ?? []).map((e) => e?.id).filter(Boolean);
|
|
703
|
+
if (ids.length === 0) return null;
|
|
704
|
+
return [...ids].sort((a, b) => (EFFORT_RANK[a] ?? 99) - (EFFORT_RANK[b] ?? 99))[0];
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async function resolveDreamEffort(ctx, route, configuredEffort, logger) {
|
|
708
|
+
// 显式 'none':用户要求省略字段(服务商自带默认生效),照旧。
|
|
709
|
+
if (configuredEffort === "none") return null;
|
|
710
|
+
if (!configuredEffort) {
|
|
711
|
+
// 未配置(开箱默认):取模型声明的最低档,避免 defaultEffort 顶上(建议 4/5)。
|
|
712
|
+
if (typeof ctx?.llm?.resolveModelInfo !== "function") return null;
|
|
713
|
+
try {
|
|
714
|
+
const info = await ctx.llm.resolveModelInfo(route.provider, route.model);
|
|
715
|
+
const reasoning = info?.reasoning;
|
|
716
|
+
if (!reasoning) return null; // 无推理声明:省略即安全(无 defaultEffort 可顶上)
|
|
717
|
+
const lowest = lowestSupportedEffort(reasoning);
|
|
718
|
+
if (!lowest) return null;
|
|
719
|
+
logger?.info?.(`dsh-mneme dream: no reasoningEffort configured for ${route.provider}:${route.model}; using lowest supported "${lowest}" instead of the model default "${reasoning.defaultEffort ?? "n/a"}"`);
|
|
720
|
+
return lowest;
|
|
721
|
+
} catch (error) {
|
|
722
|
+
// 能力查询失败:省略字段(fail-safe),withEffortFallback 照旧兜底。
|
|
723
|
+
logger?.warn?.(`dsh-mneme dream: resolveModelInfo failed (${String(error?.message ?? error)}); omitting reasoningEffort`);
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
// Capability query unavailable (older harness / minimal mocks): forward the
|
|
728
|
+
// configured value as before — absence of the API proves nothing about the
|
|
729
|
+
// model, and withEffortFallback still guards against rejection.
|
|
730
|
+
if (typeof ctx?.llm?.resolveModelInfo !== "function") return configuredEffort;
|
|
731
|
+
try {
|
|
732
|
+
const info = await ctx.llm.resolveModelInfo(route.provider, route.model);
|
|
733
|
+
const reasoning = info?.reasoning;
|
|
734
|
+
if (!reasoning) {
|
|
735
|
+
// Model declares no reasoning capability: the harness rejects ANY
|
|
736
|
+
// explicit effort for such a model, and omitting the field is safe
|
|
737
|
+
// (no reasoning capability → no defaultEffort substitution).
|
|
738
|
+
logger?.warn?.(`dsh-mneme dream: model ${route.provider}:${route.model} declares no reasoning capability; ignoring configured effort "${configuredEffort}"`);
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
const supported = reasoning.efforts?.map((effort) => effort.id) ?? [];
|
|
742
|
+
if (supported.includes(configuredEffort)) return configuredEffort;
|
|
743
|
+
// Configured effort unsupported → pick defaultEffort if it is supported,
|
|
744
|
+
// else the first declared effort, so the run never trips
|
|
745
|
+
// UNSUPPORTED_REASONING_EFFORT (nor the defaultEffort trap: we always
|
|
746
|
+
// pass an explicit value, so the harness never falls back to a poison
|
|
747
|
+
// default).
|
|
748
|
+
const picked = reasoning.defaultEffort && supported.includes(reasoning.defaultEffort)
|
|
749
|
+
? reasoning.defaultEffort
|
|
750
|
+
: supported[0];
|
|
751
|
+
if (picked) {
|
|
752
|
+
logger?.warn?.(`dsh-mneme dream: model ${route.provider}:${route.model} does not support effort "${configuredEffort}" (supported: ${supported.join(", ")}); using "${picked}"`);
|
|
753
|
+
return picked;
|
|
754
|
+
}
|
|
755
|
+
return null;
|
|
756
|
+
} catch (error) {
|
|
757
|
+
// Capability query failed — forward the configured value; withEffortFallback
|
|
758
|
+
// still retries on rejection as before.
|
|
759
|
+
logger?.warn?.(`dsh-mneme dream: resolveModelInfo failed (${String(error?.message ?? error)}); forwarding effort as configured`);
|
|
760
|
+
return configuredEffort;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Resolve the LLM route (Issue #25): an explicit plugin config
|
|
766
|
+
* (dreamProvider/dreamModel) is the user's declared override and wins; the
|
|
767
|
+
* agent default model (deployment) is only a fallback when no config route is
|
|
768
|
+
* set. In a standard DSH install agentDefaultModel always resolves, so without
|
|
769
|
+
* this ordering the config route would be dead code and dreamProvider/dreamModel
|
|
770
|
+
* could never take effect (v0.7.11 regressed this; README §config documents
|
|
771
|
+
* config-first). Falls through to undefined when no route exists — runDream
|
|
772
|
+
* then fails safe. A config→default switch is logged so it is observable.
|
|
773
|
+
*/
|
|
774
|
+
function resolveRoute(ctx, config, logger) {
|
|
775
|
+
if (config.dreamProvider && config.dreamModel) return { provider: config.dreamProvider, model: config.dreamModel };
|
|
776
|
+
try {
|
|
777
|
+
const sel = ctx.agentDefaultModel?.currentSelection?.();
|
|
778
|
+
if (sel?.provider && sel?.model) {
|
|
779
|
+
logger?.info?.("dsh-mneme dream: no dreamProvider/dreamModel config, falling back to agent default");
|
|
780
|
+
return { provider: sel.provider, model: sel.model };
|
|
781
|
+
}
|
|
782
|
+
logger?.warn?.("dsh-mneme dream: agentDefaultModel unavailable, no config route either");
|
|
783
|
+
} catch (error) {
|
|
784
|
+
logger?.warn?.(`dsh-mneme dream: agentDefaultModel lookup failed: ${String(error)}`);
|
|
785
|
+
}
|
|
786
|
+
return undefined;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// ------------------------------------------------------- semantic enhancement
|
|
790
|
+
// Best-effort: any failure here degrades to plain consolidation. The dream
|
|
791
|
+
// path must never be broken by an unavailable embedder/index.
|
|
792
|
+
|
|
793
|
+
/** Backfill + return vectors for every memory; null when impossible. */
|
|
794
|
+
async function collectVectors(memories, semantic) {
|
|
795
|
+
const { embedder, vectorIndex } = semantic;
|
|
796
|
+
if (!embedder || !vectorIndex || typeof embedder.embedSingle !== "function") return null;
|
|
797
|
+
const vectors = new Array(memories.length);
|
|
798
|
+
const missing = [];
|
|
799
|
+
for (let i = 0; i < memories.length; i++) {
|
|
800
|
+
const cached = vectorIndex.getEmbedding?.(memories[i].id);
|
|
801
|
+
if (cached) vectors[i] = cached;
|
|
802
|
+
else missing.push(i);
|
|
803
|
+
}
|
|
804
|
+
if (missing.length) {
|
|
805
|
+
const texts = missing.map((i) => [memories[i].title, memories[i].content].filter(Boolean).join("\n"));
|
|
806
|
+
const rows = await embedder.embed(texts);
|
|
807
|
+
missing.forEach((mi, j) => {
|
|
808
|
+
if (rows[j]?.length) {
|
|
809
|
+
vectors[mi] = rows[j];
|
|
810
|
+
vectorIndex.saveEmbedding(memories[mi].id, rows[j]);
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
return vectors.some((v) => !v) ? null : vectors;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Issue #125:把高相似对聚成**连通分量组**——一个簇 = 一个候选单元。直接按对展开
|
|
819
|
+
* 会把同一簇拆成互相抢占的多个决策(校验器有 one-claim 规则),成组才是正确粒度
|
|
820
|
+
* (与 sleep 冲突阶段的贪心"每记忆每轮最多进一对"同理)。
|
|
821
|
+
* 组内最小相似度用于组间排序:最该处理的簇先入选。
|
|
822
|
+
*/
|
|
823
|
+
function similarityGroups(pairs) {
|
|
824
|
+
const parent = new Map();
|
|
825
|
+
const find = (x) => {
|
|
826
|
+
let root = x;
|
|
827
|
+
while (parent.get(root) !== root) root = parent.get(root);
|
|
828
|
+
return root;
|
|
829
|
+
};
|
|
830
|
+
for (const p of pairs) {
|
|
831
|
+
if (!parent.has(p.a.id)) parent.set(p.a.id, p.a.id);
|
|
832
|
+
if (!parent.has(p.b.id)) parent.set(p.b.id, p.b.id);
|
|
833
|
+
const ra = find(p.a.id);
|
|
834
|
+
const rb = find(p.b.id);
|
|
835
|
+
if (ra !== rb) parent.set(ra, rb);
|
|
836
|
+
}
|
|
837
|
+
const byRoot = new Map();
|
|
838
|
+
for (const p of pairs) {
|
|
839
|
+
for (const m of [p.a, p.b]) {
|
|
840
|
+
const root = find(m.id);
|
|
841
|
+
if (!byRoot.has(root)) byRoot.set(root, new Map());
|
|
842
|
+
byRoot.get(root).set(m.id, m);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return [...byRoot.values()].map((members) => {
|
|
846
|
+
const list = [...members.values()];
|
|
847
|
+
const ids = new Set(list.map((m) => m.id));
|
|
848
|
+
let minSim = 1;
|
|
849
|
+
for (const p of pairs) {
|
|
850
|
+
if (ids.has(p.a.id) && ids.has(p.b.id)) minSim = Math.min(minSim, p.similarity);
|
|
851
|
+
}
|
|
852
|
+
return { members: list, minSim };
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Issue #125:为候选构造收集向量。与 collectVectors 的差别是**允许部分缺失**——
|
|
858
|
+
* 全量库只要有几条嵌入失败就整场返回 null,会让 hybrid 在真实库上几乎永远回落;
|
|
859
|
+
* 拿不到向量的条目只是不参与比较(cosineSimilarity 对非数组返回 0,不会误判)。
|
|
860
|
+
*/
|
|
861
|
+
async function collectCandidateVectors(memories, semantic, logger) {
|
|
862
|
+
const { embedder, vectorIndex } = semantic;
|
|
863
|
+
const vectors = new Array(memories.length);
|
|
864
|
+
const missing = [];
|
|
865
|
+
for (let i = 0; i < memories.length; i++) {
|
|
866
|
+
const cached = vectorIndex.getEmbedding?.(memories[i].id);
|
|
867
|
+
if (cached) vectors[i] = cached;
|
|
868
|
+
else missing.push(i);
|
|
869
|
+
}
|
|
870
|
+
if (missing.length) {
|
|
871
|
+
try {
|
|
872
|
+
const texts = missing.map((i) => [memories[i].title, memories[i].content].filter(Boolean).join("\n"));
|
|
873
|
+
const rows = await embedder.embed(texts);
|
|
874
|
+
missing.forEach((mi, j) => {
|
|
875
|
+
if (rows[j]?.length) {
|
|
876
|
+
vectors[mi] = rows[j];
|
|
877
|
+
vectorIndex.saveEmbedding?.(memories[mi].id, rows[j]);
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
} catch (error) {
|
|
881
|
+
logger?.warn?.(`[dsh-mneme] dream hybrid vector backfill failed: ${String(error)}`);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return vectors;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Issue #125:hybrid 候选成员,按"组内最小相似度"降序展平(高价值簇排前面,
|
|
889
|
+
* 受上限截断时先入选)。同类型比较沿用 findPotentialConflicts;仅当
|
|
890
|
+
* allowCrossTypeMerge 显式开启时再补跨类型高相似对。
|
|
891
|
+
* 任何失败都返回空数组——调用方回落纯窗口,dream 路径绝不因向量层而失败。
|
|
892
|
+
*/
|
|
893
|
+
async function hybridCandidateMembers(allMemories, config, semantic, logger) {
|
|
894
|
+
try {
|
|
895
|
+
const vectors = await collectCandidateVectors(allMemories, semantic, logger);
|
|
896
|
+
const minSim = config.dreamCandidateMinSim ?? 0.85;
|
|
897
|
+
const pairs = findPotentialConflicts(allMemories, vectors, minSim);
|
|
898
|
+
if (config.allowCrossTypeMerge === true) {
|
|
899
|
+
for (let i = 0; i < allMemories.length; i++) {
|
|
900
|
+
for (let j = i + 1; j < allMemories.length; j++) {
|
|
901
|
+
if (allMemories[i].type === allMemories[j].type) continue; // 同类型已由上面覆盖
|
|
902
|
+
const sim = cosineSimilarity(vectors[i], vectors[j]);
|
|
903
|
+
if (sim > minSim) pairs.push({ a: allMemories[i], b: allMemories[j], similarity: sim });
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
if (pairs.length === 0) return [];
|
|
908
|
+
return similarityGroups(pairs)
|
|
909
|
+
.sort((g1, g2) => g2.minSim - g1.minSim)
|
|
910
|
+
.flatMap((g) => g.members);
|
|
911
|
+
} catch (error) {
|
|
912
|
+
logger?.warn?.(`[dsh-mneme] dream hybrid candidate expansion failed (falling back to window): ${String(error)}`);
|
|
913
|
+
return [];
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Rebuild the vector index after dream decisions so the store and the index
|
|
919
|
+
* stay in sync: merged-away/archived/conflict-loser rows lose their vectors,
|
|
920
|
+
* the merge keeper gets a fresh one.
|
|
921
|
+
*/
|
|
922
|
+
export async function maintainIndexAfterDream(decisions, service, semantic) {
|
|
923
|
+
const { embedder, vectorIndex } = semantic;
|
|
924
|
+
if (!embedder || !vectorIndex || typeof embedder.embedSingle !== "function") return;
|
|
925
|
+
const rebuild = new Map();
|
|
926
|
+
for (const d of decisions ?? []) {
|
|
927
|
+
if (d.action === "merge") {
|
|
928
|
+
for (const id of d.ids ?? []) {
|
|
929
|
+
if (id !== d.keepSource) vectorIndex.deleteEmbedding(id);
|
|
930
|
+
}
|
|
931
|
+
if (d.keepSource) {
|
|
932
|
+
const keeper = service.getById(d.keepSource);
|
|
933
|
+
if (keeper) rebuild.set(keeper.id, [keeper.title, keeper.content].filter(Boolean).join("\n"));
|
|
934
|
+
}
|
|
935
|
+
} else if (d.action === "archive" || d.action === "conflict") {
|
|
936
|
+
for (const id of d.ids ?? [d.loser]) vectorIndex.deleteEmbedding(id);
|
|
937
|
+
} else if (d.action === "supersede") {
|
|
938
|
+
// Issue #126 review(Copilot):被取代的一方已归档 → 移除其向量(与 conflict 的
|
|
939
|
+
// loser 同等处置)。winner 正文未变化,向量保持有效。
|
|
940
|
+
vectorIndex.deleteEmbedding(d.loser);
|
|
941
|
+
} else if (d.action === "differentiate") {
|
|
942
|
+
// Issue #533:differentiate 现在一个字节都不改正文(只落裁决账本),两侧向量
|
|
943
|
+
// 因此保持有效,不需要重嵌。旧版在这里为「注记进 embedding 防再判重复」兜底
|
|
944
|
+
// ——那个假设已被复现证伪,重嵌反而是替它续命。
|
|
945
|
+
} else if (d.action === "update") {
|
|
946
|
+
const id = d.ids[0];
|
|
947
|
+
const mem = service.getById(id);
|
|
948
|
+
if (mem) {
|
|
949
|
+
vectorIndex.deleteEmbedding(id);
|
|
950
|
+
try {
|
|
951
|
+
const text = [mem.title, mem.content].filter(Boolean).join("\n");
|
|
952
|
+
const v = await embedder.embedSingle(text);
|
|
953
|
+
if (v?.length) vectorIndex.saveEmbedding(id, v);
|
|
954
|
+
} catch { /* best-effort */ }
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
for (const [id, text] of rebuild) {
|
|
959
|
+
try {
|
|
960
|
+
const v = await embedder.embedSingle(text);
|
|
961
|
+
if (v?.length) vectorIndex.saveEmbedding(id, v);
|
|
962
|
+
} catch { /* best-effort */ }
|
|
963
|
+
}
|
|
964
|
+
if (embedder.modelHash) vectorIndex.markModel?.(embedder.modelHash, embedder.dimension);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Issue #292:连续失败退避的间隔封顶(30 分钟)。封顶只拦指数「增长」,不把
|
|
968
|
+
// 用户配得比这更大的 dreamMinIntervalMinutes 基数压小(见 effectiveMinIntervalMs)。
|
|
969
|
+
const FAILURE_BACKOFF_CAP_MS = 30 * 60 * 1000;
|
|
970
|
+
|
|
971
|
+
export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChars = 5000, delayMs = 2000, minIntervalMs = 0, failureBackoff = false, logger, semantic = null, lastRunAtSeed = 0, baselineSeed = null, peakHours = "", peakMaxDeferMinutes = 120, auditPeakSkip = null, auditBaselineClamp = null, now = () => Date.now(), setTimeoutFn = setTimeout, clearTimeoutFn = clearTimeout }) {
|
|
972
|
+
let pendingTimer = null;
|
|
973
|
+
// Issue #239(第 4 项)镜像到巩固:高峰顺延定时器。与 pendingTimer 分开——两者
|
|
974
|
+
// 语义不同(一个是「马上要跑」,一个是「等出高峰再跑」),合成一个变量会让
|
|
975
|
+
// maybeSchedule 的守卫在顺延期间把新的写入触发误当成「已有待跑」而吞掉。
|
|
976
|
+
let deferTimer = null;
|
|
977
|
+
let running = false;
|
|
978
|
+
let disposed = false;
|
|
979
|
+
// Issue #89(基线半边):阈值基线跨重启持久化。baselineSeed 由调用方从
|
|
980
|
+
// dream_runs 读上次成功轮结束时的库规模(store.lastDreamBaseline)——它只活在
|
|
981
|
+
// 闭包内存里时进程重启即归零,而库里记忆远多于阈值,重启后第一条写入就满足
|
|
982
|
+
// overBase、绕过阈值开跑整轮(#291 只补了 lastRunAt 那半边)。缺种子(新库、
|
|
983
|
+
// 升级后还没跑过成功轮)或种子非法时退回 {0,0}:不假装知道基线,行为与升级前
|
|
984
|
+
// 逐字节一致——修的是「知道却不认」,不是凭空造一个基线。
|
|
985
|
+
let baseline = toBaseline(baselineSeed) ?? { count: 0, chars: 0 };
|
|
986
|
+
let inFlight = null;
|
|
987
|
+
// Issue #89(请求 2):上一次实际开跑时刻。失败/degraded 的 run 也占用
|
|
988
|
+
// 最小间隔——节流的目的正是防止失败调用连发;间隔内的触发静默跳过。
|
|
989
|
+
// lastRunAtSeed:调用方从 dream_runs 审计表读出的上次开跑时刻——
|
|
990
|
+
// 内存变量进程重启即归零,闸门对新实例放行 → 重启后立刻连发(#89 根因)。
|
|
991
|
+
let lastRunAt = lastRunAtSeed;
|
|
992
|
+
// Issue #292(#135 派生):同会话内连续失败计数。失败(onRun 抛错或返回
|
|
993
|
+
// ok:false)+1,成功清零;无返回结果的 run(no-op 桩)视为完成且无失败,
|
|
994
|
+
// 不动计数。纯内存变量、宿主重启归零——跨重启的冷却由 lastRunAtSeed(#291)
|
|
995
|
+
// 持久化负责,两者不重叠。
|
|
996
|
+
let consecutiveFailures = 0;
|
|
997
|
+
|
|
998
|
+
// Issue #292:有效最小间隔 = 基数 × 2^连续失败数,封顶 30 分钟。退避关闭或
|
|
999
|
+
// 尚无失败时逐字节返回基数(默认关 = 行为与现状一致)。基数 0 无闸可翻倍
|
|
1000
|
+
// (本键不自己产生间隔);封顶取 max(基数, cap),指数再大也不会把用户配的
|
|
1001
|
+
// 大基数压小。2^N 溢出成 Infinity 由 Math.min 兜到 cap,无需另设上限位数。
|
|
1002
|
+
function effectiveMinIntervalMs() {
|
|
1003
|
+
if (!failureBackoff || consecutiveFailures <= 0) return minIntervalMs;
|
|
1004
|
+
const cap = Math.max(minIntervalMs, FAILURE_BACKOFF_CAP_MS);
|
|
1005
|
+
return Math.min(minIntervalMs * 2 ** consecutiveFailures, cap);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function shouldTrigger(service) {
|
|
1009
|
+
const { count, chars } = activeStoreSize(service);
|
|
1010
|
+
// Issue #634(基线夹回):基线只在成功轮前进(见下方 startRun 与 #89 注释),
|
|
1011
|
+
// 而归档/删除/合并把活跃量压下去——活跃量一旦跌破基线,overBase 恒为假,
|
|
1012
|
+
// 从此再也不会巩固,且全程静默(#634 实测停摆 13 天 / 22 小时零轮)。修法:
|
|
1013
|
+
// 判定前把基线夹回当前活跃量(每轴取 min),阈值从新的低点重新起算,归掉的
|
|
1014
|
+
// 量不必重新长回来。只动闭包内存、不落库:下一次成功轮本来就会把当时的规模
|
|
1015
|
+
// 写成新基线(startRun 的 result.baseline / 现场计算),重启后种子若仍是旧峰值,
|
|
1016
|
+
// 第一次判定在这里同样被夹回——所以不需要新的持久化形状。收缩的来源不止归档
|
|
1017
|
+
// (删除、合并、暂停作用域、外来行占比变化同效果),且归档路径不触发
|
|
1018
|
+
// notifyWrite(service.notifyWrite:状态开关不算内容写入),逐写路径维护基线
|
|
1019
|
+
// 碰的面大、漏面也大——只在判定口一处收敛。夹回是罕见事件(每次「收缩→再触发」
|
|
1020
|
+
// 周期至多一次:夹回后基线 == 活跃量,直到下一轮成功把基线抬高),审计照
|
|
1021
|
+
// auditPeakSkip 的 fail-safe 口径走,记的是「这次收缩把阈值起点拉下来了」。
|
|
1022
|
+
if (count < baseline.count || chars < baseline.chars) {
|
|
1023
|
+
const previous = baseline;
|
|
1024
|
+
baseline = { count: Math.min(baseline.count, count), chars: Math.min(baseline.chars, chars) };
|
|
1025
|
+
try {
|
|
1026
|
+
auditBaselineClamp?.({ previous, count: baseline.count, chars: baseline.chars });
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
logger?.warn?.(`dsh-mneme dream: baseline-clamp audit failed: ${String(error)}`);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
const overBase = count >= baseline.count + thresholdCount || chars >= baseline.chars + thresholdChars;
|
|
1032
|
+
const overAbs = count >= thresholdCount || chars >= thresholdChars;
|
|
1033
|
+
return { trigger: overAbs && overBase, count, chars };
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function maybeSchedule(service) {
|
|
1037
|
+
if (disposed || running || pendingTimer || deferTimer) return false;
|
|
1038
|
+
// Issue #89(请求 2):最小触发间隔闸门。#292 退避开启时改用指数放大的
|
|
1039
|
+
// 有效间隔:连续失败越多,下次放行越晚(恒定失败的模型不再按固定节奏连发,
|
|
1040
|
+
// 只会越打越稀)。失败 run 本就占用间隔(lastRunAt 在开跑时刷新,见 #89),
|
|
1041
|
+
// 这里放大的是同一道闸,不新增任何状态面。间隔内的触发静默跳过:没有调用
|
|
1042
|
+
// 发生,也就没有可审计的对象(与 #89 口径一致,不写审计行)。
|
|
1043
|
+
if (minIntervalMs > 0 && now() - lastRunAt < effectiveMinIntervalMs()) return false;
|
|
1044
|
+
const { trigger, count, chars } = shouldTrigger(service);
|
|
1045
|
+
if (!trigger) return false;
|
|
1046
|
+
// Issue #239(第 4 项,错峰队列)镜像到巩固:命中高峰就不调模型。与蒸馏的差别
|
|
1047
|
+
// 在于巩固是**全局单实例**(蒸馏按会话各挂一个定时器),所以这里只需要一个
|
|
1048
|
+
// deferTimer,且不需要 deferredRuns 那套按会话去重。
|
|
1049
|
+
// baseline 刻意不刷新:阈值继续累积,留到非高峰一次性巩固(一次大 run 比多次
|
|
1050
|
+
// 小 run 省)。审计只登记一行 skip——「为什么不再做梦了」必须对用户可观测。
|
|
1051
|
+
if (isInPeakWindow(new Date(now()), peakHours)) {
|
|
1052
|
+
try {
|
|
1053
|
+
auditPeakSkip?.({ count, chars });
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
// 记账是 best-effort:写审计行失败只 warn,绝不反噬调度本身。
|
|
1056
|
+
logger?.warn?.(`dsh-mneme dream: peak-hours audit failed: ${String(error)}`);
|
|
1057
|
+
}
|
|
1058
|
+
scheduleDeferredRun(service);
|
|
1059
|
+
return false;
|
|
1060
|
+
}
|
|
1061
|
+
pendingTimer = setTimeoutFn(() => {
|
|
1062
|
+
pendingTimer = null;
|
|
1063
|
+
startRun(service);
|
|
1064
|
+
}, delayMs);
|
|
1065
|
+
return true;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Issue #239:高峰内择时补跑——挂到「距当前最近的一个高峰结束时刻」,被
|
|
1070
|
+
* peakMaxDeferMinutes 截断时到点照跑(bypassPeak),长高峰不会把巩固饿死。
|
|
1071
|
+
* 定时器 unref:不阻止宿主退出。重复触发不叠加(deferTimer 已在 maybeSchedule
|
|
1072
|
+
* 的守卫里,这里再判一次以防从其它路径进来)。
|
|
1073
|
+
*/
|
|
1074
|
+
function scheduleDeferredRun(service) {
|
|
1075
|
+
if (disposed || deferTimer) return;
|
|
1076
|
+
const at = nextOffPeakAt(new Date(now()), peakHours);
|
|
1077
|
+
if (!at) return;
|
|
1078
|
+
const maxDeferMs = (peakMaxDeferMinutes ?? 0) * 60000;
|
|
1079
|
+
let delay = Math.max(0, at.getTime() - now());
|
|
1080
|
+
const capped = maxDeferMs > 0 && delay > maxDeferMs;
|
|
1081
|
+
if (capped) delay = maxDeferMs;
|
|
1082
|
+
deferTimer = setTimeoutFn(() => {
|
|
1083
|
+
deferTimer = null;
|
|
1084
|
+
if (disposed) return;
|
|
1085
|
+
// 截断放行时仍在高峰:不再重新顺延(否则长高峰里会无限顺延,等于把巩固
|
|
1086
|
+
// 关掉)。直接开跑,与蒸馏的 bypassPeak 同口径。
|
|
1087
|
+
if (!capped && isInPeakWindow(new Date(now()), peakHours)) {
|
|
1088
|
+
// 理论上到点已出高峰;时钟跳变/时段串被改小可能落回高峰内,此时再顺延一次。
|
|
1089
|
+
scheduleDeferredRun(service);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
logger?.info?.(`dsh-mneme dream: peak-hours deferred run firing (capped=${capped}, delayMs=${delay})`);
|
|
1093
|
+
startRun(service);
|
|
1094
|
+
}, delay);
|
|
1095
|
+
deferTimer.unref?.();
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* 真正开跑。抽出来是因为两条路径都要用:写入触发的正常路径,与高峰顺延后的
|
|
1100
|
+
* 补跑路径。onRun 的调用刻意放在 Promise 里——同步抛出的异常若逃出 timer 回调
|
|
1101
|
+
* 会直接崩掉进程并跳过收尾。inFlight 让 dispose() 能等完这一轮再关库。
|
|
1102
|
+
*/
|
|
1103
|
+
function startRun(service) {
|
|
1104
|
+
running = true;
|
|
1105
|
+
lastRunAt = now();
|
|
1106
|
+
inFlight = Promise.resolve()
|
|
1107
|
+
.then(() => (onRun ? onRun() : Promise.resolve({ ok: true, skipped: true })))
|
|
1108
|
+
.then((result) => {
|
|
1109
|
+
// Refresh the baseline only for a successful run (design §5.3: an
|
|
1110
|
+
// LLM failure must not move the baseline, so the next write can
|
|
1111
|
+
// immediately re-trigger a retry). A `{ok:false}` result or a throw
|
|
1112
|
+
// keeps the old baseline. A run that reports nothing is treated as
|
|
1113
|
+
// completed without failure (no-op hooks / minimal test doubles).
|
|
1114
|
+
if (result && result.ok) {
|
|
1115
|
+
consecutiveFailures = 0; // Issue #292:成功清零,下次触发回到基数间隔
|
|
1116
|
+
try {
|
|
1117
|
+
// Issue #89(基线半边):run 自报的基线(与审计行同一份数字)优先——重启
|
|
1118
|
+
// 播种读的就是它,两条路径共用唯一口径;no-op 桩不回这个字段时退回现场
|
|
1119
|
+
// 计算,最小测试替身的行为逐字节不变。
|
|
1120
|
+
baseline = toBaseline(result.baseline) ?? shouldTrigger(service);
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
// Store closed mid-flight: keep the last known baseline.
|
|
1123
|
+
logger?.warn?.(`dsh-mneme dream: baseline refresh failed: ${String(error)}`);
|
|
1124
|
+
}
|
|
1125
|
+
} else if (result) {
|
|
1126
|
+
// Issue #292:ok:false(LLM 失败 / 空体 / 整单拒绝)计入连败;degraded
|
|
1127
|
+
// 的 run 走 ok:true(LLM 本身成功了,只是决策被部分应用)→ 算成功、清零。
|
|
1128
|
+
// 退避关闭时该计数没有消费者,行为与此前逐字节一致。
|
|
1129
|
+
consecutiveFailures += 1;
|
|
1130
|
+
}
|
|
1131
|
+
// result 为空(no-op 桩 / 最小测试替身)=完成且无失败:基线与连败计数都不动。
|
|
1132
|
+
})
|
|
1133
|
+
.catch((error) => {
|
|
1134
|
+
logger?.warn?.(`dsh-mneme dream: run failed: ${error?.message ?? error}`);
|
|
1135
|
+
// Failed runs do not refresh the baseline.
|
|
1136
|
+
consecutiveFailures += 1; // Issue #292:抛错同样计入连败
|
|
1137
|
+
})
|
|
1138
|
+
.finally(() => {
|
|
1139
|
+
running = false;
|
|
1140
|
+
inFlight = null;
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
async function dispose() {
|
|
1145
|
+
disposed = true;
|
|
1146
|
+
if (pendingTimer) { clearTimeoutFn(pendingTimer); pendingTimer = null; }
|
|
1147
|
+
// Issue #239:高峰顺延定时器同样要清,否则进程关闭后仍会触发一次巩固。
|
|
1148
|
+
if (deferTimer) { clearTimeoutFn(deferTimer); deferTimer = null; }
|
|
1149
|
+
// An in-flight run is left to complete naturally (its LLM calls are
|
|
1150
|
+
// already paid for and aborting would discard the work). Await it so the
|
|
1151
|
+
// caller can close the store only after every write has landed.
|
|
1152
|
+
if (inFlight) await inFlight.catch(() => {});
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
async function runDream(ctx, service, config) {
|
|
1156
|
+
const language = langOf(config);
|
|
1157
|
+
const logger = ctx.logger;
|
|
1158
|
+
let memories = withoutPaused(service, service.all().filter((m) => !isImportedRow(m) && !m.archived && m.type !== "summary" && m.type !== "document"));
|
|
1159
|
+
if (memories.length === 0) return { ok: true, applied: 0, skipped: true, summary: false };
|
|
1160
|
+
// Issue #89:开跑时刻既喂给审计行(created_at),也是调度器闸门的时间基准。
|
|
1161
|
+
const dreamStartedAt = Date.now();
|
|
1162
|
+
// v0.4.4 滑动窗口:只 consolidation 最近 dreamMaxSnapshotSize 条记忆,
|
|
1163
|
+
// 窗口外的旧记忆不进 snapshot(大记忆量下全量快照会撑爆 LLM 输入,配合
|
|
1164
|
+
// 隐式 keep 让 run 始终可收敛)。按 updated_at 倒序取前 maxSize 条。
|
|
1165
|
+
const maxSize = Number.isInteger(config.dreamMaxSnapshotSize) ? config.dreamMaxSnapshotSize : 200;
|
|
1166
|
+
const windowMemories = [...memories]
|
|
1167
|
+
.sort((a, b) => {
|
|
1168
|
+
const ta = String(a.updated_at ?? "");
|
|
1169
|
+
const tb = String(b.updated_at ?? "");
|
|
1170
|
+
if (ta < tb) return 1;
|
|
1171
|
+
if (ta > tb) return -1;
|
|
1172
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
1173
|
+
})
|
|
1174
|
+
.slice(0, Math.max(1, maxSize));
|
|
1175
|
+
// Issue #125:hybrid 档在窗口之外并入向量翻出的高相似组。候选**总量**仍由
|
|
1176
|
+
// maxSize(或显式 dreamCandidateMax)封顶、与库总量解耦——用"按相似度取候选"
|
|
1177
|
+
// 替代"调大窗口",输入成本才不随库增长。封顶截断时向量组优先于窗口尾部,
|
|
1178
|
+
// 否则"该合并的一对"仍会被时间窗挤掉。
|
|
1179
|
+
let selected = windowMemories;
|
|
1180
|
+
if ((config.dreamCandidateMode ?? "window") === "hybrid" && semantic?.embedder && semantic?.vectorIndex) {
|
|
1181
|
+
const extra = await hybridCandidateMembers(memories, config, semantic, logger);
|
|
1182
|
+
if (extra.length > 0) {
|
|
1183
|
+
const cap = Math.max(
|
|
1184
|
+
Math.max(1, maxSize),
|
|
1185
|
+
Number.isInteger(config.dreamCandidateMax) && config.dreamCandidateMax > 0 ? config.dreamCandidateMax : 0
|
|
1186
|
+
);
|
|
1187
|
+
const merged = [];
|
|
1188
|
+
const seen = new Set();
|
|
1189
|
+
for (const m of [...extra, ...windowMemories]) {
|
|
1190
|
+
if (seen.has(m.id)) continue;
|
|
1191
|
+
seen.add(m.id);
|
|
1192
|
+
merged.push(m);
|
|
1193
|
+
if (merged.length >= cap) break;
|
|
1194
|
+
}
|
|
1195
|
+
selected = merged;
|
|
1196
|
+
logger?.info?.(`[dsh-mneme] dream hybrid candidates: ${merged.length} (window ${windowMemories.length}, vector ${extra.length}, cap ${cap})`);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
memories = selected;
|
|
1200
|
+
const snapshot = new Map(memories.map((m) => [m.id, m]));
|
|
1201
|
+
const route = resolveRoute(ctx, config, logger);
|
|
1202
|
+
const runId = randomUUID();
|
|
1203
|
+
const snapshotHash = hashSnapshot([...snapshot.values()]);
|
|
1204
|
+
// Conflict freeze (opt-in): when enabled, conflict decisions are parked for
|
|
1205
|
+
// manual review instead of auto-adjudicated. Read once up front so the
|
|
1206
|
+
// prompt hint and the apply-split agree on the same gate.
|
|
1207
|
+
const freezeEnabled = config.conflictFreezeEnabled === true;
|
|
1208
|
+
// reflectionUpdateEnabled(默认 true,「关」= 旧记忆永不被自动改写)。同样先读
|
|
1209
|
+
// 一次,prompt 后缀与校验器选项用同一个值,避免两处各读一次 cfg 后彼此不一致。
|
|
1210
|
+
const updateEnabled = config.reflectionUpdateEnabled !== false;
|
|
1211
|
+
// Every exit (success or failure) funnels through `finish`, which writes
|
|
1212
|
+
// the audit row + receipt. A record failure is logged, never thrown —
|
|
1213
|
+
// auditing must not break the consolidation path. Failed runs still
|
|
1214
|
+
// capture their decisions/outcome when the LLM produced a validated list
|
|
1215
|
+
// (e.g. summary step failed after consolidation), so the partial write is
|
|
1216
|
+
// replayable too.
|
|
1217
|
+
const finish = (result) => {
|
|
1218
|
+
// status is derived from what actually committed: ok only when the full
|
|
1219
|
+
// decision list landed (or a summary was refreshed); noop when nothing
|
|
1220
|
+
// changed; degraded when real changes landed without a summary;
|
|
1221
|
+
// reconcile when decisions were validated but some did not commit (CAS
|
|
1222
|
+
// conflict / rollback); failed on any LLM/validation error. No fake "ok"
|
|
1223
|
+
// for an empty or partial run.
|
|
1224
|
+
const status = result.status ?? (result.ok ? "ok" : "failed");
|
|
1225
|
+
const applied = result.applied ?? 0;
|
|
1226
|
+
const summaryStored = result.summary ?? false;
|
|
1227
|
+
const receipt = buildReceipt({ runId, status, snapshotHash, inputCount: snapshot.size, applied, summaryStored });
|
|
1228
|
+
// Issue #89(基线半边):成功轮把「本轮结束后的库规模」落进审计行 = 阈值基线的
|
|
1229
|
+
// 持久化副本,重启后由 store.lastDreamBaseline 播种。判定条件必须与调度器刷新
|
|
1230
|
+
// 基线的条件严格一致(result.ok)——非 ok 轮不推进基线是既有语义,若这里也落值,
|
|
1231
|
+
// 重启后播种就会拿到一个「其实没推进过」的假基线,阈值反而被抬高。
|
|
1232
|
+
// 取不到(库已关闭等)就落 NULL:宁可不持久化,也不写一个错的基线。
|
|
1233
|
+
let baselineSnapshot = null;
|
|
1234
|
+
if (result.ok) {
|
|
1235
|
+
try {
|
|
1236
|
+
baselineSnapshot = activeStoreSize(service);
|
|
1237
|
+
} catch {
|
|
1238
|
+
baselineSnapshot = null;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
try {
|
|
1242
|
+
service.saveDreamRun({
|
|
1243
|
+
id: runId,
|
|
1244
|
+
// Issue #89:审计行记开跑时刻而非完成时刻——lastDreamRunAt 以它做
|
|
1245
|
+
// 重启后的间隔种子,落完成时刻会让 run 耗时白白计入下一轮最小间隔。
|
|
1246
|
+
created_at: new Date(dreamStartedAt).toISOString(),
|
|
1247
|
+
status,
|
|
1248
|
+
error: result.error,
|
|
1249
|
+
provider: route?.provider,
|
|
1250
|
+
model: route?.model,
|
|
1251
|
+
snapshot_hash: snapshotHash,
|
|
1252
|
+
input_count: snapshot.size,
|
|
1253
|
+
// 裁决规则版本号:config.policyEpoch(默认 0)。规则升级后该行保留
|
|
1254
|
+
// 当时的 epoch,旧裁决据此降级为历史证据(store 层 getLatestPolicyEpoch
|
|
1255
|
+
// 只负责读取当前生效版本,写入由这里完成)。
|
|
1256
|
+
policy_epoch: config.policyEpoch ?? 0,
|
|
1257
|
+
// Full input snapshot (canonical fields) so the exact arbitration
|
|
1258
|
+
// input can be rebuilt offline from the audit row alone — the
|
|
1259
|
+
// digest + decisions + outcome triple makes silent errors locatable
|
|
1260
|
+
// even after the store has moved on.
|
|
1261
|
+
// #557(dreamInputSummaryEnabled,opt-in 默认关):瘦身只动这里的落库
|
|
1262
|
+
// 副本——LLM 输入在下方由内存快照现算、snapshot_hash 在内存里对含
|
|
1263
|
+
// content 的快照计算,两者都不经过这行,prompt 与 receipt 完整性不受
|
|
1264
|
+
// 影响。organize 轮的重放载荷走 organize.js 自己的 saveDreamRun,
|
|
1265
|
+
// 不经过这里,与 reclaim 的 run_type != 'organize' 同一条边界。
|
|
1266
|
+
input: [...snapshot.values()].map((m) => (
|
|
1267
|
+
config.dreamInputSummaryEnabled === true
|
|
1268
|
+
? { id: m.id, type: m.type, title: m.title, importance: m.importance, updated_at: m.updated_at }
|
|
1269
|
+
: { id: m.id, type: m.type, title: m.title, content: m.content, importance: m.importance, updated_at: m.updated_at }
|
|
1270
|
+
)),
|
|
1271
|
+
decisions: result.decisions,
|
|
1272
|
+
outcome: result.outcome,
|
|
1273
|
+
applied,
|
|
1274
|
+
summary_stored: summaryStored,
|
|
1275
|
+
receipt,
|
|
1276
|
+
// Issue #104:degraded(合法子集已应用)轮被跳过的决策明细。写成独立字段
|
|
1277
|
+
// 而非内联进 decisions——degraded 的 decisions 是合法子集,下游按「决策
|
|
1278
|
+
// 数组」消费,内联标记会污染其它读者(失败轮的 _validationFailed 是整单
|
|
1279
|
+
// 拒绝哨兵,语义与「部分应用」不同,也不复用)。
|
|
1280
|
+
skipped: result.skipped,
|
|
1281
|
+
// #534 建议 2:拿到文本却解析/校验失败的轮,把模型原文的头尾留在行上。
|
|
1282
|
+
// 单独一列而不是内联进 decisions——decisions 的语义是「可重放决策数组」
|
|
1283
|
+
// (applyDecisions 直接吃它),塞非决策对象会污染重放者与所有按决策数组
|
|
1284
|
+
// 消费的下游;失败轮哨兵 _validationFailed 也只表达「整单拒绝」这一件事。
|
|
1285
|
+
// 流错误(result===undefined)没有文本可留,落 NULL。
|
|
1286
|
+
raw_output: result.raw_output,
|
|
1287
|
+
// Issue #89(基线半边):成功的 run 才带这两个值(见上方 baselineSnapshot
|
|
1288
|
+
// 的注释)。两列要么同时有值、要么同时为 NULL,读侧以「同时有值」认账。
|
|
1289
|
+
store_count: baselineSnapshot?.count,
|
|
1290
|
+
store_chars: baselineSnapshot?.chars
|
|
1291
|
+
});
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
logger?.warn?.(`dsh-mneme dream: failed to record audit run: ${String(error)}`);
|
|
1294
|
+
}
|
|
1295
|
+
// Issue #89(基线半边):把「与审计行同一份数字」的基线回给调度器,两条路径
|
|
1296
|
+
// (进程内刷新 / 跨重启播种)因此共享唯一口径;调度器拿不到时会退出现场计算。
|
|
1297
|
+
return { ...result, runId, receipt, snapshotHash, baseline: baselineSnapshot ?? undefined };
|
|
1298
|
+
};
|
|
1299
|
+
if (!route) {
|
|
1300
|
+
logger?.warn?.("dsh-mneme dream: no llm route available");
|
|
1301
|
+
return finish({ ok: false, error: "no llm route", summary: false });
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
let listText;
|
|
1305
|
+
// Issue #603 第 2 层:把预分组阶段算出的向量相似度留下来,供停车入队时落列。
|
|
1306
|
+
// 这是**本进程内唯一**真实的相似度来源——LLM 裁决给出的只有理由文本,从文本
|
|
1307
|
+
// 里回收数字是猜测。键与 conflict_pending 的对序归一化一致(sort 后的 id 对)。
|
|
1308
|
+
// 拿不到(无向量、跨进程重放、模型自由裁决)就写 NULL,排序按「未知」处理。
|
|
1309
|
+
const pairSimilarity = new Map();
|
|
1310
|
+
const similarityKey = (x, y) => (x < y ? `${x}\u0000${y}` : `${y}\u0000${x}`);
|
|
1311
|
+
if (semantic?.embedder && semantic?.vectorIndex) {
|
|
1312
|
+
try {
|
|
1313
|
+
const vectors = await collectVectors(memories, semantic);
|
|
1314
|
+
if (vectors) {
|
|
1315
|
+
const k = Math.min(10, Math.max(1, Math.floor(Math.sqrt(memories.length / 2))));
|
|
1316
|
+
const clusters = clusterMemories(memories, vectors, k);
|
|
1317
|
+
const conflicts = findPotentialConflicts(memories, vectors, 0.85);
|
|
1318
|
+
const conflictIds = new Set(conflicts.flatMap((c) => [c.a.id, c.b.id]));
|
|
1319
|
+
for (const c of conflicts) pairSimilarity.set(similarityKey(c.a.id, c.b.id), c.similarity);
|
|
1320
|
+
const parts = [];
|
|
1321
|
+
clusters.forEach((cluster, ci) => {
|
|
1322
|
+
parts.push(STR.clusterHeader[language](ci + 1));
|
|
1323
|
+
for (const m of cluster) {
|
|
1324
|
+
parts.push(
|
|
1325
|
+
`id=${m.id} | type=${m.type} | importance=${m.importance} | updated=${m.updated_at} | title=${m.title} | content=${m.content}` +
|
|
1326
|
+
(conflictIds.has(m.id) ? STR.conflictMark[language] : "")
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
});
|
|
1330
|
+
listText = parts.join("\n");
|
|
1331
|
+
logger?.info?.(`[dsh-mneme] dream semantic pre-group: ${clusters.length} clusters, ${conflicts.length} conflict pairs`);
|
|
1332
|
+
}
|
|
1333
|
+
} catch (error) {
|
|
1334
|
+
logger?.warn?.(`[dsh-mneme] dream semantic pre-group failed: ${String(error)}`);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
if (!listText) {
|
|
1338
|
+
listText = [...snapshot.values()].map((m) =>
|
|
1339
|
+
`id=${m.id} | type=${m.type} | importance=${m.importance} | updated=${m.updated_at} | title=${m.title} | content=${m.content}`
|
|
1340
|
+
).join("\n");
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// Freeze-aware prompt: in freeze mode the conflict branch still outputs
|
|
1344
|
+
// winner/loser (validation requires them) but they are treated as tentative
|
|
1345
|
+
// candidates — the human makes the final call, not the model.
|
|
1346
|
+
const consolidationPrompt = freezeEnabled
|
|
1347
|
+
? STR.prompts.consolidation[language] + STR.prompts.freezeSuffix[language]
|
|
1348
|
+
: STR.prompts.consolidation[language];
|
|
1349
|
+
// 两个后缀可以叠加:冻结管冲突怎么处理,关闭改写管 update 能不能用,互不相干。
|
|
1350
|
+
const promptWithUpdateGate = updateEnabled
|
|
1351
|
+
? consolidationPrompt
|
|
1352
|
+
: consolidationPrompt + STR.prompts.noUpdateSuffix[language];
|
|
1353
|
+
let decisionText;
|
|
1354
|
+
// 加固(v0.8.1):配置的 reasoningEffort 被 provider 拒收时回退重试一次
|
|
1355
|
+
// (不带该字段),避免 thinking 模型配置 low/medium 直接整单失败。解析放
|
|
1356
|
+
// 在 auditError 检查器里、闭包交回主流程,避免二次解析;解析失败同时如实
|
|
1357
|
+
// 记 audit error 并在日志带原始输出前 300 字节,便于定位"推理吞预算返回空体"。
|
|
1358
|
+
const effort = await resolveDreamEffort(ctx, route, config.dreamReasoningEffort, logger);
|
|
1359
|
+
let decisions = null;
|
|
1360
|
+
let streamFailure = "";
|
|
1361
|
+
// #534 建议 3:这一轮巩固的审计行 id(runAuditedLlm 落库后回填)。校验判决
|
|
1362
|
+
// 出来时用它把 result 补回调用层那一行,两套账才对得上。每次重试重置,只认
|
|
1363
|
+
// 最终生效的那一次调用。
|
|
1364
|
+
let auditRef = {};
|
|
1365
|
+
const runConsolidation = (withEffort, tokens) => {
|
|
1366
|
+
streamFailure = "";
|
|
1367
|
+
auditRef = {};
|
|
1368
|
+
return runAuditedLlm(ctx, service, config, {
|
|
1369
|
+
triggerSource: "autoDream",
|
|
1370
|
+
operationType: "dream_consolidate",
|
|
1371
|
+
modelId: `${route.provider}:${route.model}`,
|
|
1372
|
+
relatedMemoryIds: [...snapshot.keys()],
|
|
1373
|
+
streamError: () => streamFailure,
|
|
1374
|
+
auditRef,
|
|
1375
|
+
auditError: (text) => {
|
|
1376
|
+
decisions = extractJsonArray(text);
|
|
1377
|
+
return Array.isArray(decisions) ? null : "no json array in llm output";
|
|
1378
|
+
},
|
|
1379
|
+
metadataFrom: (text) => (Array.isArray(decisions) ? null : parseFailureMeta(text))
|
|
1380
|
+
}, (reportUsage) => streamText(ctx, {
|
|
1381
|
+
provider: route.provider,
|
|
1382
|
+
model: route.model,
|
|
1383
|
+
purpose: "compaction",
|
|
1384
|
+
maxTokens: tokens,
|
|
1385
|
+
...(withEffort && effort ? { reasoningEffort: effort } : {}),
|
|
1386
|
+
messages: [
|
|
1387
|
+
{ role: "system", content: [{ type: "text", text: promptWithUpdateGate }], source: { kind: "plugin:dsh-mneme", plugin: "dsh-mneme" } },
|
|
1388
|
+
{ role: "user", content: [{ type: "text", text: fenceListBody(language, listText) }], source: { kind: "plugin:dsh-mneme", plugin: "dsh-mneme" } }
|
|
1389
|
+
]
|
|
1390
|
+
}, reportUsage, (reason) => { streamFailure = describeStreamFailure(reason); }));
|
|
1391
|
+
};
|
|
1392
|
+
try {
|
|
1393
|
+
// Bug8: the consolidation call is audited (tokens/time/status). A throw
|
|
1394
|
+
// re-propagates to the catch below; an aborted stream returns undefined
|
|
1395
|
+
// and is treated as a failed run after the check below.
|
|
1396
|
+
decisionText = await withMaxTokensFallback(ctx, config.dreamMaxTokens ?? 4096, (tokens) => withEffortFallback(ctx, effort, () => runConsolidation(true, tokens), () => runConsolidation(false, tokens), () => streamFailure), () => streamFailure);
|
|
1397
|
+
} catch (error) {
|
|
1398
|
+
logger?.warn?.(`dsh-mneme dream: consolidation llm call failed: ${String(error)}`);
|
|
1399
|
+
return finish({ ok: false, error: "llm failed", summary: false });
|
|
1400
|
+
}
|
|
1401
|
+
if (decisionText === undefined) {
|
|
1402
|
+
logger?.warn?.(`dsh-mneme dream: consolidation llm stream aborted or errored${streamFailure ? ` (${streamFailure})` : ""}`);
|
|
1403
|
+
return finish({ ok: false, error: "llm failed", summary: false });
|
|
1404
|
+
}
|
|
1405
|
+
// #534 建议 3 的接线点:审计行已由 runAuditedLlm 落库(status=error 也只说明
|
|
1406
|
+
// 「调用层这轮不算成功」),把校验层的判决补回去,两张表才说同一件事。
|
|
1407
|
+
const annotateAudit = (patch) => {
|
|
1408
|
+
if (!auditRef.id || typeof service?.annotateLlmAudit !== "function") return;
|
|
1409
|
+
try {
|
|
1410
|
+
service.annotateLlmAudit(auditRef.id, patch);
|
|
1411
|
+
} catch (error) {
|
|
1412
|
+
logger?.warn?.(`dsh-mneme dream: failed to annotate llm audit row: ${String(error)}`);
|
|
1413
|
+
}
|
|
1414
|
+
};
|
|
1415
|
+
if (!Array.isArray(decisions)) {
|
|
1416
|
+
const head = (decisionText ?? "").slice(0, 300).replace(/\s+/g, " ").trim();
|
|
1417
|
+
logger?.warn?.(`dsh-mneme dream: no json array in llm output (raw length ${decisionText?.length ?? 0}; head: ${head})`);
|
|
1418
|
+
annotateAudit(decisionValidationMeta({ verdict: "unparsed", errors: [], skipped: [], survivorCount: 0 }));
|
|
1419
|
+
return finish({ ok: false, error: "no json array in llm output", summary: false, raw_output: rawOutputPayload(decisionText) });
|
|
1420
|
+
}
|
|
1421
|
+
const { ok, errors, skipped, resolvedShortIds, coverageShortfall } = validateDecisions(decisions, snapshot, {
|
|
1422
|
+
maxUpdatePerRun: config.reflectionUpdateMaxPerRun,
|
|
1423
|
+
// reflectionUpdateEnabled=false 时校验器单条跳过所有 update(不是整单拒绝)。
|
|
1424
|
+
allowUpdate: updateEnabled,
|
|
1425
|
+
// Issue #104 方向 2:archive 批量上限(全局闸门,超限整单拒绝)。
|
|
1426
|
+
maxArchivePerRun: config.dreamMaxArchivePerRun,
|
|
1427
|
+
// #135 成本审计派生:merge 批量上限(merge 是唯一回吐正文的动作,同款全局闸门)。
|
|
1428
|
+
maxMergePerRun: config.dreamMaxMergePerRun,
|
|
1429
|
+
minAgeHours: config.reflectionUpdateMinAgeHours,
|
|
1430
|
+
// v0.4.4 fix:显式透传,用户配 dreamImplicitKeep:false 时严格模式必须
|
|
1431
|
+
// 真正生效,dreamMinExplicitCoverage 决定隐式 keep 下的覆盖率下限。
|
|
1432
|
+
dreamImplicitKeep: config.dreamImplicitKeep,
|
|
1433
|
+
dreamMinExplicitCoverage: config.dreamMinExplicitCoverage,
|
|
1434
|
+
// Issue #89:v0.6.9(Issue #26)的宽容路径在 v0.7.11 重写中丢失——单条
|
|
1435
|
+
// 非法决策重新只跳过该条、合法子集照常应用(run 记为 degraded)。
|
|
1436
|
+
skipInvalid: config.dreamSkipInvalid !== false,
|
|
1437
|
+
allowCrossTypeMerge: config.allowCrossTypeMerge === true,
|
|
1438
|
+
// issue #339 / E8:guarded 类型 merge 护栏(默认开,dreamMergeGuard)。
|
|
1439
|
+
mergeGuard: config.dreamMergeGuard === true
|
|
1440
|
+
});
|
|
1441
|
+
// Issue #135:模型把 UUID 缩写成前缀时,唯一前缀已在校验前被解析回完整 id。
|
|
1442
|
+
// 解析量是模型输出质量的一个直接信号(>0 意味着模型在缩写 id),记一条 info
|
|
1443
|
+
// 便于事后从日志侧观察该行为的分布。
|
|
1444
|
+
if (resolvedShortIds > 0) {
|
|
1445
|
+
logger?.info?.(`[dsh-mneme] dream: resolved ${resolvedShortIds} short id prefix(es) to full ids`);
|
|
1446
|
+
}
|
|
1447
|
+
if (!ok) {
|
|
1448
|
+
// Issue #135(观测缺口):失败轮的逐条校验明细此前既不落库也基本不可见——
|
|
1449
|
+
// skipped 的 warn 写在 return 之后(失败路径到不了),dream_runs.decisions
|
|
1450
|
+
// 落 NULL,上百轮失败零现场。这里把明细前置到日志,并把
|
|
1451
|
+
// { _validationFailed, errors, skipped } 随失败行持久化,事后可从审计行
|
|
1452
|
+
// 直接定位「模型输出了什么、为何逐条非法」。
|
|
1453
|
+
logger?.warn?.(`dsh-mneme dream: invalid decisions: ${errors.join("; ")}`);
|
|
1454
|
+
if (skipped.length > 0) {
|
|
1455
|
+
logger?.warn?.(`dsh-mneme dream: ${skipped.length} invalid decision(s) skipped: ${skipped.map((s) => `decision[${s.index}]: ${s.error}`).join("; ")}`);
|
|
1456
|
+
}
|
|
1457
|
+
annotateAudit(decisionValidationMeta({ verdict: "rejected", errors, skipped, survivorCount: 0 }));
|
|
1458
|
+
return finish({
|
|
1459
|
+
ok: false,
|
|
1460
|
+
error: `invalid decisions: ${errors.length} errors`,
|
|
1461
|
+
summary: false,
|
|
1462
|
+
decisions: [{ _validationFailed: true, errors, skipped }],
|
|
1463
|
+
// #534 建议 2:整轮被拒时最该留的就是模型原文——否则事后只能看到
|
|
1464
|
+
// 「N errors」这个结论,看不到它到底吐了什么。
|
|
1465
|
+
raw_output: rawOutputPayload(decisionText)
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
const skippedInvalid = skipped.length > 0;
|
|
1469
|
+
if (skippedInvalid) {
|
|
1470
|
+
logger?.warn?.(`dsh-mneme dream: ${skipped.length} invalid decision(s) skipped (run degrades): ${skipped.map((s) => s.error).join("; ")}`);
|
|
1471
|
+
}
|
|
1472
|
+
// Issue #104 方向 1:覆盖率不足降级为 degraded(不再整单拒绝)——合法子集
|
|
1473
|
+
// 照常应用、未提及条目隐式 keep,理由随 outcome.degradations 落库供离线定位。
|
|
1474
|
+
const coverageDegraded = typeof coverageShortfall === "string" && coverageShortfall.length > 0;
|
|
1475
|
+
if (coverageDegraded) {
|
|
1476
|
+
logger?.warn?.(`dsh-mneme dream: ${coverageShortfall} (run degrades: valid subset applied, unclaimed ids kept implicitly)`);
|
|
1477
|
+
}
|
|
1478
|
+
// 校验通过(可能带若干跳过、也可能只是覆盖率不足 → degraded)。判决补回审计行:
|
|
1479
|
+
// 这一步之前没有任何地方能回答「调用成功的那一轮,决策层到底认不认」。
|
|
1480
|
+
//
|
|
1481
|
+
// verdict 必须与下方状态判定同源(CodeRabbit on #535):coverageShortfall 同样是
|
|
1482
|
+
// 校验层的降级,若这里只认 skippedInvalid,覆盖率不足的轮会被 dream_runs 记成
|
|
1483
|
+
// degraded、审计 metadata 却写 ok——两套账在最该对齐的一格上又对不上,正是本
|
|
1484
|
+
// PR 要消灭的病。
|
|
1485
|
+
annotateAudit(decisionValidationMeta({
|
|
1486
|
+
verdict: skippedInvalid || coverageDegraded ? "degraded" : "ok",
|
|
1487
|
+
errors,
|
|
1488
|
+
skipped,
|
|
1489
|
+
// 校验器已就地 splice:这个长度就是下游 apply/audit 真正会消费的条目数
|
|
1490
|
+
// (含隐式补齐的 keep)。不能再减 skipped.length——被跳过的那些早被剪掉了,
|
|
1491
|
+
// 再减一次会把「跳 1 条、存活 1 条」算成 0(本分支自测当场抓到过)。
|
|
1492
|
+
survivorCount: decisions.length
|
|
1493
|
+
}));
|
|
1494
|
+
|
|
1495
|
+
// Capture pre-update snapshots so the audit records what each update changed.
|
|
1496
|
+
const updateSnapshots = {};
|
|
1497
|
+
for (const d of decisions) {
|
|
1498
|
+
if (d.action === "update") {
|
|
1499
|
+
const mem = snapshot.get(d.ids[0]);
|
|
1500
|
+
if (mem) updateSnapshots[d.ids[0]] = { title: mem.title, content: mem.content, importance: mem.importance };
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
// Conflict freeze (opt-in): when enabled, conflict decisions are not
|
|
1505
|
+
// auto-adjudicated — no winner kept, no loser archived. The pair is parked
|
|
1506
|
+
// in conflict_pending for human review instead. Best-effort: a store
|
|
1507
|
+
// failure here must never block the run (fail-safe — the memories are left
|
|
1508
|
+
// untouched and nothing is arbitrated). The cap (conflictFreezeMaxPending)
|
|
1509
|
+
// bounds the review queue; overflow is skipped with a warning.
|
|
1510
|
+
let frozenCount = 0;
|
|
1511
|
+
const frozenIds = [];
|
|
1512
|
+
// Issue #641(enforce 档):T1 supersede 的实质产出计数与向量清理清单。声明在
|
|
1513
|
+
// freeze/enforce 两个分支外层——noChange 判定与 maintainIndexAfterDream 都在
|
|
1514
|
+
// 它们之后、函数作用域里消费。
|
|
1515
|
+
let ledgerApplied = 0;
|
|
1516
|
+
let ledgerVectorFixups = [];
|
|
1517
|
+
// v0.8.1(issue #170 复核项 3):跨 scope 的 conflict 决策两种模式都一律
|
|
1518
|
+
// 停车人工裁决、绝不自动归档——否则普通 dream 的 LLM 能归档别的 scope 的
|
|
1519
|
+
// 唯一副本,sleep 路径(scope-candidate-queue)的保护在这里缺位。同 scope
|
|
1520
|
+
// 的 conflict 决策照旧:freeze 停车 / 非 freeze 自动裁决。
|
|
1521
|
+
const isCrossScopeDecision = (d) => {
|
|
1522
|
+
const a = snapshot.get(d.winner);
|
|
1523
|
+
const b = snapshot.get(d.loser);
|
|
1524
|
+
return !!a && !!b && (
|
|
1525
|
+
scopeKeyOf(a.agent_scope) !== scopeKeyOf(b.agent_scope)
|
|
1526
|
+
|| scopeKeyOf(a.workspace_scope) !== scopeKeyOf(b.workspace_scope)
|
|
1527
|
+
);
|
|
1528
|
+
};
|
|
1529
|
+
// Issue #641(beta 第二阶段,enforce 档):dream 主链路的 conflict 决策改为
|
|
1530
|
+
// 确定性处置。LLM 判出的 conflict 对先经 triagePair 再分诊:判为 dup 的走
|
|
1531
|
+
// supersedeInfoFirstPair(与 sleep 自动档同一条契约);其余(cross-scope-dup /
|
|
1532
|
+
// evolution / conflict)只落 conflict_ledger 账本(tier + 信号),不停车、
|
|
1533
|
+
// 不归档。#616 的父子绑定不继承:enforce 与 freeze 同开时 freeze 让位——
|
|
1534
|
+
// 本段降级 shadow(conflict 照旧停车),绝不两套处置语义同时生效。
|
|
1535
|
+
const ledgerEnforce = (config.conflictLedgerMode ?? "off") === "enforce" && !freezeEnabled;
|
|
1536
|
+
if ((config.conflictLedgerMode ?? "off") === "enforce" && freezeEnabled) {
|
|
1537
|
+
logger?.warn?.("dsh-mneme dream: conflictLedgerMode=enforce coexists with conflictFreezeEnabled — degrading to freeze; disable one of them");
|
|
1538
|
+
}
|
|
1539
|
+
if (ledgerEnforce) {
|
|
1540
|
+
let t1Resolved = 0;
|
|
1541
|
+
let t3Chained = 0;
|
|
1542
|
+
let marked = 0;
|
|
1543
|
+
for (const d of decisions.filter((x) => x.action === "conflict")) {
|
|
1544
|
+
const a = snapshot.get(d.winner);
|
|
1545
|
+
const b = snapshot.get(d.loser);
|
|
1546
|
+
if (!a || !b) continue;
|
|
1547
|
+
try {
|
|
1548
|
+
// 判据必须与 sleep 侧同源(#650 评审):sleep enforce 传的是
|
|
1549
|
+
// config.conflictLedgerDupMinSimilarity,这里不传就会退回 triage.js 的
|
|
1550
|
+
// 死默认 0.9 —— 用户把阈值调高/调低后,同一对在两条路径上可能分到不同
|
|
1551
|
+
// tier(一侧判 dup 去归档,另一侧判 conflict 只记账)。
|
|
1552
|
+
const verdict = triagePair(a, b, pairSimilarity.get(similarityKey(d.winner, d.loser)), {
|
|
1553
|
+
dupMinSimilarity: config.conflictLedgerDupMinSimilarity
|
|
1554
|
+
});
|
|
1555
|
+
if (verdict.tier === "dup") {
|
|
1556
|
+
const res = supersedeInfoFirstPair({ a, b }, service, logger, config);
|
|
1557
|
+
if (res.applied) {
|
|
1558
|
+
t1Resolved += 1;
|
|
1559
|
+
ledgerApplied += 1;
|
|
1560
|
+
// 败者向量清理走下面的 maintainIndexAfterDream:T1 的 supersede 是
|
|
1561
|
+
// 本 helper 独立落地的,不在 applyList 里,必须显式补进维护清单
|
|
1562
|
+
// (否则索引里留一行已归档记忆的向量,「清得掉才检索得到」失衡)。
|
|
1563
|
+
ledgerVectorFixups.push({ action: "supersede", winner: res.winnerId, loser: res.loserId });
|
|
1564
|
+
// 先落观察行再推进状态(#650 评审):dream 路径不像 sleep 那样先跑
|
|
1565
|
+
// recordLedgerTriage,账本里可能根本没有这一对;此处若直接 UPDATE,
|
|
1566
|
+
// 命中 0 行、changes=0 被静默丢弃,于是「处置成功」与「账本有记录」
|
|
1567
|
+
// 长期背离。upsert 一次拿到确定的行,状态推进才有落点。
|
|
1568
|
+
service.saveConflictLedgerEntry({ run_id: runId, memory_a: a.id, memory_b: b.id, tier: verdict.tier, similarity: verdict.signals.similarity, signals: verdict.signals });
|
|
1569
|
+
service.setConflictLedgerStatus(a.id, b.id, "superseded");
|
|
1570
|
+
try {
|
|
1571
|
+
service.saveConflictReviewed?.({ run_id: runId, memory_a: a.id, memory_b: b.id, outcome: "supersede", judged_at: new Date().toISOString() });
|
|
1572
|
+
} catch (error) {
|
|
1573
|
+
logger?.warn?.(`dsh-mneme dream: conflict_reviewed bookkeeping failed for ${a.id}/${b.id}: ${String(error)}`);
|
|
1574
|
+
}
|
|
1575
|
+
} else {
|
|
1576
|
+
// CAS 没过(两侧在快照后被改过):不冒险自动处置,落账本留观察。
|
|
1577
|
+
service.saveConflictLedgerEntry({ run_id: runId, memory_a: a.id, memory_b: b.id, tier: verdict.tier, similarity: verdict.signals.similarity, signals: verdict.signals });
|
|
1578
|
+
}
|
|
1579
|
+
} else if (verdict.tier === "evolution") {
|
|
1580
|
+
// T3 取代链(与 sleep enforce 同款):旧行 superseded_by → 新行,
|
|
1581
|
+
// 不归档、不进 applyList。快照行可能缺 created_at(老决策),缺了
|
|
1582
|
+
// 就退回纯记账——宁可少处置,不编造时间方向。
|
|
1583
|
+
const newer = (a.created_at ?? "") >= (b.created_at ?? "") ? a : b;
|
|
1584
|
+
const older = newer === a ? b : a;
|
|
1585
|
+
try {
|
|
1586
|
+
if (service.supersedeChain(older.id, newer.id)) {
|
|
1587
|
+
t3Chained += 1;
|
|
1588
|
+
service.saveConflictLedgerEntry({ run_id: runId, memory_a: a.id, memory_b: b.id, tier: verdict.tier, similarity: verdict.signals.similarity, signals: verdict.signals });
|
|
1589
|
+
service.setConflictLedgerStatus(a.id, b.id, "superseded");
|
|
1590
|
+
} else {
|
|
1591
|
+
service.saveConflictLedgerEntry({ run_id: runId, memory_a: a.id, memory_b: b.id, tier: verdict.tier, similarity: verdict.signals.similarity, signals: verdict.signals });
|
|
1592
|
+
}
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
logger?.warn?.(`dsh-mneme dream: ledger T3 chain failed for ${a.id}/${b.id}: ${String(error)}`);
|
|
1595
|
+
}
|
|
1596
|
+
} else {
|
|
1597
|
+
service.saveConflictLedgerEntry({ run_id: runId, memory_a: a.id, memory_b: b.id, tier: verdict.tier, similarity: verdict.signals.similarity, signals: verdict.signals });
|
|
1598
|
+
}
|
|
1599
|
+
marked += 1;
|
|
1600
|
+
} catch (error) {
|
|
1601
|
+
logger?.warn?.(`dsh-mneme dream: ledger conflict mark failed for ${d.winner}/${d.loser}: ${String(error)}`);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
if (marked > 0) {
|
|
1605
|
+
logger?.info?.(`[dsh-mneme] dream: ledger enforce marked ${marked} conflict decision(s), ${t1Resolved} superseded, ${t3Chained} chained`);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
const applyList = ledgerEnforce
|
|
1609
|
+
? decisions.filter((d) => d.action !== "conflict")
|
|
1610
|
+
: freezeEnabled
|
|
1611
|
+
? decisions.filter((d) => d.action !== "conflict")
|
|
1612
|
+
: decisions.filter((d) => d.action !== "conflict" || !isCrossScopeDecision(d));
|
|
1613
|
+
if (freezeEnabled) {
|
|
1614
|
+
const conflictsToFreeze = decisions.filter((d) => d.action === "conflict");
|
|
1615
|
+
if (conflictsToFreeze.length > 0) {
|
|
1616
|
+
try {
|
|
1617
|
+
const maxPending = Number.isInteger(config.conflictFreezeMaxPending) ? config.conflictFreezeMaxPending : 100;
|
|
1618
|
+
const pendingNow = service.countConflictPending();
|
|
1619
|
+
const budget = Math.max(0, maxPending - pendingNow);
|
|
1620
|
+
const toFreeze = conflictsToFreeze.slice(0, budget);
|
|
1621
|
+
if (conflictsToFreeze.length > budget) {
|
|
1622
|
+
logger?.warn?.(`dsh-mneme dream: conflict freeze queue full (${pendingNow}/${maxPending}), skipped ${conflictsToFreeze.length - budget} conflict(s)`);
|
|
1623
|
+
}
|
|
1624
|
+
for (const d of toFreeze) {
|
|
1625
|
+
try {
|
|
1626
|
+
// 返回 undefined = 该对已被人工裁决且两侧内容未变(issue #170 复核
|
|
1627
|
+
// 项 4:内容变了才重新入队)——不算停车、不进 frozenIds。
|
|
1628
|
+
const parked = service.saveConflictPending({ run_id: runId, memory_a: d.winner, memory_b: d.loser, reason: d.reason, similarity: pairSimilarity.get(similarityKey(d.winner, d.loser)) });
|
|
1629
|
+
// 只有真的新插了一行才算停车:这一对已在队列里时返回的是旧行
|
|
1630
|
+
// (fresh=false),旧代码把它也算进 frozen,虚报由此而来(#603)。
|
|
1631
|
+
if (parked?.fresh !== true) continue;
|
|
1632
|
+
frozenCount++;
|
|
1633
|
+
frozenIds.push(d.winner, d.loser);
|
|
1634
|
+
} catch (error) {
|
|
1635
|
+
logger?.warn?.(`dsh-mneme dream: failed to freeze conflict ${d.winner}/${d.loser}: ${String(error)}`);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
} catch (error) {
|
|
1639
|
+
logger?.warn?.(`dsh-mneme dream: conflict freeze lookup failed: ${String(error)}`);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
} else if (!ledgerEnforce) {
|
|
1643
|
+
// 非 freeze:跨 scope conflict 停车(绝不进 applyDecisions)。带专属
|
|
1644
|
+
// reason + 模型给出的裁决理由,队列里读起来是「归属裁决」而非重复项。
|
|
1645
|
+
// ledger enforce 档不进这里:conflict 决策已在上面落账本(tier 含
|
|
1646
|
+
// cross-scope-dup),零停车零归档(#641)。
|
|
1647
|
+
const crossScope = decisions.filter((d) => d.action === "conflict" && isCrossScopeDecision(d));
|
|
1648
|
+
for (const d of crossScope) {
|
|
1649
|
+
try {
|
|
1650
|
+
const parked = service.saveConflictPending({
|
|
1651
|
+
run_id: runId,
|
|
1652
|
+
memory_a: d.winner,
|
|
1653
|
+
memory_b: d.loser,
|
|
1654
|
+
reason: STR.scopeConflictParkReason[language](d.reason),
|
|
1655
|
+
similarity: pairSimilarity.get(similarityKey(d.winner, d.loser))
|
|
1656
|
+
});
|
|
1657
|
+
if (parked?.fresh === true) {
|
|
1658
|
+
frozenCount++;
|
|
1659
|
+
frozenIds.push(d.winner, d.loser);
|
|
1660
|
+
}
|
|
1661
|
+
} catch (error) {
|
|
1662
|
+
logger?.warn?.(`dsh-mneme dream: failed to park cross-scope conflict ${d.winner}/${d.loser}: ${String(error)}`);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
if (crossScope.length > 0) {
|
|
1666
|
+
logger?.info?.(`[dsh-mneme] dream: parked ${crossScope.length} cross-scope conflict(s) for human ownership review`);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
// CAS-guarded, per-decision-transactional apply against the run snapshot:
|
|
1671
|
+
// a target changed during the LLM call is skipped and reported as a
|
|
1672
|
+
// conflict instead of being overwritten (item ①). Frozen conflicts are
|
|
1673
|
+
// excluded from this list (they are parked, not applied).
|
|
1674
|
+
// mint 来源:主链路的 create 落 source=dream。来源从管道给而非从决策对象取——
|
|
1675
|
+
// 见 applyDecisions 的 @param config。
|
|
1676
|
+
const { applied, conflicts, failures, committed } = applyDecisions(
|
|
1677
|
+
// runId 让 differentiate 的账本行能追到是哪一轮 dream 判的(issue #533)。
|
|
1678
|
+
applyList, service, logger, snapshot, { ...config, mintSource: "dream", runId }
|
|
1679
|
+
);
|
|
1680
|
+
// Per-record receipt chain: one row per actually-committed merge/conflict/
|
|
1681
|
+
// update verdict, stamped with the decision-basis digest + idempotency
|
|
1682
|
+
// counters (count_before → count_after). Written here, before the run audit
|
|
1683
|
+
// row, so the verdict trail always precedes the run trail it belongs to.
|
|
1684
|
+
// Bookkeeping: a write failure is logged and swallowed — it must never
|
|
1685
|
+
// block the consolidation flow.
|
|
1686
|
+
try {
|
|
1687
|
+
for (const r of buildRecordReceipts({ runId, committed, snapshot, policyEpoch: config.policyEpoch ?? 0 })) {
|
|
1688
|
+
service.saveReceipt(r);
|
|
1689
|
+
}
|
|
1690
|
+
} catch (error) {
|
|
1691
|
+
logger?.warn?.(`dsh-mneme dream: failed to write per-record receipt: ${String(error)}`);
|
|
1692
|
+
}
|
|
1693
|
+
// Attach the pre-update snapshot to the audit copy of each update decision
|
|
1694
|
+
// so the recorded row shows the before/after delta, not just the target.
|
|
1695
|
+
const auditDecisions = decisions.map((d) =>
|
|
1696
|
+
d.action === "update" && updateSnapshots[d.ids[0]]
|
|
1697
|
+
? { ...d, _before: updateSnapshots[d.ids[0]] }
|
|
1698
|
+
: d
|
|
1699
|
+
);
|
|
1700
|
+
// Outcome is derived from the ACTUALLY committed sub-steps, never from the
|
|
1701
|
+
// raw LLM decision list — a merge whose archive step rolled back must not
|
|
1702
|
+
// claim "merge-archived" (item ②). Conflicts/failures ride along so the
|
|
1703
|
+
// audit row records why the run diverged.
|
|
1704
|
+
const outcome = { ...buildOutcome(committed), conflicts, failures };
|
|
1705
|
+
if (coverageDegraded) outcome.degradations = [coverageShortfall];
|
|
1706
|
+
// Frozen conflicts were not adjudicated: mark both sides pending in the
|
|
1707
|
+
// per-id outcome so the audit row shows they were parked, not skipped.
|
|
1708
|
+
if (frozenIds.length) {
|
|
1709
|
+
for (const id of frozenIds) outcome.byId[id] = "conflict-pending";
|
|
1710
|
+
}
|
|
1711
|
+
// Decisions validated but not fully committed → reconcile (not ok).
|
|
1712
|
+
const partial = conflicts.length > 0 || failures.length > 0;
|
|
1713
|
+
// No decision landed (all-keep, or every decision skipped as an idempotent
|
|
1714
|
+
// replay) → nothing substantive changed. Distinct from a success: such a
|
|
1715
|
+
// run must never be reported as ok, or the audit claims work that never
|
|
1716
|
+
// happened and the scheduler refreshes the baseline on a false positive.
|
|
1717
|
+
// Frozen conflicts are substantive output (parked for review), so a run
|
|
1718
|
+
// that only froze conflicts is not a noop.
|
|
1719
|
+
// Issue #641:ledger enforce 的 T1 supersede 是独立于 applyList 落地的实质产出,
|
|
1720
|
+
// 计入「本轮有没有干活」,否则有归档产出的轮会被当 noop 刷基线(#634 同族误报)。
|
|
1721
|
+
const noChange = frozenCount === 0 && applied === 0 && ledgerApplied === 0 && committed.every((c) => c.action === "keep");
|
|
1722
|
+
|
|
1723
|
+
// Keep the vector index consistent with the post-dream store state.
|
|
1724
|
+
if (semantic?.embedder && semantic?.vectorIndex) {
|
|
1725
|
+
try {
|
|
1726
|
+
await maintainIndexAfterDream(ledgerVectorFixups.length ? [...applyList, ...ledgerVectorFixups] : applyList, service, semantic);
|
|
1727
|
+
} catch (error) {
|
|
1728
|
+
logger?.warn?.(`[dsh-mneme] dream index maintenance failed: ${String(error)}`);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
// Summary generation (second LLM call). A throwing stream is reported as
|
|
1733
|
+
// a failed run; summary:false marks a run that produced no summary.
|
|
1734
|
+
let summaryText;
|
|
1735
|
+
let summaryStreamFailure = "";
|
|
1736
|
+
const runSummary = (withEffort, tokens) => {
|
|
1737
|
+
summaryStreamFailure = "";
|
|
1738
|
+
// Issue #258:总览阶段可用 dreamSummaryProvider/dreamSummaryModel 走独立路由
|
|
1739
|
+
//(默认空 = 沿用 dream 路由);输入侧有 dreamSummaryMaxInputs 硬上限,
|
|
1740
|
+
// 按窗口同款 updated_at 倒序保留最新的(见下方 summaryInputs)。
|
|
1741
|
+
const summaryRoute = (config.dreamSummaryProvider && config.dreamSummaryModel)
|
|
1742
|
+
? { provider: config.dreamSummaryProvider, model: config.dreamSummaryModel }
|
|
1743
|
+
: route;
|
|
1744
|
+
return runAuditedLlm(ctx, service, config, {
|
|
1745
|
+
triggerSource: "autoDream",
|
|
1746
|
+
operationType: "dream_summarize",
|
|
1747
|
+
modelId: `${summaryRoute.provider}:${summaryRoute.model}`,
|
|
1748
|
+
relatedMemoryIds: [],
|
|
1749
|
+
streamError: () => summaryStreamFailure
|
|
1750
|
+
}, (reportUsage) => streamText(ctx, {
|
|
1751
|
+
provider: summaryRoute.provider,
|
|
1752
|
+
model: summaryRoute.model,
|
|
1753
|
+
purpose: "compaction",
|
|
1754
|
+
maxTokens: tokens,
|
|
1755
|
+
...(withEffort && effort ? { reasoningEffort: effort } : {}),
|
|
1756
|
+
messages: [
|
|
1757
|
+
{ role: "system", content: [{ type: "text", text: STR.prompts.dreamSummary[language] }], source: { kind: "plugin:dsh-mneme", plugin: "dsh-mneme" } },
|
|
1758
|
+
{ role: "user", content: [{ type: "text", text: fenceListBody(language, summaryInputs.map((m) => `- ${m.title}: ${m.content}`).join("\n"), "text") }], source: { kind: "plugin:dsh-mneme", plugin: "dsh-mneme" } }
|
|
1759
|
+
]
|
|
1760
|
+
}, reportUsage, (reason) => { summaryStreamFailure = describeStreamFailure(reason); }));
|
|
1761
|
+
};
|
|
1762
|
+
// Resident status bar (#164 alignment): the overview is THE one resident
|
|
1763
|
+
// narrative — summary tier 0, refreshed every run via _overwrite (supersede
|
|
1764
|
+
// v1, content_history traceable). Its 口径 (what snapshot produced it) is
|
|
1765
|
+
// stamped into the content so the standing answer is always auditable; the
|
|
1766
|
+
// formal evidence column lands with the narrative-bars batch.
|
|
1767
|
+
// Issue #258:输入硬上限(条数)——原全库无界,把 dream 路由指向小 ctx
|
|
1768
|
+
// 模型时 120k token 当场 CONTEXT_WINDOW_EXCEEDED(#258 实测),且随库增长
|
|
1769
|
+
// 渐进恶化。倒序排序与 consolidate 窗口同款(updated_at desc, id tiebreak),
|
|
1770
|
+
// 保留最新的;0 = 关闭上限(回归全库行为,调用方自担 ctx)。
|
|
1771
|
+
// #230:document 指针行不进总览(与 dream/sleep 五个候选池的同款排除)。
|
|
1772
|
+
// issue #471:这一处尤其不能漏——总览落库时是**无作用域**的 summary 行
|
|
1773
|
+
// (saveWithDedupe 不带 agent_scope/workspace_scope),所以被按下作用域的内容
|
|
1774
|
+
// 一旦进了总览,注入侧按作用域过滤就再也拦不住它:内容已经洗成了全局行。
|
|
1775
|
+
// 别的入口漏了只是「那一条漏过去」,这里漏了是「洗一遍之后一直漏」。
|
|
1776
|
+
const summaryAll = withoutPaused(service, service.all().filter((m) => !isImportedRow(m) && !m.archived && m.type !== "summary" && m.type !== "document"));
|
|
1777
|
+
const summaryMaxInputs = Number.isInteger(config.dreamSummaryMaxInputs) ? config.dreamSummaryMaxInputs : 0;
|
|
1778
|
+
const summaryInputs = summaryMaxInputs > 0 && summaryAll.length > summaryMaxInputs
|
|
1779
|
+
? [...summaryAll]
|
|
1780
|
+
.sort((a, b) => {
|
|
1781
|
+
const ta = String(a.updated_at ?? "");
|
|
1782
|
+
const tb = String(b.updated_at ?? "");
|
|
1783
|
+
if (ta < tb) return 1;
|
|
1784
|
+
if (ta > tb) return -1;
|
|
1785
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
1786
|
+
})
|
|
1787
|
+
.slice(0, summaryMaxInputs)
|
|
1788
|
+
: summaryAll;
|
|
1789
|
+
|
|
1790
|
+
const summaryScope = STR.summaryScope[language](
|
|
1791
|
+
summaryInputs.length,
|
|
1792
|
+
runId.slice(0, 8),
|
|
1793
|
+
new Date().toISOString().slice(0, 10)
|
|
1794
|
+
);
|
|
1795
|
+
try {
|
|
1796
|
+
// Bug8: the summary call is audited too (operation dream_summarize).
|
|
1797
|
+
summaryText = await withMaxTokensFallback(ctx, config.dreamMaxTokens ?? 2048, (tokens) => withEffortFallback(ctx, effort, () => runSummary(true, tokens), () => runSummary(false, tokens), () => summaryStreamFailure), () => summaryStreamFailure);
|
|
1798
|
+
} catch (error) {
|
|
1799
|
+
logger?.warn?.(`dsh-mneme dream: summary llm call failed: ${String(error)}`);
|
|
1800
|
+
return finish({ ok: false, error: "llm failed", applied, decisions: auditDecisions, outcome, frozen: frozenCount, summary: false });
|
|
1801
|
+
}
|
|
1802
|
+
let summaryStored = false;
|
|
1803
|
+
if (summaryText !== undefined && summaryText.trim()) {
|
|
1804
|
+
// Bug5 carve-out: the library overview is regenerated every run, so it
|
|
1805
|
+
// must REPLACE the previous overview (not append — that would grow the
|
|
1806
|
+
// summary unboundedly). `_overwrite` still archives the old overview into
|
|
1807
|
+
// content_history before replacing it. Content carries the snapshot-scope
|
|
1808
|
+
// footer computed above — the resident bar states its own 口径.
|
|
1809
|
+
service.saveWithDedupe({ type: "summary", title: STR.summaryTitle[language], content: `${summaryText.trim()}${summaryScope}`, importance: 5, source: "dream", _overwrite: true });
|
|
1810
|
+
summaryStored = true;
|
|
1811
|
+
// Re-embed the fresh summary so the index stays in sync with the store.
|
|
1812
|
+
if (semantic?.embedder && semantic?.vectorIndex) {
|
|
1813
|
+
try {
|
|
1814
|
+
// source="dream" 限定:叙述条(source=narrative)同为 summary 类型,
|
|
1815
|
+
// 不加限定时 find 可能截胡,把总览的 re-embed 花在叙述条上。
|
|
1816
|
+
const summary = service.all().find((m) => m.type === "summary" && m.source === "dream");
|
|
1817
|
+
if (summary) await reEmbedMemory(semantic, summary, logger);
|
|
1818
|
+
} catch { /* best-effort */ }
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
// Narrative bars (#164 alignment, opt-in dreamNarrativeEnabled): per-topic
|
|
1822
|
+
// on-demand narratives after consolidation + summary. Additive phase —
|
|
1823
|
+
// failures degrade to zero narratives, never break the run.
|
|
1824
|
+
let narrativesStored = 0;
|
|
1825
|
+
if (config.dreamNarrativeEnabled === true) {
|
|
1826
|
+
try {
|
|
1827
|
+
narrativesStored = await generateNarratives({ ctx, service, config, route, language, effort, semantic, logger });
|
|
1828
|
+
} catch (error) {
|
|
1829
|
+
logger?.warn?.(`dsh-mneme dream: narrative phase failed: ${String(error)}`);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
// Honest status assignment (never a fake ok):
|
|
1834
|
+
// reconcile — some decisions validated but did not commit (CAS/rollback).
|
|
1835
|
+
// noop — nothing changed and no summary persisted: truly an empty
|
|
1836
|
+
// run. ok:false keeps the scheduler from moving the baseline.
|
|
1837
|
+
// ok — either real changes landed, or a fresh summary was stored
|
|
1838
|
+
// (all-keep + summary is a substantive summary refresh).
|
|
1839
|
+
// degraded — real consolidation landed but the run did not produce its
|
|
1840
|
+
// full output: the summary came back empty/missing, or
|
|
1841
|
+
// skipInvalid dropped individually-invalid decisions
|
|
1842
|
+
// (Issue #89 — marked, not faked), or the explicit-decision
|
|
1843
|
+
// coverage fell below dreamMinExplicitCoverage (Issue #104
|
|
1844
|
+
// 方向 1 — degrade, don't reject). The valid subset was
|
|
1845
|
+
// absorbed (ok for the baseline).
|
|
1846
|
+
let status;
|
|
1847
|
+
let okResult;
|
|
1848
|
+
if (partial) {
|
|
1849
|
+
status = "reconcile";
|
|
1850
|
+
okResult = false;
|
|
1851
|
+
} else if (noChange) {
|
|
1852
|
+
status = summaryStored ? "ok" : "noop";
|
|
1853
|
+
okResult = summaryStored;
|
|
1854
|
+
} else {
|
|
1855
|
+
status = summaryStored && !skippedInvalid && !coverageDegraded ? "ok" : "degraded";
|
|
1856
|
+
okResult = true;
|
|
1857
|
+
}
|
|
1858
|
+
return finish({
|
|
1859
|
+
ok: okResult,
|
|
1860
|
+
status,
|
|
1861
|
+
// ledgerApplied 是 ledger enforce 档下 T1 supersede 的独立产出,不在 applyList
|
|
1862
|
+
// 里(#650 评审)。只把它算进 noChange 会让 receipt 与 dream_runs.applied 仍
|
|
1863
|
+
// 报 0:库真被改了,回执说没改——#619 同族假成功。计入 applied 才是唯一口径。
|
|
1864
|
+
applied: applied + ledgerApplied,
|
|
1865
|
+
decisions: auditDecisions,
|
|
1866
|
+
outcome,
|
|
1867
|
+
conflicts,
|
|
1868
|
+
failures,
|
|
1869
|
+
frozen: frozenCount,
|
|
1870
|
+
summary: summaryStored,
|
|
1871
|
+
narratives: narrativesStored,
|
|
1872
|
+
// Issue #104:只有真发生跳过时才落库(degraded 也可能仅因 summary 为空),
|
|
1873
|
+
// ok 轮留 NULL,避免「空数组」与「无此字段」两种假明细占据审计行。
|
|
1874
|
+
skipped: skippedInvalid ? skipped : undefined
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
return { maybeSchedule, runDream, dispose };
|
|
1879
|
+
}
|