@remnic/core 9.4.0 → 9.4.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/dist/access-admin-ops-surface.d.ts +5 -5
- package/dist/access-admin-ops-surface.js +22 -21
- package/dist/access-boundary.d.ts +5 -5
- package/dist/access-boundary.js +23 -22
- package/dist/access-cli.js +63 -62
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +5 -5
- package/dist/access-http.js +31 -30
- package/dist/access-identity-continuity-surface.d.ts +4 -4
- package/dist/access-identity-continuity-surface.js +22 -21
- package/dist/access-lcm-surface.d.ts +5 -5
- package/dist/access-lcm-surface.js +22 -21
- package/dist/access-mcp.d.ts +5 -5
- package/dist/access-mcp.js +27 -26
- package/dist/access-observe-write-surface.d.ts +5 -5
- package/dist/access-observe-write-surface.js +22 -21
- package/dist/access-operations-batch.js +24 -23
- package/dist/access-operations.d.ts +9 -9
- package/dist/access-operations.js +26 -25
- package/dist/access-recall-surface.d.ts +5 -5
- package/dist/access-recall-surface.js +22 -21
- package/dist/access-schema.d.ts +68 -68
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-CdW6B2qQ.d.ts → access-service-m-2VO_Ig.d.ts} +3 -3
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +22 -21
- package/dist/access-surface-catalog.d.ts +5 -5
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +1 -1
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/{auto-sync-M47VTCFI.js → auto-sync-HMHQHUAG.js} +4 -4
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +4 -4
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +5 -5
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +1 -1
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{catalog-bflYNWsM.d.ts → catalog-BxWokdC2.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +6 -6
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/{chunk-TKUQSXU4.js → chunk-27FLVMDC.js} +12 -12
- package/dist/{chunk-XXEOGY77.js → chunk-2FOD2YY3.js} +7 -7
- package/dist/{chunk-6Y4JTNX7.js → chunk-2MR3MFQB.js} +2 -2
- package/dist/{chunk-AU7Q3LSC.js → chunk-2QSZNTDO.js} +4 -4
- package/dist/{chunk-64ZYXSCO.js → chunk-2U2A5HF4.js} +10 -4
- package/dist/chunk-2U2A5HF4.js.map +1 -0
- package/dist/{chunk-RBGL4MKQ.js → chunk-3F4N4WDH.js} +75 -110
- package/dist/chunk-3F4N4WDH.js.map +1 -0
- package/dist/{chunk-4NMATTXV.js → chunk-4BFRBP45.js} +2 -2
- package/dist/{chunk-L4UZBJNM.js → chunk-4P4626RY.js} +4 -4
- package/dist/{chunk-XZJMIJLW.js → chunk-5QZDMSSN.js} +29 -29
- package/dist/chunk-64UDOIMI.js +121 -0
- package/dist/chunk-64UDOIMI.js.map +1 -0
- package/dist/{chunk-H2EJNV2Z.js → chunk-66FLU5BT.js} +2 -2
- package/dist/{chunk-MWH5MAFP.js → chunk-6BISDEN6.js} +5 -5
- package/dist/{chunk-VBVSN2TV.js → chunk-6ZIETXZ2.js} +2 -2
- package/dist/{chunk-42BBIR33.js → chunk-7N2RUBAP.js} +2 -2
- package/dist/{chunk-UQI2NHTY.js → chunk-7QWMTOIF.js} +37 -37
- package/dist/{chunk-QICQ35K5.js → chunk-7Z2DZNWN.js} +17 -8
- package/dist/{chunk-QICQ35K5.js.map → chunk-7Z2DZNWN.js.map} +1 -1
- package/dist/{chunk-JI5AVXYO.js → chunk-AKUCU7CI.js} +5 -5
- package/dist/{chunk-FT37KV7R.js → chunk-BGTAL46Y.js} +2 -2
- package/dist/{chunk-7VFZTJ7K.js → chunk-BV2TIZ4F.js} +2 -2
- package/dist/{chunk-FWPHPN3V.js → chunk-CLX5VDNY.js} +5 -5
- package/dist/{chunk-TH2KIPP7.js → chunk-CZAA5N5S.js} +2 -2
- package/dist/{chunk-7BAJAXGB.js → chunk-CZAZX7JP.js} +4 -4
- package/dist/{chunk-APMGGGTY.js → chunk-DGYCWCQ7.js} +2 -2
- package/dist/{chunk-GEVIE5NV.js → chunk-DR6PEJU3.js} +4 -4
- package/dist/{chunk-2TCZE3C2.js → chunk-EPGWWCZF.js} +2 -2
- package/dist/{chunk-OUXPVWTZ.js → chunk-FLVL4L3L.js} +5 -5
- package/dist/{chunk-YPL66L3U.js → chunk-FZGGZAAU.js} +2 -2
- package/dist/{chunk-XOE5DYQL.js → chunk-HRCACVHL.js} +2 -2
- package/dist/{chunk-KS4PRHRD.js → chunk-IC6QX5TC.js} +2 -2
- package/dist/{chunk-H25TAODC.js → chunk-ILFJ7AVS.js} +4 -4
- package/dist/{chunk-UQMCR5RW.js → chunk-JLR3JSTL.js} +3 -3
- package/dist/{chunk-5H56JAHW.js → chunk-KCJ5H6XQ.js} +2 -2
- package/dist/{chunk-H5IT5JL7.js → chunk-LOR6ZM5R.js} +3 -3
- package/dist/{chunk-VBBMTSOV.js → chunk-M7GGMGMZ.js} +6 -6
- package/dist/{chunk-G47FEGGK.js → chunk-NG7YBCJO.js} +4 -4
- package/dist/{chunk-QGS7Z7YU.js → chunk-OHDJ34CC.js} +2 -2
- package/dist/{chunk-UAOZJX5J.js → chunk-P6SYWE4G.js} +53 -6
- package/dist/chunk-P6SYWE4G.js.map +1 -0
- package/dist/{chunk-LLMZK53N.js → chunk-PUBTHW5O.js} +2 -2
- package/dist/{chunk-AGNBY3VG.js → chunk-PYHOAXTI.js} +2 -2
- package/dist/{chunk-SXN3LCUT.js → chunk-RHOZ5LLO.js} +100 -4
- package/dist/chunk-RHOZ5LLO.js.map +1 -0
- package/dist/{chunk-6KYMPV2O.js → chunk-RL6NEVXT.js} +45 -2
- package/dist/chunk-RL6NEVXT.js.map +1 -0
- package/dist/{chunk-7WV3F5DQ.js → chunk-V7IZKJI3.js} +2 -2
- package/dist/{chunk-IGF4DII6.js → chunk-X4A62ET7.js} +9 -9
- package/dist/{chunk-OH6BKCPB.js → chunk-XN3TUUX2.js} +2 -2
- package/dist/{chunk-5K5VEDFY.js → chunk-YDSWRD33.js} +4 -4
- package/dist/{chunk-ABGDGBDT.js → chunk-YPEMNNNY.js} +2 -2
- package/dist/{chunk-YM7IEUO7.js → chunk-ZPOROUTX.js} +2 -2
- package/dist/{cli-Dfnc66gs.d.ts → cli-EiBBOt3r.d.ts} +3 -3
- package/dist/cli.d.ts +6 -6
- package/dist/cli.js +51 -50
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +5 -5
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +5 -5
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +5 -5
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/contradiction/index.d.ts +1 -1
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/backend.js +2 -2
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +5 -5
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +4 -4
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/graph-dashboard-diff.d.ts +1 -1
- package/dist/graph-dashboard-key.d.ts +1 -1
- package/dist/graph-dashboard-parser.d.ts +1 -1
- package/dist/graph-edge-reinforcement.d.ts +1 -1
- package/dist/graph-snapshot.d.ts +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +17 -12
- package/dist/index.js +79 -78
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/engine.js +4 -4
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/index.js +6 -6
- package/dist/lcm/schema.js +2 -2
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/local-model-endpoint.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +5 -5
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +5 -5
- package/dist/maintenance/rebuild-memory-projection.js +6 -6
- package/dist/mcp-memory-inspector-app.d.ts +5 -5
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +23 -3
- package/dist/memory-projection-store.js +9 -2
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +2 -2
- package/dist/namespaces/migrate.js +17 -16
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +11 -10
- package/dist/namespaces/storage.d.ts +2 -2
- package/dist/namespaces/storage.js +5 -5
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +11 -2
- package/dist/operator-toolkit.js +23 -20
- package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +7 -7
- package/dist/{orchestrator-Y9wyG4jK.d.ts → orchestrator-B2Y28Z9t.d.ts} +3 -3
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +63 -62
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/{purge-IHRX4Z7X.js → purge-NDWV6WYD.js} +4 -3
- package/dist/{purge-IHRX4Z7X.js.map → purge-NDWV6WYD.js.map} +1 -1
- package/dist/qmd-Db9mOkzB.d.ts +330 -0
- package/dist/qmd-preflight.d.ts +5 -0
- package/dist/qmd-preflight.js +31 -0
- package/dist/qmd-preflight.js.map +1 -0
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +3 -305
- package/dist/qmd.js +10 -8
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-timings.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray.d.ts +1 -1
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +2 -2
- package/dist/retrieval-agents.d.ts +2 -2
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/runtime/better-sqlite.d.ts +17 -1
- package/dist/runtime/better-sqlite.js +5 -3
- package/dist/schemas.d.ts +84 -84
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +10 -9
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +15 -14
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/lancedb-backend.js +2 -2
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.js +2 -2
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/orama-backend.js +2 -2
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-bfXJNKVf.d.ts → semantic-consolidation-BDVIbLRw.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +6 -6
- package/dist/semantic-rule-promotion.js +5 -5
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +5 -5
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/session-transcript-migration.js +2 -2
- package/dist/shared-context/manager.d.ts +9 -9
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +4 -4
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/autodetect.js +1 -1
- package/dist/transfer/backup.js +1 -1
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/export-sqlite.js +2 -2
- package/dist/transfer/import-sqlite.js +2 -2
- package/dist/transfer/types.d.ts +66 -66
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-D1iBqhJA.d.ts → types-ljkd82rn.d.ts} +13 -1
- package/dist/types.d.ts +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +5 -5
- package/package.json +2 -2
- package/src/memory-projection-open-failure.test.ts +322 -0
- package/src/memory-projection-store.ts +179 -2
- package/src/operator-toolkit.ts +64 -0
- package/src/qmd-preflight.test.ts +197 -0
- package/src/qmd-preflight.ts +167 -0
- package/src/qmd.ts +115 -129
- package/src/runtime/better-sqlite.test.ts +54 -0
- package/src/runtime/better-sqlite.ts +83 -3
- package/src/wearables/cleanup.test.ts +56 -0
- package/src/wearables/cleanup.ts +22 -1
- package/src/wearables/config.test.ts +25 -0
- package/src/wearables/config.ts +31 -4
- package/src/wearables/types.ts +12 -1
- package/dist/chunk-64ZYXSCO.js.map +0 -1
- package/dist/chunk-6KYMPV2O.js.map +0 -1
- package/dist/chunk-RBGL4MKQ.js.map +0 -1
- package/dist/chunk-SXN3LCUT.js.map +0 -1
- package/dist/chunk-UAOZJX5J.js.map +0 -1
- /package/dist/{auto-sync-M47VTCFI.js.map → auto-sync-HMHQHUAG.js.map} +0 -0
- /package/dist/{chunk-TKUQSXU4.js.map → chunk-27FLVMDC.js.map} +0 -0
- /package/dist/{chunk-XXEOGY77.js.map → chunk-2FOD2YY3.js.map} +0 -0
- /package/dist/{chunk-6Y4JTNX7.js.map → chunk-2MR3MFQB.js.map} +0 -0
- /package/dist/{chunk-AU7Q3LSC.js.map → chunk-2QSZNTDO.js.map} +0 -0
- /package/dist/{chunk-4NMATTXV.js.map → chunk-4BFRBP45.js.map} +0 -0
- /package/dist/{chunk-L4UZBJNM.js.map → chunk-4P4626RY.js.map} +0 -0
- /package/dist/{chunk-XZJMIJLW.js.map → chunk-5QZDMSSN.js.map} +0 -0
- /package/dist/{chunk-H2EJNV2Z.js.map → chunk-66FLU5BT.js.map} +0 -0
- /package/dist/{chunk-MWH5MAFP.js.map → chunk-6BISDEN6.js.map} +0 -0
- /package/dist/{chunk-VBVSN2TV.js.map → chunk-6ZIETXZ2.js.map} +0 -0
- /package/dist/{chunk-42BBIR33.js.map → chunk-7N2RUBAP.js.map} +0 -0
- /package/dist/{chunk-UQI2NHTY.js.map → chunk-7QWMTOIF.js.map} +0 -0
- /package/dist/{chunk-JI5AVXYO.js.map → chunk-AKUCU7CI.js.map} +0 -0
- /package/dist/{chunk-FT37KV7R.js.map → chunk-BGTAL46Y.js.map} +0 -0
- /package/dist/{chunk-7VFZTJ7K.js.map → chunk-BV2TIZ4F.js.map} +0 -0
- /package/dist/{chunk-FWPHPN3V.js.map → chunk-CLX5VDNY.js.map} +0 -0
- /package/dist/{chunk-TH2KIPP7.js.map → chunk-CZAA5N5S.js.map} +0 -0
- /package/dist/{chunk-7BAJAXGB.js.map → chunk-CZAZX7JP.js.map} +0 -0
- /package/dist/{chunk-APMGGGTY.js.map → chunk-DGYCWCQ7.js.map} +0 -0
- /package/dist/{chunk-GEVIE5NV.js.map → chunk-DR6PEJU3.js.map} +0 -0
- /package/dist/{chunk-2TCZE3C2.js.map → chunk-EPGWWCZF.js.map} +0 -0
- /package/dist/{chunk-OUXPVWTZ.js.map → chunk-FLVL4L3L.js.map} +0 -0
- /package/dist/{chunk-YPL66L3U.js.map → chunk-FZGGZAAU.js.map} +0 -0
- /package/dist/{chunk-XOE5DYQL.js.map → chunk-HRCACVHL.js.map} +0 -0
- /package/dist/{chunk-KS4PRHRD.js.map → chunk-IC6QX5TC.js.map} +0 -0
- /package/dist/{chunk-H25TAODC.js.map → chunk-ILFJ7AVS.js.map} +0 -0
- /package/dist/{chunk-UQMCR5RW.js.map → chunk-JLR3JSTL.js.map} +0 -0
- /package/dist/{chunk-5H56JAHW.js.map → chunk-KCJ5H6XQ.js.map} +0 -0
- /package/dist/{chunk-H5IT5JL7.js.map → chunk-LOR6ZM5R.js.map} +0 -0
- /package/dist/{chunk-VBBMTSOV.js.map → chunk-M7GGMGMZ.js.map} +0 -0
- /package/dist/{chunk-G47FEGGK.js.map → chunk-NG7YBCJO.js.map} +0 -0
- /package/dist/{chunk-QGS7Z7YU.js.map → chunk-OHDJ34CC.js.map} +0 -0
- /package/dist/{chunk-LLMZK53N.js.map → chunk-PUBTHW5O.js.map} +0 -0
- /package/dist/{chunk-AGNBY3VG.js.map → chunk-PYHOAXTI.js.map} +0 -0
- /package/dist/{chunk-7WV3F5DQ.js.map → chunk-V7IZKJI3.js.map} +0 -0
- /package/dist/{chunk-IGF4DII6.js.map → chunk-X4A62ET7.js.map} +0 -0
- /package/dist/{chunk-OH6BKCPB.js.map → chunk-XN3TUUX2.js.map} +0 -0
- /package/dist/{chunk-5K5VEDFY.js.map → chunk-YDSWRD33.js.map} +0 -0
- /package/dist/{chunk-ABGDGBDT.js.map → chunk-YPEMNNNY.js.map} +0 -0
- /package/dist/{chunk-YM7IEUO7.js.map → chunk-ZPOROUTX.js.map} +0 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import { initLogger } from "./logger.js";
|
|
8
|
+
import { rebuildMemoryProjection } from "./maintenance/rebuild-memory-projection.js";
|
|
9
|
+
import {
|
|
10
|
+
__resetProjectionOpenFailureSuppressionForTest,
|
|
11
|
+
__setProjectionReadonlyOpenerForTest,
|
|
12
|
+
getMemoryProjectionPath,
|
|
13
|
+
probeProjectionHealth,
|
|
14
|
+
readProjectedMemoryBrowse,
|
|
15
|
+
} from "./memory-projection-store.js";
|
|
16
|
+
import { summarizeProjectionHealth } from "./operator-toolkit.js";
|
|
17
|
+
import { openBetterSqlite3 } from "./runtime/better-sqlite.js";
|
|
18
|
+
import { StorageManager } from "./storage.js";
|
|
19
|
+
|
|
20
|
+
// Serialized: every test mutates the process-global logger backend and the
|
|
21
|
+
// module-level projection opener / rate-limit map. concurrency:false keeps
|
|
22
|
+
// them from racing each other.
|
|
23
|
+
const SERIAL = { concurrency: false } as const;
|
|
24
|
+
|
|
25
|
+
// The opener seam expects the real opener signature; derive it without
|
|
26
|
+
// importing the (unexported) alias.
|
|
27
|
+
type ProjectionOpener = NonNullable<Parameters<typeof __setProjectionReadonlyOpenerForTest>[0]>;
|
|
28
|
+
|
|
29
|
+
function abiStyleError(): Error {
|
|
30
|
+
// Mirrors the real native-binding mismatch better-sqlite3 throws when its
|
|
31
|
+
// addon was compiled for a different Node.js ABI (issue #1829 root cause).
|
|
32
|
+
// Classified as a native mismatch by isLikelyBetterSqlite3NativeBindingError.
|
|
33
|
+
return new Error(
|
|
34
|
+
"The module was compiled against a different Node.js version using NODE_MODULE_VERSION 127. " +
|
|
35
|
+
"This version of Node.js requires NODE_MODULE_VERSION 137."
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function captureLogs(): { warns: string[]; errors: string[]; restore: () => void } {
|
|
40
|
+
const warns: string[] = [];
|
|
41
|
+
const errors: string[] = [];
|
|
42
|
+
initLogger({
|
|
43
|
+
info() {},
|
|
44
|
+
warn(msg) {
|
|
45
|
+
warns.push(msg);
|
|
46
|
+
},
|
|
47
|
+
error(msg) {
|
|
48
|
+
errors.push(msg);
|
|
49
|
+
},
|
|
50
|
+
debug() {},
|
|
51
|
+
});
|
|
52
|
+
return { warns, errors, restore: () => initLogger() };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
test("absent projection index → quiet fallback, no error log, probe reports absent", SERIAL, async () => {
|
|
56
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-proj-absent-"));
|
|
57
|
+
const { warns, restore } = captureLogs();
|
|
58
|
+
try {
|
|
59
|
+
// No projection file exists — the normal cold-install / never-built case.
|
|
60
|
+
assert.equal(probeProjectionHealth(dir).state, "absent");
|
|
61
|
+
assert.equal(readProjectedMemoryBrowse(dir, { limit: 5, offset: 0 }), null);
|
|
62
|
+
// The distinguishing guarantee from issue #1829: a MISSING file must stay
|
|
63
|
+
// quiet (no error spam). Only a PRESENT-but-unopenable file logs.
|
|
64
|
+
assert.equal(warns.length, 0);
|
|
65
|
+
} finally {
|
|
66
|
+
restore();
|
|
67
|
+
await rm(dir, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("present index + ABI-style open failure → distinct rate-limited warn + null + doctor signal", SERIAL, async () => {
|
|
72
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-proj-abi-"));
|
|
73
|
+
const { warns, restore } = captureLogs();
|
|
74
|
+
__resetProjectionOpenFailureSuppressionForTest();
|
|
75
|
+
try {
|
|
76
|
+
// Build a real projection so the index file genuinely EXISTS on disk;
|
|
77
|
+
// then inject an opener that throws the wrong-ABI error on every open.
|
|
78
|
+
await rebuildMemoryProjection({ memoryDir: dir, dryRun: false });
|
|
79
|
+
assert.ok(getMemoryProjectionPath(dir));
|
|
80
|
+
const throwingOpener: ProjectionOpener = () => {
|
|
81
|
+
throw abiStyleError();
|
|
82
|
+
};
|
|
83
|
+
const restoreOpener = __setProjectionReadonlyOpenerForTest(throwingOpener);
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
// The read path returns null (full-scan fallback trigger preserved),
|
|
87
|
+
// but now emits the DISTINCT real-error warn that was missing before.
|
|
88
|
+
const first = readProjectedMemoryBrowse(dir, { limit: 5, offset: 0 });
|
|
89
|
+
assert.equal(first, null);
|
|
90
|
+
assert.equal(warns.length, 1);
|
|
91
|
+
const msg = warns[0];
|
|
92
|
+
assert.ok(msg, "expected a distinct projection open-failure warn");
|
|
93
|
+
assert.match(msg, /present but could not be opened/);
|
|
94
|
+
assert.match(msg, /wrong Node\.js ABI/);
|
|
95
|
+
|
|
96
|
+
// Doctor lens classifies the same failure.
|
|
97
|
+
const probe = probeProjectionHealth(dir);
|
|
98
|
+
assert.equal(probe.state, "unopenable");
|
|
99
|
+
assert.equal(probe.nativeBindingMismatch, true);
|
|
100
|
+
|
|
101
|
+
// Rate limit: a second open within the window is suppressed.
|
|
102
|
+
const second = readProjectedMemoryBrowse(dir, { limit: 5, offset: 0 });
|
|
103
|
+
assert.equal(second, null);
|
|
104
|
+
assert.equal(warns.length, 1, "rate limit must suppress the repeat warn");
|
|
105
|
+
|
|
106
|
+
// After resetting suppression, the warn fires again.
|
|
107
|
+
__resetProjectionOpenFailureSuppressionForTest();
|
|
108
|
+
readProjectedMemoryBrowse(dir, { limit: 5, offset: 0 });
|
|
109
|
+
assert.equal(warns.length, 2, "warn fires again after suppression reset");
|
|
110
|
+
} finally {
|
|
111
|
+
restoreOpener();
|
|
112
|
+
}
|
|
113
|
+
} finally {
|
|
114
|
+
restore();
|
|
115
|
+
await rm(dir, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("present index + WRAPPED ABI open failure (cause = native-binding error) → still reports the native-binding hint", SERIAL, async () => {
|
|
120
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-proj-wrapped-abi-"));
|
|
121
|
+
const { warns, restore } = captureLogs();
|
|
122
|
+
__resetProjectionOpenFailureSuppressionForTest();
|
|
123
|
+
try {
|
|
124
|
+
await rebuildMemoryProjection({ memoryDir: dir, dryRun: false });
|
|
125
|
+
// Mirrors the production path: loadBetterSqlite3() catches the ABI error
|
|
126
|
+
// and re-throws a sanitized unavailableError WRAPPER whose message drops
|
|
127
|
+
// the ABI markers. The original error survives only on .cause. Before
|
|
128
|
+
// #1848 the classifier inspected only the wrapper's message and MISSED the
|
|
129
|
+
// mismatch, so the warn and doctor check wrongly suggested rebuilding the
|
|
130
|
+
// projection instead of better-sqlite3.
|
|
131
|
+
const throwingOpener: ProjectionOpener = () => {
|
|
132
|
+
throw new Error(
|
|
133
|
+
"better-sqlite3 is unavailable. Remnic attempted to load the native SQLite binding and could not.",
|
|
134
|
+
{ cause: abiStyleError() },
|
|
135
|
+
);
|
|
136
|
+
};
|
|
137
|
+
const restoreOpener = __setProjectionReadonlyOpenerForTest(throwingOpener);
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
// Browse warn carries the native-binding ABI hint (not the projection-rebuild hint).
|
|
141
|
+
const first = readProjectedMemoryBrowse(dir, { limit: 5, offset: 0 });
|
|
142
|
+
assert.equal(first, null);
|
|
143
|
+
assert.equal(warns.length, 1);
|
|
144
|
+
assert.match(warns[0], /wrong Node\.js ABI/);
|
|
145
|
+
assert.doesNotMatch(warns[0], /rebuild-memory-projection/);
|
|
146
|
+
|
|
147
|
+
// Doctor lens classifies the SAME wrapped failure as a native-binding mismatch.
|
|
148
|
+
const probe = probeProjectionHealth(dir);
|
|
149
|
+
assert.equal(probe.state, "unopenable");
|
|
150
|
+
assert.equal(probe.nativeBindingMismatch, true);
|
|
151
|
+
} finally {
|
|
152
|
+
restoreOpener();
|
|
153
|
+
}
|
|
154
|
+
} finally {
|
|
155
|
+
restore();
|
|
156
|
+
await rm(dir, { recursive: true, force: true });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("present index + non-ABI open failure → distinct warn without the native-binding hint", SERIAL, async () => {
|
|
161
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-proj-cantopen-"));
|
|
162
|
+
const { warns, restore } = captureLogs();
|
|
163
|
+
__resetProjectionOpenFailureSuppressionForTest();
|
|
164
|
+
try {
|
|
165
|
+
await rebuildMemoryProjection({ memoryDir: dir, dryRun: false });
|
|
166
|
+
// A corrupt-file / permission style failure: carries a stable SQLite
|
|
167
|
+
// code but is NOT a native-binding mismatch. Tests the failure CLASS,
|
|
168
|
+
// not just the reported ABI instance (hardening playbook).
|
|
169
|
+
const cantOpen = Object.assign(new Error("unable to open database file"), {
|
|
170
|
+
code: "SQLITE_CANTOPEN",
|
|
171
|
+
});
|
|
172
|
+
const restoreOpener = __setProjectionReadonlyOpenerForTest((() => {
|
|
173
|
+
throw cantOpen;
|
|
174
|
+
}) as ProjectionOpener);
|
|
175
|
+
try {
|
|
176
|
+
assert.equal(readProjectedMemoryBrowse(dir, { limit: 5, offset: 0 }), null);
|
|
177
|
+
assert.equal(warns.length, 1);
|
|
178
|
+
const msg = warns[0];
|
|
179
|
+
assert.ok(msg, "expected a distinct projection open-failure warn");
|
|
180
|
+
assert.match(msg, /present but could not be opened/);
|
|
181
|
+
assert.match(msg, /SQLITE_CANTOPEN/);
|
|
182
|
+
assert.doesNotMatch(msg, /native binding/);
|
|
183
|
+
const probe = probeProjectionHealth(dir);
|
|
184
|
+
assert.equal(probe.state, "unopenable");
|
|
185
|
+
assert.equal(probe.nativeBindingMismatch, false);
|
|
186
|
+
} finally {
|
|
187
|
+
restoreOpener();
|
|
188
|
+
}
|
|
189
|
+
} finally {
|
|
190
|
+
restore();
|
|
191
|
+
await rm(dir, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("full-scan fallback still returns results when the projection cannot serve", SERIAL, async () => {
|
|
196
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-proj-fullscan-"));
|
|
197
|
+
const { restore } = captureLogs();
|
|
198
|
+
try {
|
|
199
|
+
const storage = new StorageManager(dir);
|
|
200
|
+
const a = await storage.writeMemory("fact", "alpha synthetic fact", { source: "test" });
|
|
201
|
+
const b = await storage.writeMemory("fact", "beta synthetic fact", { source: "test" });
|
|
202
|
+
|
|
203
|
+
// Absent projection: the browse returns null (the trigger the access
|
|
204
|
+
// layer keys on to run its full-corpus scan) ...
|
|
205
|
+
assert.equal(readProjectedMemoryBrowse(dir, { limit: 10, offset: 0 }), null);
|
|
206
|
+
|
|
207
|
+
// ... and the full-corpus data source the access layer scans still
|
|
208
|
+
// holds every memory (issue #1829: the fallback must keep serving).
|
|
209
|
+
const full = [...(await storage.readAllMemories()), ...(await storage.readArchivedMemories())];
|
|
210
|
+
const ids = new Set(full.map((m) => m.frontmatter.id));
|
|
211
|
+
assert.ok(ids.has(a.id));
|
|
212
|
+
assert.ok(ids.has(b.id));
|
|
213
|
+
assert.equal(full.length, 2);
|
|
214
|
+
|
|
215
|
+
// Regression guard for the exact #1829 failure: build the projection,
|
|
216
|
+
// then make it unopenable, and confirm the null trigger STILL fires so
|
|
217
|
+
// the full scan is reached (not a throw, not a stale cached page).
|
|
218
|
+
await rebuildMemoryProjection({ memoryDir: dir, dryRun: false });
|
|
219
|
+
const restoreOpener = __setProjectionReadonlyOpenerForTest((() => {
|
|
220
|
+
throw abiStyleError();
|
|
221
|
+
}) as ProjectionOpener);
|
|
222
|
+
try {
|
|
223
|
+
assert.equal(
|
|
224
|
+
readProjectedMemoryBrowse(dir, { limit: 10, offset: 0 }),
|
|
225
|
+
null,
|
|
226
|
+
"unopenable projection must return null so the full scan runs"
|
|
227
|
+
);
|
|
228
|
+
} finally {
|
|
229
|
+
restoreOpener();
|
|
230
|
+
}
|
|
231
|
+
} finally {
|
|
232
|
+
restore();
|
|
233
|
+
await rm(dir, { recursive: true, force: true });
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("present + valid projection → probe openable + doctor ok (schema validation passes)", SERIAL, async () => {
|
|
238
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-proj-healthy-"));
|
|
239
|
+
try {
|
|
240
|
+
await rebuildMemoryProjection({ memoryDir: dir, dryRun: false });
|
|
241
|
+
const probe = probeProjectionHealth(dir);
|
|
242
|
+
assert.equal(probe.state, "openable");
|
|
243
|
+
assert.equal(probe.nativeBindingMismatch, false);
|
|
244
|
+
// Doctor lens reports OK for a schema-valid projection (issue #1848 round 2).
|
|
245
|
+
const check = summarizeProjectionHealth({ memoryDir: dir });
|
|
246
|
+
assert.equal(check.status, "ok");
|
|
247
|
+
assert.match(check.summary, /opens cleanly/i);
|
|
248
|
+
} finally {
|
|
249
|
+
await rm(dir, { recursive: true, force: true });
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("present projection with MISSING schema tables → probe present-but-invalid (doctor non-ok, rebuild hint)", SERIAL, async () => {
|
|
254
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-proj-no-tables-"));
|
|
255
|
+
try {
|
|
256
|
+
// Create a sqlite file that OPENS cleanly but has NO projection tables
|
|
257
|
+
// (mirrors a half-built / stale / externally-created .sqlite that opens
|
|
258
|
+
// but cannot serve any browse query — issue #1848 round 2 root cause).
|
|
259
|
+
const dbPath = getMemoryProjectionPath(dir);
|
|
260
|
+
await mkdir(path.dirname(dbPath), { recursive: true });
|
|
261
|
+
const empty = openBetterSqlite3(dbPath); // creates a valid empty db
|
|
262
|
+
empty.close();
|
|
263
|
+
|
|
264
|
+
// Before #1848 round 2 this reported "openable" → doctor said ok while
|
|
265
|
+
// browse silently fell back to full-corpus scans on every memory list.
|
|
266
|
+
const probe = probeProjectionHealth(dir);
|
|
267
|
+
assert.equal(probe.state, "present-but-invalid");
|
|
268
|
+
assert.equal(probe.nativeBindingMismatch, false);
|
|
269
|
+
assert.ok(probe.detail, "expected a path-free detail for the schema failure");
|
|
270
|
+
|
|
271
|
+
// Doctor lens surfaces this as an ERROR with a rebuild hint — NOT ok.
|
|
272
|
+
const check = summarizeProjectionHealth({ memoryDir: dir });
|
|
273
|
+
assert.equal(check.status, "error");
|
|
274
|
+
assert.match(check.summary, /missing or unreadable/i);
|
|
275
|
+
assert.ok(check.remediation);
|
|
276
|
+
assert.match(check.remediation, /rebuild-memory-projection/);
|
|
277
|
+
|
|
278
|
+
// Browse still returns null (full-scan fallback trigger) — the projection
|
|
279
|
+
// is unusable, so the access layer must reach the full corpus.
|
|
280
|
+
assert.equal(readProjectedMemoryBrowse(dir, { limit: 5, offset: 0 }), null);
|
|
281
|
+
} finally {
|
|
282
|
+
await rm(dir, { recursive: true, force: true });
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("present projection with memory_current MISSING required columns → probe present-but-invalid (doctor non-ok, rebuild hint)", SERIAL, async () => {
|
|
287
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-proj-no-cols-"));
|
|
288
|
+
try {
|
|
289
|
+
// A sqlite file that opens cleanly AND has a `memory_current` table, but
|
|
290
|
+
// the table is stale/half-built with none of the columns
|
|
291
|
+
// readProjectedMemoryBrowse SELECTs (issue #1848 round 3 root cause). The
|
|
292
|
+
// round-2 check only verified the table exists + answers `SELECT 1`, so a
|
|
293
|
+
// file like `CREATE TABLE memory_current (x INTEGER)` was reported
|
|
294
|
+
// "openable" → doctor said healthy while browse threw on the missing
|
|
295
|
+
// memory_id/path_rel/category/... columns and silently fell back.
|
|
296
|
+
const dbPath = getMemoryProjectionPath(dir);
|
|
297
|
+
await mkdir(path.dirname(dbPath), { recursive: true });
|
|
298
|
+
const stale = openBetterSqlite3(dbPath); // creates a valid db
|
|
299
|
+
stale.exec("CREATE TABLE memory_current (x INTEGER)");
|
|
300
|
+
stale.close();
|
|
301
|
+
|
|
302
|
+
// Before round 3 this reported "openable" → doctor said ok.
|
|
303
|
+
const probe = probeProjectionHealth(dir);
|
|
304
|
+
assert.equal(probe.state, "present-but-invalid");
|
|
305
|
+
assert.equal(probe.nativeBindingMismatch, false);
|
|
306
|
+
// displayErrorDetail surfaces only the path-free error class (ProjectionSchemaError).
|
|
307
|
+
assert.ok(probe.detail, "expected a path-free detail for the missing-column schema failure");
|
|
308
|
+
|
|
309
|
+
// Doctor lens surfaces this as an ERROR with a rebuild hint — NOT ok.
|
|
310
|
+
const check = summarizeProjectionHealth({ memoryDir: dir });
|
|
311
|
+
assert.equal(check.status, "error");
|
|
312
|
+
assert.match(check.summary, /missing or unreadable/i);
|
|
313
|
+
assert.ok(check.remediation);
|
|
314
|
+
assert.match(check.remediation, /rebuild-memory-projection/);
|
|
315
|
+
|
|
316
|
+
// Browse still returns null (full-scan fallback trigger) — the projection
|
|
317
|
+
// cannot serve queries missing the browse columns.
|
|
318
|
+
assert.equal(readProjectedMemoryBrowse(dir, { limit: 5, offset: 0 }), null);
|
|
319
|
+
} finally {
|
|
320
|
+
await rm(dir, { recursive: true, force: true });
|
|
321
|
+
}
|
|
322
|
+
});
|
|
@@ -12,7 +12,10 @@ import type {
|
|
|
12
12
|
MemoryProjectionCurrentState,
|
|
13
13
|
MemoryStatus,
|
|
14
14
|
} from "./types.js";
|
|
15
|
+
import { log } from "./logger.js";
|
|
15
16
|
import {
|
|
17
|
+
displayErrorDetail,
|
|
18
|
+
isLikelyBetterSqlite3NativeBindingError,
|
|
16
19
|
openBetterSqlite3,
|
|
17
20
|
type BetterSqlite3Database,
|
|
18
21
|
} from "./runtime/better-sqlite.js";
|
|
@@ -314,14 +317,188 @@ export function initializeMemoryProjectionDb(db: BetterSqlite3Database): void {
|
|
|
314
317
|
.run("schemaVersion", String(MEMORY_PROJECTION_SCHEMA_VERSION));
|
|
315
318
|
}
|
|
316
319
|
|
|
320
|
+
// Test seam (issue #1829): the readonly projection opener is overridable so
|
|
321
|
+
// tests can inject an ABI-style throw against a PRESENT file without faking a
|
|
322
|
+
// corrupt native binding. Default delegates to the shared opener.
|
|
323
|
+
type ProjectionReadonlyOpener = typeof openBetterSqlite3;
|
|
324
|
+
let projectionReadonlyOpener: ProjectionReadonlyOpener = openBetterSqlite3;
|
|
325
|
+
|
|
326
|
+
/** @internal Test seam: override the readonly projection opener; returns a restore fn. */
|
|
327
|
+
export function __setProjectionReadonlyOpenerForTest(
|
|
328
|
+
opener: ProjectionReadonlyOpener | null,
|
|
329
|
+
): () => void {
|
|
330
|
+
const previous = projectionReadonlyOpener;
|
|
331
|
+
projectionReadonlyOpener = opener ?? openBetterSqlite3;
|
|
332
|
+
return () => {
|
|
333
|
+
projectionReadonlyOpener = previous;
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Rate-limited suppression for open failures — mirrors warnProjectionFallback
|
|
338
|
+
// in storage-guards.ts (5-min dedup per memoryDir). A present-but-unopenable
|
|
339
|
+
// projection is a real failure that must surface, but it repeats on every
|
|
340
|
+
// browse, so the log is throttled to keep output actionable (issue #1829).
|
|
341
|
+
const PROJECTION_OPEN_FAILURE_LOG_INTERVAL_MS = 5 * 60_000;
|
|
342
|
+
const projectionOpenFailureLoggedAt = new Map<string, number>();
|
|
343
|
+
|
|
344
|
+
/** @internal Test seam: clear the rate-limit dedup map. */
|
|
345
|
+
export function __resetProjectionOpenFailureSuppressionForTest(): void {
|
|
346
|
+
projectionOpenFailureLoggedAt.clear();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function logProjectionOpenFailure(memoryDir: string, error: unknown): void {
|
|
350
|
+
const now = Date.now();
|
|
351
|
+
const warnedAt = projectionOpenFailureLoggedAt.get(memoryDir) ?? 0;
|
|
352
|
+
if (now - warnedAt < PROJECTION_OPEN_FAILURE_LOG_INTERVAL_MS) return;
|
|
353
|
+
projectionOpenFailureLoggedAt.set(memoryDir, now);
|
|
354
|
+
// Path-free detail only (rule 11 / CodeQL js/stack-trace-exposure): the raw
|
|
355
|
+
// message can embed absolute loader paths; displayErrorDetail returns class +
|
|
356
|
+
// code. This is the DISTINCT signal that was missing before #1829, when a
|
|
357
|
+
// wrong-ABI build collapsed into the same silent null as a missing file.
|
|
358
|
+
const detail = displayErrorDetail(error);
|
|
359
|
+
const nativeMismatch = isLikelyBetterSqlite3NativeBindingError(error);
|
|
360
|
+
const detailSuffix = detail ? ` (${detail})` : "";
|
|
361
|
+
const abiSuffix = nativeMismatch
|
|
362
|
+
? " — native binding built for the wrong Node.js ABI; rebuild better-sqlite3 for this process"
|
|
363
|
+
: "";
|
|
364
|
+
log.warn(
|
|
365
|
+
`storage.memory-projection: index present but could not be opened${detailSuffix}${abiSuffix}; falling back to full-corpus scan`,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
317
369
|
function openProjectionReadonly(memoryDir: string): BetterSqlite3Database | null {
|
|
318
370
|
const dbPath = getMemoryProjectionPath(memoryDir);
|
|
371
|
+
// A MISSING file is a normal fallback (cold install, never built). A file
|
|
372
|
+
// that EXISTS but cannot open (wrong ABI / corrupt / bad perms) is a real
|
|
373
|
+
// failure that must surface (issue #1829): previously both collapsed into
|
|
374
|
+
// the same silent null, hiding native-binding mismatches behind 17-27s
|
|
375
|
+
// full-corpus scans on every memory list.
|
|
376
|
+
if (!fs.existsSync(dbPath)) return null;
|
|
319
377
|
try {
|
|
320
|
-
return
|
|
321
|
-
} catch {
|
|
378
|
+
return projectionReadonlyOpener(dbPath, { readonly: true, fileMustExist: true });
|
|
379
|
+
} catch (error) {
|
|
380
|
+
logProjectionOpenFailure(memoryDir, error);
|
|
322
381
|
return null;
|
|
323
382
|
}
|
|
324
383
|
}
|
|
384
|
+
|
|
385
|
+
export type ProjectionHealthState = "absent" | "openable" | "present-but-invalid" | "unopenable";
|
|
386
|
+
|
|
387
|
+
export interface ProjectionHealthProbe {
|
|
388
|
+
state: ProjectionHealthState;
|
|
389
|
+
/** Path-free detail (class + code) when state !== "absent"/"openable"; "" otherwise. */
|
|
390
|
+
detail: string;
|
|
391
|
+
/** true when the open failure is classified as a native-binding ABI mismatch. */
|
|
392
|
+
nativeBindingMismatch: boolean;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Confirm the projection schema is present and queryable, not just that the
|
|
397
|
+
* sqlite file opens (issue #1848 round 2). A file that opens but has the
|
|
398
|
+
* projection tables missing — or is corrupt and only fails on first statement
|
|
399
|
+
* — previously reported `openable`/ok, so `remnic doctor` said healthy while
|
|
400
|
+
* browse silently fell back to full-corpus scans. `memory_current` is the
|
|
401
|
+
* core browse table; if it is absent or unreadable the projection cannot
|
|
402
|
+
* serve any query. Throws so the probe can report a distinct state.
|
|
403
|
+
*/
|
|
404
|
+
// Named so displayErrorDetail surfaces a clear, path-free identifier in the
|
|
405
|
+
// doctor report rather than a generic "Error".
|
|
406
|
+
class ProjectionSchemaError extends Error {
|
|
407
|
+
constructor(message: string) {
|
|
408
|
+
super(message);
|
|
409
|
+
this.name = "ProjectionSchemaError";
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Columns `readProjectedMemoryBrowse` SELECTs from `memory_current` with no
|
|
415
|
+
* graceful fallback. `tags_json`/`preview_text` fall back via
|
|
416
|
+
* memoryCurrentSelectExpressions and `path_valid` falls back via the legacy
|
|
417
|
+
* full-scan branch — none of those make a browse query throw. A projection
|
|
418
|
+
* whose `memory_current` lacks any of these columns cannot serve browse
|
|
419
|
+
* queries: the SELECT throws and browse silently falls back to a full-corpus
|
|
420
|
+
* scan. Enumerated directly from readProjectedMemoryBrowse's SELECT list +
|
|
421
|
+
* ORDER BY references (issue #1848 round 3).
|
|
422
|
+
*/
|
|
423
|
+
const REQUIRED_MEMORY_CURRENT_COLUMNS = [
|
|
424
|
+
"memory_id",
|
|
425
|
+
"path_rel",
|
|
426
|
+
"category",
|
|
427
|
+
"status",
|
|
428
|
+
"created_at",
|
|
429
|
+
"updated_at",
|
|
430
|
+
"entity_ref",
|
|
431
|
+
] as const;
|
|
432
|
+
|
|
433
|
+
function assertProjectionSchemaReadable(db: BetterSqlite3Database): void {
|
|
434
|
+
const row = db
|
|
435
|
+
.prepare(
|
|
436
|
+
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'memory_current'",
|
|
437
|
+
)
|
|
438
|
+
.get() as { name: string } | undefined;
|
|
439
|
+
if (!row?.name) {
|
|
440
|
+
throw new ProjectionSchemaError(
|
|
441
|
+
"projection schema is missing or was not initialized (memory_current table not found)",
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
// Round 3 (issue #1848): verify the columns readProjectedMemoryBrowse
|
|
445
|
+
// actually SELECT are present, not just that the table exists. A file like
|
|
446
|
+
// `CREATE TABLE memory_current (x INTEGER)` passed the round-2 table-existence
|
|
447
|
+
// + SELECT-1 check but cannot serve browse queries, so browse silently fell
|
|
448
|
+
// back to full-corpus scans while `remnic doctor` reported healthy. Classify
|
|
449
|
+
// such a stale/half-built table as present-but-invalid so the operator gets a
|
|
450
|
+
// rebuild hint instead of a false-clean bill of health.
|
|
451
|
+
const columns = listTableColumns(db, "memory_current");
|
|
452
|
+
const missing = REQUIRED_MEMORY_CURRENT_COLUMNS.filter((c) => !columns.has(c));
|
|
453
|
+
if (missing.length > 0) {
|
|
454
|
+
throw new ProjectionSchemaError(
|
|
455
|
+
`projection memory_current table is missing required column(s): ${missing.join(", ")}`,
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
// Also confirm the table is queryable — catches physical corruption that
|
|
459
|
+
// only surfaces on first read (the file opens but statements fail).
|
|
460
|
+
db.prepare("SELECT 1 FROM memory_current LIMIT 1").get();
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Stat + attempt-open + schema-validate the memory projection WITHOUT keeping
|
|
465
|
+
* a handle, for the `remnic doctor` health check (issue #1829 / #1848).
|
|
466
|
+
* Distinguishes four states so operators can tell "not built yet" (normal) from
|
|
467
|
+
* "exists but broken" (needs a rebuild / native-binding rebuild). Never throws.
|
|
468
|
+
*/
|
|
469
|
+
export function probeProjectionHealth(memoryDir: string): ProjectionHealthProbe {
|
|
470
|
+
const dbPath = getMemoryProjectionPath(memoryDir);
|
|
471
|
+
if (!fs.existsSync(dbPath)) {
|
|
472
|
+
return { state: "absent", detail: "", nativeBindingMismatch: false };
|
|
473
|
+
}
|
|
474
|
+
let db: BetterSqlite3Database | null = null;
|
|
475
|
+
try {
|
|
476
|
+
db = projectionReadonlyOpener(dbPath, { readonly: true, fileMustExist: true });
|
|
477
|
+
// Validate the projection is actually USABLE, not just that the file
|
|
478
|
+
// opens (issue #1848 round 2): a file that opens but has the schema
|
|
479
|
+
// missing/corrupt previously reported ok, so doctor said healthy while
|
|
480
|
+
// browse silently fell back to full-corpus scans.
|
|
481
|
+
assertProjectionSchemaReadable(db);
|
|
482
|
+
return { state: "openable", detail: "", nativeBindingMismatch: false };
|
|
483
|
+
} catch (error) {
|
|
484
|
+
// If the file OPENED (db assigned) but the schema query failed, the
|
|
485
|
+
// native binding is fine — the projection itself is stale/corrupt.
|
|
486
|
+
if (db) {
|
|
487
|
+
return {
|
|
488
|
+
state: "present-but-invalid",
|
|
489
|
+
detail: displayErrorDetail(error),
|
|
490
|
+
nativeBindingMismatch: false,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
state: "unopenable",
|
|
495
|
+
detail: displayErrorDetail(error),
|
|
496
|
+
nativeBindingMismatch: isLikelyBetterSqlite3NativeBindingError(error),
|
|
497
|
+
};
|
|
498
|
+
} finally {
|
|
499
|
+
db?.close();
|
|
500
|
+
}
|
|
501
|
+
}
|
|
325
502
|
function updateProjectionBestEffort(
|
|
326
503
|
memoryDir: string,
|
|
327
504
|
sql: string,
|
package/src/operator-toolkit.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
resolveOpenClawWorkspaceStatePath,
|
|
12
12
|
} from "./native-knowledge.js";
|
|
13
13
|
import { StorageManager } from "./storage.js";
|
|
14
|
+
import { probeProjectionHealth } from "./memory-projection-store.js";
|
|
14
15
|
import { listNamespaces } from "./namespaces/migrate.js";
|
|
15
16
|
import { summarizeNamespaceMaintenanceHealth } from "./maintenance/namespace-maintenance-fanout.js";
|
|
16
17
|
import {
|
|
@@ -1094,6 +1095,64 @@ export function summarizeProvenanceCoverage(
|
|
|
1094
1095
|
return { counts, total: memories.length };
|
|
1095
1096
|
}
|
|
1096
1097
|
|
|
1098
|
+
/**
|
|
1099
|
+
* Memory projection health (issue #1829). A present-but-unopenable projection
|
|
1100
|
+
* (wrong ABI / corrupt / bad perms) previously collapsed into the same silent
|
|
1101
|
+
* null as a missing file, hiding native-binding mismatches behind full-corpus
|
|
1102
|
+
* scans. This check stats + attempt-opens the projection so `remnic doctor`
|
|
1103
|
+
* distinguishes "not built yet" (normal) from "exists but broken" (needs a
|
|
1104
|
+
* rebuild / native-binding rebuild). Never throws.
|
|
1105
|
+
*/
|
|
1106
|
+
export function summarizeProjectionHealth(
|
|
1107
|
+
config: Pick<PluginConfig, "memoryDir">,
|
|
1108
|
+
): OperatorDoctorCheck {
|
|
1109
|
+
const probe = probeProjectionHealth(config.memoryDir);
|
|
1110
|
+
if (probe.state === "absent") {
|
|
1111
|
+
return {
|
|
1112
|
+
key: "memory_projection",
|
|
1113
|
+
status: "ok",
|
|
1114
|
+
summary: "Memory projection index not built yet (normal until first maintenance run).",
|
|
1115
|
+
details: { state: probe.state },
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
if (probe.state === "openable") {
|
|
1119
|
+
return {
|
|
1120
|
+
key: "memory_projection",
|
|
1121
|
+
status: "ok",
|
|
1122
|
+
summary: "Memory projection index opens cleanly.",
|
|
1123
|
+
details: { state: probe.state },
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
if (probe.state === "present-but-invalid") {
|
|
1127
|
+
const schemaDetailSuffix = probe.detail ? ` (${probe.detail})` : "";
|
|
1128
|
+
return {
|
|
1129
|
+
key: "memory_projection",
|
|
1130
|
+
status: "error",
|
|
1131
|
+
summary: `Memory projection index exists but its schema is missing or unreadable${schemaDetailSuffix}; memory browse is falling back to full-corpus scans.`,
|
|
1132
|
+
remediation:
|
|
1133
|
+
"Rebuild the projection with `remnic rebuild-memory-projection --write`.",
|
|
1134
|
+
details: { state: probe.state, detail: probe.detail },
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
const nativeSuffix = probe.nativeBindingMismatch
|
|
1138
|
+
? " Native binding appears built for the wrong Node.js ABI."
|
|
1139
|
+
: "";
|
|
1140
|
+
const detailSuffix = probe.detail ? ` (${probe.detail})` : "";
|
|
1141
|
+
return {
|
|
1142
|
+
key: "memory_projection",
|
|
1143
|
+
status: "error",
|
|
1144
|
+
summary: `Memory projection index exists but could not be opened${detailSuffix}.${nativeSuffix}`,
|
|
1145
|
+
remediation: probe.nativeBindingMismatch
|
|
1146
|
+
? "Rebuild the better-sqlite3 native binding for this Node.js (`node scripts/ensure-better-sqlite3.mjs` or `pnpm rebuild better-sqlite3`), then restart."
|
|
1147
|
+
: "Rebuild the projection with `remnic rebuild-memory-projection --write`; check file permissions and integrity.",
|
|
1148
|
+
details: {
|
|
1149
|
+
state: probe.state,
|
|
1150
|
+
detail: probe.detail,
|
|
1151
|
+
nativeBindingMismatch: probe.nativeBindingMismatch,
|
|
1152
|
+
},
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1097
1156
|
export async function runOperatorDoctor(options: OperatorDoctorOptions): Promise<OperatorDoctorReport> {
|
|
1098
1157
|
const now = options.now ?? new Date();
|
|
1099
1158
|
const configStatus = await loadCliPluginConfig(options.configPath);
|
|
@@ -1211,6 +1270,11 @@ export async function runOperatorDoctor(options: OperatorDoctorOptions): Promise
|
|
|
1211
1270
|
details: meta,
|
|
1212
1271
|
});
|
|
1213
1272
|
|
|
1273
|
+
// Memory projection health (issue #1829). Surfaces a present-but-unopenable
|
|
1274
|
+
// projection (wrong ABI / corrupt) that previously degraded every memory
|
|
1275
|
+
// list to a silent full-corpus scan. Absent and openable are both "ok".
|
|
1276
|
+
checks.push(summarizeProjectionHealth(config));
|
|
1277
|
+
|
|
1214
1278
|
// Provenance coverage (issue #1575 PR 3). Operators need to see the
|
|
1215
1279
|
// verified/unverified/none distribution grow as extraction backfills.
|
|
1216
1280
|
// Best-effort: a read failure surfaces as a warn, not an error.
|