@remnic/core 9.3.719 → 9.3.721
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-boundary.d.ts +10 -7
- package/dist/access-boundary.js +37 -34
- package/dist/access-cli.js +71 -66
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +11 -8
- package/dist/access-http.js +44 -41
- package/dist/access-mcp.d.ts +11 -8
- package/dist/access-mcp.js +40 -37
- package/dist/access-operations-batch.js +38 -35
- package/dist/access-operations.d.ts +11 -8
- package/dist/access-operations.js +39 -36
- package/dist/access-schema.d.ts +19 -19
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-CZbnt0t2.d.ts → access-service-Dj3I-VBS.d.ts} +3 -3
- package/dist/access-service.d.ts +11 -8
- package/dist/access-service.js +36 -33
- package/dist/access-surface-catalog.d.ts +10 -7
- 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 +4 -2
- package/dist/active-recall.js.map +1 -1
- package/dist/adapters/index.js +7 -7
- package/dist/adapters/registry.js +3 -3
- package/dist/{auto-sync-24NNS3GU.js → auto-sync-F3KCSQFJ.js} +5 -5
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +11 -8
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +12 -10
- 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/calibration.js +2 -2
- package/dist/capabilities.d.ts +30 -2
- package/dist/capabilities.js +3 -1
- package/dist/{catalog-CEYlMs9X.d.ts → catalog-F-OEVbRQ.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +15 -13
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory-graph.d.ts +4 -0
- package/dist/causal-trajectory-graph.js +2 -2
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-TH7WMHVK.js → chunk-3NN73CM7.js} +2 -2
- package/dist/{chunk-A5TUPZ3N.js → chunk-4SF232E6.js} +60 -30
- package/dist/chunk-4SF232E6.js.map +1 -0
- package/dist/{chunk-DGTDRIUO.js → chunk-4T3BMHKD.js} +2 -2
- package/dist/{chunk-EH27LHML.js → chunk-5O42UKXI.js} +2 -2
- package/dist/{chunk-KRGC4EDX.js → chunk-6FAMMBQ3.js} +2 -2
- package/dist/{chunk-FEB7W5JG.js → chunk-6OIDDKAA.js} +27 -6
- package/dist/chunk-6OIDDKAA.js.map +1 -0
- package/dist/{chunk-67HG7KFE.js → chunk-6YKIYQU7.js} +2 -2
- package/dist/{chunk-LXSG4L55.js → chunk-7CTNUVHZ.js} +2 -2
- package/dist/{chunk-UCJ5CDTK.js → chunk-A5XNBK2D.js} +22 -14
- package/dist/chunk-A5XNBK2D.js.map +1 -0
- package/dist/{chunk-7EEVXW7X.js → chunk-A7TJZLPK.js} +2 -2
- package/dist/{chunk-P5IPFZVH.js → chunk-B7WISIAJ.js} +5 -5
- package/dist/chunk-BCDKBDSY.js +173 -0
- package/dist/chunk-BCDKBDSY.js.map +1 -0
- package/dist/{chunk-JAKYZGV4.js → chunk-BXEOHV7K.js} +3 -3
- package/dist/chunk-C4X4QGIO.js +60 -0
- package/dist/chunk-C4X4QGIO.js.map +1 -0
- package/dist/{chunk-S3OPGGZB.js → chunk-D7EBTQ24.js} +2 -2
- package/dist/{chunk-IO5NQEGZ.js → chunk-DKT3X5C3.js} +2 -2
- package/dist/{chunk-PBH4JUAB.js → chunk-DVRHLN2O.js} +18 -3
- package/dist/chunk-DVRHLN2O.js.map +1 -0
- package/dist/{chunk-S35FX6MU.js → chunk-EBDA5TYA.js} +2 -2
- package/dist/{chunk-2PTBLS2S.js → chunk-EDOQM4E4.js} +2 -2
- package/dist/{chunk-YTZLBBEB.js → chunk-EEJMD2L2.js} +3 -3
- package/dist/{chunk-YGKUAX2B.js → chunk-EHQ5HO5T.js} +11 -11
- package/dist/chunk-EHQ5HO5T.js.map +1 -0
- package/dist/{chunk-JIZGYQYH.js → chunk-EM2LDXJ2.js} +4 -4
- package/dist/{chunk-YYOK4GVF.js → chunk-EVPNWN7Z.js} +10 -10
- package/dist/{chunk-HLIR3RJ7.js → chunk-EXUCUPV2.js} +2 -2
- package/dist/{chunk-F3PVXKHP.js → chunk-GDUOCHCD.js} +27 -21
- package/dist/chunk-GDUOCHCD.js.map +1 -0
- package/dist/{chunk-L5ASZA2F.js → chunk-GJSL4WG2.js} +17 -2
- package/dist/chunk-GJSL4WG2.js.map +1 -0
- package/dist/{chunk-2SXMCQEQ.js → chunk-GZQXOQQ2.js} +3 -3
- package/dist/{chunk-2KAYTPPT.js → chunk-HCVWF657.js} +12 -12
- package/dist/{chunk-N3DUH6OO.js → chunk-ILFSEIRT.js} +1 -1
- package/dist/chunk-ILFSEIRT.js.map +1 -0
- package/dist/{chunk-4KZGLDJJ.js → chunk-J3JPB7ZI.js} +2 -2
- package/dist/{chunk-3JJWNZTT.js → chunk-L2B3EE7B.js} +102 -42
- package/dist/chunk-L2B3EE7B.js.map +1 -0
- package/dist/{chunk-NFRBADNR.js → chunk-LA3HZKES.js} +610 -369
- package/dist/chunk-LA3HZKES.js.map +1 -0
- package/dist/{chunk-LQ6JI4VH.js → chunk-LD53WPMU.js} +4 -4
- package/dist/{chunk-URM3HW2R.js → chunk-LLUGAZ3B.js} +2 -2
- package/dist/{chunk-RVLB4E6N.js → chunk-MDBZTH7N.js} +5 -32
- package/dist/chunk-MDBZTH7N.js.map +1 -0
- package/dist/{chunk-U5H7WHTS.js → chunk-N77ELJ2D.js} +4 -4
- package/dist/{chunk-2VHMVWPU.js → chunk-NXRUUDUF.js} +2 -2
- package/dist/{chunk-DIFU33GU.js → chunk-OHTOPFRF.js} +2 -2
- package/dist/{chunk-74MYJSKB.js → chunk-ORAK3LNZ.js} +2 -2
- package/dist/{chunk-REWQAFSA.js → chunk-PD42EYQN.js} +2 -2
- package/dist/chunk-PHK3HARR.js +32 -0
- package/dist/chunk-PHK3HARR.js.map +1 -0
- package/dist/{chunk-773DPI6T.js → chunk-PKMQK2WY.js} +4 -4
- package/dist/{chunk-72VDBALG.js → chunk-QD6QP5P6.js} +63 -54
- package/dist/chunk-QD6QP5P6.js.map +1 -0
- package/dist/{chunk-CTOQEZSN.js → chunk-QR67FU5G.js} +2 -2
- package/dist/{chunk-AU7Q3LSC.js → chunk-RKNJBZ55.js} +7 -7
- package/dist/{chunk-YI7RZO7Y.js → chunk-SQNED75R.js} +1 -1
- package/dist/{chunk-5CEJH5ZN.js → chunk-TAPSNIMA.js} +4 -4
- package/dist/{chunk-IF362TF6.js → chunk-TQT3USCG.js} +2 -2
- package/dist/chunk-TQT3USCG.js.map +1 -0
- package/dist/{chunk-5RHM43BJ.js → chunk-TXQCG6SE.js} +3 -3
- package/dist/{chunk-ANI5DUAE.js → chunk-UIXXE6NI.js} +7 -7
- package/dist/{chunk-2COL35PK.js → chunk-UO5ZHIIH.js} +2 -2
- package/dist/{chunk-OJU73BUX.js → chunk-VCLGUIFM.js} +2 -2
- package/dist/{chunk-5J4OL7TJ.js → chunk-VUA4S7P6.js} +4 -4
- package/dist/{chunk-A66YRSOS.js → chunk-WQYSDPMA.js} +2 -2
- package/dist/chunk-XGTBNVIU.js +79 -0
- package/dist/chunk-XGTBNVIU.js.map +1 -0
- package/dist/{chunk-OZ2KQ7HF.js → chunk-YEE5KJQQ.js} +2 -2
- package/dist/chunk-YN2UN7N3.js +217 -0
- package/dist/chunk-YN2UN7N3.js.map +1 -0
- package/dist/{chunk-WRFKZEO6.js → chunk-YQGHW5ML.js} +4 -4
- package/dist/{chunk-APUJIMDM.js → chunk-Z4RU5VPM.js} +2 -2
- package/dist/{chunk-DCWIQFNA.js → chunk-ZUDM75KG.js} +4 -4
- package/dist/{cli-1orcdXOY.d.ts → cli-P_GaYcCY.d.ts} +3 -3
- package/dist/cli.d.ts +9 -6
- package/dist/cli.js +67 -63
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +12 -10
- 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 +4 -2
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +12 -10
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +13 -11
- 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 +12 -10
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +11 -8
- package/dist/extraction-faithfulness.d.ts +8 -2
- package/dist/extraction-faithfulness.js +4 -3
- 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-judge.js +3 -3
- package/dist/extraction.d.ts +1 -1
- package/dist/extraction.js +5 -4
- package/dist/faithfulness-config.d.ts +55 -0
- package/dist/faithfulness-config.js +11 -0
- package/dist/faithfulness-config.js.map +1 -0
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/fallback-llm.js +2 -2
- package/dist/{forget-U66KPDPY.js → forget-OEE5OELK.js} +2 -2
- package/dist/graph-dashboard-diff.d.ts +4 -0
- package/dist/graph-dashboard-key.d.ts +4 -0
- package/dist/graph-dashboard-parser.d.ts +4 -0
- package/dist/{graph-edge-decay-JLOF3YW4.js → graph-edge-decay-QPRJQ7DL.js} +3 -3
- package/dist/graph-edge-reinforcement.d.ts +4 -0
- package/dist/graph-snapshot.d.ts +7 -3
- package/dist/graph-snapshot.js +3 -3
- package/dist/graph.d.ts +7 -1
- package/dist/graph.js +2 -2
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +571 -570
- package/dist/index.js +104 -99
- package/dist/index.js.map +1 -1
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/index.js +4 -4
- 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 +96 -0
- package/dist/local-model-endpoint.js +12 -0
- package/dist/local-model-endpoint.js.map +1 -0
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +12 -10
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +12 -10
- package/dist/maintenance/rebuild-memory-projection.js +13 -11
- package/dist/mcp-memory-inspector-app.d.ts +11 -8
- 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 +1 -1
- 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 +22 -20
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +9 -9
- package/dist/namespaces/storage.d.ts +2 -2
- package/dist/namespaces/storage.js +12 -10
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +3 -2
- package/dist/operator-toolkit.js +32 -28
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +14 -12
- package/dist/{orchestrator-kOxredSr.d.ts → orchestrator-f5xv19kp.d.ts} +178 -81
- package/dist/orchestrator.d.ts +8 -5
- package/dist/orchestrator.js +56 -52
- 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/provenance.js +2 -1
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-planner-llm.js +2 -2
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +18 -1
- package/dist/recall-xray.js +2 -2
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +5 -3
- package/dist/retrieval-agents.d.ts +1 -1
- 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/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +8 -8
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +14 -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-B58v3S-a.d.ts → semantic-consolidation-BQm1e1Pn.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +13 -11
- package/dist/semantic-rule-promotion.js +12 -10
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +12 -10
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +45 -1
- package/dist/storage.js +11 -9
- package/dist/summarizer.d.ts +1 -1
- package/dist/summarizer.js +3 -3
- 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/backup.js +2 -2
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +1 -1
- package/dist/trust-score-stage.d.ts +178 -0
- package/dist/trust-score-stage.js +18 -0
- package/dist/trust-score-stage.js.map +1 -0
- package/dist/trust-score.d.ts +164 -0
- package/dist/trust-score.js +22 -0
- package/dist/trust-score.js.map +1 -0
- package/dist/{types-ACmDOScJ.d.ts → types-DwO1Ot4n.d.ts} +56 -2
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +12 -10
- package/package.json +2 -2
- package/src/access-service.ts +8 -5
- package/src/capabilities-cluster-b.test.ts +78 -0
- package/src/capabilities.test.ts +11 -1
- package/src/capabilities.ts +58 -0
- package/src/cli/research-status-commands.ts +7 -3
- package/src/cli.ts +11 -6
- package/src/config.ts +60 -30
- package/src/extraction-faithfulness.test.ts +175 -0
- package/src/extraction-faithfulness.ts +53 -1
- package/src/faithfulness-config.test.ts +96 -0
- package/src/faithfulness-config.ts +138 -0
- package/src/graph-snapshot.ts +3 -3
- package/src/graph.ts +8 -7
- package/src/lifecycle/tombstones.test.ts +242 -1
- package/src/lifecycle/tombstones.ts +157 -70
- package/src/local-model-endpoint.test.ts +155 -0
- package/src/local-model-endpoint.ts +189 -0
- package/src/operator-toolkit.ts +24 -7
- package/src/orchestration/tier-migration-coordinator.ts +286 -0
- package/src/orchestrator.ts +331 -222
- package/src/recall-xray.test.ts +62 -0
- package/src/recall-xray.ts +34 -0
- package/src/trust-score-recall-paths.test.ts +157 -0
- package/src/trust-score-stage.test.ts +350 -0
- package/src/trust-score-stage.ts +398 -0
- package/src/trust-score.test.ts +282 -0
- package/src/trust-score.ts +449 -0
- package/src/types.ts +61 -1
- package/dist/chunk-3JJWNZTT.js.map +0 -1
- package/dist/chunk-72VDBALG.js.map +0 -1
- package/dist/chunk-A5TUPZ3N.js.map +0 -1
- package/dist/chunk-F3PVXKHP.js.map +0 -1
- package/dist/chunk-FEB7W5JG.js.map +0 -1
- package/dist/chunk-IF362TF6.js.map +0 -1
- package/dist/chunk-L5ASZA2F.js.map +0 -1
- package/dist/chunk-N3DUH6OO.js.map +0 -1
- package/dist/chunk-NFRBADNR.js.map +0 -1
- package/dist/chunk-PBH4JUAB.js.map +0 -1
- package/dist/chunk-RVLB4E6N.js.map +0 -1
- package/dist/chunk-UCJ5CDTK.js.map +0 -1
- package/dist/chunk-YGKUAX2B.js.map +0 -1
- /package/dist/{auto-sync-24NNS3GU.js.map → auto-sync-F3KCSQFJ.js.map} +0 -0
- /package/dist/{chunk-TH7WMHVK.js.map → chunk-3NN73CM7.js.map} +0 -0
- /package/dist/{chunk-DGTDRIUO.js.map → chunk-4T3BMHKD.js.map} +0 -0
- /package/dist/{chunk-EH27LHML.js.map → chunk-5O42UKXI.js.map} +0 -0
- /package/dist/{chunk-KRGC4EDX.js.map → chunk-6FAMMBQ3.js.map} +0 -0
- /package/dist/{chunk-67HG7KFE.js.map → chunk-6YKIYQU7.js.map} +0 -0
- /package/dist/{chunk-LXSG4L55.js.map → chunk-7CTNUVHZ.js.map} +0 -0
- /package/dist/{chunk-7EEVXW7X.js.map → chunk-A7TJZLPK.js.map} +0 -0
- /package/dist/{chunk-P5IPFZVH.js.map → chunk-B7WISIAJ.js.map} +0 -0
- /package/dist/{chunk-JAKYZGV4.js.map → chunk-BXEOHV7K.js.map} +0 -0
- /package/dist/{chunk-S3OPGGZB.js.map → chunk-D7EBTQ24.js.map} +0 -0
- /package/dist/{chunk-IO5NQEGZ.js.map → chunk-DKT3X5C3.js.map} +0 -0
- /package/dist/{chunk-S35FX6MU.js.map → chunk-EBDA5TYA.js.map} +0 -0
- /package/dist/{chunk-2PTBLS2S.js.map → chunk-EDOQM4E4.js.map} +0 -0
- /package/dist/{chunk-YTZLBBEB.js.map → chunk-EEJMD2L2.js.map} +0 -0
- /package/dist/{chunk-JIZGYQYH.js.map → chunk-EM2LDXJ2.js.map} +0 -0
- /package/dist/{chunk-YYOK4GVF.js.map → chunk-EVPNWN7Z.js.map} +0 -0
- /package/dist/{chunk-HLIR3RJ7.js.map → chunk-EXUCUPV2.js.map} +0 -0
- /package/dist/{chunk-2SXMCQEQ.js.map → chunk-GZQXOQQ2.js.map} +0 -0
- /package/dist/{chunk-2KAYTPPT.js.map → chunk-HCVWF657.js.map} +0 -0
- /package/dist/{chunk-4KZGLDJJ.js.map → chunk-J3JPB7ZI.js.map} +0 -0
- /package/dist/{chunk-LQ6JI4VH.js.map → chunk-LD53WPMU.js.map} +0 -0
- /package/dist/{chunk-URM3HW2R.js.map → chunk-LLUGAZ3B.js.map} +0 -0
- /package/dist/{chunk-U5H7WHTS.js.map → chunk-N77ELJ2D.js.map} +0 -0
- /package/dist/{chunk-2VHMVWPU.js.map → chunk-NXRUUDUF.js.map} +0 -0
- /package/dist/{chunk-DIFU33GU.js.map → chunk-OHTOPFRF.js.map} +0 -0
- /package/dist/{chunk-74MYJSKB.js.map → chunk-ORAK3LNZ.js.map} +0 -0
- /package/dist/{chunk-REWQAFSA.js.map → chunk-PD42EYQN.js.map} +0 -0
- /package/dist/{chunk-773DPI6T.js.map → chunk-PKMQK2WY.js.map} +0 -0
- /package/dist/{chunk-CTOQEZSN.js.map → chunk-QR67FU5G.js.map} +0 -0
- /package/dist/{chunk-AU7Q3LSC.js.map → chunk-RKNJBZ55.js.map} +0 -0
- /package/dist/{chunk-YI7RZO7Y.js.map → chunk-SQNED75R.js.map} +0 -0
- /package/dist/{chunk-5CEJH5ZN.js.map → chunk-TAPSNIMA.js.map} +0 -0
- /package/dist/{chunk-5RHM43BJ.js.map → chunk-TXQCG6SE.js.map} +0 -0
- /package/dist/{chunk-ANI5DUAE.js.map → chunk-UIXXE6NI.js.map} +0 -0
- /package/dist/{chunk-2COL35PK.js.map → chunk-UO5ZHIIH.js.map} +0 -0
- /package/dist/{chunk-OJU73BUX.js.map → chunk-VCLGUIFM.js.map} +0 -0
- /package/dist/{chunk-5J4OL7TJ.js.map → chunk-VUA4S7P6.js.map} +0 -0
- /package/dist/{chunk-A66YRSOS.js.map → chunk-WQYSDPMA.js.map} +0 -0
- /package/dist/{chunk-OZ2KQ7HF.js.map → chunk-YEE5KJQQ.js.map} +0 -0
- /package/dist/{chunk-WRFKZEO6.js.map → chunk-YQGHW5ML.js.map} +0 -0
- /package/dist/{chunk-APUJIMDM.js.map → chunk-Z4RU5VPM.js.map} +0 -0
- /package/dist/{chunk-DCWIQFNA.js.map → chunk-ZUDM75KG.js.map} +0 -0
- /package/dist/{forget-U66KPDPY.js.map → forget-OEE5OELK.js.map} +0 -0
- /package/dist/{graph-edge-decay-JLOF3YW4.js.map → graph-edge-decay-QPRJQ7DL.js.map} +0 -0
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
// - Semantic tier (4) ships dark, off by default (rule 48).
|
|
41
41
|
// ---------------------------------------------------------------------------
|
|
42
42
|
|
|
43
|
-
import { serializeMutations } from "../utils/serialize-mutations.js";
|
|
43
|
+
import { serializeMutations, withHeldFileLock } from "../utils/serialize-mutations.js";
|
|
44
44
|
|
|
45
45
|
/** Why a tombstone was emitted. */
|
|
46
46
|
export type TombstoneReason =
|
|
@@ -129,6 +129,20 @@ export interface TombstoneStoreOptions {
|
|
|
129
129
|
* skipped entirely.
|
|
130
130
|
*/
|
|
131
131
|
semanticSimilarity?: (a: string, b: string) => number;
|
|
132
|
+
/**
|
|
133
|
+
* Cross-process write-lock timings for the tombstone JSONL mutation lock
|
|
134
|
+
* (issue #1639). The secure-store append is a read-merge-write (read
|
|
135
|
+
* encrypted → decrypt → concat → re-encrypt → atomic rename), so two
|
|
136
|
+
* Remnic processes appending concurrently can each read the same contents
|
|
137
|
+
* and the last writer drops the other's entry — breaking the
|
|
138
|
+
* non-resurrection invariant. The lock serializes the mutation across
|
|
139
|
+
* processes. When omitted, defaults that fit the short tombstone critical
|
|
140
|
+
* section (mirroring the summary-snapshot lock): staleMs 30s, maxWaitMs 5s,
|
|
141
|
+
* heartbeatMs floor(staleMs/3).
|
|
142
|
+
*/
|
|
143
|
+
readonly lockStaleMs?: number;
|
|
144
|
+
readonly lockMaxWaitMs?: number;
|
|
145
|
+
readonly lockHeartbeatMs?: number;
|
|
132
146
|
}
|
|
133
147
|
|
|
134
148
|
/** Injected file I/O — the StorageManager wires its secure-store-aware
|
|
@@ -261,7 +275,33 @@ export class TombstoneStore {
|
|
|
261
275
|
private readonly namespace: string,
|
|
262
276
|
private readonly options: TombstoneStoreOptions,
|
|
263
277
|
private readonly io: TombstoneFileIo,
|
|
264
|
-
) {
|
|
278
|
+
) {
|
|
279
|
+
this.lockStaleMs = this.options.lockStaleMs ?? 30_000;
|
|
280
|
+
this.lockMaxWaitMs = this.options.lockMaxWaitMs ?? 5_000;
|
|
281
|
+
this.lockHeartbeatMs =
|
|
282
|
+
this.options.lockHeartbeatMs ?? Math.max(100, Math.floor(this.lockStaleMs / 3));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Resolved cross-process write-lock timings (issue #1639). Defaults mirror
|
|
287
|
+
* the summary-snapshot lock: a tombstone append critical section is
|
|
288
|
+
* sub-second, so a stale lock older than 30s is a crashed holder; bounded
|
|
289
|
+
* acquisition gives up after 5s and strict-fails rather than clobbering a
|
|
290
|
+
* concurrent writer (which would reintroduce the lost-write race this lock
|
|
291
|
+
* closes).
|
|
292
|
+
*/
|
|
293
|
+
private readonly lockStaleMs: number;
|
|
294
|
+
private readonly lockMaxWaitMs: number;
|
|
295
|
+
private readonly lockHeartbeatMs: number;
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Sibling advisory lockfile: `<tombstones>.jsonl` → `<tombstones>.lock`.
|
|
299
|
+
* Shared by append, revoke, and rebuild so every JSONL mutation serializes
|
|
300
|
+
* against the others across processes (issue #1639: "shared with rebuilds").
|
|
301
|
+
*/
|
|
302
|
+
private get lockPath(): string {
|
|
303
|
+
return this.filePath.replace(/\.jsonl$/i, "") + ".lock";
|
|
304
|
+
}
|
|
265
305
|
|
|
266
306
|
/** Lazy load + build the in-memory index. Idempotent. */
|
|
267
307
|
async load(): Promise<void> {
|
|
@@ -473,10 +513,48 @@ export class TombstoneStore {
|
|
|
473
513
|
|
|
474
514
|
private serializeAppend(entry: TombstoneEntry): Promise<void> {
|
|
475
515
|
const line = JSON.stringify(entry) + "\n";
|
|
476
|
-
// serializeMutations
|
|
477
|
-
//
|
|
516
|
+
// serializeMutations (in-process, rule 40 rejection recovery) wraps the
|
|
517
|
+
// cross-process lock: within one process appends are queued, and across
|
|
518
|
+
// processes the advisory lockfile serializes the read-merge-write so two
|
|
519
|
+
// Remnic processes appending concurrently cannot drop each other's entry
|
|
520
|
+
// (issue #1639). Under the lock we re-sync the in-memory index against
|
|
521
|
+
// disk first, so indexEntry builds on the peer's just-appended entries.
|
|
478
522
|
return serializeMutations(`tombstone:${this.filePath}`, () =>
|
|
479
|
-
this.
|
|
523
|
+
this.withWriteLock(async () => {
|
|
524
|
+
await this.ensureFreshAgainstDisk();
|
|
525
|
+
await this.io.append(this.filePath, line);
|
|
526
|
+
}),
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Acquire the cross-process tombstone write lock and run `task` under it
|
|
532
|
+
* (issue #1639). Strict-fail on acquisition timeout: the secure-store append
|
|
533
|
+
* is a read-merge-write, so a best-effort unlocked run would clobber a
|
|
534
|
+
* concurrent writer — reintroducing the lost-write race this lock closes.
|
|
535
|
+
* The thrown error surfaces to the caller (rule 34: never silently drop);
|
|
536
|
+
* rebuild recovers the tombstone on the next maintenance cycle.
|
|
537
|
+
*
|
|
538
|
+
* In-process callers are already serialized by serializeMutations (one key
|
|
539
|
+
* for append/revoke/rebuild), so this lock only contends across processes —
|
|
540
|
+
* the single-process daemon (default deployment) acquires it immediately.
|
|
541
|
+
*/
|
|
542
|
+
private withWriteLock<T>(task: () => Promise<T>): Promise<T> {
|
|
543
|
+
return withHeldFileLock(
|
|
544
|
+
this.lockPath,
|
|
545
|
+
{
|
|
546
|
+
staleMs: this.lockStaleMs,
|
|
547
|
+
maxWaitMs: this.lockMaxWaitMs,
|
|
548
|
+
heartbeatMs: this.lockHeartbeatMs,
|
|
549
|
+
},
|
|
550
|
+
async (acquired) => {
|
|
551
|
+
if (!acquired) {
|
|
552
|
+
throw new Error(
|
|
553
|
+
"could not acquire tombstone write lock (contention timeout or filesystem error)",
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
return task();
|
|
557
|
+
},
|
|
480
558
|
);
|
|
481
559
|
}
|
|
482
560
|
|
|
@@ -595,72 +673,81 @@ export class TombstoneStore {
|
|
|
595
673
|
* Returns the count of tombstone entries written.
|
|
596
674
|
*/
|
|
597
675
|
async rebuild(retiredMemories: ReadonlyArray<RetiredMemoryRecord>): Promise<number> {
|
|
598
|
-
//
|
|
599
|
-
//
|
|
600
|
-
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
//
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
)
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
676
|
+
// The full read-merge-write runs UNDER the cross-process lock (issue #1639,
|
|
677
|
+
// cursor/codex review): we ensureFreshAgainstDisk() first so the in-memory
|
|
678
|
+
// index reflects any peer append/revoke that landed while we waited for the
|
|
679
|
+
// lock, THEN compute the payload. Rebuild rewrites the entire JSONL, so a
|
|
680
|
+
// payload built from a stale this.entries would overwrite a peer's just-
|
|
681
|
+
// written entry and resurrect a retired fact — the exact invariant this
|
|
682
|
+
// store exists to enforce. serializeMutations (in-process) wraps the
|
|
683
|
+
// cross-process lock; the key is unified with append/revoke so they cannot
|
|
684
|
+
// interleave in-process either.
|
|
685
|
+
const rebuiltCount = await serializeMutations(`tombstone:${this.filePath}`, () =>
|
|
686
|
+
this.withWriteLock(async () => {
|
|
687
|
+
await this.ensureFreshAgainstDisk();
|
|
688
|
+
// Preserve existing revocations (all namespaces — ids are globally
|
|
689
|
+
// unique) so a rebuild does not silently un-revoke.
|
|
690
|
+
const existingRevocations = this.entries.filter((e) => e.kind === "revocation");
|
|
691
|
+
// Preserve tombstone entries from OTHER namespaces when the backing
|
|
692
|
+
// file is shared (issue #1579 thread Oc2MJ). rebuild rewrites the
|
|
693
|
+
// entire file; without preserving foreign entries, rebuilding
|
|
694
|
+
// namespace A would silently delete namespace B's tombstones, allowing
|
|
695
|
+
// resurrection in B.
|
|
696
|
+
const foreignTombstones = this.entries.filter(
|
|
697
|
+
(e) => e.kind === "tombstone" && e.namespace !== this.namespace,
|
|
698
|
+
);
|
|
699
|
+
// Reuse existing tombstone ids for source-equivalent entries so a prior
|
|
700
|
+
// revocation (which references the tombstone id) survives rebuild —
|
|
701
|
+
// minting fresh ids would orphan the revocation and silently un-revoke.
|
|
702
|
+
// Issue #1579 thread Oci-T: key the reuse map by (sourceMemoryId,
|
|
703
|
+
// supersessionKey). See the append path above for the full rationale.
|
|
704
|
+
const existingBySource = new Map<string, string>();
|
|
705
|
+
for (const e of this.entries) {
|
|
706
|
+
if (e.kind === "tombstone") {
|
|
707
|
+
existingBySource.set(`${e.sourceMemoryId}\u{0000}${e.supersessionKey ?? ""}`, e.id);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
const rebuilt: TombstoneEntry[] = retiredMemories.map((m) => ({
|
|
711
|
+
id:
|
|
712
|
+
existingBySource.get(`${m.memoryId}\u{0000}${m.supersessionKey ?? ""}`) ??
|
|
713
|
+
newTombstoneId(),
|
|
714
|
+
kind: "tombstone" as const,
|
|
715
|
+
reason: m.reason,
|
|
716
|
+
sourceMemoryId: m.memoryId,
|
|
717
|
+
contentHash: m.contentHash ?? this.options.hashContent(m.rawContent),
|
|
718
|
+
normalizedText: this.options.normalizeText(m.rawContent),
|
|
719
|
+
...(m.entityRef ? { entityRef: m.entityRef } : {}),
|
|
720
|
+
...(m.supersessionKey ? { supersessionKey: m.supersessionKey } : {}),
|
|
721
|
+
namespace: this.namespace,
|
|
722
|
+
createdAt: m.createdAt,
|
|
723
|
+
createdBy: m.createdBy,
|
|
724
|
+
}));
|
|
725
|
+
// Sort deterministically (rule 38): createdAt, then id for stability.
|
|
726
|
+
rebuilt.sort((a, b) =>
|
|
727
|
+
a.createdAt === b.createdAt
|
|
728
|
+
? a.id < b.id ? -1 : a.id > b.id ? 1 : 0
|
|
729
|
+
: a.createdAt < b.createdAt ? -1 : 1,
|
|
730
|
+
);
|
|
731
|
+
const all = [...rebuilt, ...existingRevocations, ...foreignTombstones].sort((a, b) =>
|
|
732
|
+
a.createdAt === b.createdAt
|
|
733
|
+
? a.id < b.id ? -1 : a.id > b.id ? 1 : 0
|
|
734
|
+
: a.createdAt < b.createdAt ? -1 : 1,
|
|
735
|
+
);
|
|
736
|
+
const serialized =
|
|
737
|
+
all.map((e) => JSON.stringify(e)).join("\n") + (all.length > 0 ? "\n" : "");
|
|
738
|
+
await this.io.write(this.filePath, serialized);
|
|
739
|
+
// Update the in-memory index under the same lock so it stays consistent
|
|
740
|
+
// with the file we just wrote (no window for a lookup to see a stale
|
|
741
|
+
// index that misses a just-rebuilt entry).
|
|
742
|
+
this.resetIndex();
|
|
743
|
+
for (const entry of all) this.indexEntry(entry);
|
|
744
|
+
this.corruptedLines = 0;
|
|
745
|
+
this.loaded = true;
|
|
746
|
+
this.markWritten();
|
|
747
|
+
return rebuilt.length;
|
|
748
|
+
}),
|
|
657
749
|
);
|
|
658
|
-
|
|
659
|
-
for (const entry of all) this.indexEntry(entry);
|
|
660
|
-
this.corruptedLines = 0;
|
|
661
|
-
this.loaded = true;
|
|
662
|
-
this.markWritten();
|
|
663
|
-
return rebuilt.length;
|
|
750
|
+
return rebuiltCount;
|
|
664
751
|
}
|
|
665
752
|
}
|
|
666
753
|
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the local model-lab endpoint resolver + openai-compatible caller
|
|
3
|
+
* (issue #1585). Pure unit tests — the fetch caller takes an injected
|
|
4
|
+
* `fetchImpl`, so no network, no GPU, no live server.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
resolveFaithfulnessGateEndpoint,
|
|
12
|
+
resolveCorrectionIntentEndpoint,
|
|
13
|
+
callOpenAiCompatibleChat,
|
|
14
|
+
type EndpointChatMessage,
|
|
15
|
+
} from "./local-model-endpoint.js";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Resolvers
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
test("resolveFaithfulnessGateEndpoint: null when either key empty (default = no pointer)", () => {
|
|
22
|
+
assert.equal(resolveFaithfulnessGateEndpoint({ extractionFaithfulnessModel: "", extractionFaithfulnessBaseUrl: "" }), null);
|
|
23
|
+
assert.equal(resolveFaithfulnessGateEndpoint({ extractionFaithfulnessModel: "m", extractionFaithfulnessBaseUrl: "" }), null);
|
|
24
|
+
assert.equal(resolveFaithfulnessGateEndpoint({ extractionFaithfulnessModel: "", extractionFaithfulnessBaseUrl: "http://x/v1" }), null);
|
|
25
|
+
// whitespace-only counts as empty.
|
|
26
|
+
assert.equal(resolveFaithfulnessGateEndpoint({ extractionFaithfulnessModel: " ", extractionFaithfulnessBaseUrl: "http://x/v1" }), null);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("resolveFaithfulnessGateEndpoint: returns trimmed endpoint when both keys set", () => {
|
|
30
|
+
const ep = resolveFaithfulnessGateEndpoint({
|
|
31
|
+
extractionFaithfulnessModel: " remnic-faithfulness-gate-v1 ",
|
|
32
|
+
extractionFaithfulnessBaseUrl: "http://localhost:11434/v1",
|
|
33
|
+
});
|
|
34
|
+
assert.deepEqual(ep, { baseUrl: "http://localhost:11434/v1", model: "remnic-faithfulness-gate-v1" });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("resolveCorrectionIntentEndpoint: same contract as the gate resolver", () => {
|
|
38
|
+
assert.equal(resolveCorrectionIntentEndpoint({ correctionIntentModel: "", correctionIntentBaseUrl: "" }), null);
|
|
39
|
+
assert.equal(resolveCorrectionIntentEndpoint({ correctionIntentModel: "m", correctionIntentBaseUrl: "" }), null);
|
|
40
|
+
assert.deepEqual(
|
|
41
|
+
resolveCorrectionIntentEndpoint({ correctionIntentModel: "ci-v1", correctionIntentBaseUrl: "http://localhost:8000/v1" }),
|
|
42
|
+
{ baseUrl: "http://localhost:8000/v1", model: "ci-v1" },
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// callOpenAiCompatibleChat — injected fetch, no network
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/** Build a minimal Response-shaped object the caller reads (ok, json, .json). */
|
|
51
|
+
function jsonResponse(status: number, body: unknown): Response {
|
|
52
|
+
return new Response(JSON.stringify(body), {
|
|
53
|
+
status,
|
|
54
|
+
headers: { "content-type": "application/json" },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const MESSAGES: EndpointChatMessage[] = [
|
|
59
|
+
{ role: "system", content: "verifier" },
|
|
60
|
+
{ role: "user", content: "fact vs quote" },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
test("callOpenAiCompatibleChat: happy path — first choice content + reported model", async () => {
|
|
64
|
+
let calledUrl = "";
|
|
65
|
+
let calledBody: Record<string, unknown> | undefined;
|
|
66
|
+
const fakeFetch = (url: string, init: RequestInit): Promise<Response> => {
|
|
67
|
+
calledUrl = url;
|
|
68
|
+
calledBody = JSON.parse(String(init.body));
|
|
69
|
+
return Promise.resolve(
|
|
70
|
+
jsonResponse(200, {
|
|
71
|
+
model: "remnic-faithfulness-gate-v1",
|
|
72
|
+
choices: [{ message: { content: '[{"index":0,"verdict":"entailed"}]' } }],
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
const result = await callOpenAiCompatibleChat(
|
|
77
|
+
{ baseUrl: "http://localhost:11434/v1", model: "remnic-faithfulness-gate-v1" },
|
|
78
|
+
MESSAGES,
|
|
79
|
+
{ timeoutMs: 5000, temperature: 0.1, maxTokens: 2048, responseFormatJson: true },
|
|
80
|
+
fakeFetch as typeof fetch,
|
|
81
|
+
);
|
|
82
|
+
assert.equal(result?.content, '[{"index":0,"verdict":"entailed"}]');
|
|
83
|
+
assert.equal(result?.modelUsed, "remnic-faithfulness-gate-v1");
|
|
84
|
+
// URL joined correctly (no double slash), body carries model + json_object format.
|
|
85
|
+
assert.equal(calledUrl, "http://localhost:11434/v1/chat/completions");
|
|
86
|
+
assert.equal(calledBody?.model, "remnic-faithfulness-gate-v1");
|
|
87
|
+
assert.deepEqual(calledBody?.response_format, { type: "json_object" });
|
|
88
|
+
assert.equal(calledBody?.max_tokens, 2048);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("callOpenAiCompatibleChat: trailing slash on baseUrl is tolerated", async () => {
|
|
92
|
+
const seen = (await callOpenAiCompatibleChat(
|
|
93
|
+
{ baseUrl: "http://localhost:11434/v1/", model: "m" },
|
|
94
|
+
MESSAGES,
|
|
95
|
+
{ timeoutMs: 1000 },
|
|
96
|
+
((url: string) => {
|
|
97
|
+
assert.equal(url, "http://localhost:11434/v1/chat/completions");
|
|
98
|
+
return Promise.resolve(jsonResponse(200, { choices: [{ message: { content: "ok" } }] }));
|
|
99
|
+
}) as typeof fetch,
|
|
100
|
+
))!;
|
|
101
|
+
assert.equal(seen.content, "ok");
|
|
102
|
+
assert.equal(seen.modelUsed, "m"); // server reported no model → falls back to requested name
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("callOpenAiCompatibleChat: returns null on non-2xx (fail closed)", async () => {
|
|
106
|
+
const result = await callOpenAiCompatibleChat(
|
|
107
|
+
{ baseUrl: "http://x/v1", model: "m" },
|
|
108
|
+
MESSAGES,
|
|
109
|
+
{ timeoutMs: 1000 },
|
|
110
|
+
(() => Promise.resolve(jsonResponse(500, { error: "boom" }))) as typeof fetch,
|
|
111
|
+
);
|
|
112
|
+
assert.equal(result, null);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("callOpenAiCompatibleChat: returns null on network error (fail closed)", async () => {
|
|
116
|
+
const result = await callOpenAiCompatibleChat(
|
|
117
|
+
{ baseUrl: "http://x/v1", model: "m" },
|
|
118
|
+
MESSAGES,
|
|
119
|
+
{ timeoutMs: 1000 },
|
|
120
|
+
(() => Promise.reject(new Error("ECONNREFUSED"))) as typeof fetch,
|
|
121
|
+
);
|
|
122
|
+
assert.equal(result, null);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("callOpenAiCompatibleChat: returns null on malformed body (no fabricated shape)", async () => {
|
|
126
|
+
for (const body of [
|
|
127
|
+
null,
|
|
128
|
+
{},
|
|
129
|
+
{ choices: [] },
|
|
130
|
+
{ choices: [{ message: {} }] },
|
|
131
|
+
{ choices: [{ message: { content: 42 } }] }, // non-string content
|
|
132
|
+
{ choices: "not-an-array" },
|
|
133
|
+
]) {
|
|
134
|
+
const result = await callOpenAiCompatibleChat(
|
|
135
|
+
{ baseUrl: "http://x/v1", model: "m" },
|
|
136
|
+
MESSAGES,
|
|
137
|
+
{ timeoutMs: 1000 },
|
|
138
|
+
(() => Promise.resolve(jsonResponse(200, body))) as typeof fetch,
|
|
139
|
+
);
|
|
140
|
+
assert.equal(result, null, `body ${JSON.stringify(body)} should yield null`);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("callOpenAiCompatibleChat: returns null on .json() throw", async () => {
|
|
145
|
+
const result = await callOpenAiCompatibleChat(
|
|
146
|
+
{ baseUrl: "http://x/v1", model: "m" },
|
|
147
|
+
MESSAGES,
|
|
148
|
+
{ timeoutMs: 1000 },
|
|
149
|
+
(() =>
|
|
150
|
+
Promise.resolve(
|
|
151
|
+
new Response("not-json", { status: 200, headers: { "content-type": "application/json" } }),
|
|
152
|
+
)) as typeof fetch,
|
|
153
|
+
);
|
|
154
|
+
assert.equal(result, null);
|
|
155
|
+
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local fine-tuned model endpoint resolver + openai-compatible caller
|
|
3
|
+
* (issue #1585 model-lab integration).
|
|
4
|
+
*
|
|
5
|
+
* Remnic's two classification workloads — the extraction faithfulness gate
|
|
6
|
+
* (#1576) and passive-correction intent detection (#1581) — can run on small
|
|
7
|
+
* fine-tuned models served locally (Ollama / vLLM) instead of prompted
|
|
8
|
+
* frontier LLMs. The model-lab (`model-lab/`) produces those models; this
|
|
9
|
+
* module is the config-side half: it resolves the optional local endpoint
|
|
10
|
+
* pointer from config and, when set, makes a minimal openai-compatible
|
|
11
|
+
* chat-completions call to it.
|
|
12
|
+
*
|
|
13
|
+
* Design:
|
|
14
|
+
* - **Pointers are optional.** Empty defaults preserve the existing routing
|
|
15
|
+
* chain exactly (rule 39 / byte-identical pre-feature). A resolver returns
|
|
16
|
+
* `null` when the pointer is unset, so callers short-circuit before any
|
|
17
|
+
* network.
|
|
18
|
+
* - **Graceful degradation.** A local-endpoint failure (network, non-2xx,
|
|
19
|
+
* malformed body, timeout) returns `null`; callers fall back to the
|
|
20
|
+
* configured chain (checklist §4 — never block writes on a classifier).
|
|
21
|
+
* - **No inline casts.** The fetch JSON is narrowed with `in` / `typeof`
|
|
22
|
+
* guards (project rule: never assert a shape to read one property).
|
|
23
|
+
* - **Injectable fetch.** `callOpenAiCompatibleChat` takes an optional
|
|
24
|
+
* `fetchImpl` so the caller is unit-testable without a live server.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type { PluginConfig } from "./types.js";
|
|
28
|
+
|
|
29
|
+
/** A resolved local endpoint: where to call + which model to name. */
|
|
30
|
+
export interface LocalModelEndpoint {
|
|
31
|
+
/** Base URL, e.g. `http://localhost:11434/v1` (no trailing slash required). */
|
|
32
|
+
baseUrl: string;
|
|
33
|
+
/** Model name the server expects, e.g. `remnic-faithfulness-gate-v1`. */
|
|
34
|
+
model: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A chat message in the openai-compatible shape this caller sends. */
|
|
38
|
+
export interface EndpointChatMessage {
|
|
39
|
+
role: "system" | "user" | "assistant";
|
|
40
|
+
content: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Options for a single local-endpoint chat call. */
|
|
44
|
+
export interface EndpointChatOptions {
|
|
45
|
+
/** Per-call timeout in ms. The caller's batch AbortSignal is NOT forwarded
|
|
46
|
+
* (matches the local-LLM precedent: each attempt is bounded by its own
|
|
47
|
+
* timer so one slow server can't starve the batch). */
|
|
48
|
+
timeoutMs: number;
|
|
49
|
+
/** Sampling temperature — classification wants low entropy (0.0–0.2). */
|
|
50
|
+
temperature?: number;
|
|
51
|
+
/** Max output tokens. */
|
|
52
|
+
maxTokens?: number;
|
|
53
|
+
/** Request JSON-object output (the gate emits a JSON verdict array). */
|
|
54
|
+
responseFormatJson?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Result of a local-endpoint chat call. */
|
|
58
|
+
export interface EndpointChatResult {
|
|
59
|
+
content: string;
|
|
60
|
+
/** The model the server reports it used (falls back to the requested name). */
|
|
61
|
+
modelUsed: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the faithfulness-gate local endpoint from config (issue #1585).
|
|
66
|
+
*
|
|
67
|
+
* Returns the endpoint only when BOTH a base URL and a model name are
|
|
68
|
+
* configured; otherwise `null` (gate uses its existing routing chain). The
|
|
69
|
+
* gate's verifier needs a model identity, so a base URL alone is intentionally
|
|
70
|
+
* insufficient — silently calling whatever the server's default model is would
|
|
71
|
+
* be an unreviewed behavioral change.
|
|
72
|
+
*/
|
|
73
|
+
export function resolveFaithfulnessGateEndpoint(
|
|
74
|
+
config: Pick<PluginConfig, "extractionFaithfulnessModel" | "extractionFaithfulnessBaseUrl">,
|
|
75
|
+
): LocalModelEndpoint | null {
|
|
76
|
+
const baseUrl = config.extractionFaithfulnessBaseUrl.trim();
|
|
77
|
+
const model = config.extractionFaithfulnessModel.trim();
|
|
78
|
+
if (!baseUrl || !model) return null;
|
|
79
|
+
return { baseUrl, model };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolve the correction-intent local endpoint from config (issue #1581/#1585).
|
|
84
|
+
*
|
|
85
|
+
* Same contract as the gate resolver. Consumption by the model-backed
|
|
86
|
+
* detector is the consuming child's job — the rule-based
|
|
87
|
+
* `detectPassiveCorrections` remains the default path; this pointer lets a
|
|
88
|
+
* future model-backed detector route to a fine-tuned local classifier when an
|
|
89
|
+
* operator configures one.
|
|
90
|
+
*/
|
|
91
|
+
export function resolveCorrectionIntentEndpoint(
|
|
92
|
+
config: Pick<PluginConfig, "correctionIntentModel" | "correctionIntentBaseUrl">,
|
|
93
|
+
): LocalModelEndpoint | null {
|
|
94
|
+
const baseUrl = config.correctionIntentBaseUrl.trim();
|
|
95
|
+
const model = config.correctionIntentModel.trim();
|
|
96
|
+
if (!baseUrl || !model) return null;
|
|
97
|
+
return { baseUrl, model };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type FetchLike = typeof fetch;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Make an openai-compatible chat-completions call to a local endpoint.
|
|
104
|
+
*
|
|
105
|
+
* POSTs to `${baseUrl}/chat/completions` with `{ model, messages, ... }` and
|
|
106
|
+
* returns the first choice's content. Any failure (non-2xx, network error,
|
|
107
|
+
* malformed body, timeout) returns `null` so callers fall back gracefully.
|
|
108
|
+
*
|
|
109
|
+
* The response is narrowed with runtime guards — never an unchecked cast — so
|
|
110
|
+
* a surprising server response fails closed (`null`) rather than reading a
|
|
111
|
+
* fabricated field.
|
|
112
|
+
*/
|
|
113
|
+
export async function callOpenAiCompatibleChat(
|
|
114
|
+
endpoint: LocalModelEndpoint,
|
|
115
|
+
messages: EndpointChatMessage[],
|
|
116
|
+
options: EndpointChatOptions,
|
|
117
|
+
fetchImpl: FetchLike = fetch,
|
|
118
|
+
): Promise<EndpointChatResult | null> {
|
|
119
|
+
const url = joinBaseUrl(endpoint.baseUrl, "/chat/completions");
|
|
120
|
+
const controller = new AbortController();
|
|
121
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
122
|
+
try {
|
|
123
|
+
const body: Record<string, unknown> = {
|
|
124
|
+
model: endpoint.model,
|
|
125
|
+
messages,
|
|
126
|
+
temperature: options.temperature ?? 0.1,
|
|
127
|
+
};
|
|
128
|
+
if (typeof options.maxTokens === "number") {
|
|
129
|
+
body.max_tokens = options.maxTokens;
|
|
130
|
+
}
|
|
131
|
+
if (options.responseFormatJson) {
|
|
132
|
+
body.response_format = { type: "json_object" };
|
|
133
|
+
}
|
|
134
|
+
let response: Response;
|
|
135
|
+
try {
|
|
136
|
+
response = await fetchImpl(url, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: { "content-type": "application/json" },
|
|
139
|
+
body: JSON.stringify(body),
|
|
140
|
+
signal: controller.signal,
|
|
141
|
+
});
|
|
142
|
+
} catch {
|
|
143
|
+
// Network error or abort — fail closed, let the caller fall back.
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
if (!response.ok) return null;
|
|
147
|
+
const parsed: unknown = await response.json().catch(() => null);
|
|
148
|
+
const content = extractFirstChoiceContent(parsed);
|
|
149
|
+
if (content === null) return null;
|
|
150
|
+
return { content, modelUsed: extractModel(parsed) ?? endpoint.model };
|
|
151
|
+
} finally {
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Join a base URL and a path, tolerating a trailing slash on the base. */
|
|
157
|
+
function joinBaseUrl(base: string, path: string): string {
|
|
158
|
+
return base.endsWith("/") ? `${base.slice(0, -1)}${path}` : `${base}${path}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Narrow the openai-compatible response and pull the first choice's content.
|
|
163
|
+
*
|
|
164
|
+
* `in` / `typeof` guards only — no `as` cast (project rule). Returns `null`
|
|
165
|
+
* for any shape that isn't `{ choices: [{ message: { content: string } }, …] }`.
|
|
166
|
+
*/
|
|
167
|
+
function extractFirstChoiceContent(data: unknown): string | null {
|
|
168
|
+
if (!data || typeof data !== "object") return null;
|
|
169
|
+
// After `in`, TS narrows the property to `unknown` — read it directly, no cast.
|
|
170
|
+
if (!("choices" in data) || !Array.isArray(data.choices)) return null;
|
|
171
|
+
const choices: unknown[] = data.choices;
|
|
172
|
+
for (const choice of choices) {
|
|
173
|
+
if (!choice || typeof choice !== "object") continue;
|
|
174
|
+
if (!("message" in choice)) continue;
|
|
175
|
+
const message = choice.message;
|
|
176
|
+
if (!message || typeof message !== "object") continue;
|
|
177
|
+
if (!("content" in message)) continue;
|
|
178
|
+
const content = message.content;
|
|
179
|
+
if (typeof content === "string" && content.length > 0) return content;
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Narrow the `model` field from the response (best-effort, may be absent). */
|
|
185
|
+
function extractModel(data: unknown): string | null {
|
|
186
|
+
if (!data || typeof data !== "object" || !("model" in data)) return null;
|
|
187
|
+
const model = data.model;
|
|
188
|
+
return typeof model === "string" && model.length > 0 ? model : null;
|
|
189
|
+
}
|