hippo-memory 1.59.0 → 1.61.0
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/README.md +1 -1
- package/dist/ablation.js +9 -27
- package/dist/agent-memories/apply.js +4 -1
- package/dist/agent-memories/claude-code.js +1 -1
- package/dist/agent-memories/codex.js +1 -1
- package/dist/agent-memories/gemini.js +1 -1
- package/dist/agent-memories/legacy.js +1 -1
- package/dist/agent-memories/source.js +1 -1
- package/dist/agent-memories/sync.js +8 -3
- package/dist/ambient-store.d.ts +14 -0
- package/dist/ambient-store.js +90 -0
- package/dist/ambient.d.ts +23 -0
- package/dist/ambient.js +72 -50
- package/dist/api/assemble.d.ts +93 -0
- package/dist/api/assemble.js +152 -0
- package/dist/api/audit.d.ts +17 -0
- package/dist/api/audit.js +23 -0
- package/dist/api/auth.d.ts +79 -0
- package/dist/api/auth.js +178 -0
- package/dist/api/context-types.d.ts +105 -0
- package/dist/api/context-types.js +3 -0
- package/dist/api/context.d.ts +31 -0
- package/dist/api/context.js +705 -0
- package/dist/api/dormant.d.ts +30 -0
- package/dist/api/dormant.js +140 -0
- package/dist/api/drill-down.d.ts +84 -0
- package/dist/api/drill-down.js +123 -0
- package/dist/api/forget.d.ts +57 -0
- package/dist/api/forget.js +87 -0
- package/dist/api/goals.d.ts +18 -0
- package/dist/api/goals.js +33 -0
- package/dist/api/learn.d.ts +31 -0
- package/dist/api/learn.js +88 -0
- package/dist/api/outcome.d.ts +61 -0
- package/dist/api/outcome.js +66 -0
- package/dist/api/promote.d.ts +54 -0
- package/dist/api/promote.js +203 -0
- package/dist/api/quarantine.d.ts +24 -0
- package/dist/api/quarantine.js +121 -0
- package/dist/api/recall-types.d.ts +390 -0
- package/dist/api/recall-types.js +3 -0
- package/dist/api/recall.d.ts +36 -0
- package/dist/api/recall.js +634 -0
- package/dist/api/remember.d.ts +35 -0
- package/dist/api/remember.js +43 -0
- package/dist/api/sleep.d.ts +136 -0
- package/dist/api/sleep.js +271 -0
- package/dist/api/tokens.d.ts +26 -0
- package/dist/api/tokens.js +60 -0
- package/dist/api/types.d.ts +56 -0
- package/dist/api/types.js +38 -0
- package/dist/api.d.ts +20 -1260
- package/dist/api.js +25 -2711
- package/dist/audit.d.ts +3 -0
- package/dist/audit.js +7 -3
- package/dist/auth.d.ts +45 -4
- package/dist/auth.js +125 -48
- package/dist/autolearn.js +7 -4
- package/dist/capture/command.d.ts +33 -0
- package/dist/capture/command.js +264 -0
- package/dist/capture/compact.d.ts +44 -0
- package/dist/capture/compact.js +354 -0
- package/dist/capture/extract.d.ts +21 -0
- package/dist/capture/extract.js +464 -0
- package/dist/capture/transcript.d.ts +40 -0
- package/dist/capture/transcript.js +193 -0
- package/dist/capture-error.js +2 -1
- package/dist/churn-git.js +4 -2
- package/dist/cli/audit.d.ts +3 -0
- package/dist/cli/audit.js +159 -0
- package/dist/cli/auth.d.ts +2 -0
- package/dist/cli/auth.js +171 -0
- package/dist/cli/briefs.d.ts +4 -0
- package/dist/cli/briefs.js +435 -0
- package/dist/cli/card.d.ts +3 -0
- package/dist/cli/card.js +333 -0
- package/dist/cli/context.d.ts +15 -0
- package/dist/cli/context.js +366 -0
- package/dist/cli/continuity.d.ts +6 -0
- package/dist/cli/continuity.js +445 -0
- package/dist/cli/curate.d.ts +19 -0
- package/dist/cli/curate.js +555 -0
- package/dist/cli/dag.d.ts +5 -0
- package/dist/cli/dag.js +177 -0
- package/dist/cli/decisions.d.ts +4 -0
- package/dist/cli/decisions.js +528 -0
- package/dist/cli/eval.d.ts +5 -0
- package/dist/cli/eval.js +213 -0
- package/dist/cli/explain.d.ts +4 -0
- package/dist/cli/explain.js +150 -0
- package/dist/cli/goals.d.ts +2 -0
- package/dist/cli/goals.js +196 -0
- package/dist/cli/hook-blocks.d.ts +18 -0
- package/dist/cli/hook-blocks.js +233 -0
- package/dist/cli/init.d.ts +2 -0
- package/dist/cli/init.js +305 -0
- package/dist/cli/maintenance.d.ts +4 -0
- package/dist/cli/maintenance.js +179 -0
- package/dist/cli/playbooks.d.ts +4 -0
- package/dist/cli/playbooks.js +556 -0
- package/dist/cli/projects.js +1 -1
- package/dist/cli/recall.d.ts +7 -0
- package/dist/cli/recall.js +597 -0
- package/dist/cli/remember.d.ts +5 -0
- package/dist/cli/remember.js +442 -0
- package/dist/cli/serve.d.ts +5 -0
- package/dist/cli/serve.js +40 -0
- package/dist/cli/session-hooks.d.ts +29 -0
- package/dist/cli/session-hooks.js +637 -0
- package/dist/cli/setup.d.ts +4 -0
- package/dist/cli/setup.js +376 -0
- package/dist/cli/shared.d.ts +15 -19
- package/dist/cli/shared.js +50 -341
- package/dist/cli/slack.d.ts +2 -0
- package/dist/cli/slack.js +171 -0
- package/dist/cli/status.d.ts +18 -0
- package/dist/cli/status.js +400 -0
- package/dist/cli/transfer.d.ts +10 -0
- package/dist/cli/transfer.js +438 -0
- package/dist/cli/usage.d.ts +85 -0
- package/dist/cli/usage.js +741 -0
- package/dist/cli.d.ts +319 -13
- package/dist/cli.js +436 -8580
- package/dist/client.js +15 -8
- package/dist/compaction-record.js +12 -9
- package/dist/config.js +12 -11
- package/dist/connectors/github/backfill.js +94 -87
- package/dist/connectors/github/cli-impl.js +5 -3
- package/dist/connectors/github/dlq.js +67 -54
- package/dist/connectors/github/ingest.js +34 -35
- package/dist/connectors/github/tenant-routing.js +3 -2
- package/dist/connectors/github/webhook.js +135 -216
- package/dist/connectors/slack/dlq.js +49 -61
- package/dist/connectors/slack/ingest.js +55 -48
- package/dist/connectors/slack/tenant-routing.js +4 -3
- package/dist/connectors/slack/webhook.js +72 -74
- package/dist/consolidate/conflicts.d.ts +10 -0
- package/dist/consolidate/conflicts.js +178 -0
- package/dist/consolidate/decay.d.ts +12 -0
- package/dist/consolidate/decay.js +145 -0
- package/dist/consolidate/llm-passes.d.ts +3 -0
- package/dist/consolidate/llm-passes.js +141 -0
- package/dist/consolidate/merge.d.ts +7 -0
- package/dist/consolidate/merge.js +251 -0
- package/dist/consolidate/physics-pass.d.ts +3 -0
- package/dist/consolidate/physics-pass.js +60 -0
- package/dist/consolidate/run.d.ts +69 -0
- package/dist/consolidate/run.js +76 -0
- package/dist/consolidate/sleep.d.ts +18 -0
- package/dist/consolidate/sleep.js +209 -0
- package/dist/consolidate/traces.d.ts +4 -0
- package/dist/consolidate/traces.js +178 -0
- package/dist/context-auto.js +7 -11
- package/dist/context-render.d.ts +1 -1
- package/dist/context-render.js +1 -1
- package/dist/customer-notes.d.ts +3 -0
- package/dist/customer-notes.js +6 -4
- package/dist/dag.js +13 -12
- package/dist/dashboard-actions.d.ts +20 -0
- package/dist/dashboard-actions.js +88 -0
- package/dist/dashboard-params.d.ts +45 -0
- package/dist/dashboard-params.js +127 -0
- package/dist/dashboard-queries.d.ts +15 -0
- package/dist/dashboard-queries.js +355 -0
- package/dist/dashboard-snapshot.d.ts +138 -0
- package/dist/dashboard-snapshot.js +308 -0
- package/dist/dashboard-types.d.ts +163 -0
- package/dist/dashboard-types.js +3 -0
- package/dist/dashboard.d.ts +6 -7
- package/dist/dashboard.js +231 -203
- package/dist/db/busy.d.ts +5 -0
- package/dist/db/busy.js +24 -0
- package/dist/db/continuity.d.ts +5 -0
- package/dist/db/continuity.js +145 -0
- package/dist/db/meta.d.ts +8 -0
- package/dist/db/meta.js +35 -0
- package/dist/db/migrate.d.ts +9 -0
- package/dist/db/migrate.js +138 -0
- package/dist/db/migrations/index.d.ts +5 -0
- package/dist/db/migrations/index.js +109 -0
- package/dist/db/migrations/types.d.ts +14 -0
- package/dist/db/migrations/types.js +2 -0
- package/dist/db/migrations/v01.d.ts +3 -0
- package/dist/db/migrations/v01.js +38 -0
- package/dist/db/migrations/v02.d.ts +3 -0
- package/dist/db/migrations/v02.js +21 -0
- package/dist/db/migrations/v03.d.ts +3 -0
- package/dist/db/migrations/v03.js +22 -0
- package/dist/db/migrations/v04.d.ts +3 -0
- package/dist/db/migrations/v04.js +28 -0
- package/dist/db/migrations/v05.d.ts +3 -0
- package/dist/db/migrations/v05.js +21 -0
- package/dist/db/migrations/v06.d.ts +3 -0
- package/dist/db/migrations/v06.js +25 -0
- package/dist/db/migrations/v07.d.ts +3 -0
- package/dist/db/migrations/v07.js +13 -0
- package/dist/db/migrations/v08.d.ts +3 -0
- package/dist/db/migrations/v08.js +8 -0
- package/dist/db/migrations/v09.d.ts +3 -0
- package/dist/db/migrations/v09.js +13 -0
- package/dist/db/migrations/v10.d.ts +3 -0
- package/dist/db/migrations/v10.js +17 -0
- package/dist/db/migrations/v11.d.ts +3 -0
- package/dist/db/migrations/v11.js +15 -0
- package/dist/db/migrations/v12.d.ts +3 -0
- package/dist/db/migrations/v12.js +11 -0
- package/dist/db/migrations/v13.d.ts +3 -0
- package/dist/db/migrations/v13.js +15 -0
- package/dist/db/migrations/v14.d.ts +3 -0
- package/dist/db/migrations/v14.js +66 -0
- package/dist/db/migrations/v15.d.ts +3 -0
- package/dist/db/migrations/v15.js +42 -0
- package/dist/db/migrations/v16.d.ts +3 -0
- package/dist/db/migrations/v16.js +61 -0
- package/dist/db/migrations/v17.d.ts +3 -0
- package/dist/db/migrations/v17.js +46 -0
- package/dist/db/migrations/v18.d.ts +3 -0
- package/dist/db/migrations/v18.js +60 -0
- package/dist/db/migrations/v19.d.ts +3 -0
- package/dist/db/migrations/v19.js +28 -0
- package/dist/db/migrations/v20.d.ts +3 -0
- package/dist/db/migrations/v20.js +42 -0
- package/dist/db/migrations/v21.d.ts +3 -0
- package/dist/db/migrations/v21.js +16 -0
- package/dist/db/migrations/v22.d.ts +3 -0
- package/dist/db/migrations/v22.js +82 -0
- package/dist/db/migrations/v23.d.ts +3 -0
- package/dist/db/migrations/v23.js +48 -0
- package/dist/db/migrations/v24.d.ts +3 -0
- package/dist/db/migrations/v24.js +72 -0
- package/dist/db/migrations/v25.d.ts +3 -0
- package/dist/db/migrations/v25.js +46 -0
- package/dist/db/migrations/v26.d.ts +3 -0
- package/dist/db/migrations/v26.js +23 -0
- package/dist/db/migrations/v27.d.ts +3 -0
- package/dist/db/migrations/v27.js +57 -0
- package/dist/db/migrations/v28.d.ts +3 -0
- package/dist/db/migrations/v28.js +38 -0
- package/dist/db/migrations/v29.d.ts +3 -0
- package/dist/db/migrations/v29.js +78 -0
- package/dist/db/migrations/v30.d.ts +3 -0
- package/dist/db/migrations/v30.js +92 -0
- package/dist/db/migrations/v31.d.ts +3 -0
- package/dist/db/migrations/v31.js +74 -0
- package/dist/db/migrations/v32.d.ts +3 -0
- package/dist/db/migrations/v32.js +102 -0
- package/dist/db/migrations/v33.d.ts +3 -0
- package/dist/db/migrations/v33.js +104 -0
- package/dist/db/migrations/v34.d.ts +3 -0
- package/dist/db/migrations/v34.js +93 -0
- package/dist/db/migrations/v35.d.ts +3 -0
- package/dist/db/migrations/v35.js +98 -0
- package/dist/db/migrations/v36.d.ts +3 -0
- package/dist/db/migrations/v36.js +98 -0
- package/dist/db/migrations/v37.d.ts +3 -0
- package/dist/db/migrations/v37.js +219 -0
- package/dist/db/migrations/v38.d.ts +3 -0
- package/dist/db/migrations/v38.js +277 -0
- package/dist/db/migrations/v39.d.ts +3 -0
- package/dist/db/migrations/v39.js +59 -0
- package/dist/db/migrations/v40.d.ts +3 -0
- package/dist/db/migrations/v40.js +74 -0
- package/dist/db/migrations/v41.d.ts +3 -0
- package/dist/db/migrations/v41.js +43 -0
- package/dist/db/migrations/v42.d.ts +3 -0
- package/dist/db/migrations/v42.js +41 -0
- package/dist/db/migrations/v43.d.ts +3 -0
- package/dist/db/migrations/v43.js +67 -0
- package/dist/db/migrations/v44.d.ts +3 -0
- package/dist/db/migrations/v44.js +28 -0
- package/dist/db/migrations/v45.d.ts +3 -0
- package/dist/db/migrations/v45.js +30 -0
- package/dist/db/migrations/v46.d.ts +3 -0
- package/dist/db/migrations/v46.js +25 -0
- package/dist/db/migrations/v47.d.ts +3 -0
- package/dist/db/migrations/v47.js +17 -0
- package/dist/db/migrations/v48.d.ts +3 -0
- package/dist/db/migrations/v48.js +10 -0
- package/dist/db/migrations/v49.d.ts +3 -0
- package/dist/db/migrations/v49.js +31 -0
- package/dist/db/migrations/v50.d.ts +3 -0
- package/dist/db/migrations/v50.js +67 -0
- package/dist/db/migrations/v51.d.ts +3 -0
- package/dist/db/migrations/v51.js +14 -0
- package/dist/db/migrations/v52.d.ts +3 -0
- package/dist/db/migrations/v52.js +7 -0
- package/dist/db/open.d.ts +23 -0
- package/dist/db/open.js +146 -0
- package/dist/db/sqlite.d.ts +21 -0
- package/dist/db/sqlite.js +8 -0
- package/dist/db/tables.d.ts +7 -0
- package/dist/db/tables.js +38 -0
- package/dist/db.d.ts +6 -46
- package/dist/db.js +5 -3036
- package/dist/decisions.d.ts +4 -1
- package/dist/decisions.js +9 -7
- package/dist/dedupe.js +3 -2
- package/dist/delivery-recorder.js +5 -1
- package/dist/doctor.js +4 -3
- package/dist/dormant.js +1 -0
- package/dist/embedding-provider.d.ts +1 -1
- package/dist/embedding-provider.js +7 -5
- package/dist/embeddings.d.ts +9 -52
- package/dist/embeddings.js +47 -298
- package/dist/env.d.ts +75 -0
- package/dist/env.js +119 -0
- package/dist/eval-suite.js +1 -1
- package/dist/eval.js +2 -2
- package/dist/extract.js +5 -4
- package/dist/gated-write.js +3 -1
- package/dist/goals.d.ts +3 -1
- package/dist/goals.js +18 -0
- package/dist/graph/read.d.ts +73 -0
- package/dist/graph/read.js +325 -0
- package/dist/graph/rows.d.ts +45 -0
- package/dist/graph/rows.js +51 -0
- package/dist/graph/types.d.ts +83 -0
- package/dist/graph/types.js +11 -0
- package/dist/graph/write.d.ts +93 -0
- package/dist/{graph.js → graph/write.js} +8 -391
- package/dist/graph-extract.js +2 -1
- package/dist/graph-recall.d.ts +1 -1
- package/dist/graph-recall.js +2 -2
- package/dist/graph-stream.js +1 -1
- package/dist/graph-view.d.ts +1 -1
- package/dist/graph-view.js +1 -1
- package/dist/half-life-migration.d.ts +1 -1
- package/dist/half-life-migration.js +2 -1
- package/dist/handoff.js +3 -0
- package/dist/hooks/codex-session.d.ts +8 -0
- package/dist/hooks/codex-session.js +76 -0
- package/dist/hooks/codex-wrapper.d.ts +55 -0
- package/dist/hooks/codex-wrapper.js +288 -0
- package/dist/hooks/json-hooks.d.ts +63 -0
- package/dist/hooks/json-hooks.js +356 -0
- package/dist/hooks/opencode.d.ts +50 -0
- package/dist/hooks/opencode.js +202 -0
- package/dist/hooks/shared.d.ts +54 -0
- package/dist/hooks/shared.js +77 -0
- package/dist/http-retry.d.ts +2 -0
- package/dist/http-retry.js +4 -3
- package/dist/http-util.d.ts +3 -0
- package/dist/http-util.js +10 -0
- package/dist/{importers.d.ts → importers/core.d.ts} +13 -17
- package/dist/importers/core.js +141 -0
- package/dist/importers/markdown-parse.d.ts +41 -0
- package/dist/importers/markdown-parse.js +132 -0
- package/dist/importers/markdown.d.ts +3 -0
- package/dist/importers/markdown.js +92 -0
- package/dist/importers/sources.d.ts +7 -0
- package/dist/importers/sources.js +229 -0
- package/dist/importers/vault.d.ts +11 -0
- package/dist/importers/vault.js +352 -0
- package/dist/incidents.d.ts +3 -0
- package/dist/incidents.js +8 -5
- package/dist/index.d.ts +25 -6
- package/dist/index.js +23 -6
- package/dist/invalidation.js +2 -1
- package/dist/judgment.js +7 -3
- package/dist/keyset.d.ts +13 -0
- package/dist/keyset.js +8 -0
- package/dist/local-embedding.d.ts +13 -0
- package/dist/local-embedding.js +165 -0
- package/dist/log.d.ts +7 -0
- package/dist/log.js +19 -1
- package/dist/mcp/admin-tools.d.ts +8 -0
- package/dist/mcp/admin-tools.js +116 -0
- package/dist/mcp/format.d.ts +27 -0
- package/dist/mcp/format.js +135 -0
- package/dist/mcp/memory-tools.d.ts +5 -0
- package/dist/mcp/memory-tools.js +83 -0
- package/dist/mcp/protocol.d.ts +83 -0
- package/dist/mcp/protocol.js +55 -0
- package/dist/mcp/recall-tools.d.ts +6 -0
- package/dist/mcp/recall-tools.js +320 -0
- package/dist/mcp/request.d.ts +10 -0
- package/dist/mcp/request.js +163 -0
- package/dist/mcp/server.d.ts +4 -70
- package/dist/mcp/server.js +8 -1172
- package/dist/mcp/session-state.d.ts +11 -0
- package/dist/mcp/session-state.js +28 -0
- package/dist/mcp/stdio.d.ts +8 -0
- package/dist/mcp/stdio.js +79 -0
- package/dist/mcp/tools.d.ts +11 -0
- package/dist/mcp/tools.js +247 -0
- package/dist/memory.d.ts +8 -3
- package/dist/memory.js +27 -1
- package/dist/multihop.d.ts +1 -1
- package/dist/multihop.js +2 -1
- package/dist/owner-validation.js +2 -1
- package/dist/physics-state.js +10 -7
- package/dist/policies.d.ts +3 -0
- package/dist/policies.js +8 -6
- package/dist/postinstall.js +3 -2
- package/dist/predictions/planning-fallacy.d.ts +100 -0
- package/dist/predictions/planning-fallacy.js +190 -0
- package/dist/{predictions.d.ts → predictions/store.d.ts} +7 -102
- package/dist/predictions/store.js +434 -0
- package/dist/processes.d.ts +3 -0
- package/dist/processes.js +8 -5
- package/dist/project-briefs.d.ts +3 -0
- package/dist/project-briefs.js +7 -5
- package/dist/project-identity.d.ts +1 -1
- package/dist/project-identity.js +12 -6
- package/dist/project-merge.js +3 -1
- package/dist/quarantine.d.ts +2 -1
- package/dist/quarantine.js +8 -5
- package/dist/raw-archive-mirror-cleanup.js +2 -1
- package/dist/raw-archive.js +1 -1
- package/dist/recall-history.js +3 -2
- package/dist/recall-pipeline.d.ts +2 -2
- package/dist/recall-pipeline.js +16 -8
- package/dist/recall-scope.js +1 -1
- package/dist/recall-trace.d.ts +3 -3
- package/dist/recall-trace.js +10 -14
- package/dist/refine-llm.js +20 -11
- package/dist/reject-flow.js +6 -1
- package/dist/rerankers/clef.d.ts +2 -2
- package/dist/rerankers/clef.js +72 -30
- package/dist/rerankers/cross-encoder.js +6 -4
- package/dist/rerankers/jev.d.ts +4 -2
- package/dist/rerankers/jev.js +24 -19
- package/dist/rerankers/llm.d.ts +4 -2
- package/dist/rerankers/llm.js +61 -41
- package/dist/rerankers/types.d.ts +1 -1
- package/dist/salience.js +1 -1
- package/dist/same-text.d.ts +2 -0
- package/dist/same-text.js +4 -0
- package/dist/scheduler.d.ts +1 -0
- package/dist/scheduler.js +26 -3
- package/dist/scope.js +4 -3
- package/dist/search/as-of.d.ts +10 -0
- package/dist/search/as-of.js +22 -0
- package/dist/search/bm25-search.d.ts +14 -0
- package/dist/search/bm25-search.js +43 -0
- package/dist/search/bm25.d.ts +15 -0
- package/dist/search/bm25.js +54 -0
- package/dist/search/boosts.d.ts +54 -0
- package/dist/search/boosts.js +94 -0
- package/dist/search/breakdown.d.ts +7 -0
- package/dist/search/breakdown.js +20 -0
- package/dist/search/explain.d.ts +25 -0
- package/dist/search/explain.js +31 -0
- package/dist/search/finalize.d.ts +8 -0
- package/dist/search/finalize.js +52 -0
- package/dist/search/fusion.d.ts +27 -0
- package/dist/search/fusion.js +42 -0
- package/dist/search/hybrid-score.d.ts +20 -0
- package/dist/search/hybrid-score.js +73 -0
- package/dist/search/hybrid.d.ts +46 -0
- package/dist/search/hybrid.js +64 -0
- package/dist/search/physics-search.d.ts +29 -0
- package/dist/search/physics-search.js +162 -0
- package/dist/search/rerank.d.ts +10 -0
- package/dist/search/rerank.js +72 -0
- package/dist/search/temporal.d.ts +15 -0
- package/dist/search/temporal.js +45 -0
- package/dist/search/types.d.ts +91 -0
- package/dist/search/types.js +2 -0
- package/dist/search/vector.d.ts +30 -0
- package/dist/search/vector.js +71 -0
- package/dist/secret-detect.d.ts +2 -0
- package/dist/secret-detect.js +3 -1
- package/dist/server/auth.d.ts +52 -0
- package/dist/server/auth.js +221 -0
- package/dist/server/client-ip.d.ts +25 -0
- package/dist/server/client-ip.js +92 -0
- package/dist/server/cursor.d.ts +23 -0
- package/dist/server/cursor.js +58 -0
- package/dist/server/lifecycle.d.ts +7 -0
- package/dist/server/lifecycle.js +29 -0
- package/dist/server/mcp-http.d.ts +5 -0
- package/dist/server/mcp-http.js +199 -0
- package/dist/server/request.d.ts +33 -0
- package/dist/server/request.js +103 -0
- package/dist/server/routes/admin.d.ts +9 -0
- package/dist/server/routes/admin.js +158 -0
- package/dist/server/routes/customer-notes.d.ts +7 -0
- package/dist/server/routes/customer-notes.js +112 -0
- package/dist/server/routes/decisions.d.ts +7 -0
- package/dist/server/routes/decisions.js +133 -0
- package/dist/server/routes/incidents.d.ts +7 -0
- package/dist/server/routes/incidents.js +126 -0
- package/dist/server/routes/memories.d.ts +10 -0
- package/dist/server/routes/memories.js +179 -0
- package/dist/server/routes/policies.d.ts +8 -0
- package/dist/server/routes/policies.js +151 -0
- package/dist/server/routes/predictions.d.ts +7 -0
- package/dist/server/routes/predictions.js +164 -0
- package/dist/server/routes/processes.d.ts +7 -0
- package/dist/server/routes/processes.js +161 -0
- package/dist/server/routes/project-briefs.d.ts +8 -0
- package/dist/server/routes/project-briefs.js +136 -0
- package/dist/server/routes/recall.d.ts +8 -0
- package/dist/server/routes/recall.js +340 -0
- package/dist/server/routes/skills.d.ts +8 -0
- package/dist/server/routes/skills.js +150 -0
- package/dist/server/types.d.ts +56 -0
- package/dist/server/types.js +2 -0
- package/dist/server/validation.d.ts +14 -0
- package/dist/server/validation.js +91 -0
- package/dist/server.d.ts +7 -61
- package/dist/server.js +92 -2368
- package/dist/session-digest.d.ts +1 -1
- package/dist/session-digest.js +9 -2
- package/dist/shared.d.ts +3 -1
- package/dist/shared.js +46 -40
- package/dist/skills.d.ts +3 -0
- package/dist/skills.js +7 -5
- package/dist/stdin.js +3 -2
- package/dist/store/audit-event.d.ts +19 -0
- package/dist/store/audit-event.js +33 -0
- package/dist/store/candidates.d.ts +42 -0
- package/dist/store/candidates.js +152 -0
- package/dist/store/conflicts.d.ts +44 -0
- package/dist/store/conflicts.js +444 -0
- package/dist/store/delete-and-batch.d.ts +63 -0
- package/dist/store/delete-and-batch.js +310 -0
- package/dist/store/entry-reads.d.ts +92 -0
- package/dist/store/entry-reads.js +255 -0
- package/dist/store/entry-row.d.ts +67 -0
- package/dist/store/entry-row.js +208 -0
- package/dist/store/entry-writes.d.ts +46 -0
- package/dist/store/entry-writes.js +148 -0
- package/dist/store/handoffs.d.ts +26 -0
- package/dist/store/handoffs.js +184 -0
- package/dist/store/index-and-stats.d.ts +52 -0
- package/dist/store/index-and-stats.js +214 -0
- package/dist/store/markdown.d.ts +10 -0
- package/dist/store/markdown.js +108 -0
- package/dist/store/mirrors.d.ts +49 -0
- package/dist/store/mirrors.js +312 -0
- package/dist/store/open.d.ts +15 -0
- package/dist/store/open.js +223 -0
- package/dist/store/rows.d.ts +178 -0
- package/dist/store/rows.js +158 -0
- package/dist/store/search-rows.d.ts +86 -0
- package/dist/store/search-rows.js +254 -0
- package/dist/store/sessions.d.ts +83 -0
- package/dist/store/sessions.js +272 -0
- package/dist/store/summaries.d.ts +94 -0
- package/dist/store/summaries.js +377 -0
- package/dist/store/tenant-lookup.d.ts +12 -0
- package/dist/store/tenant-lookup.js +16 -0
- package/dist/store-cards.js +2 -1
- package/dist/summary-dirty.d.ts +4 -0
- package/dist/summary-dirty.js +32 -0
- package/dist/support-bundle.js +4 -3
- package/dist/tenant.js +2 -2
- package/dist/token-ledger.js +1 -0
- package/dist/tokenize.d.ts +2 -0
- package/dist/tokenize.js +16 -0
- package/dist/transcript-tail.d.ts +7 -0
- package/dist/transcript-tail.js +48 -0
- package/dist/vector-store.d.ts +27 -0
- package/dist/vector-store.js +210 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/working-memory.js +1 -1
- package/dist/yaml.js +36 -11
- package/dist-ui/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dist-ui/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dist-ui/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dist-ui/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dist-ui/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/dist-ui/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
- package/dist-ui/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dist-ui/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dist-ui/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dist-ui/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dist-ui/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dist-ui/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dist-ui/assets/index-DPN7cP19.js +33 -0
- package/dist-ui/assets/index-dFloKRVr.css +1 -0
- package/dist-ui/index.html +3 -25
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/dist/capture.d.ts +0 -155
- package/dist/capture.js +0 -1293
- package/dist/consolidate.d.ts +0 -58
- package/dist/consolidate.js +0 -1123
- package/dist/graph.d.ts +0 -245
- package/dist/hooks.d.ts +0 -208
- package/dist/hooks.js +0 -1073
- package/dist/importers.js +0 -897
- package/dist/predictions.js +0 -620
- package/dist/search.d.ts +0 -320
- package/dist/search.js +0 -968
- package/dist/store.d.ts +0 -744
- package/dist/store.js +0 -3372
- package/dist-ui/assets/d3-BiWEKnn4.js +0 -1
- package/dist-ui/assets/index-BhT8RvO6.js +0 -61
- package/dist-ui/assets/index-RoXXJ5dq.css +0 -1
- package/dist-ui/assets/three-BDgTxR1l.js +0 -4112
package/dist/consolidate.js
DELETED
|
@@ -1,1123 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Consolidation engine ("Sleep") for Hippo.
|
|
3
|
-
*
|
|
4
|
-
* Steps:
|
|
5
|
-
* 1. Decay pass - remove entries below strength threshold
|
|
6
|
-
* 2. Merge pass - find episodic entries with high text overlap, create semantic summaries
|
|
7
|
-
* 3. Stats tracking
|
|
8
|
-
*/
|
|
9
|
-
import { evalNow, isRecallBoostAblated } from './ablation.js';
|
|
10
|
-
import { Layer, calculateStrength, canAutoDelete, createMemory, markRetrieved } from './memory.js';
|
|
11
|
-
import { loadAllEntries, batchWriteAndDelete, appendConsolidationRun, replaceDetectedConflicts, loadSessionDecayContext, incrementSleepCount, findPromotableSessions, traceExistsForSession, listSessionEvents, memoriesBackingObjects, } from './store.js';
|
|
12
|
-
import { tokenize } from './tokenize.js';
|
|
13
|
-
import { jaccardMinShared, overlapPartners } from './overlap-index.js';
|
|
14
|
-
import { compareEntryIdentity } from './compare.js';
|
|
15
|
-
import { duplicateKey, mergedText } from './same-text.js';
|
|
16
|
-
import { successorAfterRetirement } from './merged-row.js';
|
|
17
|
-
import { openHippoDb, closeHippoDb } from './db.js';
|
|
18
|
-
import { rejectionDigest, findRejectedValue } from './rejection.js';
|
|
19
|
-
import { countExpiredDormant, purgeExpiredDormant } from './dormant.js';
|
|
20
|
-
import { detectSecret } from './secret-detect.js';
|
|
21
|
-
import { loadPhysicsState, savePhysicsState, refreshParticleProperties } from './physics-state.js';
|
|
22
|
-
import { simulate } from './physics.js';
|
|
23
|
-
import { loadConfig } from './config.js';
|
|
24
|
-
import { sampleForReplay } from './replay.js';
|
|
25
|
-
import { renderTraceContent } from './trace.js';
|
|
26
|
-
import { resolveTenantId } from './tenant.js';
|
|
27
|
-
import { rescueSet, rankNonPinnedByTenant, validateWeights } from './memory-value.js';
|
|
28
|
-
import { MEMORY_VALUE_WEIGHTS, SOURCE_ARTIFACT_SHA256 } from './memory-value-weights.js';
|
|
29
|
-
import { appendAuditEvent, reportAuditWriteFailure } from './audit.js';
|
|
30
|
-
import { migrateDefaultHalfLife, LEGACY_TYPED_HALF_LIFE } from './half-life-migration.js';
|
|
31
|
-
import { derivationScope, commonDerivationScope, derivationPartitionKey } from './recall-scope.js';
|
|
32
|
-
import { isQuarantineScope } from './quarantine.js';
|
|
33
|
-
import { NO_MERGE_TAGS } from './shared.js';
|
|
34
|
-
const DECAY_THRESHOLD = 0.05;
|
|
35
|
-
const MERGE_OVERLAP_THRESHOLD = 0.35; // Jaccard similarity for "related"
|
|
36
|
-
const MERGE_MIN_CLUSTER = 2; // minimum cluster size to merge
|
|
37
|
-
const MERGE_MAX_SOURCES = 5; // with MERGE_MAX_CHARS, keeps a merged row near 500 tokens, a third of the 1,500-token context budget
|
|
38
|
-
const MERGE_MAX_CHARS = 2000; // total source text; sources past either cap stay unmerged and keep their half-life
|
|
39
|
-
// Half-life scale for merged source episodics. Demotion must go through
|
|
40
|
-
// half_life_days: calculateStrength() recomputes live strength from
|
|
41
|
-
// last_retrieved/half_life and never reads the stored strength field, so a
|
|
42
|
-
// stored-strength write is inert for ranking and gets overwritten by the
|
|
43
|
-
// next sleep's decay pass anyway.
|
|
44
|
-
const MERGE_SOURCE_HALF_LIFE_FACTOR = 0.3;
|
|
45
|
-
// Contradictions should be gated by content overlap, not shared tags. Tags like
|
|
46
|
-
// `feedback` / `policy` are too coarse and can make unrelated rules look like
|
|
47
|
-
// conflicts before the polarity heuristics run.
|
|
48
|
-
// Jaccard threshold on stopword-filtered tokens. Only applied after a polarity
|
|
49
|
-
// signal has already been detected (explicit pair or inferred negation), so
|
|
50
|
-
// this just filters out drive-by topic similarity, not semantic drift.
|
|
51
|
-
const CONFLICT_OVERLAP_THRESHOLD = 0.5;
|
|
52
|
-
// Minimum distinctive shared tokens before we trust an overlap score. Filters
|
|
53
|
-
// out cases where two memories share only common English + a project name.
|
|
54
|
-
const CONFLICT_MIN_RARE_SHARED = 2;
|
|
55
|
-
// Polarity is detected on the first N words only. A stray "not" in the middle
|
|
56
|
-
// of a long memory shouldn't flip the whole thing negative.
|
|
57
|
-
const POLARITY_WINDOW_WORDS = 40;
|
|
58
|
-
const CONFLICT_STOPWORDS = new Set([
|
|
59
|
-
'the', 'a', 'an', 'is', 'was', 'are', 'were', 'be', 'been', 'being', 'to', 'of', 'in',
|
|
60
|
-
'for', 'on', 'with', 'at', 'by', 'from', 'it', 'this', 'that', 'and', 'or', 'but', 'so',
|
|
61
|
-
'if', 'as', 'we', 'i', 'you', 'they', 'he', 'she', 'my', 'our', 'your', 'its', 'his',
|
|
62
|
-
'her', 'their', 'up', 'out', 'just', 'also', 'then', 'than', 'some', 'all', 'any',
|
|
63
|
-
'each', 'very', 'too', 'do', 'did', 'does', 'has', 'had', 'have', 'will', 'would',
|
|
64
|
-
'could', 'should', 'may', 'might', 'can', 'shall', 'when', 'where', 'what', 'which',
|
|
65
|
-
'who', 'how', 'why', 'there', 'here', 'about', 'into', 'over', 'after', 'before',
|
|
66
|
-
'between', 'through', 'during', 'against', 'within', 'without', 'toward', 'upon',
|
|
67
|
-
'more', 'most', 'less', 'least', 'other', 'such', 'same', 'new', 'old', 'one', 'two',
|
|
68
|
-
]);
|
|
69
|
-
const REPLAY_COUNT_DEFAULT = 5;
|
|
70
|
-
function keptAsWritten(entry) {
|
|
71
|
-
return entry.tags.some((tag) => NO_MERGE_TAGS.has(tag));
|
|
72
|
-
}
|
|
73
|
-
function isJsonString(value) {
|
|
74
|
-
return typeof value === 'string';
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Run a full consolidation pass.
|
|
78
|
-
*/
|
|
79
|
-
export async function consolidate(hippoRoot, options = {}) {
|
|
80
|
-
const now = options.now ?? evalNow(); // honors HIPPO_FAKE_NOW (eval-only; see ablation.ts)
|
|
81
|
-
const dryRun = options.dryRun ?? false;
|
|
82
|
-
const result = {
|
|
83
|
-
decayed: 0,
|
|
84
|
-
removed: 0,
|
|
85
|
-
dormant: 0,
|
|
86
|
-
dormantExpired: 0,
|
|
87
|
-
merged: 0,
|
|
88
|
-
semanticCreated: 0,
|
|
89
|
-
replayed: 0,
|
|
90
|
-
promotedTraces: 0,
|
|
91
|
-
tracesSkippedMixedScope: 0,
|
|
92
|
-
extractionCandidates: 0,
|
|
93
|
-
extracted: 0,
|
|
94
|
-
dagCandidateClusters: 0,
|
|
95
|
-
dagSummariesCreated: 0,
|
|
96
|
-
summariesRebuilt: 0,
|
|
97
|
-
summariesRebuildFailed: 0,
|
|
98
|
-
summariesZeroChildSkipped: 0,
|
|
99
|
-
summariesRebuildRefused: 0,
|
|
100
|
-
summariesRebuildCapped: false,
|
|
101
|
-
entityProfilesCreated: 0,
|
|
102
|
-
dryRun,
|
|
103
|
-
details: [],
|
|
104
|
-
physicsSimulated: 0,
|
|
105
|
-
};
|
|
106
|
-
// A changed default half-life moves memories still on the old base first,
|
|
107
|
-
// so this pass decays them at the new one (src/half-life-migration.ts).
|
|
108
|
-
const halfLife = migrateDefaultHalfLife(hippoRoot, loadConfig(hippoRoot).defaultHalfLifeDays, { dryRun });
|
|
109
|
-
if (halfLife.rescaled > 0) {
|
|
110
|
-
result.details.push(` ⏳ ${dryRun ? 'would move' : 'moved'} ${halfLife.rescaled} memories from the ${halfLife.from}-day to the ${halfLife.to}-day half-life`);
|
|
111
|
-
}
|
|
112
|
-
if (halfLife.typed > 0) {
|
|
113
|
-
result.details.push(` ⏳ ${dryRun ? 'would move' : 'moved'} ${halfLife.typed} memories of decisions, incidents and other objects from the ${LEGACY_TYPED_HALF_LIFE}-day to the ${halfLife.to}-day half-life`);
|
|
114
|
-
}
|
|
115
|
-
// L9: host-wide by design. Consolidation runs across all tenants in one
|
|
116
|
-
// pass — per-tenant filtering would create N consolidation runs per host
|
|
117
|
-
// with no cross-tenant dedup. The api.sleep audit row tags this with the
|
|
118
|
-
// admin synthetic actor; see api.ts:2050 for the rationale.
|
|
119
|
-
const all = loadAllEntries(hippoRoot);
|
|
120
|
-
if (dryRun)
|
|
121
|
-
for (const e of all)
|
|
122
|
-
e.half_life_days = halfLife.halfLives.get(e.id) ?? e.half_life_days;
|
|
123
|
-
const backingObjects = memoriesBackingObjects(hippoRoot);
|
|
124
|
-
// Retirable: auto-deletable (never pinned, raw or kept for good) and not backing a first-class object.
|
|
125
|
-
const retirable = (entry) => canAutoDelete(entry) && !backingObjects.has(entry.id);
|
|
126
|
-
const snapshot = new Map(structuredClone(all).map((e) => [e.id, e]));
|
|
127
|
-
// Load decay options from config + session context
|
|
128
|
-
const config = loadConfig(hippoRoot);
|
|
129
|
-
const sessionCtx = loadSessionDecayContext(hippoRoot);
|
|
130
|
-
const decayOpts = {
|
|
131
|
-
decayBasis: config.decayBasis,
|
|
132
|
-
avgSessionIntervalDays: sessionCtx.avgSessionIntervalDays,
|
|
133
|
-
sleepCount: sessionCtx.sleepCount,
|
|
134
|
-
};
|
|
135
|
-
// Collect all writes/deletes and batch them at the end
|
|
136
|
-
const pendingWrites = [];
|
|
137
|
-
const pendingDeletes = [];
|
|
138
|
-
const pendingDormant = [];
|
|
139
|
-
// A faded, unpinned, unrescued memory leaves active memory one of three
|
|
140
|
-
// ways. A raw receipt is append-only: trg_memories_raw_append_only aborts
|
|
141
|
-
// a DELETE, and with it this whole cycle's batch and every later sleep,
|
|
142
|
-
// so it stays where it is (stored strength refreshed) but sits out the
|
|
143
|
-
// rest of this cycle the way a deleted row would. Anything else goes
|
|
144
|
-
// dormant when config.dormant is on, and is deleted otherwise.
|
|
145
|
-
// Only called for rows `retirable` allows (never pinned, raw, kept for good or backing a first-class object).
|
|
146
|
-
const retireFaded = (entry, strength) => {
|
|
147
|
-
const why = `(strength ${strength.toFixed(4)} < ${DECAY_THRESHOLD})`;
|
|
148
|
-
// A faded secret is deleted, never kept dormant: keeping it would hold a
|
|
149
|
-
// credential on disk that the user reasonably expects forgetting removed.
|
|
150
|
-
if (config.dormant.enabled && !detectSecret(entry).flagged) {
|
|
151
|
-
result.dormant++;
|
|
152
|
-
result.details.push(` 💤 dormant ${entry.id} ${why}`);
|
|
153
|
-
pendingDormant.push({ entry: { ...entry, strength }, strength, reason: 'decay', dormantAt: now.toISOString() });
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
result.removed++;
|
|
157
|
-
result.details.push(` 🗑 removed ${entry.id} ${why}`);
|
|
158
|
-
pendingDeletes.push(entry.id);
|
|
159
|
-
};
|
|
160
|
-
// -------------------------------------------------------------------------
|
|
161
|
-
// 1. Decay pass
|
|
162
|
-
// -------------------------------------------------------------------------
|
|
163
|
-
// LC2-E3 (opt-in, default off; docs/plans/2026-08-10-lc2-e3-mv-wiring.md):
|
|
164
|
-
// flag OFF keeps the single-phase loop below byte-identical to pre-E3
|
|
165
|
-
// behavior (pre-registered gate G2). Flag ON restructures into two phases:
|
|
166
|
-
// phase 1 classifies every entry (condemned vs survivor) with ZERO
|
|
167
|
-
// commits; phase 2 runs rescueSet over the per-tenant candidate groups,
|
|
168
|
-
// then commits — rescued entries get the standard survivor bookkeeping
|
|
169
|
-
// refresh (stored strength + effective confidence; no half-life edits, no
|
|
170
|
-
// rank-derived writes) and are pushed to survivors so they fully
|
|
171
|
-
// participate in this cycle's merge/physics/conflict passes; non-rescued
|
|
172
|
-
// condemned entries follow the existing pendingDeletes/result.removed/
|
|
173
|
-
// details path.
|
|
174
|
-
const survivors = [];
|
|
175
|
-
let rescuedIds = new Set();
|
|
176
|
-
// Carried forward to the post-flush "4. Log run" section below, where the
|
|
177
|
-
// mv_rescue audit rows are actually written (code-review fix: writing them
|
|
178
|
-
// here, before batchWriteAndDelete, would assert rescues for a cycle whose
|
|
179
|
-
// effects might never land if a later phase throws).
|
|
180
|
-
let rescuedEntries = [];
|
|
181
|
-
let rankById = new Map();
|
|
182
|
-
if (config.memoryValue.enabled) {
|
|
183
|
-
// --- Phase 1: classify (zero commits) ---
|
|
184
|
-
const condemned = [];
|
|
185
|
-
const strengthById = new Map();
|
|
186
|
-
for (const entry of all) {
|
|
187
|
-
const strength = calculateStrength(entry, now, decayOpts);
|
|
188
|
-
strengthById.set(entry.id, strength);
|
|
189
|
-
if (retirable(entry) && strength < DECAY_THRESHOLD) {
|
|
190
|
-
condemned.push(entry);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
// --- Phase 2a: rescue decision (pure compute) ---
|
|
194
|
-
// Runs under --dry-run too (only the pendingDeletes flush and the audit write in
|
|
195
|
-
// "4. Log run" below stay !dryRun-gated), so the preview matches what a
|
|
196
|
-
// real run would decide.
|
|
197
|
-
const condemnedIds = new Set(condemned.map((e) => e.id));
|
|
198
|
-
// Fail-loud must not depend on condemnation traffic (round-2 code-review
|
|
199
|
-
// P2-2): validate the frozen weights constant unconditionally, even on a
|
|
200
|
-
// sleep with nothing condemned.
|
|
201
|
-
validateWeights();
|
|
202
|
-
if (condemnedIds.size > 0) {
|
|
203
|
-
// Compute the per-tenant ranking ONCE (round-2 code-review P2-2):
|
|
204
|
-
// rankById feeds both rescueSet's decision (via precomputedRanks,
|
|
205
|
-
// skipping its own internal rankNonPinnedByTenant call) and the
|
|
206
|
-
// detail/audit rank context below, so the whole-store ranking pass
|
|
207
|
-
// runs a single time per sleep instead of twice, and only when there
|
|
208
|
-
// is actually something condemned to rank against.
|
|
209
|
-
rankById = rankNonPinnedByTenant(all, now);
|
|
210
|
-
rescuedIds = rescueSet(all, condemnedIds, now, MEMORY_VALUE_WEIGHTS, SOURCE_ARTIFACT_SHA256, rankById);
|
|
211
|
-
// Review-round F2: entries with a non-finite computed feature (e.g. a
|
|
212
|
-
// malformed `created` string -> Date.parse NaN) score -Infinity in
|
|
213
|
-
// rankById and rescueSet's explicit finite-score guard means they can
|
|
214
|
-
// never be rescued. Surfaced here as one details warning line naming
|
|
215
|
-
// them, rather than leaving it a silent NaN-driven scoring detail.
|
|
216
|
-
const nonFiniteIds = [...rankById.entries()]
|
|
217
|
-
.filter(([, info]) => !Number.isFinite(info.score))
|
|
218
|
-
.map(([id]) => id);
|
|
219
|
-
if (nonFiniteIds.length > 0) {
|
|
220
|
-
result.details.push(` ⚠️ memory-value: skipped ${nonFiniteIds.length} entr${nonFiniteIds.length === 1 ? 'y' : 'ies'} ` +
|
|
221
|
-
`with non-finite computed features (never rescued): ${nonFiniteIds.join(', ')}`);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
// --- Phase 2b: commit, one pass over `all` in ITS ORIGINAL ORDER ---
|
|
225
|
-
// (review-round F4: rescued entries used to be appended at the tail of
|
|
226
|
-
// survivors, systematically starving them in downstream order-sensitive
|
|
227
|
-
// passes like extraction's slice(0,20) — a single pass over `all`
|
|
228
|
-
// preserves flag-off's ordering semantics exactly.)
|
|
229
|
-
for (const entry of all) {
|
|
230
|
-
const strength = strengthById.get(entry.id);
|
|
231
|
-
if (retirable(entry) && strength < DECAY_THRESHOLD) {
|
|
232
|
-
if (rescuedIds.has(entry.id)) {
|
|
233
|
-
// Rescued (D1): standard survivor stored-strength refresh (P2-1).
|
|
234
|
-
// Confidence is left alone here: it is an epistemic tier, not a
|
|
235
|
-
// cached computation, so resolveConfidence derives it on read.
|
|
236
|
-
const updated = { ...entry, strength };
|
|
237
|
-
survivors.push(updated);
|
|
238
|
-
if (!dryRun && strength !== entry.strength) {
|
|
239
|
-
pendingWrites.push(updated);
|
|
240
|
-
}
|
|
241
|
-
result.decayed++;
|
|
242
|
-
rescuedEntries.push(updated);
|
|
243
|
-
const rank = rankById.get(entry.id);
|
|
244
|
-
const rankNote = rank
|
|
245
|
-
? ` - rescued (rank ${rank.rank}/${rank.totalNonPinned} in tenant ${rank.tenantId}, top ${rank.keepN})`
|
|
246
|
-
: ' - rescued';
|
|
247
|
-
result.details.push(` 🛟 ${entry.id} (strength ${strength.toFixed(4)} < ${DECAY_THRESHOLD})${rankNote}`);
|
|
248
|
-
}
|
|
249
|
-
else {
|
|
250
|
-
retireFaded(entry, strength);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
const updated = { ...entry, strength };
|
|
255
|
-
survivors.push(updated);
|
|
256
|
-
if (!dryRun && strength !== entry.strength) {
|
|
257
|
-
pendingWrites.push(updated);
|
|
258
|
-
}
|
|
259
|
-
result.decayed++;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
else {
|
|
264
|
-
for (const entry of all) {
|
|
265
|
-
const strength = calculateStrength(entry, now, decayOpts);
|
|
266
|
-
if (retirable(entry) && strength < DECAY_THRESHOLD) {
|
|
267
|
-
retireFaded(entry, strength);
|
|
268
|
-
}
|
|
269
|
-
else {
|
|
270
|
-
// Only strength is a cached computation; confidence stays as stored.
|
|
271
|
-
const updated = { ...entry, strength };
|
|
272
|
-
survivors.push(updated);
|
|
273
|
-
if (!dryRun && strength !== entry.strength) {
|
|
274
|
-
pendingWrites.push(updated);
|
|
275
|
-
}
|
|
276
|
-
result.decayed++;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
// AT1 rejection-guard db handle (docs/plans/2026-08-15-at1-rejected-value-tombstone.md):
|
|
281
|
-
// covers BOTH the auto-promote pass (1.4, immediately below) and the merge
|
|
282
|
-
// pass (3, further down) — both build deterministic content that
|
|
283
|
-
// batchWriteAndDelete writes through the guard's bypass, so both need a
|
|
284
|
-
// producer-side tombstone check before pushing to pendingWrites.
|
|
285
|
-
//
|
|
286
|
-
// T3 fix (2026-08-15 hardening pass, perf hygiene): memoized lazy getter,
|
|
287
|
-
// not an eager open. The handle only serves these two tombstone checks —
|
|
288
|
-
// a sleep with zero promotable sessions and zero merge clusters never
|
|
289
|
-
// reaches either use site, so opening it unconditionally on every
|
|
290
|
-
// non-dry-run sleep paid a db-open cost for nothing. dryRun still never
|
|
291
|
-
// opens (getConsolidateDb short-circuits before touching the handle).
|
|
292
|
-
// Every `if (consolidateDb)` guard below becomes `const consolidateDb =
|
|
293
|
-
// getConsolidateDb();` at its use site — same semantics, opened on first
|
|
294
|
-
// real use. consolidateDbOpened (not just a truthy handle check) is the
|
|
295
|
-
// single source of truth for "was this ever opened", so the finally below
|
|
296
|
-
// closes it exactly once and never double-opens.
|
|
297
|
-
//
|
|
298
|
-
// AT1 P2 fix (codex, handle-leak restructure): the getter's lifetime must
|
|
299
|
-
// start IMMEDIATELY before the try whose finally closes it, covering every
|
|
300
|
-
// phase that can touch it — not just the merge pass. Previously the
|
|
301
|
-
// try/finally wrapped only section 3 (merge pass); an exception thrown by
|
|
302
|
-
// auto-promote (1.4), replay (1.5), batch extraction (1.6), the DAG
|
|
303
|
-
// passes (1.7-1.9), or physics (2) would propagate past an open handle
|
|
304
|
-
// with nothing to close it. No behavior change on the happy path — each
|
|
305
|
-
// of those phases already best-effort catches its own exceptions today
|
|
306
|
-
// (physics has its own try/catch below; each DAG pass wraps its own
|
|
307
|
-
// dynamic import + call in try/catch) — this only closes the handle on
|
|
308
|
-
// the rare path where one of them throws past its own catch. Deliberately
|
|
309
|
-
// NOT re-indented (mechanical wrap only, kept surgical): every statement
|
|
310
|
-
// between this try and its finally (below, at the end of the merge pass)
|
|
311
|
-
// stays at its original indentation.
|
|
312
|
-
let consolidateDbHandle = null;
|
|
313
|
-
let consolidateDbOpened = false;
|
|
314
|
-
const getConsolidateDb = () => {
|
|
315
|
-
if (dryRun)
|
|
316
|
-
return null;
|
|
317
|
-
if (!consolidateDbOpened) {
|
|
318
|
-
consolidateDbHandle = openHippoDb(hippoRoot);
|
|
319
|
-
consolidateDbOpened = true;
|
|
320
|
-
}
|
|
321
|
-
return consolidateDbHandle;
|
|
322
|
-
};
|
|
323
|
-
// Declared here (not at the merge pass, its point of use) so it survives
|
|
324
|
-
// this try/finally — a `let` declared INSIDE the try would go out of
|
|
325
|
-
// scope before the `if (mergesSkippedRejected > 0)` check that reads it
|
|
326
|
-
// after the finally closes the handle.
|
|
327
|
-
let mergesSkippedRejected = 0;
|
|
328
|
-
try {
|
|
329
|
-
// -------------------------------------------------------------------------
|
|
330
|
-
// 1.4. Auto-promote complete sessions to traces
|
|
331
|
-
// -------------------------------------------------------------------------
|
|
332
|
-
//
|
|
333
|
-
// For each session within the configured window that has a `session_complete`
|
|
334
|
-
// event and no existing trace (idempotency via the source_session_id column),
|
|
335
|
-
// render the action sequence as markdown and persist a Layer.Trace memory.
|
|
336
|
-
// Traces inherit decay, search, replay, and physics from the base MemoryEntry.
|
|
337
|
-
if (!dryRun && config.autoTraceCapture !== false) {
|
|
338
|
-
let tracesSkippedRejected = 0;
|
|
339
|
-
const windowDays = config.autoTraceWindowDays ?? 7;
|
|
340
|
-
const sinceMs = now.getTime() - windowDays * 24 * 60 * 60 * 1000;
|
|
341
|
-
// Auto-trace currently runs in a single-tenant context (the env-resolved
|
|
342
|
-
// tenant for this process). Multi-tenant deployments that want
|
|
343
|
-
// consolidation across all tenants need a per-tenant loop layered on top
|
|
344
|
-
// of this — tracked in docs/plans/2026-05-02-continuity-tables-tenant-scope.md.
|
|
345
|
-
const consolidationTenant = resolveTenantId({});
|
|
346
|
-
const promotable = findPromotableSessions(hippoRoot, consolidationTenant, sinceMs);
|
|
347
|
-
for (const session of promotable) {
|
|
348
|
-
// Idempotency: skip if a trace for this session already exists.
|
|
349
|
-
if (traceExistsForSession(hippoRoot, consolidationTenant, session.session_id))
|
|
350
|
-
continue;
|
|
351
|
-
const events = listSessionEvents(hippoRoot, consolidationTenant, {
|
|
352
|
-
session_id: session.session_id,
|
|
353
|
-
limit: 1000,
|
|
354
|
-
});
|
|
355
|
-
// T7: a mixed-scope session would otherwise leak into one trace.
|
|
356
|
-
const sessionScope = commonDerivationScope(events.map((e) => e.scope));
|
|
357
|
-
if (!sessionScope.ok) {
|
|
358
|
-
result.tracesSkippedMixedScope++;
|
|
359
|
-
result.details.push(` ⏭ skipped session ${session.session_id}: events span mixed scopes`);
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
const completeEvent = events.find((e) => e.event_type === 'session_complete');
|
|
363
|
-
if (!completeEvent)
|
|
364
|
-
continue; // defence-in-depth; findPromotableSessions filters already.
|
|
365
|
-
const outcomeRaw = completeEvent.content;
|
|
366
|
-
if (outcomeRaw !== 'success' && outcomeRaw !== 'failure' && outcomeRaw !== 'partial') {
|
|
367
|
-
// Malformed terminal event — skip rather than crash the whole sleep.
|
|
368
|
-
continue;
|
|
369
|
-
}
|
|
370
|
-
const outcome = outcomeRaw;
|
|
371
|
-
const steps = events
|
|
372
|
-
.filter((e) => e.event_type !== 'session_complete')
|
|
373
|
-
.map((e) => ({ action: e.content, observation: '' }));
|
|
374
|
-
// SAFETY: session event metadata is a free-form Record<string, unknown>
|
|
375
|
-
// bag; summary is optional and is only trusted once isJsonString below
|
|
376
|
-
// confirms it is actually a string.
|
|
377
|
-
const summaryValue = completeEvent.metadata.summary;
|
|
378
|
-
const summary = isJsonString(summaryValue) ? summaryValue : '(untitled)';
|
|
379
|
-
const trace = createMemory(renderTraceContent({ task: summary, steps, outcome }), {
|
|
380
|
-
layer: Layer.Trace,
|
|
381
|
-
trace_outcome: outcome,
|
|
382
|
-
source_session_id: session.session_id,
|
|
383
|
-
tags: ['auto-promoted'],
|
|
384
|
-
source: 'auto-promote',
|
|
385
|
-
scope: sessionScope.scope,
|
|
386
|
-
// T1 fix (2026-08-15 hardening pass): stamp the trace into the SAME
|
|
387
|
-
// tenant the traceExistsForSession idempotency check (above) runs
|
|
388
|
-
// under. Before
|
|
389
|
-
// this, createMemory omitted tenantId and the trace always landed
|
|
390
|
-
// 'default' (memory.ts:535) while the idempotency check ran under
|
|
391
|
-
// consolidationTenant — for any non-default tenant that check never
|
|
392
|
-
// hit, and the trace regenerated every sleep.
|
|
393
|
-
tenantId: consolidationTenant,
|
|
394
|
-
baseHalfLifeDays: config.defaultHalfLifeDays,
|
|
395
|
-
});
|
|
396
|
-
// AT1 (same producer-side pattern as the merge pass below): traceExistsForSession
|
|
397
|
-
// only sees rows CURRENTLY in the store — once a rejected trace is
|
|
398
|
-
// removed, that idempotency check no longer blocks regeneration, and
|
|
399
|
-
// this write would otherwise reach batchWriteAndDelete's guard bypass
|
|
400
|
-
// unchecked, resurrecting it every sleep. Check under THE ENTRY'S OWN
|
|
401
|
-
// stamped tenantId (read off `trace` after createMemory — never guess
|
|
402
|
-
// the tenant) + the built content's digest. A hit skips the push
|
|
403
|
-
// entirely: not counted as promoted, not added to survivors.
|
|
404
|
-
const consolidateDb = getConsolidateDb();
|
|
405
|
-
if (consolidateDb) {
|
|
406
|
-
const traceDigest = rejectionDigest(trace.content);
|
|
407
|
-
const tombstone = findRejectedValue(consolidateDb, trace.tenantId, traceDigest);
|
|
408
|
-
if (tombstone) {
|
|
409
|
-
tracesSkippedRejected++;
|
|
410
|
-
try {
|
|
411
|
-
appendAuditEvent(consolidateDb, {
|
|
412
|
-
tenantId: trace.tenantId,
|
|
413
|
-
actor: 'sleep',
|
|
414
|
-
op: 'reject_refusal',
|
|
415
|
-
metadata: {
|
|
416
|
-
digest: traceDigest,
|
|
417
|
-
reason: tombstone.reason,
|
|
418
|
-
sourceSessionId: session.session_id,
|
|
419
|
-
},
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
catch (error) {
|
|
423
|
-
reportAuditWriteFailure('reject_refusal', String(error));
|
|
424
|
-
}
|
|
425
|
-
continue;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
pendingWrites.push(trace);
|
|
429
|
-
survivors.push(trace);
|
|
430
|
-
result.promotedTraces++;
|
|
431
|
-
result.details.push(` 🧬 promoted trace ${trace.id} from session ${session.session_id} (${outcome})`);
|
|
432
|
-
}
|
|
433
|
-
if (result.promotedTraces > 0) {
|
|
434
|
-
result.details.push(` 🧬 promoted ${result.promotedTraces} trace${result.promotedTraces === 1 ? '' : 's'} from completed session${result.promotedTraces === 1 ? '' : 's'}`);
|
|
435
|
-
}
|
|
436
|
-
if (tracesSkippedRejected > 0) {
|
|
437
|
-
console.error(`consolidate: skipped ${tracesSkippedRejected} auto-promoted trace(s) whose content matches a rejected value`);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
// -------------------------------------------------------------------------
|
|
441
|
-
// 1.5. Replay pass — rehearse high-value survivors
|
|
442
|
-
// -------------------------------------------------------------------------
|
|
443
|
-
//
|
|
444
|
-
// Biologically-inspired counterpart to hippocampal replay during slow-wave
|
|
445
|
-
// sleep: sample N memories weighted by outcome + valence + under-rehearsal
|
|
446
|
-
// + idle time, then apply the same retrieval-strengthening `markRetrieved`
|
|
447
|
-
// applies to real queries. Distinct from decay (removal), physics (motion),
|
|
448
|
-
// and merge (compression) — this is the "rehearse the important stuff so
|
|
449
|
-
// it doesn't fade" pass.
|
|
450
|
-
{
|
|
451
|
-
const replayCount = config.replay?.count ?? REPLAY_COUNT_DEFAULT;
|
|
452
|
-
// EVAL-ONLY ablation (see ablation.ts): replay rehearsal IS recall
|
|
453
|
-
// strengthening (same markRetrieved dynamics), so the strengthen-off arm
|
|
454
|
-
// silences the whole pass - markRetrieved would return unmutated entries
|
|
455
|
-
// and persisting them anyway would still refresh updated_at / mirrors.
|
|
456
|
-
if (replayCount > 0 && survivors.length > 0 && !isRecallBoostAblated()) {
|
|
457
|
-
const seed = Math.floor(now.getTime() / 1000) & 0xffffffff;
|
|
458
|
-
const picked = sampleForReplay(survivors, replayCount, now, seed);
|
|
459
|
-
if (picked.length > 0) {
|
|
460
|
-
const rehearsed = markRetrieved(picked, now);
|
|
461
|
-
const rehearsedById = new Map(rehearsed.map((e) => [e.id, e]));
|
|
462
|
-
// Update survivors in place so downstream passes see rehearsed state.
|
|
463
|
-
for (let i = 0; i < survivors.length; i++) {
|
|
464
|
-
const replacement = rehearsedById.get(survivors[i].id);
|
|
465
|
-
if (replacement)
|
|
466
|
-
survivors[i] = replacement;
|
|
467
|
-
}
|
|
468
|
-
result.replayed = rehearsed.length;
|
|
469
|
-
result.details.push(` 💭 replayed ${rehearsed.length} memor${rehearsed.length === 1 ? 'y' : 'ies'}: ` +
|
|
470
|
-
rehearsed.map((e) => e.id).join(', '));
|
|
471
|
-
if (!dryRun) {
|
|
472
|
-
for (const r of rehearsed)
|
|
473
|
-
pendingWrites.push(r);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
// -------------------------------------------------------------------------
|
|
479
|
-
// 1.6. Batch extraction — extract facts from episodic memories missing them
|
|
480
|
-
// -------------------------------------------------------------------------
|
|
481
|
-
const extractedFromIds = new Set(survivors.filter((e) => e.extracted_from).map((e) => e.extracted_from));
|
|
482
|
-
const extractionCandidates = survivors.filter((e) => e.layer === Layer.Episodic && !e.superseded_by && !extractedFromIds.has(e.id) && !keptAsWritten(e));
|
|
483
|
-
result.extractionCandidates = extractionCandidates.length;
|
|
484
|
-
// extraction.enabled=false is the opt-out for every LLM phase below, key or no key.
|
|
485
|
-
const apiKey = config.extraction.enabled !== false ? (process.env.ANTHROPIC_API_KEY ?? '') : '';
|
|
486
|
-
const llmErrorsSeen = new Set();
|
|
487
|
-
const llmError = (phase) => (msg) => {
|
|
488
|
-
const line = ` ⚠️ ${phase}: ${msg}`;
|
|
489
|
-
if (llmErrorsSeen.has(line))
|
|
490
|
-
return;
|
|
491
|
-
llmErrorsSeen.add(line);
|
|
492
|
-
result.details.push(line);
|
|
493
|
-
console.error(`consolidate ${phase}: ${msg}`);
|
|
494
|
-
};
|
|
495
|
-
const llmOpts = { apiKey, model: config.extraction.model, fetcher: options.fetcher };
|
|
496
|
-
if (apiKey && extractionCandidates.length > 0 && !dryRun) {
|
|
497
|
-
const { extractFacts, storeExtractedFacts } = await import('./extract.js');
|
|
498
|
-
const batchLimit = 20;
|
|
499
|
-
let extractedCount = 0;
|
|
500
|
-
for (const candidate of extractionCandidates.slice(0, batchLimit)) {
|
|
501
|
-
try {
|
|
502
|
-
const facts = await extractFacts(candidate.content, { ...llmOpts, onError: llmError('extraction') });
|
|
503
|
-
if (facts.length > 0) {
|
|
504
|
-
storeExtractedFacts(hippoRoot, candidate, facts);
|
|
505
|
-
extractedCount += facts.length;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
catch (err) {
|
|
509
|
-
llmError('extraction')(String(err));
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
result.extracted = extractedCount;
|
|
513
|
-
}
|
|
514
|
-
// -------------------------------------------------------------------------
|
|
515
|
-
// 1.7. DAG summarization — cluster extracted facts and generate summaries
|
|
516
|
-
// -------------------------------------------------------------------------
|
|
517
|
-
const extractedFacts = survivors.filter((e) => e.tags.includes('extracted') && e.dag_level === 1 && !e.superseded_by);
|
|
518
|
-
if (apiKey && extractedFacts.length >= 3 && !dryRun) {
|
|
519
|
-
try {
|
|
520
|
-
const { buildDag } = await import('./dag.js');
|
|
521
|
-
const dagResult = await buildDag(hippoRoot, extractedFacts, { ...llmOpts, onError: llmError('dag') });
|
|
522
|
-
result.dagCandidateClusters = dagResult.candidateClusters;
|
|
523
|
-
result.dagSummariesCreated = dagResult.summariesCreated;
|
|
524
|
-
if (dagResult.summariesCreated > 0) {
|
|
525
|
-
result.details.push(` 🌳 DAG: ${dagResult.summariesCreated} summaries created, ${dagResult.factsLinked} facts linked`);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
catch (err) {
|
|
529
|
-
llmError('dag')(String(err));
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
// -------------------------------------------------------------------------
|
|
533
|
-
// 1.8. DAG summary rebuild — drain dirty queue from E2's child-write hooks
|
|
534
|
-
// -------------------------------------------------------------------------
|
|
535
|
-
// Consumer of E2's summary_dirty flag. Walks dirty L2 summaries, regenerates
|
|
536
|
-
// each via generateDagSummary, atomically refreshes content + 6 metadata
|
|
537
|
-
// columns + clears summary_dirty (with FTS sync). Same apiKey/dryRun gate
|
|
538
|
-
// as buildDag above. Cap HIPPO_DAG_REBUILD_CAP (default 20, hard ceiling
|
|
539
|
-
// 1000) prevents runaway LLM cost.
|
|
540
|
-
if (apiKey && !dryRun) {
|
|
541
|
-
try {
|
|
542
|
-
const { rebuildDirtySummaries } = await import('./dag.js');
|
|
543
|
-
const rawCap = parseInt(process.env.HIPPO_DAG_REBUILD_CAP ?? '20', 10);
|
|
544
|
-
// R1 MED must-fix: hard ceiling so misconfigured env can't burn
|
|
545
|
-
// unbounded LLM cost.
|
|
546
|
-
const cap = Number.isFinite(rawCap) && rawCap > 0
|
|
547
|
-
? Math.min(rawCap, 1000)
|
|
548
|
-
: 20;
|
|
549
|
-
const rebuildResult = await rebuildDirtySummaries(hippoRoot, { ...llmOpts, onError: llmError('dag rebuild'), cap });
|
|
550
|
-
result.summariesRebuilt = rebuildResult.rebuilt;
|
|
551
|
-
result.summariesRebuildFailed = rebuildResult.failed;
|
|
552
|
-
result.summariesZeroChildSkipped = rebuildResult.zeroChildSkipped;
|
|
553
|
-
result.summariesRebuildRefused = rebuildResult.refused;
|
|
554
|
-
result.summariesRebuildCapped = rebuildResult.capped;
|
|
555
|
-
if (rebuildResult.rebuilt > 0 || rebuildResult.zeroChildSkipped > 0 || rebuildResult.failed > 0 || rebuildResult.refused > 0) {
|
|
556
|
-
const parts = [];
|
|
557
|
-
if (rebuildResult.rebuilt > 0)
|
|
558
|
-
parts.push(`${rebuildResult.rebuilt} rebuilt`);
|
|
559
|
-
if (rebuildResult.refused > 0)
|
|
560
|
-
parts.push(`${rebuildResult.refused} refused`);
|
|
561
|
-
if (rebuildResult.zeroChildSkipped > 0)
|
|
562
|
-
parts.push(`${rebuildResult.zeroChildSkipped} zero-child-skipped`);
|
|
563
|
-
if (rebuildResult.failed > 0)
|
|
564
|
-
parts.push(`${rebuildResult.failed} failed`);
|
|
565
|
-
if (rebuildResult.capped)
|
|
566
|
-
parts.push(`CAPPED@${cap}`);
|
|
567
|
-
result.details.push(` 🌳 DAG rebuild: ${parts.join(', ')}`);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
catch (err) {
|
|
571
|
-
llmError('dag rebuild')(String(err));
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
// -------------------------------------------------------------------------
|
|
575
|
-
// 1.9. DAG entity profiles — cluster L2 topic summaries into L3 profiles
|
|
576
|
-
// -------------------------------------------------------------------------
|
|
577
|
-
// E5 phase: aggregate per-entity L2 summaries (e.g. all the speaker:Alice
|
|
578
|
-
// topic summaries) into a single L3 entity profile. Runs even when phase
|
|
579
|
-
// 1.7 buildDag was skipped (re-clusters existing L2s every sleep).
|
|
580
|
-
//
|
|
581
|
-
// Uses loadAllL2Summaries (not `survivors`) because phase 1.7 wrote new
|
|
582
|
-
// L2s directly via writeEntry without pushing back into survivors.
|
|
583
|
-
if (apiKey && !dryRun) {
|
|
584
|
-
try {
|
|
585
|
-
const { buildEntityProfiles } = await import('./dag.js');
|
|
586
|
-
const { loadAllL2Summaries } = await import('./store.js');
|
|
587
|
-
const l2Summaries = loadAllL2Summaries(hippoRoot);
|
|
588
|
-
if (l2Summaries.length >= 2) {
|
|
589
|
-
const profileResult = await buildEntityProfiles(hippoRoot, l2Summaries, { ...llmOpts, onError: llmError('dag profiles') });
|
|
590
|
-
result.entityProfilesCreated = profileResult.profilesCreated;
|
|
591
|
-
if (profileResult.profilesCreated > 0) {
|
|
592
|
-
result.details.push(` 🌲 DAG L3: ${profileResult.profilesCreated} entity profiles, ${profileResult.l2sLinked} L2s linked`);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
catch (err) {
|
|
597
|
-
llmError('dag profiles')(String(err));
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
// -------------------------------------------------------------------------
|
|
601
|
-
// 2. Physics simulation pass
|
|
602
|
-
// -------------------------------------------------------------------------
|
|
603
|
-
if (!dryRun) {
|
|
604
|
-
try {
|
|
605
|
-
const physicsEnabled = config.physics.enabled === true
|
|
606
|
-
|| (config.physics.enabled === 'auto');
|
|
607
|
-
if (physicsEnabled) {
|
|
608
|
-
const db = openHippoDb(hippoRoot);
|
|
609
|
-
try {
|
|
610
|
-
const physicsMap = loadPhysicsState(db);
|
|
611
|
-
const particles = Array.from(physicsMap.values());
|
|
612
|
-
if (particles.length > 0) {
|
|
613
|
-
// Build entry lookup for property refresh
|
|
614
|
-
const entryMap = new Map(survivors.map(e => [e.id, e]));
|
|
615
|
-
refreshParticleProperties(particles, entryMap, now);
|
|
616
|
-
// Build conflict pairs from survivors
|
|
617
|
-
const conflictPairs = new Map();
|
|
618
|
-
for (const entry of survivors) {
|
|
619
|
-
if (entry.conflicts_with.length > 0) {
|
|
620
|
-
const set = conflictPairs.get(entry.id) ?? new Set();
|
|
621
|
-
for (const cid of entry.conflicts_with)
|
|
622
|
-
set.add(cid);
|
|
623
|
-
conflictPairs.set(entry.id, set);
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
// Build half-life lookup
|
|
627
|
-
const halfLives = new Map();
|
|
628
|
-
for (const entry of survivors) {
|
|
629
|
-
halfLives.set(entry.id, entry.half_life_days);
|
|
630
|
-
}
|
|
631
|
-
const ctx = {
|
|
632
|
-
conflictPairs,
|
|
633
|
-
halfLives,
|
|
634
|
-
config: config.physics,
|
|
635
|
-
};
|
|
636
|
-
const stats = simulate(particles, ctx);
|
|
637
|
-
savePhysicsState(db, particles);
|
|
638
|
-
result.physicsSimulated = stats.particleCount;
|
|
639
|
-
result.details.push(` ⚛️ physics: ${stats.particleCount} particles, ` +
|
|
640
|
-
`avg vel ${stats.avgVelocityMagnitude.toFixed(4)}, ` +
|
|
641
|
-
`energy ${stats.energy.total.toFixed(4)}`);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
finally {
|
|
645
|
-
closeHippoDb(db);
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
catch (error) {
|
|
650
|
-
result.details.push(` ⚠️ physics simulation skipped: ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
const byId = new Map(all.map((e) => [e.id, e]));
|
|
654
|
-
const rejectedIn = (tenantId) => (text) => {
|
|
655
|
-
const db = getConsolidateDb();
|
|
656
|
-
return db !== null && findRejectedValue(db, tenantId, rejectionDigest(text)) !== null;
|
|
657
|
-
};
|
|
658
|
-
for (let i = survivors.length - 1; i >= 0; i--) {
|
|
659
|
-
const row = survivors[i];
|
|
660
|
-
const successor = retirable(row) ? successorAfterRetirement(row, byId, rejectedIn(row.tenantId)) : undefined;
|
|
661
|
-
if (successor === undefined)
|
|
662
|
-
continue;
|
|
663
|
-
result.details.push(` ✂️ ${row.id} held a retired text${successor ? `, ${successor.id} holds the rest` : ''}`);
|
|
664
|
-
if (dryRun)
|
|
665
|
-
continue;
|
|
666
|
-
pendingDeletes.push(row.id);
|
|
667
|
-
if (successor) {
|
|
668
|
-
pendingWrites.push(successor);
|
|
669
|
-
survivors[i] = successor;
|
|
670
|
-
}
|
|
671
|
-
else {
|
|
672
|
-
survivors.splice(i, 1);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
// -------------------------------------------------------------------------
|
|
676
|
-
// 3. Merge pass - episodic entries only
|
|
677
|
-
// -------------------------------------------------------------------------
|
|
678
|
-
const alreadyMergedIds = new Set(survivors.flatMap((e) => e.parents));
|
|
679
|
-
const mergeCandidates = survivors.filter((e) => e.layer === Layer.Episodic && !e.superseded_by && !keptAsWritten(e) && !alreadyMergedIds.has(e.id)
|
|
680
|
-
&& !e.pinned // a pin merged with a look-alike would read as one of two values
|
|
681
|
-
&& tokenize(e.content).length > 0);
|
|
682
|
-
const used = new Set();
|
|
683
|
-
// T1 fix (2026-08-15 hardening pass): partition by tenantId BEFORE the
|
|
684
|
-
// overlap loop so a cluster can never span tenants. Previously textOverlap
|
|
685
|
-
// clustered across the whole host-wide `survivors` list with no tenant
|
|
686
|
-
// boundary, and mergeContents concatenated cross-tenant content into one
|
|
687
|
-
// row. Map preserves insertion order, so single-tenant stores (every row
|
|
688
|
-
// 'default') get exactly one partition and iterate in the same order as
|
|
689
|
-
// before this fix — byte-identical behavior there.
|
|
690
|
-
const mergeCandidatesByTenant = new Map();
|
|
691
|
-
for (const entry of mergeCandidates) {
|
|
692
|
-
const key = derivationPartitionKey(entry.tenantId, entry.scope, entry.origin_project);
|
|
693
|
-
const bucket = mergeCandidatesByTenant.get(key);
|
|
694
|
-
if (bucket)
|
|
695
|
-
bucket.push(entry);
|
|
696
|
-
else
|
|
697
|
-
mergeCandidatesByTenant.set(key, [entry]);
|
|
698
|
-
}
|
|
699
|
-
// AT1 consolidation-loop fix (docs/plans/2026-08-15-at1-rejected-value-tombstone.md):
|
|
700
|
-
// reuses the single consolidateDb handle opened once above (before the
|
|
701
|
-
// auto-promote pass, 1.4) for the whole non-dry-run consolidate — see that
|
|
702
|
-
// declaration's comment. Only needed for real writes — a dry-run preview
|
|
703
|
-
// never reaches batchWriteAndDelete's guard bypass, so there is nothing
|
|
704
|
-
// here for it to protect against. (mergesSkippedRejected itself is
|
|
705
|
-
// declared up at the try's opening above 1.4, not here — it has to
|
|
706
|
-
// survive the try/finally that now wraps this whole section; see the
|
|
707
|
-
// handle-leak restructure comment there.)
|
|
708
|
-
for (const [, tenantCandidates] of mergeCandidatesByTenant) {
|
|
709
|
-
const mergeTenant = tenantCandidates[0].tenantId;
|
|
710
|
-
const mergeScope = derivationScope(tenantCandidates[0].scope);
|
|
711
|
-
const mergeOrigin = tenantCandidates[0].origin_project;
|
|
712
|
-
const partnersOf = mergePartners(tenantCandidates.map((e) => e.content));
|
|
713
|
-
for (let i = 0; i < tenantCandidates.length; i++) {
|
|
714
|
-
if (used.has(tenantCandidates[i].id) || tenantCandidates[i].content.length > MERGE_MAX_CHARS)
|
|
715
|
-
continue;
|
|
716
|
-
const related = [tenantCandidates[i]];
|
|
717
|
-
for (const j of partnersOf(i)) {
|
|
718
|
-
if (!used.has(tenantCandidates[j].id))
|
|
719
|
-
related.push(tenantCandidates[j]);
|
|
720
|
-
}
|
|
721
|
-
const cluster = [];
|
|
722
|
-
let clusterChars = 0;
|
|
723
|
-
for (const e of related) {
|
|
724
|
-
if (cluster.length === MERGE_MAX_SOURCES || clusterChars + e.content.length > MERGE_MAX_CHARS)
|
|
725
|
-
continue;
|
|
726
|
-
cluster.push(e);
|
|
727
|
-
clusterChars += e.content.length;
|
|
728
|
-
}
|
|
729
|
-
if (cluster.length < MERGE_MIN_CLUSTER)
|
|
730
|
-
continue;
|
|
731
|
-
// Create a semantic summary
|
|
732
|
-
const mergedContent = mergeContents(cluster);
|
|
733
|
-
const allTags = Array.from(new Set(cluster.flatMap((e) => e.tags))).sort();
|
|
734
|
-
const maxValence = pickStrongestValence(cluster);
|
|
735
|
-
// AT1 P2 fix: build the semantic entry FIRST — createMemory is cheap
|
|
736
|
-
// and pure — so the tombstone check below runs under the tenant the
|
|
737
|
-
// row will ACTUALLY land in.
|
|
738
|
-
// T1 fix: createMemory now receives tenantId: mergeTenant (the
|
|
739
|
-
// partition's tenant — every member of `cluster` shares it by
|
|
740
|
-
// construction), so the row lands in its source tenant instead of
|
|
741
|
-
// always 'default'.
|
|
742
|
-
let semantic = null;
|
|
743
|
-
if (!dryRun) {
|
|
744
|
-
semantic = {
|
|
745
|
-
...createMemory(mergedContent, {
|
|
746
|
-
layer: Layer.Semantic,
|
|
747
|
-
tags: allTags,
|
|
748
|
-
emotional_valence: maxValence,
|
|
749
|
-
schema_fit: 0.7,
|
|
750
|
-
source: 'consolidation',
|
|
751
|
-
confidence: 'inferred',
|
|
752
|
-
tenantId: mergeTenant,
|
|
753
|
-
scope: mergeScope,
|
|
754
|
-
baseHalfLifeDays: config.defaultHalfLifeDays,
|
|
755
|
-
}),
|
|
756
|
-
origin_project: mergeOrigin,
|
|
757
|
-
parents: cluster.map((e) => e.id),
|
|
758
|
-
};
|
|
759
|
-
}
|
|
760
|
-
// mergeContents is DETERMINISTIC CONCATENATION (not an LLM paraphrase)
|
|
761
|
-
// — if a human rejected exactly this byte-identical rollup before, an
|
|
762
|
-
// unguarded sleep would regenerate it every cycle and
|
|
763
|
-
// batchWriteAndDelete's guard bypass (store.ts) would silently
|
|
764
|
-
// re-assert it forever. This producer-side check is what makes that
|
|
765
|
-
// bypass safe. A hit skips the WHOLE cluster: sources stay unmerged —
|
|
766
|
-
// not demoted, not deleted — so a later sleep gets another chance if
|
|
767
|
-
// the tombstone is lifted.
|
|
768
|
-
const consolidateDb = getConsolidateDb();
|
|
769
|
-
if (consolidateDb && semantic) {
|
|
770
|
-
const newDigest = rejectionDigest(semantic.content);
|
|
771
|
-
const oldDigest = rejectionDigest(legacyMergeContents(related)); // tombstones from older releases hold this format's digest
|
|
772
|
-
const newHit = findRejectedValue(consolidateDb, semantic.tenantId, newDigest);
|
|
773
|
-
const tombstone = newHit ?? findRejectedValue(consolidateDb, semantic.tenantId, oldDigest);
|
|
774
|
-
const mergeDigest = newHit ? newDigest : oldDigest;
|
|
775
|
-
if (tombstone) {
|
|
776
|
-
// Still mark used — these members are not re-tried against a
|
|
777
|
-
// DIFFERENT cluster within this same pass; next sleep re-clusters
|
|
778
|
-
// them fresh.
|
|
779
|
-
const rejected = newHit ? cluster : related; // the old format digested the uncapped list, so rows past the cap were rejected too
|
|
780
|
-
for (const e of rejected)
|
|
781
|
-
used.add(e.id);
|
|
782
|
-
mergesSkippedRejected++;
|
|
783
|
-
try {
|
|
784
|
-
appendAuditEvent(consolidateDb, {
|
|
785
|
-
tenantId: semantic.tenantId,
|
|
786
|
-
actor: 'sleep',
|
|
787
|
-
op: 'reject_refusal',
|
|
788
|
-
metadata: {
|
|
789
|
-
digest: mergeDigest,
|
|
790
|
-
reason: tombstone.reason,
|
|
791
|
-
sourceIds: rejected.map((e) => e.id),
|
|
792
|
-
},
|
|
793
|
-
});
|
|
794
|
-
}
|
|
795
|
-
catch (error) {
|
|
796
|
-
reportAuditWriteFailure('reject_refusal', String(error));
|
|
797
|
-
}
|
|
798
|
-
continue;
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
// Mark cluster members as used
|
|
802
|
-
for (const e of cluster)
|
|
803
|
-
used.add(e.id);
|
|
804
|
-
result.merged += cluster.length;
|
|
805
|
-
result.details.push(` 🔀 merged ${cluster.length} episodic entries into semantic: "${mergedContent.slice(0, 60)}..."`);
|
|
806
|
-
if (!dryRun && semantic) {
|
|
807
|
-
pendingWrites.push(semantic);
|
|
808
|
-
result.semanticCreated++;
|
|
809
|
-
// Demote source episodics (they've been compressed into neocortex):
|
|
810
|
-
// scale half_life_days so they decay sooner while staying recoverable.
|
|
811
|
-
// Immediate ranking is deliberately unchanged: the 2026-06-10 DAG
|
|
812
|
-
// slice-1 eval measured that dropping children below a worse-retrieving
|
|
813
|
-
// summary regresses budget-bounded QA (docs/evals/). The stored
|
|
814
|
-
// strength is refreshed to the live value so inspect, replay sampling,
|
|
815
|
-
// and strength-sorted assembly see the truth instead of a fake 0.3.
|
|
816
|
-
// Mutate in place (not a copy): `cluster` holds the same object
|
|
817
|
-
// references as `survivors`, and the later detectConflicts(survivors)
|
|
818
|
-
// pass in this same run must see the post-demotion half-life, or it
|
|
819
|
-
// can persist conflicts for entries the just-written state excludes.
|
|
820
|
-
for (const e of cluster) {
|
|
821
|
-
e.half_life_days = Math.max(1, Math.floor(e.half_life_days * MERGE_SOURCE_HALF_LIFE_FACTOR));
|
|
822
|
-
e.strength = calculateStrength(e, now, decayOpts);
|
|
823
|
-
pendingWrites.push(e);
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
finally {
|
|
830
|
-
if (consolidateDbHandle)
|
|
831
|
-
closeHippoDb(consolidateDbHandle);
|
|
832
|
-
}
|
|
833
|
-
if (mergesSkippedRejected > 0) {
|
|
834
|
-
console.error(`consolidate: skipped ${mergesSkippedRejected} merge(s) whose content matches a rejected value`);
|
|
835
|
-
}
|
|
836
|
-
result.removedIds = pendingDeletes;
|
|
837
|
-
// One transaction; the snapshot keeps what the DAG passes and other writers changed while sleep ran.
|
|
838
|
-
// Dormant moves ride in the same transaction (src/dormant.ts).
|
|
839
|
-
if (!dryRun) {
|
|
840
|
-
const left = new Set(batchWriteAndDelete(hippoRoot, pendingWrites, pendingDeletes, { snapshot, dormant: pendingDormant }));
|
|
841
|
-
for (const id of [...pendingDeletes, ...pendingDormant.map((m) => m.entry.id)]) {
|
|
842
|
-
if (!left.has(id))
|
|
843
|
-
result.details.push(` ↩ ${id} not removed: pinned or already gone before sleep saved`);
|
|
844
|
-
}
|
|
845
|
-
result.removedIds = pendingDeletes.filter((id) => left.has(id));
|
|
846
|
-
result.removed = result.removedIds.length;
|
|
847
|
-
result.dormant = pendingDormant.filter((m) => left.has(m.entry.id)).length;
|
|
848
|
-
}
|
|
849
|
-
// Dormant retention: a dormant memory nobody restored within
|
|
850
|
-
// dormant.retentionDays is deleted for good (0 keeps them forever). Runs
|
|
851
|
-
// even when dormant.enabled is off, so turning it off still ages out what
|
|
852
|
-
// earlier sleeps kept.
|
|
853
|
-
if (config.dormant.retentionDays > 0) {
|
|
854
|
-
const cutoff = new Date(now.getTime() - config.dormant.retentionDays * 24 * 60 * 60 * 1000).toISOString();
|
|
855
|
-
const db = openHippoDb(hippoRoot);
|
|
856
|
-
try {
|
|
857
|
-
result.dormantExpired = dryRun ? countExpiredDormant(db, cutoff) : purgeExpiredDormant(db, cutoff);
|
|
858
|
-
}
|
|
859
|
-
finally {
|
|
860
|
-
closeHippoDb(db);
|
|
861
|
-
}
|
|
862
|
-
if (result.dormantExpired > 0) {
|
|
863
|
-
result.details.push(` ⌛ ${dryRun ? 'would expire' : 'expired'} ${result.dormantExpired} dormant memor${result.dormantExpired === 1 ? 'y' : 'ies'} older than ${config.dormant.retentionDays} days`);
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
// -------------------------------------------------------------------------
|
|
867
|
-
// 4. Log run
|
|
868
|
-
// -------------------------------------------------------------------------
|
|
869
|
-
if (!dryRun) {
|
|
870
|
-
const detectedConflicts = detectConflicts(survivors, now, decayOpts, rescuedIds);
|
|
871
|
-
replaceDetectedConflicts(hippoRoot, detectedConflicts, now.toISOString());
|
|
872
|
-
if (detectedConflicts.length > 0) {
|
|
873
|
-
result.details.push(` ⚠️ detected ${detectedConflicts.length} memory conflict${detectedConflicts.length === 1 ? '' : 's'}`);
|
|
874
|
-
}
|
|
875
|
-
appendConsolidationRun(hippoRoot, {
|
|
876
|
-
timestamp: now.toISOString(),
|
|
877
|
-
decayed: result.decayed,
|
|
878
|
-
merged: result.merged,
|
|
879
|
-
removed: result.removed,
|
|
880
|
-
});
|
|
881
|
-
incrementSleepCount(hippoRoot);
|
|
882
|
-
// One audit row per rescue (attributability, D1). Written here, AFTER
|
|
883
|
-
// batchWriteAndDelete above has committed this cycle's writes/deletes
|
|
884
|
-
// (and after conflict detection + run logging), not inline in the decay
|
|
885
|
-
// pass — same durability posture as api.ts's top-level 'consolidate'
|
|
886
|
-
// summary audit row (written only once the whole sleep has completed).
|
|
887
|
-
// Writing it earlier would assert rescues for a cycle whose effects
|
|
888
|
-
// never landed if a later phase threw. Real writes only — dry-run
|
|
889
|
-
// previews the decision (details line above) but persists nothing.
|
|
890
|
-
if (rescuedEntries.length > 0) {
|
|
891
|
-
try {
|
|
892
|
-
const auditDb = openHippoDb(hippoRoot);
|
|
893
|
-
try {
|
|
894
|
-
// Review-round F5: per-row try/catch, not one try/catch around the
|
|
895
|
-
// whole loop — a single failed appendAuditEvent must not silently
|
|
896
|
-
// drop every remaining row. Mirrors the physics pass's
|
|
897
|
-
// skipped-warning precedent (line ~564 above): count losses, keep
|
|
898
|
-
// the overall fail-soft posture, tell the operator via details.
|
|
899
|
-
let auditFailures = 0;
|
|
900
|
-
for (const entry of rescuedEntries) {
|
|
901
|
-
try {
|
|
902
|
-
const rank = rankById.get(entry.id);
|
|
903
|
-
appendAuditEvent(auditDb, {
|
|
904
|
-
tenantId: entry.tenantId,
|
|
905
|
-
actor: 'sleep',
|
|
906
|
-
op: 'mv_rescue',
|
|
907
|
-
targetId: entry.id,
|
|
908
|
-
metadata: rank
|
|
909
|
-
? { rank: rank.rank, totalNonPinned: rank.totalNonPinned, keepN: rank.keepN, score: rank.score }
|
|
910
|
-
: {},
|
|
911
|
-
});
|
|
912
|
-
}
|
|
913
|
-
catch (error) {
|
|
914
|
-
auditFailures++;
|
|
915
|
-
reportAuditWriteFailure('mv_rescue', String(error), entry.id);
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
if (auditFailures > 0) {
|
|
919
|
-
result.details.push(` ⚠️ memory-value: ${auditFailures} mv_rescue audit row${auditFailures === 1 ? '' : 's'} ` +
|
|
920
|
-
`failed to write (the rescue itself still landed)`);
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
finally {
|
|
924
|
-
closeHippoDb(auditDb);
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
catch {
|
|
928
|
-
// openHippoDb/closeHippoDb-level failure: audit must never crash a
|
|
929
|
-
// mutation (mirrors store.ts's audit() posture).
|
|
930
|
-
result.details.push(` ⚠️ memory-value: mv_rescue audit unavailable this cycle ` +
|
|
931
|
-
`(${rescuedEntries.length} rescue${rescuedEntries.length === 1 ? '' : 's'} not audited)`);
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
return result;
|
|
936
|
-
}
|
|
937
|
-
// ---------------------------------------------------------------------------
|
|
938
|
-
// Helpers
|
|
939
|
-
// ---------------------------------------------------------------------------
|
|
940
|
-
function mergeContents(entries) {
|
|
941
|
-
// Each distinct text goes in once and in full (the merge demotes every source), one bullet with its lines indented, so heldTextKeys can read it back.
|
|
942
|
-
// Newest first says which version is current; compareEntryIdentity settles ties, so the row and its rejection digest depend only on the sources.
|
|
943
|
-
const sorted = [...entries].sort((a, b) => (Date.parse(b.created) - Date.parse(a.created)) || compareEntryIdentity(a, b));
|
|
944
|
-
const texts = new Map();
|
|
945
|
-
for (const e of sorted) {
|
|
946
|
-
if (!texts.has(duplicateKey(e.content)))
|
|
947
|
-
texts.set(duplicateKey(e.content), e.content);
|
|
948
|
-
}
|
|
949
|
-
const header = entries.length === 2 ? '[Consolidated from 2 related memories, newest first]' : `[Consolidated pattern from ${entries.length} related memories, newest first]`;
|
|
950
|
-
return mergedText(header, [...texts.values()]);
|
|
951
|
-
}
|
|
952
|
-
function legacyMergeContents(entries) {
|
|
953
|
-
// The old format dropped text, so it is only ever digested to match rejections recorded against it, never written.
|
|
954
|
-
const sorted = [...entries].sort((a, b) => (b.content.length - a.content.length) || compareEntryIdentity(a, b));
|
|
955
|
-
if (entries.length === 2)
|
|
956
|
-
return `[Consolidated from ${entries.length} related memories]\n\n${sorted[0].content}`;
|
|
957
|
-
const bullets = sorted.map((e) => `- ${e.content.split('\n')[0].slice(0, 120)}`).join('\n');
|
|
958
|
-
return `[Consolidated pattern from ${entries.length} related memories]\n\n${bullets}`;
|
|
959
|
-
}
|
|
960
|
-
function pickStrongestValence(entries) {
|
|
961
|
-
const order = ['critical', 'negative', 'positive', 'neutral'];
|
|
962
|
-
for (const v of order) {
|
|
963
|
-
if (entries.some((e) => e.emotional_valence === v))
|
|
964
|
-
return v;
|
|
965
|
-
}
|
|
966
|
-
return 'neutral';
|
|
967
|
-
}
|
|
968
|
-
/** Maps i to each j > i, ascending, whose text overlap with i reaches the merge threshold; every text needs at least one token. */
|
|
969
|
-
export function mergePartners(contents) {
|
|
970
|
-
const sets = contents.map((text) => new Set(tokenize(text)));
|
|
971
|
-
const candidatesOf = overlapPartners(sets, jaccardMinShared(MERGE_OVERLAP_THRESHOLD));
|
|
972
|
-
return (i) => candidatesOf(i).filter((j) => jaccardSets(sets[i], sets[j]) >= MERGE_OVERLAP_THRESHOLD);
|
|
973
|
-
}
|
|
974
|
-
/** Pairs of live non-semantic memories that contradict each other, in survivor order. */
|
|
975
|
-
export function detectConflicts(entries, now, decayOpts = {},
|
|
976
|
-
// LC2-E3 (opt-in, default off): ids rescued by this cycle's decay pass.
|
|
977
|
-
// detectConflicts recomputes its own strength>=DECAY_THRESHOLD survivor
|
|
978
|
-
// filter independently of the decay pass above; without this bypass,
|
|
979
|
-
// rescued entries would be silently re-excluded from conflict detection
|
|
980
|
-
// every cycle even though the decay pass just decided to keep them.
|
|
981
|
-
// Default empty set: flag-off behavior is unchanged.
|
|
982
|
-
rescuedIds = new Set()) {
|
|
983
|
-
const survivors = entries.filter((entry) => entry.layer !== Layer.Semantic
|
|
984
|
-
// CD5: an unreviewed quarantined row must not taint a visible memory as conflicted.
|
|
985
|
-
&& !isQuarantineScope(entry.scope ?? null)
|
|
986
|
-
&& (rescuedIds.has(entry.id) || calculateStrength(entry, now, decayOpts) >= DECAY_THRESHOLD));
|
|
987
|
-
const detected = [];
|
|
988
|
-
const profiles = survivors.map((entry) => conflictProfile(entry.content));
|
|
989
|
-
const partnersOf = overlapPartners(profiles.map((p) => p.distinct), jaccardMinShared(CONFLICT_OVERLAP_THRESHOLD, CONFLICT_MIN_RARE_SHARED));
|
|
990
|
-
for (let i = 0; i < survivors.length; i++) {
|
|
991
|
-
for (const j of partnersOf(i)) {
|
|
992
|
-
// Traces are variants of each other, not contradictions. Two
|
|
993
|
-
// strategies for the same task can both be valid; conflict detection
|
|
994
|
-
// exists for stated-rule disagreement, not strategy diversity.
|
|
995
|
-
if (survivors[i].layer === Layer.Trace && survivors[j].layer === Layer.Trace)
|
|
996
|
-
continue;
|
|
997
|
-
if (survivors[i].superseded_by || survivors[j].superseded_by)
|
|
998
|
-
continue;
|
|
999
|
-
if (!recalledTogether(survivors[i], survivors[j]))
|
|
1000
|
-
continue;
|
|
1001
|
-
if ([survivors[i], survivors[j]].some((e) => e.tags.includes('extracted') || e.tags.includes('session-digest')))
|
|
1002
|
-
continue;
|
|
1003
|
-
const reasonAndScore = describeConflict(profiles[i], profiles[j]);
|
|
1004
|
-
if (!reasonAndScore)
|
|
1005
|
-
continue;
|
|
1006
|
-
detected.push({
|
|
1007
|
-
memory_a_id: survivors[i].id,
|
|
1008
|
-
memory_b_id: survivors[j].id,
|
|
1009
|
-
reason: reasonAndScore.reason,
|
|
1010
|
-
score: reasonAndScore.score,
|
|
1011
|
-
});
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
return detected;
|
|
1015
|
-
}
|
|
1016
|
-
/** One project's ambient context can show both: same tenant, and the same project or a user-global row beside a project's. */
|
|
1017
|
-
function recalledTogether(a, b) {
|
|
1018
|
-
if (a.tenantId !== b.tenantId)
|
|
1019
|
-
return false;
|
|
1020
|
-
const [x, y] = [a.origin_project ?? null, b.origin_project ?? null];
|
|
1021
|
-
return x === y || (x === '' && y !== null) || (y === '' && x !== null);
|
|
1022
|
-
}
|
|
1023
|
-
function conflictProfile(text) {
|
|
1024
|
-
const opening = openingWindow(text);
|
|
1025
|
-
// Polarity is measured only in the first POLARITY_WINDOW_WORDS, so a stray
|
|
1026
|
-
// negation deep in a prose memory doesn't flip the intent.
|
|
1027
|
-
// Pad with spaces so space-delimited patterns match words at the start/end.
|
|
1028
|
-
return { distinct: distinctiveTokens(text), polarity: inferConflictPolarity(opening), window: ' ' + opening.toLowerCase() + ' ' };
|
|
1029
|
-
}
|
|
1030
|
-
function describeConflict(a, b) {
|
|
1031
|
-
// Jaccard on stopword-stripped tokens. Defer the threshold check until we
|
|
1032
|
-
// know whether an explicit polarity pair is present (lower bar for those).
|
|
1033
|
-
const overlapScore = jaccardSets(a.distinct, b.distinct);
|
|
1034
|
-
// Require at least N shared distinctive tokens so two short memories sharing
|
|
1035
|
-
// only "the project name" don't register.
|
|
1036
|
-
let shared = 0;
|
|
1037
|
-
for (const t of a.distinct)
|
|
1038
|
-
if (b.distinct.has(t))
|
|
1039
|
-
shared++;
|
|
1040
|
-
if (shared < CONFLICT_MIN_RARE_SHARED)
|
|
1041
|
-
return null;
|
|
1042
|
-
const conflictType = classifyConflictType(a.window, b.window, a.polarity, b.polarity);
|
|
1043
|
-
if (!conflictType)
|
|
1044
|
-
return null;
|
|
1045
|
-
if (overlapScore < CONFLICT_OVERLAP_THRESHOLD)
|
|
1046
|
-
return null;
|
|
1047
|
-
return {
|
|
1048
|
-
reason: conflictType,
|
|
1049
|
-
score: overlapScore,
|
|
1050
|
-
};
|
|
1051
|
-
}
|
|
1052
|
-
function distinctiveTokens(text) {
|
|
1053
|
-
return new Set(text
|
|
1054
|
-
.toLowerCase()
|
|
1055
|
-
.replace(/[^\w\s]/g, ' ')
|
|
1056
|
-
.split(/\s+/)
|
|
1057
|
-
.filter((t) => t.length > 2 && !CONFLICT_STOPWORDS.has(t)));
|
|
1058
|
-
}
|
|
1059
|
-
function jaccardSets(a, b) {
|
|
1060
|
-
if (a.size === 0 && b.size === 0)
|
|
1061
|
-
return 1;
|
|
1062
|
-
if (a.size === 0 || b.size === 0)
|
|
1063
|
-
return 0;
|
|
1064
|
-
let inter = 0;
|
|
1065
|
-
for (const t of a)
|
|
1066
|
-
if (b.has(t))
|
|
1067
|
-
inter++;
|
|
1068
|
-
const union = a.size + b.size - inter;
|
|
1069
|
-
return union === 0 ? 0 : inter / union;
|
|
1070
|
-
}
|
|
1071
|
-
function openingWindow(text) {
|
|
1072
|
-
return text.split(/\s+/).slice(0, POLARITY_WINDOW_WORDS).join(' ');
|
|
1073
|
-
}
|
|
1074
|
-
// Takes opening windows only, so " on " and " off " as prepositions deep in long prose don't read as enabled/disabled.
|
|
1075
|
-
function classifyConflictType(a, b, aPolarity, bPolarity) {
|
|
1076
|
-
// Tightened tokens: require whole-word boundaries so " on " alone doesn't
|
|
1077
|
-
// match "on/off". Pair only `enabled` ↔ `disabled` and explicit on/off in
|
|
1078
|
-
// imperative context.
|
|
1079
|
-
const enabledDisabled = (containsAny(a, [' enabled ', ' enable ']) && containsAny(b, [' disabled ', ' disable ']))
|
|
1080
|
-
|| (containsAny(b, [' enabled ', ' enable ']) && containsAny(a, [' disabled ', ' disable ']));
|
|
1081
|
-
if (enabledDisabled)
|
|
1082
|
-
return 'enabled/disabled mismatch on overlapping statement';
|
|
1083
|
-
const trueFalse = (containsAny(a, [' true ', ' true.', ' true,', ' yes ']) && containsAny(b, [' false ', ' false.', ' false,', ' no ']))
|
|
1084
|
-
|| (containsAny(b, [' true ', ' true.', ' true,', ' yes ']) && containsAny(a, [' false ', ' false.', ' false,', ' no ']));
|
|
1085
|
-
if (trueFalse)
|
|
1086
|
-
return 'true/false mismatch on overlapping statement';
|
|
1087
|
-
const alwaysNever = (containsAny(a, [' always ', ' must ']) && containsAny(b, [' never ', ' must not ']))
|
|
1088
|
-
|| (containsAny(b, [' always ', ' must ']) && containsAny(a, [' never ', ' must not ']));
|
|
1089
|
-
if (alwaysNever)
|
|
1090
|
-
return 'always/never mismatch on overlapping statement';
|
|
1091
|
-
if ((aPolarity === 'positive' && bPolarity === 'negative') || (aPolarity === 'negative' && bPolarity === 'positive')) {
|
|
1092
|
-
return 'negation polarity mismatch on overlapping statement';
|
|
1093
|
-
}
|
|
1094
|
-
return null;
|
|
1095
|
-
}
|
|
1096
|
-
function inferConflictPolarity(text) {
|
|
1097
|
-
const lowered = ` ${text.toLowerCase()} `;
|
|
1098
|
-
const negativePatterns = [
|
|
1099
|
-
' not ', ' never ', ' no ', " don't ", ' do not ', " doesn't ", ' does not ',
|
|
1100
|
-
" can't ", ' cannot ', " shouldn't ", ' should not ', ' disabled ', ' disable ', ' off ',
|
|
1101
|
-
' false ', ' missing ', ' broken ', ' failed ',
|
|
1102
|
-
];
|
|
1103
|
-
const positivePatterns = [
|
|
1104
|
-
' enabled ', ' enable ', ' works ', ' working ', ' true ', ' available ', ' present ', ' on ',
|
|
1105
|
-
' always ', ' must ',
|
|
1106
|
-
];
|
|
1107
|
-
if (containsAny(lowered, negativePatterns))
|
|
1108
|
-
return 'negative';
|
|
1109
|
-
if (containsAny(lowered, positivePatterns))
|
|
1110
|
-
return 'positive';
|
|
1111
|
-
return 'neutral';
|
|
1112
|
-
}
|
|
1113
|
-
function stripConflictPolarity(text) {
|
|
1114
|
-
return text
|
|
1115
|
-
.toLowerCase()
|
|
1116
|
-
.replace(/\b(?:not|never|no|don['’]?t|do\s+not|doesn['’]?t|does\s+not|can['’]?t|cannot|shouldn['’]?t|should\s+not|enabled|enable|disabled|disable|on|off|true|false|always|must|must\s+not|works?|working|missing|broken|failed|available|present)\b/g, ' ')
|
|
1117
|
-
.replace(/\s+/g, ' ')
|
|
1118
|
-
.trim();
|
|
1119
|
-
}
|
|
1120
|
-
function containsAny(text, needles) {
|
|
1121
|
-
return needles.some((needle) => text.includes(needle));
|
|
1122
|
-
}
|
|
1123
|
-
//# sourceMappingURL=consolidate.js.map
|