@modusensus/dsh-mneme 0.14.0-beta.1 → 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 -830
- package/assets/icon.svg +3 -3
- package/bin/cli.mjs +964 -964
- package/bin/dsh-mneme-choro.mjs +262 -262
- 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 -404
- 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 -3600
- 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 -14322
- package/lib/commands.js +95 -95
- package/lib/config-keys.js +144 -144
- package/lib/config.js +1361 -1344
- 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 -945
- package/lib/dream/narratives.js +44 -44
- package/lib/dream/sleep.js +2027 -2027
- package/lib/dream/triage.js +139 -139
- package/lib/dream.js +1879 -1879
- 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 -119
- package/lib/image-describe.js +158 -0
- package/lib/index.js +852 -852
- package/lib/inject.js +656 -651
- 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 -595
- 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 -3268
- package/lib/session-gate.js +91 -91
- package/lib/session-usage.js +350 -350
- package/lib/settings.js +827 -817
- 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 -5895
- 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 -267
- 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 -640
- package/lib/wiki/render.js +261 -261
- package/lib/wiki/scan.js +371 -371
- package/lib/wiki/t0.js +500 -500
- package/lib/wiki/t1-jobs.js +232 -232
- package/lib/wiki/t1.js +609 -609
- package/lib/write-admission.js +404 -404
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +1 -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 -210
- package/scripts/release-notes.mjs +164 -164
- 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 -3600
- 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 -1344
- 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 -945
- package/src/dream/narratives.js +44 -44
- package/src/dream/sleep.js +2027 -2027
- package/src/dream/triage.js +139 -139
- package/src/dream.js +1879 -1879
- 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 -119
- package/src/image-describe.js +158 -0
- package/src/index.js +852 -852
- package/src/inject.js +656 -651
- 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 -595
- 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 -3268
- package/src/session-gate.js +91 -91
- package/src/session-usage.js +350 -350
- package/src/settings.js +827 -817
- 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 -5895
- 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 -267
- 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 -640
- package/src/wiki/render.js +261 -261
- package/src/wiki/scan.js +371 -371
- package/src/wiki/t0.js +500 -500
- package/src/wiki/t1-jobs.js +232 -232
- package/src/wiki/t1.js +609 -609
- 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 -2998
- 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 -4425
- 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 -452
- package/test/conflict-ledger.test.js +242 -242
- 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 -432
- 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 -174
- 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 -333
- 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 -259
- 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 -109
- 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 -553
- 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 -90
- package/test/panel-tokens.test.js +245 -245
- 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 -156
- package/test/release-notes.test.js +265 -265
- 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 -188
- 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 -1455
- 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 -734
- package/test/wiki-claims.test.js +194 -194
- 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 -202
- package/test/wiki-queries.test.js +398 -398
- package/test/wiki-scan.test.js +208 -208
- package/test/wiki-store.test.js +190 -190
- package/test/wiki-t0.test.js +456 -456
- package/test/wiki-t1-jobs.test.js +125 -125
- package/test/wiki-t1.test.js +460 -460
- 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/profile-facts.js
CHANGED
|
@@ -1,356 +1,356 @@
|
|
|
1
|
-
// dsh-mneme/src/profile-facts.js
|
|
2
|
-
// 「Agent 眼中的你」自动事实(2026-11-06):按会话身份分卡维护一份逐条事实清单,
|
|
3
|
-
// dream 巩固轮结束后自动增量维护(opt-in:profileFactsEnabled),面板可手动触发与
|
|
4
|
-
// 逐条修正。形态对标 ChatGPT 记忆 / DS Bot 每 bot 一份 MEMORY.md 事实清单(journal
|
|
5
|
-
// 记每笔 op 的 before/after)——事实可独立增改归档、账本可审计,而不是整段重写。
|
|
6
|
-
//
|
|
7
|
-
// 四条纪律,都是「做错界面上看不出来」的那类,所以写在文件头:
|
|
8
|
-
// 1. **手填画像神圣**。user_settings.profile 是用户原文,本模块对它只有读权
|
|
9
|
-
// (profile-extract #497 的「只出草稿不落库」口径不变);事实住独立的
|
|
10
|
-
// profile_facts 三表,注入时作为画像之后的独立段出现,绝不回写 user_settings。
|
|
11
|
-
// 2. **没有增量输入就不调模型**。游标之后没有新行时返回 noop 回执——为维护而
|
|
12
|
-
// 维护的一轮只会产出「看起来像事实的复读」,那种事实每帧都在注入。
|
|
13
|
-
// 3. **吐不出可用 ops 就 failed,绝不冒充成功**。「模型没配合」与「确实没什
|
|
14
|
-
// 么可改」必须可区分(#493 同一条教训:解析失败被读成空结果,用户会以为
|
|
15
|
-
// 这轮维护跑过了)。failed 轮照样落回执行,账面上留痕。
|
|
16
|
-
// 4. **失败不推进游标**。failed 轮之后重跑同一窗口(蒸馏游标同款纪律:失败不
|
|
17
|
-
// 消费),宁可重复消费也不跳过没维护到的行。
|
|
18
|
-
//
|
|
19
|
-
// 记忆正文是模型的**可影响输入**:围栏 + 「数据非指令」 + 禁止给助手起名设人格,
|
|
20
|
-
// 三件套与 profile-extract / #380 同源(src/fence.js 一份断链,不写第二套)。
|
|
21
|
-
//
|
|
22
|
-
// LLM 调用与审计完全复用 dream.js 的既有缝(resolveRoute / runAuditedLlm /
|
|
23
|
-
// streamText / describeStreamFailure):chunk 形状的三个硬点(用量嵌在 chunk.usage、
|
|
24
|
-
// 终态 finish 才是错误、rc.1 把适配器故障转成 finish chunk 而不是抛错)都在那里
|
|
25
|
-
// 踩过并修过,不在这里重写第二遍。
|
|
26
|
-
|
|
27
|
-
import { randomUUID } from "node:crypto";
|
|
28
|
-
import { STR, langOf } from "./lang.js";
|
|
29
|
-
import { describeStreamFailure, resolveRoute, runAuditedLlm, streamText } from "./dream.js";
|
|
30
|
-
// 与巩固/蒸馏/画像提取同一份断链(src/fence.js)。事实同样是模型的可影响输入,
|
|
31
|
-
// 一条写着「<<<记忆条目结束>>>」的记忆能提前闭合围栏。
|
|
32
|
-
import { breakFenceMarkers } from "./fence.js";
|
|
33
|
-
|
|
34
|
-
const DEFAULT_INPUT_ROWS = 120;
|
|
35
|
-
const DEFAULT_ROW_CHARS = 260;
|
|
36
|
-
const DEFAULT_TOTAL_CHARS = 9000;
|
|
37
|
-
// 与 prompt 里的「单轮最多 12 个 op」同一个数:超出的切片丢弃并记进回执 detail,
|
|
38
|
-
// 不让一轮失控的输出把清单搅成毛线团。清单长度本身不设硬顶——prompt 要求模型
|
|
39
|
-
// archive 冗余,注入侧另有逐行/总长截断兜底(user-settings.js),撑不爆每一帧。
|
|
40
|
-
const MAX_OPS_PER_RUN = 12;
|
|
41
|
-
|
|
42
|
-
/** 带 code 的失败:路由按 code 映射状态码,调用方据此区分「没东西可提」与「模型没配合」。 */
|
|
43
|
-
export class ProfileFactsError extends Error {
|
|
44
|
-
constructor(code, message) {
|
|
45
|
-
super(message ?? code);
|
|
46
|
-
this.name = "ProfileFactsError";
|
|
47
|
-
this.code = code;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* 单个 op 的形状谓词;批量口径(路由层 400 闸)与解析层共用这一份——两处各抄
|
|
53
|
-
* 一遍的话,改形状就会漏一边(评审 P3:同一谓词两份皮,P1 的洞因此要修两处)。
|
|
54
|
-
* add 与 update 都必须带非空 content:update 缺 content 时 `String(undefined)`
|
|
55
|
-
* 会把「undefined」三个字母写进事实正文并从此每帧注入(评审 P1 实测复现的洞)。
|
|
56
|
-
*/
|
|
57
|
-
function isValidFactOp(o) {
|
|
58
|
-
return !!o && typeof o === "object"
|
|
59
|
-
&& (o.op === "add" || o.op === "update" || o.op === "archive")
|
|
60
|
-
&& (o.op !== "add" && o.op !== "update" || (typeof o.content === "string" && o.content.trim()))
|
|
61
|
-
&& (o.op === "add" || typeof o.id === "string" && o.id.trim());
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** 批量口径:待写入的 op 批必须非空且逐个形状合格(空批走「没什么可改」,不进这条)。 */
|
|
65
|
-
export function isValidFactOps(ops) {
|
|
66
|
-
return Array.isArray(ops) && ops.length > 0 && ops.every(isValidFactOp);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* 从模型输出里取 ops 数组。先剥 markdown 围栏,再退到「第一个 [ 或 { 到最后一个
|
|
71
|
-
* ] 或 }」的宽取——与 parseProfileDraft 同款两步。形状校验:op 只认 add/update/
|
|
72
|
-
* archive;add 必须带非空 content;update/archive 必须带 id(id 是否真实存在由
|
|
73
|
-
* apply 层裁决——这里只做形状校验)。取不到可用结构返回 null,绝不返回空数组
|
|
74
|
-
* 冒充成功(空 ops 是合法的「没什么可改」,但那必须是 JSON 里真的写着空数组)。
|
|
75
|
-
*/
|
|
76
|
-
export function parseFactOps(text) {
|
|
77
|
-
if (typeof text !== "string" || !text.trim()) return null;
|
|
78
|
-
const cleaned = text.replace(/```(?:json)?\s*([\s\S]*?)```/gi, "$1").trim();
|
|
79
|
-
const candidates = [];
|
|
80
|
-
const arrOpen = cleaned.indexOf("[");
|
|
81
|
-
const objOpen = cleaned.indexOf("{");
|
|
82
|
-
const start = arrOpen >= 0 && (objOpen < 0 || arrOpen < objOpen) ? arrOpen : objOpen;
|
|
83
|
-
const end = Math.max(cleaned.lastIndexOf("]"), cleaned.lastIndexOf("}"));
|
|
84
|
-
if (start >= 0 && end > start) candidates.push(cleaned.slice(start, end + 1));
|
|
85
|
-
if (cleaned.startsWith("{") || cleaned.startsWith("[")) candidates.push(cleaned);
|
|
86
|
-
for (const candidate of candidates) {
|
|
87
|
-
let value;
|
|
88
|
-
try {
|
|
89
|
-
value = JSON.parse(candidate);
|
|
90
|
-
} catch {
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
const ops = Array.isArray(value) ? value : (value && typeof value === "object" && Array.isArray(value.ops) ? value.ops : null);
|
|
94
|
-
// 空 ops 是合法的「没什么可改」(runFor 会以 ok + opsApplied:0 收尾),所以
|
|
95
|
-
// 这里只验逐 op 形状、不要求非空——非空是批量写入口径(isValidFactOps)。
|
|
96
|
-
if (!ops || !ops.every(isValidFactOp)) continue;
|
|
97
|
-
return ops;
|
|
98
|
-
}
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** 采集增量输入的截断读数:多取一行只为如实回答「还有没有」(profile-extract 同款)。 */
|
|
103
|
-
function collectInputRows(store, agentKey, cursor) {
|
|
104
|
-
const fetched = store.listFactInputRows(agentKey, { since: cursor, limit: DEFAULT_INPUT_ROWS + 1 });
|
|
105
|
-
const rows = [];
|
|
106
|
-
let chars = 0;
|
|
107
|
-
let truncated = fetched.length > DEFAULT_INPUT_ROWS;
|
|
108
|
-
for (const row of fetched.slice(0, DEFAULT_INPUT_ROWS)) {
|
|
109
|
-
const content = String(row.content ?? "").slice(0, DEFAULT_ROW_CHARS);
|
|
110
|
-
if (content.length < String(row.content ?? "").length) truncated = true;
|
|
111
|
-
const size = `${row.type} ${row.title} ${content}`.length;
|
|
112
|
-
// 首行保底:单行就超总预算时也收进来(截断已如实标记)——否则游标永远
|
|
113
|
-
// 推不进、这张卡变成永久 noop(评审 P3:首行即超预算的死角)。
|
|
114
|
-
if (rows.length && chars + size > DEFAULT_TOTAL_CHARS) {
|
|
115
|
-
truncated = true;
|
|
116
|
-
break;
|
|
117
|
-
}
|
|
118
|
-
chars += size;
|
|
119
|
-
rows.push(row);
|
|
120
|
-
}
|
|
121
|
-
return { rows, truncated, charsUsed: chars };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* @param {{ ctx: object, service: object, store: object, config: object, logger?: object }} deps
|
|
126
|
-
* ctx 要带 llm(宿主 LLM 句柄);settings 不参与——本模块按纪律绝不写 user_settings。
|
|
127
|
-
* @returns object with runFor / applyManualOps / cardKeys / factsProvider / listFacts / listJournal / listRuns
|
|
128
|
-
*/
|
|
129
|
-
export function createProfileFacts({ ctx, service, store, config, logger }) {
|
|
130
|
-
const log = logger ?? ctx?.logger;
|
|
131
|
-
const language = langOf(config);
|
|
132
|
-
|
|
133
|
-
/** 卡片清单:''(default 兜底)恒在;非空键来自记忆行的 scope 值 ∪ 存量卡片。 */
|
|
134
|
-
function cardKeys() {
|
|
135
|
-
const keys = store.listFactCardKeys()
|
|
136
|
-
.filter((k) => typeof k === "string" && k.trim())
|
|
137
|
-
.map((k) => k.trim());
|
|
138
|
-
return ["", ...new Set(keys)];
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** 注入 provider 读口:renderUserSettingsBlock 的 factsProvider(面板预览共用)。 */
|
|
142
|
-
function factsProvider() {
|
|
143
|
-
return store.listActiveFactsGrouped();
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function listFacts(agentKey, { includeArchived = false } = {}) {
|
|
147
|
-
return store.listProfileFacts(agentKey, { includeArchived });
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function listJournal(agentKey, { limit = 20 } = {}) {
|
|
151
|
-
return store.listProfileFactJournal(agentKey, { limit });
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function listRuns(agentKey, { limit = 10 } = {}) {
|
|
155
|
-
return store.listProfileFactRuns(agentKey, { limit });
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* ops 落库(自动轮与人工面板共用这一份):单事务,逐 op 记账(scope_changes 式
|
|
160
|
-
* before/after)。跨卡守卫:op.id 必须属于本卡——LLM 幻觉出的别卡 id 不能当成
|
|
161
|
-
* 合法目标;精确重复的 add(与既有活跃事实 trim 后相同)跳过并计数,不落库。
|
|
162
|
-
*/
|
|
163
|
-
function applyOps(agentKey, ops, { actor, runId = null, at = new Date().toISOString() } = {}) {
|
|
164
|
-
const failures = [];
|
|
165
|
-
let applied = 0;
|
|
166
|
-
let dupsSkipped = 0;
|
|
167
|
-
const existing = store.listProfileFacts(agentKey, { includeArchived: true });
|
|
168
|
-
const existingById = new Map(existing.map((f) => [f.id, f]));
|
|
169
|
-
const activeContents = new Set(
|
|
170
|
-
existing.filter((f) => f.status === "active").map((f) => String(f.content ?? "").trim())
|
|
171
|
-
);
|
|
172
|
-
store.runAtomically(() => {
|
|
173
|
-
for (const op of ops) {
|
|
174
|
-
try {
|
|
175
|
-
if (op.op === "add") {
|
|
176
|
-
const content = String(op.content).trim();
|
|
177
|
-
if (activeContents.has(content)) {
|
|
178
|
-
dupsSkipped += 1;
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
const sourceIds = Array.isArray(op.source_ids)
|
|
182
|
-
? op.source_ids.filter((id) => typeof id === "string" && id.trim()).map((id) => id.trim())
|
|
183
|
-
: [];
|
|
184
|
-
const id = randomUUID();
|
|
185
|
-
store.insertProfileFact({ id, agentKey, content, sourceMemoryIds: sourceIds, at });
|
|
186
|
-
store.insertProfileFactJournal({ at, agentKey, runId, factId: id, op: "add", before: null, after: content, actor });
|
|
187
|
-
activeContents.add(content);
|
|
188
|
-
applied += 1;
|
|
189
|
-
} else if (op.op === "update" || op.op === "archive") {
|
|
190
|
-
const prev = existingById.get(op.id);
|
|
191
|
-
if (!prev || prev.agent_key !== agentKey) {
|
|
192
|
-
failures.push({ op: op.op, id: op.id ?? null, reason: "fact not found on this card" });
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
const nextContent = op.op === "update" ? String(op.content).trim() : prev.content;
|
|
196
|
-
const nextStatus = op.op === "archive" ? "archived" : prev.status;
|
|
197
|
-
store.updateProfileFact(prev.id, { content: nextContent, status: nextStatus, at });
|
|
198
|
-
store.insertProfileFactJournal({
|
|
199
|
-
at, agentKey, runId, factId: prev.id, op: op.op,
|
|
200
|
-
before: prev.content, after: op.op === "update" ? nextContent : null, actor
|
|
201
|
-
});
|
|
202
|
-
if (op.op === "update") {
|
|
203
|
-
// 去重集合要跟上新值:同批 [update A→B, add B] 不收新值的话会落两条
|
|
204
|
-
// 相同的活跃事实,而 dupsSkipped 还是 0(评审 P2 实测复现)。
|
|
205
|
-
activeContents.delete(String(prev.content ?? "").trim());
|
|
206
|
-
activeContents.add(nextContent);
|
|
207
|
-
}
|
|
208
|
-
applied += 1;
|
|
209
|
-
} else {
|
|
210
|
-
failures.push({ op: op.op ?? null, id: op.id ?? null, reason: "unsupported op" });
|
|
211
|
-
}
|
|
212
|
-
} catch (error) {
|
|
213
|
-
// 逐 op 吞错是刻意的:一条 journal 写失败不该把整批已落的事实回滚掉
|
|
214
|
-
// (失败进 failures 由回执如实报)。代价是与「单事务」注释的原子粒度
|
|
215
|
-
// 有出入——真故障通常连 COMMIT 一起失败,这里只是收窄那道缝。
|
|
216
|
-
failures.push({ op: op.op ?? null, id: op.id ?? null, reason: String(error?.message ?? error) });
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
return { applied, failures, dupsSkipped };
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/** 面板人工修正入口:actor=manual、runId=null,账本与自动轮同一份。 */
|
|
224
|
-
function applyManualOps(agentKey, ops) {
|
|
225
|
-
return applyOps(agentKey, ops, { actor: "manual" });
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* 跑一轮维护。返回回执 `{ runId, status, inputCount, opsApplied, failures, ... }`;
|
|
230
|
-
* no-route / stream-failed / unparseable 以 ProfileFactsError 抛出(回执已先落库,
|
|
231
|
-
* API 层按 code 映射状态码)。
|
|
232
|
-
*/
|
|
233
|
-
async function runFor(agentKey, { trigger = "manual" } = {}) {
|
|
234
|
-
const key = typeof agentKey === "string" ? agentKey.trim() : "";
|
|
235
|
-
const runId = randomUUID();
|
|
236
|
-
const startedAt = new Date().toISOString();
|
|
237
|
-
const finish = (status, extra = {}) => {
|
|
238
|
-
const receipt = { runId, agentKey: key, status, ...extra };
|
|
239
|
-
store.insertProfileFactRun({
|
|
240
|
-
id: runId,
|
|
241
|
-
agentKey: key,
|
|
242
|
-
triggerSource: trigger === "auto" ? "auto" : "manual",
|
|
243
|
-
status,
|
|
244
|
-
cursor: receipt.cursor ?? null,
|
|
245
|
-
inputCount: receipt.inputCount ?? 0,
|
|
246
|
-
opsApplied: receipt.opsApplied ?? 0,
|
|
247
|
-
detail: receipt.detail ? JSON.stringify(receipt.detail) : null,
|
|
248
|
-
startedAt,
|
|
249
|
-
finishedAt: new Date().toISOString()
|
|
250
|
-
});
|
|
251
|
-
return receipt;
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
const cursor = store.profileFactsCursor(key);
|
|
255
|
-
const { rows, truncated } = collectInputRows(store, key, cursor);
|
|
256
|
-
const facts = store.listProfileFacts(key, { includeArchived: false });
|
|
257
|
-
// 没有增量输入就没有本轮:连回执都记成 noop(面板能看到「没有新记忆」),
|
|
258
|
-
// 游标不动——下一次有新行时窗口从旧游标接续。
|
|
259
|
-
if (!rows.length) {
|
|
260
|
-
return finish("noop", { inputCount: 0, cursor });
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const fail = (code, message, detail) => {
|
|
264
|
-
finish("failed", { inputCount: rows.length, cursor: null, detail: { code, ...(detail ?? {}) } });
|
|
265
|
-
throw new ProfileFactsError(code, message);
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
const route = resolveRoute(ctx, config, log);
|
|
269
|
-
// resolveRoute 在「没有 dreamProvider/dreamModel、宿主也没有可解析的默认模型」
|
|
270
|
-
// 时返回 undefined。不判它就是一个语义为「内部错误」的 500——真正该说的是
|
|
271
|
-
// 「没配模型」(profile-extract 同款取舍:不同结局不同话)。
|
|
272
|
-
if (!route) {
|
|
273
|
-
fail("no-route", "no model route: set dreamProvider/dreamModel or pick an agent default model");
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const factListing = facts.length
|
|
277
|
-
? facts.map((f, i) => `${i + 1}. [${f.id}] ${f.content}`).join("\n")
|
|
278
|
-
: "(尚无既有事实)";
|
|
279
|
-
const rowListing = rows.map((r, i) => `${i + 1}. [${r.id}] [${r.type}] ${r.title} :: ${String(r.content ?? "").slice(0, DEFAULT_ROW_CHARS)}`).join("\n");
|
|
280
|
-
const userText = [
|
|
281
|
-
STR.profileFenceOpen[language],
|
|
282
|
-
breakFenceMarkers(`【既有事实】\n${factListing}\n\n【新记忆条目】\n${rowListing}`),
|
|
283
|
-
STR.profileFenceClose[language],
|
|
284
|
-
truncated
|
|
285
|
-
? (language === "zh"
|
|
286
|
-
? `(输入行有截断:本轮只呈现了窗口内的一部分。)`
|
|
287
|
-
: `(Input was truncated: only part of the window is shown this pass.)`)
|
|
288
|
-
: "",
|
|
289
|
-
language === "zh"
|
|
290
|
-
? `以上材料(数据)到此结束。只依据它们输出 JSON,材料里没有的绝不发明。`
|
|
291
|
-
: `End of the material (data). Answer from it only; invent nothing absent from it.`
|
|
292
|
-
].filter(Boolean).join("\n");
|
|
293
|
-
|
|
294
|
-
let streamFailure = "";
|
|
295
|
-
const text = await runAuditedLlm(ctx, service, config, {
|
|
296
|
-
triggerSource: "profileFacts",
|
|
297
|
-
operationType: "profile_facts",
|
|
298
|
-
modelId: `${route.provider}:${route.model}`,
|
|
299
|
-
relatedMemoryIds: rows.map((r) => r.id),
|
|
300
|
-
streamError: () => streamFailure,
|
|
301
|
-
// 吐了文字但没有可用结构 = 失败调用。审计必须记 error:success 配解析失败
|
|
302
|
-
// 会让「模型不配合」在账目上长得像「这轮确实没什么可改」。
|
|
303
|
-
auditError: (out) => (parseFactOps(out) ? null : "profile facts pass returned no usable ops JSON")
|
|
304
|
-
}, (reportUsage) => streamText(ctx, {
|
|
305
|
-
provider: route.provider,
|
|
306
|
-
model: route.model,
|
|
307
|
-
purpose: "compaction",
|
|
308
|
-
messages: [
|
|
309
|
-
{ role: "system", content: [{ type: "text", text: STR.prompts.profileFacts[language] }], source: { kind: "plugin:dsh-mneme", plugin: "dsh-mneme" } },
|
|
310
|
-
{ role: "user", content: [{ type: "text", text: userText }], source: { kind: "plugin:dsh-mneme", plugin: "dsh-mneme" } }
|
|
311
|
-
]
|
|
312
|
-
}, reportUsage, (reason) => { streamFailure = describeStreamFailure(reason); }));
|
|
313
|
-
|
|
314
|
-
if (text === undefined) {
|
|
315
|
-
fail("stream-failed", streamFailure || "llm stream aborted or errored");
|
|
316
|
-
}
|
|
317
|
-
const ops = parseFactOps(text);
|
|
318
|
-
if (!ops) {
|
|
319
|
-
fail("unparseable", "profile facts output was not a usable ops JSON object");
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const sliced = ops.length > MAX_OPS_PER_RUN ? ops.slice(0, MAX_OPS_PER_RUN) : ops;
|
|
323
|
-
const { applied, failures, dupsSkipped } = applyOps(key, sliced, { actor: "auto", runId });
|
|
324
|
-
// 游标取本轮实际消费到的最大 updated_at:ok 轮才推进(含「ops 为空但窗口
|
|
325
|
-
// 已看过」的轮——那是合法的「没什么可改」,不重跑同一窗口)。
|
|
326
|
-
const nextCursor = rows.reduce((m, r) => (String(r.updated_at ?? "") > m ? String(r.updated_at) : m), String(cursor ?? ""));
|
|
327
|
-
const receipt = finish("ok", {
|
|
328
|
-
cursor: nextCursor || cursor,
|
|
329
|
-
inputCount: rows.length,
|
|
330
|
-
opsApplied: applied,
|
|
331
|
-
detail: {
|
|
332
|
-
truncated,
|
|
333
|
-
opsProposed: ops.length,
|
|
334
|
-
opsSliced: ops.length - sliced.length,
|
|
335
|
-
dupsSkipped,
|
|
336
|
-
failures
|
|
337
|
-
}
|
|
338
|
-
});
|
|
339
|
-
store.pruneProfileFactJournal(key);
|
|
340
|
-
return receipt;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/** 自动轮候选(dream 成功后由 index.js 逐卡 runFor):只列有增量输入的卡——
|
|
344
|
-
* 无输入的卡连 noop 回执都不落,dream 每轮一次的自动维护不生产回执噪音。 */
|
|
345
|
-
function autoPassCandidates() {
|
|
346
|
-
return cardKeys().filter((key) => {
|
|
347
|
-
try {
|
|
348
|
-
return !!store.countFactInputRows(key, { since: store.profileFactsCursor(key) });
|
|
349
|
-
} catch {
|
|
350
|
-
return true; // 读数失败按「有待维护」处理,交给 runFor 的回执说话
|
|
351
|
-
}
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
return { runFor, applyManualOps, cardKeys, factsProvider, listFacts, listJournal, listRuns, autoPassCandidates };
|
|
356
|
-
}
|
|
1
|
+
// dsh-mneme/src/profile-facts.js
|
|
2
|
+
// 「Agent 眼中的你」自动事实(2026-11-06):按会话身份分卡维护一份逐条事实清单,
|
|
3
|
+
// dream 巩固轮结束后自动增量维护(opt-in:profileFactsEnabled),面板可手动触发与
|
|
4
|
+
// 逐条修正。形态对标 ChatGPT 记忆 / DS Bot 每 bot 一份 MEMORY.md 事实清单(journal
|
|
5
|
+
// 记每笔 op 的 before/after)——事实可独立增改归档、账本可审计,而不是整段重写。
|
|
6
|
+
//
|
|
7
|
+
// 四条纪律,都是「做错界面上看不出来」的那类,所以写在文件头:
|
|
8
|
+
// 1. **手填画像神圣**。user_settings.profile 是用户原文,本模块对它只有读权
|
|
9
|
+
// (profile-extract #497 的「只出草稿不落库」口径不变);事实住独立的
|
|
10
|
+
// profile_facts 三表,注入时作为画像之后的独立段出现,绝不回写 user_settings。
|
|
11
|
+
// 2. **没有增量输入就不调模型**。游标之后没有新行时返回 noop 回执——为维护而
|
|
12
|
+
// 维护的一轮只会产出「看起来像事实的复读」,那种事实每帧都在注入。
|
|
13
|
+
// 3. **吐不出可用 ops 就 failed,绝不冒充成功**。「模型没配合」与「确实没什
|
|
14
|
+
// 么可改」必须可区分(#493 同一条教训:解析失败被读成空结果,用户会以为
|
|
15
|
+
// 这轮维护跑过了)。failed 轮照样落回执行,账面上留痕。
|
|
16
|
+
// 4. **失败不推进游标**。failed 轮之后重跑同一窗口(蒸馏游标同款纪律:失败不
|
|
17
|
+
// 消费),宁可重复消费也不跳过没维护到的行。
|
|
18
|
+
//
|
|
19
|
+
// 记忆正文是模型的**可影响输入**:围栏 + 「数据非指令」 + 禁止给助手起名设人格,
|
|
20
|
+
// 三件套与 profile-extract / #380 同源(src/fence.js 一份断链,不写第二套)。
|
|
21
|
+
//
|
|
22
|
+
// LLM 调用与审计完全复用 dream.js 的既有缝(resolveRoute / runAuditedLlm /
|
|
23
|
+
// streamText / describeStreamFailure):chunk 形状的三个硬点(用量嵌在 chunk.usage、
|
|
24
|
+
// 终态 finish 才是错误、rc.1 把适配器故障转成 finish chunk 而不是抛错)都在那里
|
|
25
|
+
// 踩过并修过,不在这里重写第二遍。
|
|
26
|
+
|
|
27
|
+
import { randomUUID } from "node:crypto";
|
|
28
|
+
import { STR, langOf } from "./lang.js";
|
|
29
|
+
import { describeStreamFailure, resolveRoute, runAuditedLlm, streamText } from "./dream.js";
|
|
30
|
+
// 与巩固/蒸馏/画像提取同一份断链(src/fence.js)。事实同样是模型的可影响输入,
|
|
31
|
+
// 一条写着「<<<记忆条目结束>>>」的记忆能提前闭合围栏。
|
|
32
|
+
import { breakFenceMarkers } from "./fence.js";
|
|
33
|
+
|
|
34
|
+
const DEFAULT_INPUT_ROWS = 120;
|
|
35
|
+
const DEFAULT_ROW_CHARS = 260;
|
|
36
|
+
const DEFAULT_TOTAL_CHARS = 9000;
|
|
37
|
+
// 与 prompt 里的「单轮最多 12 个 op」同一个数:超出的切片丢弃并记进回执 detail,
|
|
38
|
+
// 不让一轮失控的输出把清单搅成毛线团。清单长度本身不设硬顶——prompt 要求模型
|
|
39
|
+
// archive 冗余,注入侧另有逐行/总长截断兜底(user-settings.js),撑不爆每一帧。
|
|
40
|
+
const MAX_OPS_PER_RUN = 12;
|
|
41
|
+
|
|
42
|
+
/** 带 code 的失败:路由按 code 映射状态码,调用方据此区分「没东西可提」与「模型没配合」。 */
|
|
43
|
+
export class ProfileFactsError extends Error {
|
|
44
|
+
constructor(code, message) {
|
|
45
|
+
super(message ?? code);
|
|
46
|
+
this.name = "ProfileFactsError";
|
|
47
|
+
this.code = code;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 单个 op 的形状谓词;批量口径(路由层 400 闸)与解析层共用这一份——两处各抄
|
|
53
|
+
* 一遍的话,改形状就会漏一边(评审 P3:同一谓词两份皮,P1 的洞因此要修两处)。
|
|
54
|
+
* add 与 update 都必须带非空 content:update 缺 content 时 `String(undefined)`
|
|
55
|
+
* 会把「undefined」三个字母写进事实正文并从此每帧注入(评审 P1 实测复现的洞)。
|
|
56
|
+
*/
|
|
57
|
+
function isValidFactOp(o) {
|
|
58
|
+
return !!o && typeof o === "object"
|
|
59
|
+
&& (o.op === "add" || o.op === "update" || o.op === "archive")
|
|
60
|
+
&& (o.op !== "add" && o.op !== "update" || (typeof o.content === "string" && o.content.trim()))
|
|
61
|
+
&& (o.op === "add" || typeof o.id === "string" && o.id.trim());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 批量口径:待写入的 op 批必须非空且逐个形状合格(空批走「没什么可改」,不进这条)。 */
|
|
65
|
+
export function isValidFactOps(ops) {
|
|
66
|
+
return Array.isArray(ops) && ops.length > 0 && ops.every(isValidFactOp);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 从模型输出里取 ops 数组。先剥 markdown 围栏,再退到「第一个 [ 或 { 到最后一个
|
|
71
|
+
* ] 或 }」的宽取——与 parseProfileDraft 同款两步。形状校验:op 只认 add/update/
|
|
72
|
+
* archive;add 必须带非空 content;update/archive 必须带 id(id 是否真实存在由
|
|
73
|
+
* apply 层裁决——这里只做形状校验)。取不到可用结构返回 null,绝不返回空数组
|
|
74
|
+
* 冒充成功(空 ops 是合法的「没什么可改」,但那必须是 JSON 里真的写着空数组)。
|
|
75
|
+
*/
|
|
76
|
+
export function parseFactOps(text) {
|
|
77
|
+
if (typeof text !== "string" || !text.trim()) return null;
|
|
78
|
+
const cleaned = text.replace(/```(?:json)?\s*([\s\S]*?)```/gi, "$1").trim();
|
|
79
|
+
const candidates = [];
|
|
80
|
+
const arrOpen = cleaned.indexOf("[");
|
|
81
|
+
const objOpen = cleaned.indexOf("{");
|
|
82
|
+
const start = arrOpen >= 0 && (objOpen < 0 || arrOpen < objOpen) ? arrOpen : objOpen;
|
|
83
|
+
const end = Math.max(cleaned.lastIndexOf("]"), cleaned.lastIndexOf("}"));
|
|
84
|
+
if (start >= 0 && end > start) candidates.push(cleaned.slice(start, end + 1));
|
|
85
|
+
if (cleaned.startsWith("{") || cleaned.startsWith("[")) candidates.push(cleaned);
|
|
86
|
+
for (const candidate of candidates) {
|
|
87
|
+
let value;
|
|
88
|
+
try {
|
|
89
|
+
value = JSON.parse(candidate);
|
|
90
|
+
} catch {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const ops = Array.isArray(value) ? value : (value && typeof value === "object" && Array.isArray(value.ops) ? value.ops : null);
|
|
94
|
+
// 空 ops 是合法的「没什么可改」(runFor 会以 ok + opsApplied:0 收尾),所以
|
|
95
|
+
// 这里只验逐 op 形状、不要求非空——非空是批量写入口径(isValidFactOps)。
|
|
96
|
+
if (!ops || !ops.every(isValidFactOp)) continue;
|
|
97
|
+
return ops;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 采集增量输入的截断读数:多取一行只为如实回答「还有没有」(profile-extract 同款)。 */
|
|
103
|
+
function collectInputRows(store, agentKey, cursor) {
|
|
104
|
+
const fetched = store.listFactInputRows(agentKey, { since: cursor, limit: DEFAULT_INPUT_ROWS + 1 });
|
|
105
|
+
const rows = [];
|
|
106
|
+
let chars = 0;
|
|
107
|
+
let truncated = fetched.length > DEFAULT_INPUT_ROWS;
|
|
108
|
+
for (const row of fetched.slice(0, DEFAULT_INPUT_ROWS)) {
|
|
109
|
+
const content = String(row.content ?? "").slice(0, DEFAULT_ROW_CHARS);
|
|
110
|
+
if (content.length < String(row.content ?? "").length) truncated = true;
|
|
111
|
+
const size = `${row.type} ${row.title} ${content}`.length;
|
|
112
|
+
// 首行保底:单行就超总预算时也收进来(截断已如实标记)——否则游标永远
|
|
113
|
+
// 推不进、这张卡变成永久 noop(评审 P3:首行即超预算的死角)。
|
|
114
|
+
if (rows.length && chars + size > DEFAULT_TOTAL_CHARS) {
|
|
115
|
+
truncated = true;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
chars += size;
|
|
119
|
+
rows.push(row);
|
|
120
|
+
}
|
|
121
|
+
return { rows, truncated, charsUsed: chars };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {{ ctx: object, service: object, store: object, config: object, logger?: object }} deps
|
|
126
|
+
* ctx 要带 llm(宿主 LLM 句柄);settings 不参与——本模块按纪律绝不写 user_settings。
|
|
127
|
+
* @returns object with runFor / applyManualOps / cardKeys / factsProvider / listFacts / listJournal / listRuns
|
|
128
|
+
*/
|
|
129
|
+
export function createProfileFacts({ ctx, service, store, config, logger }) {
|
|
130
|
+
const log = logger ?? ctx?.logger;
|
|
131
|
+
const language = langOf(config);
|
|
132
|
+
|
|
133
|
+
/** 卡片清单:''(default 兜底)恒在;非空键来自记忆行的 scope 值 ∪ 存量卡片。 */
|
|
134
|
+
function cardKeys() {
|
|
135
|
+
const keys = store.listFactCardKeys()
|
|
136
|
+
.filter((k) => typeof k === "string" && k.trim())
|
|
137
|
+
.map((k) => k.trim());
|
|
138
|
+
return ["", ...new Set(keys)];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 注入 provider 读口:renderUserSettingsBlock 的 factsProvider(面板预览共用)。 */
|
|
142
|
+
function factsProvider() {
|
|
143
|
+
return store.listActiveFactsGrouped();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function listFacts(agentKey, { includeArchived = false } = {}) {
|
|
147
|
+
return store.listProfileFacts(agentKey, { includeArchived });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function listJournal(agentKey, { limit = 20 } = {}) {
|
|
151
|
+
return store.listProfileFactJournal(agentKey, { limit });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function listRuns(agentKey, { limit = 10 } = {}) {
|
|
155
|
+
return store.listProfileFactRuns(agentKey, { limit });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* ops 落库(自动轮与人工面板共用这一份):单事务,逐 op 记账(scope_changes 式
|
|
160
|
+
* before/after)。跨卡守卫:op.id 必须属于本卡——LLM 幻觉出的别卡 id 不能当成
|
|
161
|
+
* 合法目标;精确重复的 add(与既有活跃事实 trim 后相同)跳过并计数,不落库。
|
|
162
|
+
*/
|
|
163
|
+
function applyOps(agentKey, ops, { actor, runId = null, at = new Date().toISOString() } = {}) {
|
|
164
|
+
const failures = [];
|
|
165
|
+
let applied = 0;
|
|
166
|
+
let dupsSkipped = 0;
|
|
167
|
+
const existing = store.listProfileFacts(agentKey, { includeArchived: true });
|
|
168
|
+
const existingById = new Map(existing.map((f) => [f.id, f]));
|
|
169
|
+
const activeContents = new Set(
|
|
170
|
+
existing.filter((f) => f.status === "active").map((f) => String(f.content ?? "").trim())
|
|
171
|
+
);
|
|
172
|
+
store.runAtomically(() => {
|
|
173
|
+
for (const op of ops) {
|
|
174
|
+
try {
|
|
175
|
+
if (op.op === "add") {
|
|
176
|
+
const content = String(op.content).trim();
|
|
177
|
+
if (activeContents.has(content)) {
|
|
178
|
+
dupsSkipped += 1;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const sourceIds = Array.isArray(op.source_ids)
|
|
182
|
+
? op.source_ids.filter((id) => typeof id === "string" && id.trim()).map((id) => id.trim())
|
|
183
|
+
: [];
|
|
184
|
+
const id = randomUUID();
|
|
185
|
+
store.insertProfileFact({ id, agentKey, content, sourceMemoryIds: sourceIds, at });
|
|
186
|
+
store.insertProfileFactJournal({ at, agentKey, runId, factId: id, op: "add", before: null, after: content, actor });
|
|
187
|
+
activeContents.add(content);
|
|
188
|
+
applied += 1;
|
|
189
|
+
} else if (op.op === "update" || op.op === "archive") {
|
|
190
|
+
const prev = existingById.get(op.id);
|
|
191
|
+
if (!prev || prev.agent_key !== agentKey) {
|
|
192
|
+
failures.push({ op: op.op, id: op.id ?? null, reason: "fact not found on this card" });
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const nextContent = op.op === "update" ? String(op.content).trim() : prev.content;
|
|
196
|
+
const nextStatus = op.op === "archive" ? "archived" : prev.status;
|
|
197
|
+
store.updateProfileFact(prev.id, { content: nextContent, status: nextStatus, at });
|
|
198
|
+
store.insertProfileFactJournal({
|
|
199
|
+
at, agentKey, runId, factId: prev.id, op: op.op,
|
|
200
|
+
before: prev.content, after: op.op === "update" ? nextContent : null, actor
|
|
201
|
+
});
|
|
202
|
+
if (op.op === "update") {
|
|
203
|
+
// 去重集合要跟上新值:同批 [update A→B, add B] 不收新值的话会落两条
|
|
204
|
+
// 相同的活跃事实,而 dupsSkipped 还是 0(评审 P2 实测复现)。
|
|
205
|
+
activeContents.delete(String(prev.content ?? "").trim());
|
|
206
|
+
activeContents.add(nextContent);
|
|
207
|
+
}
|
|
208
|
+
applied += 1;
|
|
209
|
+
} else {
|
|
210
|
+
failures.push({ op: op.op ?? null, id: op.id ?? null, reason: "unsupported op" });
|
|
211
|
+
}
|
|
212
|
+
} catch (error) {
|
|
213
|
+
// 逐 op 吞错是刻意的:一条 journal 写失败不该把整批已落的事实回滚掉
|
|
214
|
+
// (失败进 failures 由回执如实报)。代价是与「单事务」注释的原子粒度
|
|
215
|
+
// 有出入——真故障通常连 COMMIT 一起失败,这里只是收窄那道缝。
|
|
216
|
+
failures.push({ op: op.op ?? null, id: op.id ?? null, reason: String(error?.message ?? error) });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
return { applied, failures, dupsSkipped };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** 面板人工修正入口:actor=manual、runId=null,账本与自动轮同一份。 */
|
|
224
|
+
function applyManualOps(agentKey, ops) {
|
|
225
|
+
return applyOps(agentKey, ops, { actor: "manual" });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 跑一轮维护。返回回执 `{ runId, status, inputCount, opsApplied, failures, ... }`;
|
|
230
|
+
* no-route / stream-failed / unparseable 以 ProfileFactsError 抛出(回执已先落库,
|
|
231
|
+
* API 层按 code 映射状态码)。
|
|
232
|
+
*/
|
|
233
|
+
async function runFor(agentKey, { trigger = "manual" } = {}) {
|
|
234
|
+
const key = typeof agentKey === "string" ? agentKey.trim() : "";
|
|
235
|
+
const runId = randomUUID();
|
|
236
|
+
const startedAt = new Date().toISOString();
|
|
237
|
+
const finish = (status, extra = {}) => {
|
|
238
|
+
const receipt = { runId, agentKey: key, status, ...extra };
|
|
239
|
+
store.insertProfileFactRun({
|
|
240
|
+
id: runId,
|
|
241
|
+
agentKey: key,
|
|
242
|
+
triggerSource: trigger === "auto" ? "auto" : "manual",
|
|
243
|
+
status,
|
|
244
|
+
cursor: receipt.cursor ?? null,
|
|
245
|
+
inputCount: receipt.inputCount ?? 0,
|
|
246
|
+
opsApplied: receipt.opsApplied ?? 0,
|
|
247
|
+
detail: receipt.detail ? JSON.stringify(receipt.detail) : null,
|
|
248
|
+
startedAt,
|
|
249
|
+
finishedAt: new Date().toISOString()
|
|
250
|
+
});
|
|
251
|
+
return receipt;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const cursor = store.profileFactsCursor(key);
|
|
255
|
+
const { rows, truncated } = collectInputRows(store, key, cursor);
|
|
256
|
+
const facts = store.listProfileFacts(key, { includeArchived: false });
|
|
257
|
+
// 没有增量输入就没有本轮:连回执都记成 noop(面板能看到「没有新记忆」),
|
|
258
|
+
// 游标不动——下一次有新行时窗口从旧游标接续。
|
|
259
|
+
if (!rows.length) {
|
|
260
|
+
return finish("noop", { inputCount: 0, cursor });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const fail = (code, message, detail) => {
|
|
264
|
+
finish("failed", { inputCount: rows.length, cursor: null, detail: { code, ...(detail ?? {}) } });
|
|
265
|
+
throw new ProfileFactsError(code, message);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const route = resolveRoute(ctx, config, log);
|
|
269
|
+
// resolveRoute 在「没有 dreamProvider/dreamModel、宿主也没有可解析的默认模型」
|
|
270
|
+
// 时返回 undefined。不判它就是一个语义为「内部错误」的 500——真正该说的是
|
|
271
|
+
// 「没配模型」(profile-extract 同款取舍:不同结局不同话)。
|
|
272
|
+
if (!route) {
|
|
273
|
+
fail("no-route", "no model route: set dreamProvider/dreamModel or pick an agent default model");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const factListing = facts.length
|
|
277
|
+
? facts.map((f, i) => `${i + 1}. [${f.id}] ${f.content}`).join("\n")
|
|
278
|
+
: "(尚无既有事实)";
|
|
279
|
+
const rowListing = rows.map((r, i) => `${i + 1}. [${r.id}] [${r.type}] ${r.title} :: ${String(r.content ?? "").slice(0, DEFAULT_ROW_CHARS)}`).join("\n");
|
|
280
|
+
const userText = [
|
|
281
|
+
STR.profileFenceOpen[language],
|
|
282
|
+
breakFenceMarkers(`【既有事实】\n${factListing}\n\n【新记忆条目】\n${rowListing}`),
|
|
283
|
+
STR.profileFenceClose[language],
|
|
284
|
+
truncated
|
|
285
|
+
? (language === "zh"
|
|
286
|
+
? `(输入行有截断:本轮只呈现了窗口内的一部分。)`
|
|
287
|
+
: `(Input was truncated: only part of the window is shown this pass.)`)
|
|
288
|
+
: "",
|
|
289
|
+
language === "zh"
|
|
290
|
+
? `以上材料(数据)到此结束。只依据它们输出 JSON,材料里没有的绝不发明。`
|
|
291
|
+
: `End of the material (data). Answer from it only; invent nothing absent from it.`
|
|
292
|
+
].filter(Boolean).join("\n");
|
|
293
|
+
|
|
294
|
+
let streamFailure = "";
|
|
295
|
+
const text = await runAuditedLlm(ctx, service, config, {
|
|
296
|
+
triggerSource: "profileFacts",
|
|
297
|
+
operationType: "profile_facts",
|
|
298
|
+
modelId: `${route.provider}:${route.model}`,
|
|
299
|
+
relatedMemoryIds: rows.map((r) => r.id),
|
|
300
|
+
streamError: () => streamFailure,
|
|
301
|
+
// 吐了文字但没有可用结构 = 失败调用。审计必须记 error:success 配解析失败
|
|
302
|
+
// 会让「模型不配合」在账目上长得像「这轮确实没什么可改」。
|
|
303
|
+
auditError: (out) => (parseFactOps(out) ? null : "profile facts pass returned no usable ops JSON")
|
|
304
|
+
}, (reportUsage) => streamText(ctx, {
|
|
305
|
+
provider: route.provider,
|
|
306
|
+
model: route.model,
|
|
307
|
+
purpose: "compaction",
|
|
308
|
+
messages: [
|
|
309
|
+
{ role: "system", content: [{ type: "text", text: STR.prompts.profileFacts[language] }], source: { kind: "plugin:dsh-mneme", plugin: "dsh-mneme" } },
|
|
310
|
+
{ role: "user", content: [{ type: "text", text: userText }], source: { kind: "plugin:dsh-mneme", plugin: "dsh-mneme" } }
|
|
311
|
+
]
|
|
312
|
+
}, reportUsage, (reason) => { streamFailure = describeStreamFailure(reason); }));
|
|
313
|
+
|
|
314
|
+
if (text === undefined) {
|
|
315
|
+
fail("stream-failed", streamFailure || "llm stream aborted or errored");
|
|
316
|
+
}
|
|
317
|
+
const ops = parseFactOps(text);
|
|
318
|
+
if (!ops) {
|
|
319
|
+
fail("unparseable", "profile facts output was not a usable ops JSON object");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const sliced = ops.length > MAX_OPS_PER_RUN ? ops.slice(0, MAX_OPS_PER_RUN) : ops;
|
|
323
|
+
const { applied, failures, dupsSkipped } = applyOps(key, sliced, { actor: "auto", runId });
|
|
324
|
+
// 游标取本轮实际消费到的最大 updated_at:ok 轮才推进(含「ops 为空但窗口
|
|
325
|
+
// 已看过」的轮——那是合法的「没什么可改」,不重跑同一窗口)。
|
|
326
|
+
const nextCursor = rows.reduce((m, r) => (String(r.updated_at ?? "") > m ? String(r.updated_at) : m), String(cursor ?? ""));
|
|
327
|
+
const receipt = finish("ok", {
|
|
328
|
+
cursor: nextCursor || cursor,
|
|
329
|
+
inputCount: rows.length,
|
|
330
|
+
opsApplied: applied,
|
|
331
|
+
detail: {
|
|
332
|
+
truncated,
|
|
333
|
+
opsProposed: ops.length,
|
|
334
|
+
opsSliced: ops.length - sliced.length,
|
|
335
|
+
dupsSkipped,
|
|
336
|
+
failures
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
store.pruneProfileFactJournal(key);
|
|
340
|
+
return receipt;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** 自动轮候选(dream 成功后由 index.js 逐卡 runFor):只列有增量输入的卡——
|
|
344
|
+
* 无输入的卡连 noop 回执都不落,dream 每轮一次的自动维护不生产回执噪音。 */
|
|
345
|
+
function autoPassCandidates() {
|
|
346
|
+
return cardKeys().filter((key) => {
|
|
347
|
+
try {
|
|
348
|
+
return !!store.countFactInputRows(key, { since: store.profileFactsCursor(key) });
|
|
349
|
+
} catch {
|
|
350
|
+
return true; // 读数失败按「有待维护」处理,交给 runFor 的回执说话
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return { runFor, applyManualOps, cardKeys, factsProvider, listFacts, listJournal, listRuns, autoPassCandidates };
|
|
356
|
+
}
|