@remnic/core 9.3.612 → 9.3.614
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-cli.js +58 -57
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +4 -2
- package/dist/access-http.js +22 -22
- package/dist/access-mcp.d.ts +9 -2
- package/dist/access-mcp.js +19 -19
- package/dist/access-schema.d.ts +12 -12
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-D2J9dh_9.d.ts → access-service-DGG_2xPK.d.ts} +1 -1
- package/dist/access-service.d.ts +2 -2
- package/dist/access-service.js +16 -16
- package/dist/active-recall.js +20 -3
- package/dist/active-recall.js.map +1 -1
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/behavior-learner.js +2 -3
- package/dist/behavior-learner.js.map +1 -1
- package/dist/bootstrap.d.ts +1 -1
- package/dist/briefing.js +3 -3
- package/dist/buffer.d.ts +1 -1
- package/dist/buffer.js +1 -1
- package/dist/calibration.d.ts +5 -2
- package/dist/calibration.js +7 -5
- package/dist/calibration.js.map +1 -1
- package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-YO5QJ6L3.js} +2 -2
- package/dist/causal-consolidation.d.ts +8 -2
- package/dist/causal-consolidation.js +13 -11
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/{chunk-3BP57I6J.js → chunk-2F6NP3NT.js} +2 -1
- package/dist/{chunk-3BP57I6J.js.map → chunk-2F6NP3NT.js.map} +1 -1
- package/dist/{chunk-AU7Q3LSC.js → chunk-2QSZNTDO.js} +4 -4
- package/dist/{chunk-HSVJGWYS.js → chunk-2ROPI5OE.js} +2 -2
- package/dist/{chunk-C4SQJZAF.js → chunk-2SGJY2UY.js} +6 -3
- package/dist/chunk-2SGJY2UY.js.map +1 -0
- package/dist/{chunk-ZDTVJXIP.js → chunk-3MAONBX3.js} +13 -5
- package/dist/chunk-3MAONBX3.js.map +1 -0
- package/dist/{chunk-G3Z3QEF5.js → chunk-3PY7VHV7.js} +2 -2
- package/dist/chunk-3PY7VHV7.js.map +1 -0
- package/dist/{chunk-CF3ZF2YU.js → chunk-3QSU4NFF.js} +3 -3
- package/dist/{chunk-AJA46VX5.js → chunk-3T74IZB3.js} +11 -2
- package/dist/chunk-3T74IZB3.js.map +1 -0
- package/dist/{chunk-KVEVLBKC.js → chunk-4HFJQCJZ.js} +13 -8
- package/dist/chunk-4HFJQCJZ.js.map +1 -0
- package/dist/{chunk-KGK2QKWL.js → chunk-4R4KTDIE.js} +1 -1
- package/dist/chunk-4R4KTDIE.js.map +1 -0
- package/dist/{chunk-OI27U2HT.js → chunk-5BTCT236.js} +2 -2
- package/dist/{chunk-CO7ZO4TU.js → chunk-5VDJMYTF.js} +2 -2
- package/dist/{chunk-BFBF3XEF.js → chunk-6BDVBBBY.js} +33 -25
- package/dist/{chunk-BFBF3XEF.js.map → chunk-6BDVBBBY.js.map} +1 -1
- package/dist/{chunk-EAZGEEG2.js → chunk-6L46YAEZ.js} +45 -9
- package/dist/chunk-6L46YAEZ.js.map +1 -0
- package/dist/{chunk-YFS5OEKO.js → chunk-7MLB4NCL.js} +2 -2
- package/dist/{chunk-IOTENEVL.js → chunk-7YQFWOF7.js} +57 -50
- package/dist/chunk-7YQFWOF7.js.map +1 -0
- package/dist/{chunk-2QANQKSQ.js → chunk-ADNZVFXG.js} +15 -15
- package/dist/{chunk-LZ3VEOU5.js → chunk-AL4RAJL5.js} +22 -5
- package/dist/chunk-AL4RAJL5.js.map +1 -0
- package/dist/{chunk-557IAFPD.js → chunk-APRRL26Q.js} +2 -2
- package/dist/{chunk-QDDHYAKV.js → chunk-AZDOWD2L.js} +2 -2
- package/dist/{chunk-TH67Q46T.js → chunk-B6FDZPCF.js} +17 -9
- package/dist/chunk-B6FDZPCF.js.map +1 -0
- package/dist/{chunk-MLT75J5S.js → chunk-B6SU7YSE.js} +3 -3
- package/dist/{chunk-FXKPZ3H6.js → chunk-BPSGLMQ4.js} +2 -2
- package/dist/{chunk-2NLLXCJG.js → chunk-BXLOS5AJ.js} +2 -2
- package/dist/{chunk-NOMEVTUD.js → chunk-C6C7XVKG.js} +5 -4
- package/dist/chunk-C6C7XVKG.js.map +1 -0
- package/dist/{chunk-XKIQZXUB.js → chunk-CI7RKSRE.js} +7 -1
- package/dist/chunk-CI7RKSRE.js.map +1 -0
- package/dist/{chunk-IK34DVAC.js → chunk-CIOMS6DI.js} +2 -2
- package/dist/{chunk-2I5JGH3M.js → chunk-CYEPCZN5.js} +2 -2
- package/dist/{chunk-2I5JGH3M.js.map → chunk-CYEPCZN5.js.map} +1 -1
- package/dist/{chunk-JHMFYY7L.js → chunk-DCGT4FPP.js} +13 -5
- package/dist/chunk-DCGT4FPP.js.map +1 -0
- package/dist/{chunk-7DZRO2DC.js → chunk-DEPRLVLK.js} +2 -2
- package/dist/{chunk-CSKLPDN6.js → chunk-DEVUWMME.js} +52 -19
- package/dist/chunk-DEVUWMME.js.map +1 -0
- package/dist/{chunk-DHGSZ3UD.js → chunk-DGNQRNLL.js} +2 -2
- package/dist/{chunk-X7Y7WX73.js → chunk-DQEMWVMT.js} +1 -1
- package/dist/chunk-FAV25DUZ.js +12 -0
- package/dist/chunk-FAV25DUZ.js.map +1 -0
- package/dist/{chunk-ETUPBUHB.js → chunk-GDASG7NC.js} +2 -2
- package/dist/{chunk-L227SKTB.js → chunk-GDB4J2H3.js} +17 -1
- package/dist/chunk-GDB4J2H3.js.map +1 -0
- package/dist/{chunk-IP73YCZP.js → chunk-GLPBYIXN.js} +4 -2
- package/dist/chunk-GLPBYIXN.js.map +1 -0
- package/dist/{chunk-4HP7HIE3.js → chunk-HP5FMB6L.js} +2 -2
- package/dist/{chunk-EVZFIAPG.js → chunk-IBTZEBUD.js} +23 -10
- package/dist/chunk-IBTZEBUD.js.map +1 -0
- package/dist/{chunk-DOX2CG6Y.js → chunk-IEUU7O4F.js} +2 -2
- package/dist/{chunk-JNANKJLN.js → chunk-JOASJWQR.js} +2 -2
- package/dist/chunk-JOASJWQR.js.map +1 -0
- package/dist/{chunk-WSGF57U2.js → chunk-JQDZQ4TB.js} +2 -2
- package/dist/{chunk-HINSGUA7.js → chunk-KBL3JJR6.js} +9 -13
- package/dist/chunk-KBL3JJR6.js.map +1 -0
- package/dist/{chunk-W7L6HXUC.js → chunk-LXOM6IQU.js} +2 -2
- package/dist/{chunk-G6R5UD3Q.js → chunk-MGN7VHWQ.js} +42 -1
- package/dist/{chunk-G6R5UD3Q.js.map → chunk-MGN7VHWQ.js.map} +1 -1
- package/dist/{chunk-DLJ4IR6M.js → chunk-MHQC2WU2.js} +2 -2
- package/dist/chunk-MHQC2WU2.js.map +1 -0
- package/dist/{chunk-6JGNHWCI.js → chunk-OBIRVF36.js} +3 -3
- package/dist/{chunk-CHCA44C3.js → chunk-ODPLEWB6.js} +3 -3
- package/dist/chunk-ODPLEWB6.js.map +1 -0
- package/dist/{chunk-HENLZHIT.js → chunk-OIF36KGD.js} +7 -4
- package/dist/chunk-OIF36KGD.js.map +1 -0
- package/dist/{chunk-GUPISBV2.js → chunk-PP2JH3GP.js} +2 -2
- package/dist/{chunk-OXJBNGBK.js → chunk-PSUB67YB.js} +2 -2
- package/dist/{chunk-UWY7GIVS.js → chunk-PYIFUBRK.js} +45 -13
- package/dist/chunk-PYIFUBRK.js.map +1 -0
- package/dist/{chunk-KIB7SDIJ.js → chunk-Q6YIJGXJ.js} +2 -2
- package/dist/{chunk-PPPZY2EU.js → chunk-QEMCQFDW.js} +2 -2
- package/dist/{chunk-ZT3EGNLR.js → chunk-QPD426WT.js} +2 -2
- package/dist/{chunk-RLV3PQGH.js → chunk-QVO4YOB7.js} +6 -6
- package/dist/{chunk-GMAG2HS4.js → chunk-RG3LBSGH.js} +46 -9
- package/dist/chunk-RG3LBSGH.js.map +1 -0
- package/dist/{chunk-XSWKORGM.js → chunk-S53OYO3F.js} +3 -1
- package/dist/chunk-S53OYO3F.js.map +1 -0
- package/dist/{chunk-YCN4BVDK.js → chunk-SCPFRKIT.js} +4 -2
- package/dist/chunk-SCPFRKIT.js.map +1 -0
- package/dist/{chunk-HJNQQICM.js → chunk-T5XWMMU2.js} +107 -50
- package/dist/chunk-T5XWMMU2.js.map +1 -0
- package/dist/{chunk-NZPF2SYV.js → chunk-T7N6KQGS.js} +138 -5
- package/dist/chunk-T7N6KQGS.js.map +1 -0
- package/dist/{chunk-VJXSUAO7.js → chunk-TNOWU6RP.js} +13 -10
- package/dist/chunk-TNOWU6RP.js.map +1 -0
- package/dist/{chunk-PCI747N2.js → chunk-TZVQQTG4.js} +48 -19
- package/dist/chunk-TZVQQTG4.js.map +1 -0
- package/dist/{chunk-KQAFEZQX.js → chunk-VDX2J7OX.js} +2 -2
- package/dist/{chunk-IK7DCC5H.js → chunk-VMGLYN42.js} +2 -2
- package/dist/{chunk-5RPTH6AU.js → chunk-VPGUMLBA.js} +8 -7
- package/dist/chunk-VPGUMLBA.js.map +1 -0
- package/dist/{chunk-KM2A35EO.js → chunk-WB3LYXC5.js} +11 -7
- package/dist/chunk-WB3LYXC5.js.map +1 -0
- package/dist/{chunk-NSKYFGDL.js → chunk-X4QQB7O6.js} +2 -2
- package/dist/{chunk-HPWVAEET.js → chunk-X6IRLNOO.js} +3 -7
- package/dist/chunk-X6IRLNOO.js.map +1 -0
- package/dist/{chunk-46GJIW5M.js → chunk-XAZOWLW4.js} +5 -5
- package/dist/{chunk-46GJIW5M.js.map → chunk-XAZOWLW4.js.map} +1 -1
- package/dist/{chunk-XPSVGJYA.js → chunk-YRMKDTKF.js} +12 -9
- package/dist/chunk-YRMKDTKF.js.map +1 -0
- package/dist/{chunk-6ZZP4EJF.js → chunk-ZJR7VG5L.js} +3 -3
- package/dist/{chunk-6ZZP4EJF.js.map → chunk-ZJR7VG5L.js.map} +1 -1
- package/dist/{cli-OrfKXNU4.d.ts → cli-DWeu7eTY.d.ts} +6 -2
- package/dist/cli.d.ts +3 -3
- package/dist/cli.js +60 -59
- package/dist/compounding/engine.js +3 -3
- package/dist/compounding/preference-consolidator.js +39 -11
- package/dist/compounding/preference-consolidator.js.map +1 -1
- package/dist/config.js +1 -1
- package/dist/connectors/codex-materialize-runner.js +3 -3
- package/dist/connectors/index.js +3 -3
- package/dist/consolidation-provenance-check.js +1 -1
- package/dist/contradiction/index.js +4 -4
- package/dist/conversation-index/backend.js +2 -2
- package/dist/conversation-index/indexer.js +1 -1
- package/dist/cross-namespace-budget.js +1 -1
- package/dist/enrichment/index.js +1 -1
- package/dist/entity-retrieval.js +3 -3
- package/dist/evals.js +1 -1
- package/dist/explicit-capture.d.ts +1 -1
- package/dist/extraction-judge.js +8 -1
- package/dist/extraction.js +2 -2
- package/dist/fallback-llm.d.ts +23 -6
- package/dist/fallback-llm.js +5 -3
- package/dist/{first-start-migration-GYJWIH36.js → first-start-migration-FF7YFGRP.js} +6 -6
- package/dist/index.d.ts +3 -3
- package/dist/index.js +94 -93
- package/dist/index.js.map +1 -1
- package/dist/lcm/archive.js +2 -2
- package/dist/lcm/engine.js +5 -5
- package/dist/lcm/index.js +7 -7
- package/dist/lcm/summarizer.js +3 -3
- package/dist/maintenance/memory-governance-cron.d.ts +6 -4
- package/dist/maintenance/memory-governance-cron.js +1 -1
- package/dist/maintenance/memory-governance.js +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/mcp-memory-inspector-app.d.ts +2 -2
- package/dist/mcp-memory-inspector-app.js +1 -1
- package/dist/migrate/from-engram.js +1 -1
- package/dist/namespaces/migrate.js +16 -15
- package/dist/namespaces/search.js +12 -11
- package/dist/namespaces/storage.js +3 -3
- package/dist/network/webdav.d.ts +2 -0
- package/dist/network/webdav.js +1 -1
- package/dist/objective-state-writers.js +2 -2
- package/dist/operator-toolkit.d.ts +3 -1
- package/dist/operator-toolkit.js +21 -20
- package/dist/{orchestrator-DTRQG75J.d.ts → orchestrator-CqWOjfgl.d.ts} +46 -3
- package/dist/orchestrator.d.ts +1 -1
- package/dist/orchestrator.js +47 -44
- package/dist/patterns-cli.js +1 -1
- package/dist/qmd-recall-cache.d.ts +2 -0
- package/dist/qmd-recall-cache.js +1 -1
- package/dist/qmd.d.ts +37 -2
- package/dist/qmd.js +4 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-planner-llm.d.ts +57 -0
- package/dist/recall-planner-llm.js +167 -0
- package/dist/recall-planner-llm.js.map +1 -0
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.js +2 -2
- package/dist/resume-bundles.js +2 -2
- package/dist/retrieval-agents.js +2 -2
- package/dist/routing/store.js +1 -1
- package/dist/search/factory.js +11 -10
- package/dist/search/index.js +11 -10
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/lancedb-backend.js +3 -2
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.js +3 -2
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/noop-backend.js +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/orama-backend.js +3 -2
- package/dist/search/port.d.ts +6 -1
- package/dist/search/port.js +7 -0
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/search/remote-backend.js +1 -1
- package/dist/semantic-consolidation.js +4 -4
- package/dist/semantic-rule-promotion.js +3 -3
- package/dist/semantic-rule-verifier.js +3 -3
- package/dist/session-observer-state.js +1 -1
- package/dist/storage.js +2 -2
- package/dist/summarizer.js +2 -2
- package/dist/temporal-index.js +1 -1
- package/dist/{tier-stats-SKML2OSF.js → tier-stats-3LYQ3VV5.js} +3 -3
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +2 -2
- package/dist/transfer/export-sqlite.js +1 -1
- package/dist/types.d.ts +32 -0
- package/dist/types.js +1 -1
- package/dist/utility-learner.js +1 -1
- package/dist/utility-runtime.js +2 -2
- package/dist/verified-recall.js +3 -3
- package/dist/work/board.js +2 -2
- package/dist/work/storage.d.ts +2 -0
- package/dist/work/storage.js +1 -1
- package/package.json +1 -1
- package/src/access-http.ts +3 -0
- package/src/access-mcp.test.ts +51 -0
- package/src/access-mcp.ts +26 -5
- package/src/active-recall.test.ts +40 -0
- package/src/active-recall.ts +19 -2
- package/src/behavior-learner.ts +5 -3
- package/src/buffer-session.test.ts +58 -0
- package/src/buffer-surprise-trigger.test.ts +4 -18
- package/src/buffer.ts +39 -11
- package/src/calibration.ts +10 -4
- package/src/causal-consolidation.test.ts +47 -2
- package/src/causal-consolidation.ts +13 -9
- package/src/cli.ts +19 -4
- package/src/compounding/engine.ts +2 -0
- package/src/compounding/preference-consolidator.test.ts +292 -0
- package/src/compounding/preference-consolidator.ts +55 -19
- package/src/config.test.ts +213 -0
- package/src/config.ts +175 -4
- package/src/connectors/codex-materialize-runner.ts +7 -4
- package/src/consolidation-provenance-check.ts +24 -5
- package/src/conversation-index/indexer.test.ts +22 -0
- package/src/conversation-index/indexer.ts +7 -3
- package/src/cross-namespace-budget.test.ts +44 -21
- package/src/cross-namespace-budget.ts +2 -2
- package/src/enrichment/pipeline.ts +11 -16
- package/src/evals.ts +1 -1
- package/src/extraction-judge-chain.test.ts +55 -0
- package/src/extraction-judge.ts +7 -9
- package/src/extraction.ts +16 -5
- package/src/fallback-llm.test.ts +600 -1
- package/src/fallback-llm.ts +91 -22
- package/src/maintenance/memory-governance-cron.ts +39 -29
- package/src/mcp-memory-inspector-app.ts +54 -12
- package/src/message-parts/index.ts +6 -0
- package/src/message-parts/message-parts.test.ts +30 -0
- package/src/migrate/from-engram.ts +19 -5
- package/src/namespaces/search.test.ts +15 -2
- package/src/namespaces/search.ts +1 -1
- package/src/network/webdav.ts +61 -21
- package/src/operator-toolkit.ts +6 -2
- package/src/orchestrator.ts +173 -20
- package/src/qmd-client.test.ts +85 -0
- package/src/qmd-recall-cache.test.ts +16 -0
- package/src/qmd-recall-cache.ts +7 -0
- package/src/qmd.test.ts +54 -0
- package/src/qmd.ts +119 -19
- package/src/recall-planner-llm.test.ts +224 -0
- package/src/recall-planner-llm.ts +289 -0
- package/src/routing/store.ts +4 -8
- package/src/search/factory.ts +3 -0
- package/src/search/lancedb-backend.ts +15 -3
- package/src/search/meilisearch-backend.ts +70 -7
- package/src/search/noop-backend.ts +5 -1
- package/src/search/orama-backend.ts +15 -3
- package/src/search/port.ts +15 -0
- package/src/search/remote-backend.ts +5 -1
- package/src/session-observer-state.ts +1 -1
- package/src/summarizer.ts +3 -3
- package/src/temporal-index.test.ts +18 -0
- package/src/temporal-index.ts +45 -0
- package/src/training-export/cli-date-validation.test.ts +36 -0
- package/src/training-export/date-parse.ts +21 -2
- package/src/transfer/export-sqlite.ts +3 -0
- package/src/types.ts +35 -0
- package/src/utility-learner.ts +1 -0
- package/src/work/storage.ts +23 -0
- package/dist/chunk-5RPTH6AU.js.map +0 -1
- package/dist/chunk-AJA46VX5.js.map +0 -1
- package/dist/chunk-C4SQJZAF.js.map +0 -1
- package/dist/chunk-CHCA44C3.js.map +0 -1
- package/dist/chunk-CSKLPDN6.js.map +0 -1
- package/dist/chunk-DLJ4IR6M.js.map +0 -1
- package/dist/chunk-EAZGEEG2.js.map +0 -1
- package/dist/chunk-EVZFIAPG.js.map +0 -1
- package/dist/chunk-G3Z3QEF5.js.map +0 -1
- package/dist/chunk-GMAG2HS4.js.map +0 -1
- package/dist/chunk-HENLZHIT.js.map +0 -1
- package/dist/chunk-HINSGUA7.js.map +0 -1
- package/dist/chunk-HJNQQICM.js.map +0 -1
- package/dist/chunk-HPWVAEET.js.map +0 -1
- package/dist/chunk-IOTENEVL.js.map +0 -1
- package/dist/chunk-IP73YCZP.js.map +0 -1
- package/dist/chunk-JHMFYY7L.js.map +0 -1
- package/dist/chunk-JNANKJLN.js.map +0 -1
- package/dist/chunk-KGK2QKWL.js.map +0 -1
- package/dist/chunk-KM2A35EO.js.map +0 -1
- package/dist/chunk-KVEVLBKC.js.map +0 -1
- package/dist/chunk-L227SKTB.js.map +0 -1
- package/dist/chunk-LZ3VEOU5.js.map +0 -1
- package/dist/chunk-NOMEVTUD.js.map +0 -1
- package/dist/chunk-NZPF2SYV.js.map +0 -1
- package/dist/chunk-PCI747N2.js.map +0 -1
- package/dist/chunk-TH67Q46T.js.map +0 -1
- package/dist/chunk-UWY7GIVS.js.map +0 -1
- package/dist/chunk-VJXSUAO7.js.map +0 -1
- package/dist/chunk-XKIQZXUB.js.map +0 -1
- package/dist/chunk-XPSVGJYA.js.map +0 -1
- package/dist/chunk-XSWKORGM.js.map +0 -1
- package/dist/chunk-YCN4BVDK.js.map +0 -1
- package/dist/chunk-ZDTVJXIP.js.map +0 -1
- /package/dist/{capsule-crypto-7FJQINUR.js.map → capsule-crypto-YO5QJ6L3.js.map} +0 -0
- /package/dist/{chunk-AU7Q3LSC.js.map → chunk-2QSZNTDO.js.map} +0 -0
- /package/dist/{chunk-HSVJGWYS.js.map → chunk-2ROPI5OE.js.map} +0 -0
- /package/dist/{chunk-CF3ZF2YU.js.map → chunk-3QSU4NFF.js.map} +0 -0
- /package/dist/{chunk-OI27U2HT.js.map → chunk-5BTCT236.js.map} +0 -0
- /package/dist/{chunk-CO7ZO4TU.js.map → chunk-5VDJMYTF.js.map} +0 -0
- /package/dist/{chunk-YFS5OEKO.js.map → chunk-7MLB4NCL.js.map} +0 -0
- /package/dist/{chunk-2QANQKSQ.js.map → chunk-ADNZVFXG.js.map} +0 -0
- /package/dist/{chunk-557IAFPD.js.map → chunk-APRRL26Q.js.map} +0 -0
- /package/dist/{chunk-QDDHYAKV.js.map → chunk-AZDOWD2L.js.map} +0 -0
- /package/dist/{chunk-MLT75J5S.js.map → chunk-B6SU7YSE.js.map} +0 -0
- /package/dist/{chunk-FXKPZ3H6.js.map → chunk-BPSGLMQ4.js.map} +0 -0
- /package/dist/{chunk-2NLLXCJG.js.map → chunk-BXLOS5AJ.js.map} +0 -0
- /package/dist/{chunk-IK34DVAC.js.map → chunk-CIOMS6DI.js.map} +0 -0
- /package/dist/{chunk-7DZRO2DC.js.map → chunk-DEPRLVLK.js.map} +0 -0
- /package/dist/{chunk-DHGSZ3UD.js.map → chunk-DGNQRNLL.js.map} +0 -0
- /package/dist/{chunk-X7Y7WX73.js.map → chunk-DQEMWVMT.js.map} +0 -0
- /package/dist/{chunk-ETUPBUHB.js.map → chunk-GDASG7NC.js.map} +0 -0
- /package/dist/{chunk-4HP7HIE3.js.map → chunk-HP5FMB6L.js.map} +0 -0
- /package/dist/{chunk-DOX2CG6Y.js.map → chunk-IEUU7O4F.js.map} +0 -0
- /package/dist/{chunk-WSGF57U2.js.map → chunk-JQDZQ4TB.js.map} +0 -0
- /package/dist/{chunk-W7L6HXUC.js.map → chunk-LXOM6IQU.js.map} +0 -0
- /package/dist/{chunk-6JGNHWCI.js.map → chunk-OBIRVF36.js.map} +0 -0
- /package/dist/{chunk-GUPISBV2.js.map → chunk-PP2JH3GP.js.map} +0 -0
- /package/dist/{chunk-OXJBNGBK.js.map → chunk-PSUB67YB.js.map} +0 -0
- /package/dist/{chunk-KIB7SDIJ.js.map → chunk-Q6YIJGXJ.js.map} +0 -0
- /package/dist/{chunk-PPPZY2EU.js.map → chunk-QEMCQFDW.js.map} +0 -0
- /package/dist/{chunk-ZT3EGNLR.js.map → chunk-QPD426WT.js.map} +0 -0
- /package/dist/{chunk-RLV3PQGH.js.map → chunk-QVO4YOB7.js.map} +0 -0
- /package/dist/{chunk-KQAFEZQX.js.map → chunk-VDX2J7OX.js.map} +0 -0
- /package/dist/{chunk-IK7DCC5H.js.map → chunk-VMGLYN42.js.map} +0 -0
- /package/dist/{chunk-NSKYFGDL.js.map → chunk-X4QQB7O6.js.map} +0 -0
- /package/dist/{first-start-migration-GYJWIH36.js.map → first-start-migration-FF7YFGRP.js.map} +0 -0
- /package/dist/{tier-stats-SKML2OSF.js.map → tier-stats-3LYQ3VV5.js.map} +0 -0
package/src/config.test.ts
CHANGED
|
@@ -3,6 +3,63 @@ import test from "node:test";
|
|
|
3
3
|
|
|
4
4
|
import { parseConfig } from "./config.js";
|
|
5
5
|
|
|
6
|
+
test("parseConfig emitLegacyTools defaults to true and coerces config/env (issue #1427)", () => {
|
|
7
|
+
// Default: legacy aliases on, for backward compatibility.
|
|
8
|
+
assert.equal(parseConfig({}).emitLegacyTools, true);
|
|
9
|
+
// `null` means "unset → use default", consistent with the repo convention for
|
|
10
|
+
// optional fields (e.g. taskModelChain: null → undefined). Not a hard error.
|
|
11
|
+
assert.equal(parseConfig({ emitLegacyTools: null }).emitLegacyTools, true);
|
|
12
|
+
// Boolean + boolean-like string config values.
|
|
13
|
+
assert.equal(parseConfig({ emitLegacyTools: false }).emitLegacyTools, false);
|
|
14
|
+
assert.equal(parseConfig({ emitLegacyTools: "false" }).emitLegacyTools, false);
|
|
15
|
+
assert.equal(parseConfig({ emitLegacyTools: "0" }).emitLegacyTools, false);
|
|
16
|
+
assert.equal(parseConfig({ emitLegacyTools: "true" }).emitLegacyTools, true);
|
|
17
|
+
|
|
18
|
+
// Env var fallback (REMNIC_ preferred, ENGRAM_ legacy) when config field absent.
|
|
19
|
+
const prevRemnic = process.env.REMNIC_EMIT_LEGACY_TOOLS;
|
|
20
|
+
const prevEngram = process.env.ENGRAM_EMIT_LEGACY_TOOLS;
|
|
21
|
+
try {
|
|
22
|
+
process.env.REMNIC_EMIT_LEGACY_TOOLS = "false";
|
|
23
|
+
assert.equal(parseConfig({}).emitLegacyTools, false, "REMNIC_ env disables");
|
|
24
|
+
// Explicit config field wins over env.
|
|
25
|
+
assert.equal(parseConfig({ emitLegacyTools: true }).emitLegacyTools, true, "config field wins over env");
|
|
26
|
+
delete process.env.REMNIC_EMIT_LEGACY_TOOLS;
|
|
27
|
+
process.env.ENGRAM_EMIT_LEGACY_TOOLS = "false";
|
|
28
|
+
assert.equal(parseConfig({}).emitLegacyTools, false, "ENGRAM_ env fallback disables");
|
|
29
|
+
} finally {
|
|
30
|
+
if (prevRemnic === undefined) delete process.env.REMNIC_EMIT_LEGACY_TOOLS;
|
|
31
|
+
else process.env.REMNIC_EMIT_LEGACY_TOOLS = prevRemnic;
|
|
32
|
+
if (prevEngram === undefined) delete process.env.ENGRAM_EMIT_LEGACY_TOOLS;
|
|
33
|
+
else process.env.ENGRAM_EMIT_LEGACY_TOOLS = prevEngram;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("parseConfig rejects a present-but-malformed emitLegacyTools (gotcha #51, #1427)", () => {
|
|
38
|
+
// A typo must fail fast, not silently fall through to the default (true) and
|
|
39
|
+
// re-enable legacy tool advertising.
|
|
40
|
+
for (const bad of ["fales", "maybe", 2, "2", "enabled"]) {
|
|
41
|
+
assert.throws(
|
|
42
|
+
() => parseConfig({ emitLegacyTools: bad }),
|
|
43
|
+
/emitLegacyTools must be a boolean-like value/,
|
|
44
|
+
`emitLegacyTools=${JSON.stringify(bad)} should throw`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
// Malformed env var also fails fast (only when the config field is absent).
|
|
48
|
+
const prev = process.env.REMNIC_EMIT_LEGACY_TOOLS;
|
|
49
|
+
try {
|
|
50
|
+
process.env.REMNIC_EMIT_LEGACY_TOOLS = "maybe";
|
|
51
|
+
assert.throws(
|
|
52
|
+
() => parseConfig({}),
|
|
53
|
+
/REMNIC_EMIT_LEGACY_TOOLS must be a boolean-like value/,
|
|
54
|
+
);
|
|
55
|
+
// An explicit valid config field overrides a malformed env (field wins first).
|
|
56
|
+
assert.equal(parseConfig({ emitLegacyTools: false }).emitLegacyTools, false);
|
|
57
|
+
} finally {
|
|
58
|
+
if (prev === undefined) delete process.env.REMNIC_EMIT_LEGACY_TOOLS;
|
|
59
|
+
else process.env.REMNIC_EMIT_LEGACY_TOOLS = prev;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
6
63
|
test("parseConfig expands tilde paths for core storage directories", () => {
|
|
7
64
|
const previousHome = process.env.HOME;
|
|
8
65
|
process.env.HOME = "/Users/remnic-test";
|
|
@@ -72,6 +129,33 @@ test("parseConfig codex missing entirely → installExtension defaults to true",
|
|
|
72
129
|
assert.equal(result.codex.installExtension, true);
|
|
73
130
|
});
|
|
74
131
|
|
|
132
|
+
test("parseConfig recallPlannerLlmEnabled defaults to false and coerces boolean-like strings (opt-in, issue #1367)", () => {
|
|
133
|
+
assert.equal(parseConfig({}).recallPlannerLlmEnabled, false);
|
|
134
|
+
assert.equal(parseConfig({ recallPlannerLlmEnabled: true }).recallPlannerLlmEnabled, true);
|
|
135
|
+
// CLI/env surfaces pass strings — these must enable the gate (gotcha #36).
|
|
136
|
+
assert.equal(parseConfig({ recallPlannerLlmEnabled: "true" }).recallPlannerLlmEnabled, true);
|
|
137
|
+
assert.equal(parseConfig({ recallPlannerLlmEnabled: "1" }).recallPlannerLlmEnabled, true);
|
|
138
|
+
assert.equal(parseConfig({ recallPlannerLlmEnabled: "on" }).recallPlannerLlmEnabled, true);
|
|
139
|
+
// Boolean-like falses and junk stay off.
|
|
140
|
+
assert.equal(parseConfig({ recallPlannerLlmEnabled: "false" }).recallPlannerLlmEnabled, false);
|
|
141
|
+
assert.equal(parseConfig({ recallPlannerLlmEnabled: "0" }).recallPlannerLlmEnabled, false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("parseConfig coerces boolean-like strings for all recallPlanner gates (issue #1367, gotcha #36)", () => {
|
|
145
|
+
// Rollout switches must honor string config from CLI/env surfaces.
|
|
146
|
+
assert.equal(parseConfig({ recallPlannerShadowMode: "true" }).recallPlannerShadowMode, true);
|
|
147
|
+
assert.equal(parseConfig({ recallPlannerShadowMode: "off" }).recallPlannerShadowMode, false);
|
|
148
|
+
assert.equal(parseConfig({}).recallPlannerShadowMode, false);
|
|
149
|
+
|
|
150
|
+
assert.equal(parseConfig({ recallPlannerTelemetryEnabled: "false" }).recallPlannerTelemetryEnabled, false);
|
|
151
|
+
assert.equal(parseConfig({}).recallPlannerTelemetryEnabled, true);
|
|
152
|
+
|
|
153
|
+
// The enable gate must be disableable via string "false" (the old `!== false`
|
|
154
|
+
// check treated "false" as truthy → could not disable).
|
|
155
|
+
assert.equal(parseConfig({ recallPlannerEnabled: "false" }).recallPlannerEnabled, false);
|
|
156
|
+
assert.equal(parseConfig({}).recallPlannerEnabled, true);
|
|
157
|
+
});
|
|
158
|
+
|
|
75
159
|
test("parseConfig dreaming.maxEntries=0 preserves the runtime disable switch", () => {
|
|
76
160
|
const result = parseConfig({ dreaming: { maxEntries: 0 } });
|
|
77
161
|
assert.equal(result.dreaming.maxEntries, 0);
|
|
@@ -116,6 +200,59 @@ test("parseConfig initGateTimeoutMs defaults to OpenClaw cold-start budget", ()
|
|
|
116
200
|
assert.equal(result.initGateTimeoutMs, 30_000);
|
|
117
201
|
});
|
|
118
202
|
|
|
203
|
+
test("parseConfig qmdSearchStrategy defaults to hybrid and validates the enum", () => {
|
|
204
|
+
// Default must equal the historical lex+vec+hyde behavior. Issue #1335.
|
|
205
|
+
assert.equal(parseConfig({}).qmdSearchStrategy, "hybrid");
|
|
206
|
+
assert.equal(parseConfig({ qmdSearchStrategy: "hybrid" }).qmdSearchStrategy, "hybrid");
|
|
207
|
+
assert.equal(parseConfig({ qmdSearchStrategy: "lex-vec" }).qmdSearchStrategy, "lex-vec");
|
|
208
|
+
assert.equal(parseConfig({ qmdSearchStrategy: "lex" }).qmdSearchStrategy, "lex");
|
|
209
|
+
assert.equal(parseConfig({ qmdSearchStrategy: "LEX" }).qmdSearchStrategy, "lex");
|
|
210
|
+
|
|
211
|
+
for (const value of ["hyde", "vec", "bm25", "", 42]) {
|
|
212
|
+
assert.throws(
|
|
213
|
+
() => parseConfig({ qmdSearchStrategy: value }),
|
|
214
|
+
/qmdSearchStrategy must be one of/,
|
|
215
|
+
`invalid qmdSearchStrategy ${String(value)} should throw`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("parseConfig qmdSubprocessStrategy defaults to query (honors QMD query intent)", () => {
|
|
221
|
+
// Default must remain `qmd query` (LLM expansion + rerank) per CLAUDE.md gotcha #7.
|
|
222
|
+
assert.equal(parseConfig({}).qmdSubprocessStrategy, "query");
|
|
223
|
+
assert.equal(parseConfig({ qmdSubprocessStrategy: "query" }).qmdSubprocessStrategy, "query");
|
|
224
|
+
assert.equal(parseConfig({ qmdSubprocessStrategy: "search" }).qmdSubprocessStrategy, "search");
|
|
225
|
+
assert.equal(parseConfig({ qmdSubprocessStrategy: "SEARCH" }).qmdSubprocessStrategy, "search");
|
|
226
|
+
|
|
227
|
+
for (const value of ["bm25", "vsearch", "", 7]) {
|
|
228
|
+
assert.throws(
|
|
229
|
+
() => parseConfig({ qmdSubprocessStrategy: value }),
|
|
230
|
+
/qmdSubprocessStrategy must be one of/,
|
|
231
|
+
`invalid qmdSubprocessStrategy ${String(value)} should throw`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("parseConfig qmdDaemonTimeoutMs defaults to 8000 and clamps valid integers", () => {
|
|
237
|
+
assert.equal(parseConfig({}).qmdDaemonTimeoutMs, 8_000);
|
|
238
|
+
assert.equal(parseConfig({ qmdDaemonTimeoutMs: 20_000 }).qmdDaemonTimeoutMs, 20_000);
|
|
239
|
+
assert.equal(parseConfig({ qmdDaemonTimeoutMs: "20000" }).qmdDaemonTimeoutMs, 20_000);
|
|
240
|
+
// Below floor clamps up; above ceiling clamps down.
|
|
241
|
+
assert.equal(parseConfig({ qmdDaemonTimeoutMs: 100 }).qmdDaemonTimeoutMs, 1_000);
|
|
242
|
+
assert.equal(parseConfig({ qmdDaemonTimeoutMs: 999_999 }).qmdDaemonTimeoutMs, 120_000);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("parseConfig qmdDaemonTimeoutMs rejects non-numeric and non-integer input", () => {
|
|
246
|
+
// gotcha #51 + codex review on #1422: silent coercion hides config mistakes.
|
|
247
|
+
for (const value of ["abc", "", 2500.9, "2500.9", Number.NaN, Infinity, true, {}]) {
|
|
248
|
+
assert.throws(
|
|
249
|
+
() => parseConfig({ qmdDaemonTimeoutMs: value }),
|
|
250
|
+
/qmdDaemonTimeoutMs must be an integer/,
|
|
251
|
+
`invalid qmdDaemonTimeoutMs ${String(value)} should throw`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
119
256
|
test("parseConfig initGateTimeoutMs accepts CLI-style numeric strings", () => {
|
|
120
257
|
const result = parseConfig({ initGateTimeoutMs: "45000" });
|
|
121
258
|
assert.equal(result.initGateTimeoutMs, 45_000);
|
|
@@ -140,6 +277,82 @@ test("parseConfig modelSource=gateway does not inherit OPENAI_API_KEY from the p
|
|
|
140
277
|
}
|
|
141
278
|
});
|
|
142
279
|
|
|
280
|
+
test("parseConfig normalizes taskModelChain", () => {
|
|
281
|
+
const cfg = parseConfig({
|
|
282
|
+
taskModelChain: {
|
|
283
|
+
primary: " openai/cheap-primary ",
|
|
284
|
+
fallbacks: ["openai/cheap-primary", " fireworks/accounts/fireworks/models/glm-5p1 ", ""],
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
assert.deepEqual(cfg.taskModelChain, {
|
|
289
|
+
primary: "openai/cheap-primary",
|
|
290
|
+
fallbacks: ["fireworks/accounts/fireworks/models/glm-5p1"],
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("parseConfig treats an absent taskModelChain as not configured", () => {
|
|
295
|
+
assert.equal(parseConfig({}).taskModelChain, undefined);
|
|
296
|
+
assert.equal(parseConfig({ taskModelChain: null }).taskModelChain, undefined);
|
|
297
|
+
assert.equal(parseConfig({ taskModelChain: undefined }).taskModelChain, undefined);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("parseConfig rejects a present-but-malformed taskModelChain (gotcha #51)", () => {
|
|
301
|
+
// A typo'd chain must surface loudly instead of silently reverting to defaults.
|
|
302
|
+
assert.throws(() => parseConfig({ taskModelChain: [] }), /taskModelChain must be an object/);
|
|
303
|
+
assert.throws(() => parseConfig({ taskModelChain: "openai/x" }), /taskModelChain must be an object/);
|
|
304
|
+
assert.throws(() => parseConfig({ taskModelChain: { primary: " " } }), /taskModelChain\.primary is required/);
|
|
305
|
+
assert.throws(() => parseConfig({ taskModelChain: { fallbacks: ["openai/fallback-only"] } }), /taskModelChain\.primary is required/);
|
|
306
|
+
assert.throws(
|
|
307
|
+
() => parseConfig({ taskModelChain: { primary: "openai/p", fallbacks: "not-an-array" } }),
|
|
308
|
+
/taskModelChain\.fallbacks must be an array/,
|
|
309
|
+
);
|
|
310
|
+
assert.throws(
|
|
311
|
+
() => parseConfig({ taskModelChain: { primary: "openai/p", fallbacks: [123] } }),
|
|
312
|
+
/taskModelChain\.fallbacks must contain only strings/,
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("parseConfig rejects unqualified taskModelChain model strings (codex review #1425)", () => {
|
|
317
|
+
// A slash-less id like "gpt-4.1" parses here but FallbackLlmClient.parseModelString
|
|
318
|
+
// drops it, leaving the chain silently using a different model — reject at parse.
|
|
319
|
+
assert.throws(
|
|
320
|
+
() => parseConfig({ taskModelChain: { primary: "gpt-4.1" } }),
|
|
321
|
+
/taskModelChain\.primary must be in "provider\/model" form/,
|
|
322
|
+
);
|
|
323
|
+
assert.throws(
|
|
324
|
+
() => parseConfig({ taskModelChain: { primary: "openai/" } }),
|
|
325
|
+
/taskModelChain\.primary must be in "provider\/model" form/,
|
|
326
|
+
);
|
|
327
|
+
assert.throws(
|
|
328
|
+
() => parseConfig({ taskModelChain: { primary: "/gpt-4.1" } }),
|
|
329
|
+
/taskModelChain\.primary must be in "provider\/model" form/,
|
|
330
|
+
);
|
|
331
|
+
assert.throws(
|
|
332
|
+
() => parseConfig({ taskModelChain: { primary: "openai/gpt", fallbacks: ["bare-model"] } }),
|
|
333
|
+
/taskModelChain\.fallbacks entries must be in "provider\/model" form/,
|
|
334
|
+
);
|
|
335
|
+
// Multi-slash provider/model paths remain valid.
|
|
336
|
+
assert.deepEqual(
|
|
337
|
+
parseConfig({
|
|
338
|
+
taskModelChain: { primary: "fireworks/accounts/fireworks/models/glm-5p1" },
|
|
339
|
+
}).taskModelChain,
|
|
340
|
+
{ primary: "fireworks/accounts/fireworks/models/glm-5p1" },
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("parseConfig rejects unknown taskModelChain keys (codex review #1425)", () => {
|
|
345
|
+
// A misspelled "fallback" must not silently drop the fallback chain.
|
|
346
|
+
assert.throws(
|
|
347
|
+
() => parseConfig({ taskModelChain: { primary: "openai/p", fallback: ["openai/q"] } }),
|
|
348
|
+
/taskModelChain has unknown property: fallback/,
|
|
349
|
+
);
|
|
350
|
+
assert.throws(
|
|
351
|
+
() => parseConfig({ taskModelChain: { primary: "openai/p", fallbackModels: ["openai/q"], extra: 1 } }),
|
|
352
|
+
/taskModelChain has unknown properties:/,
|
|
353
|
+
);
|
|
354
|
+
});
|
|
355
|
+
|
|
143
356
|
test("parseConfig modelSource=gateway still honors an explicit openaiApiKey override", () => {
|
|
144
357
|
const original = process.env.OPENAI_API_KEY;
|
|
145
358
|
process.env.OPENAI_API_KEY = "sk-env-should-not-be-used";
|
package/src/config.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
HeartbeatConfig,
|
|
14
14
|
IdentityInjectionMode,
|
|
15
15
|
MemoryOsPresetName,
|
|
16
|
+
AgentPersonaModelConfig,
|
|
16
17
|
PluginConfig,
|
|
17
18
|
PrincipalRule,
|
|
18
19
|
RecallPipelineConfig,
|
|
@@ -69,6 +70,81 @@ function parseBoundedIntegerMs(
|
|
|
69
70
|
return Math.min(max, Math.max(min, Math.floor(coerced)));
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
// A gateway model string must be "provider/model" — at least one "/" with a
|
|
74
|
+
// non-empty provider segment and a non-empty model segment. This mirrors
|
|
75
|
+
// FallbackLlmClient.parseModelString (which requires >= 2 slash-parts), so a
|
|
76
|
+
// model the runtime would silently drop is rejected at config time instead.
|
|
77
|
+
function isQualifiedModelString(value: string): boolean {
|
|
78
|
+
const slash = value.indexOf("/");
|
|
79
|
+
return slash > 0 && slash < value.length - 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseModelChainConfig(
|
|
83
|
+
value: unknown,
|
|
84
|
+
keyName: string,
|
|
85
|
+
): AgentPersonaModelConfig | undefined {
|
|
86
|
+
// Absent → not configured (no error).
|
|
87
|
+
if (value === undefined || value === null) return undefined;
|
|
88
|
+
|
|
89
|
+
// Present but malformed → reject loudly rather than silently dropping it,
|
|
90
|
+
// so a typo'd chain surfaces instead of quietly reverting to defaults
|
|
91
|
+
// (gotcha #51). Issue #1365 / PR #1370.
|
|
92
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`${keyName} must be an object like { "primary": "provider/model", "fallbacks": ["provider/model", ...] }; got ${JSON.stringify(value)}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
const raw = value as Record<string, unknown>;
|
|
98
|
+
// Reject unknown keys (matches the manifest's additionalProperties:false) so a
|
|
99
|
+
// misspelled "fallback"/"fallbackModels" doesn't silently drop the fallback
|
|
100
|
+
// chain (gotcha #51, codex review #1425).
|
|
101
|
+
const unknownKeys = Object.keys(raw).filter((k) => k !== "primary" && k !== "fallbacks");
|
|
102
|
+
if (unknownKeys.length > 0) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`${keyName} has unknown propert${unknownKeys.length === 1 ? "y" : "ies"}: ${unknownKeys.join(", ")}. Allowed: "primary", "fallbacks".`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
if (typeof raw.primary !== "string" || raw.primary.trim().length === 0) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`${keyName}.primary is required and must be a non-empty "provider/model" string; got ${JSON.stringify(raw.primary)}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const primary = raw.primary.trim();
|
|
113
|
+
if (!isQualifiedModelString(primary)) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`${keyName}.primary must be in "provider/model" form (e.g. "zai/glm-4.7-flash"); got ${JSON.stringify(primary)}`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let dedupedFallbacks: string[] | undefined;
|
|
120
|
+
if (raw.fallbacks !== undefined) {
|
|
121
|
+
if (!Array.isArray(raw.fallbacks)) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`${keyName}.fallbacks must be an array of "provider/model" strings; got ${JSON.stringify(raw.fallbacks)}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (raw.fallbacks.some((item) => typeof item !== "string")) {
|
|
127
|
+
throw new Error(`${keyName}.fallbacks must contain only strings`);
|
|
128
|
+
}
|
|
129
|
+
const trimmed = raw.fallbacks
|
|
130
|
+
.map((item) => (item as string).trim())
|
|
131
|
+
.filter((item) => item.length > 0 && item !== primary);
|
|
132
|
+
for (const fb of trimmed) {
|
|
133
|
+
if (!isQualifiedModelString(fb)) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`${keyName}.fallbacks entries must be in "provider/model" form; got ${JSON.stringify(fb)}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
dedupedFallbacks = [...new Set(trimmed)];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
primary,
|
|
144
|
+
...(dedupedFallbacks && dedupedFallbacks.length > 0 ? { fallbacks: dedupedFallbacks } : {}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
72
148
|
function parsePositiveInteger(value: unknown, keyName: string): number | undefined {
|
|
73
149
|
if (value === undefined || value === null) return undefined;
|
|
74
150
|
const coerced = coerceNumber(value);
|
|
@@ -139,6 +215,51 @@ function parseQmdChunkStrategy(value: unknown): "auto" | "regex" {
|
|
|
139
215
|
throw new Error(`qmdChunkStrategy must be "auto" or "regex"; got ${JSON.stringify(value)}`);
|
|
140
216
|
}
|
|
141
217
|
|
|
218
|
+
// Issue #1335. Default "hybrid" preserves the historical lex+vec+hyde daemon plan.
|
|
219
|
+
function parseQmdSearchStrategy(value: unknown): "hybrid" | "lex-vec" | "lex" {
|
|
220
|
+
if (value === undefined || value === null) return "hybrid";
|
|
221
|
+
if (typeof value !== "string") {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`qmdSearchStrategy must be one of "hybrid", "lex-vec", or "lex"; got ${JSON.stringify(value)}`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
const normalized = value.trim().toLowerCase();
|
|
227
|
+
if (normalized === "hybrid" || normalized === "lex-vec" || normalized === "lex") {
|
|
228
|
+
return normalized;
|
|
229
|
+
}
|
|
230
|
+
throw new Error(
|
|
231
|
+
`qmdSearchStrategy must be one of "hybrid", "lex-vec", or "lex"; got ${JSON.stringify(value)}`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Issue #1335. Reject non-numeric / non-integer timeouts rather than silently
|
|
236
|
+
// coercing them (gotcha #51), then clamp valid integers to the documented bounds.
|
|
237
|
+
function parseQmdDaemonTimeoutMs(value: unknown): number {
|
|
238
|
+
if (value === undefined || value === null) return 8_000;
|
|
239
|
+
const coerced = coerceNumber(value);
|
|
240
|
+
if (coerced === undefined || !Number.isInteger(coerced)) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`qmdDaemonTimeoutMs must be an integer number of milliseconds between 1000 and 120000; got ${JSON.stringify(value)}`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
return Math.min(120_000, Math.max(1_000, coerced));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Issue #1335. Default "query" keeps `qmd query` (LLM expansion + rerank) per gotcha #7.
|
|
249
|
+
function parseQmdSubprocessStrategy(value: unknown): "query" | "search" {
|
|
250
|
+
if (value === undefined || value === null) return "query";
|
|
251
|
+
if (typeof value !== "string") {
|
|
252
|
+
throw new Error(
|
|
253
|
+
`qmdSubprocessStrategy must be one of "query" or "search"; got ${JSON.stringify(value)}`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
const normalized = value.trim().toLowerCase();
|
|
257
|
+
if (normalized === "query" || normalized === "search") return normalized;
|
|
258
|
+
throw new Error(
|
|
259
|
+
`qmdSubprocessStrategy must be one of "query" or "search"; got ${JSON.stringify(value)}`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
142
263
|
function parseOptionalNonEmptyString(value: unknown): string | undefined {
|
|
143
264
|
if (typeof value !== "string") return undefined;
|
|
144
265
|
const normalized = value.trim();
|
|
@@ -230,6 +351,38 @@ function coerceBooleanLike(value: unknown): boolean | undefined {
|
|
|
230
351
|
return undefined;
|
|
231
352
|
}
|
|
232
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Resolve the `emitLegacyTools` opt-out (issue #1427): config field wins, then
|
|
356
|
+
* the REMNIC_/ENGRAM_ env var, then default true. A *present-but-malformed*
|
|
357
|
+
* value fails fast rather than silently re-enabling legacy aliases — this knob
|
|
358
|
+
* controls the advertised MCP `tools/list` surface, so a typo like
|
|
359
|
+
* `emitLegacyTools=fales` must not be misread as `true` (gotcha #51).
|
|
360
|
+
*/
|
|
361
|
+
function resolveEmitLegacyTools(configValue: unknown): boolean {
|
|
362
|
+
const ACCEPTED = "true/false/1/0/yes/no/on/off";
|
|
363
|
+
if (configValue !== undefined && configValue !== null) {
|
|
364
|
+
const coerced = coerceBooleanLike(configValue);
|
|
365
|
+
if (coerced === undefined) {
|
|
366
|
+
throw new Error(
|
|
367
|
+
`emitLegacyTools must be a boolean-like value (${ACCEPTED}); got ${JSON.stringify(configValue)}`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
return coerced;
|
|
371
|
+
}
|
|
372
|
+
const envRaw =
|
|
373
|
+
readEnvVar("REMNIC_EMIT_LEGACY_TOOLS") ?? readEnvVar("ENGRAM_EMIT_LEGACY_TOOLS");
|
|
374
|
+
if (envRaw !== undefined) {
|
|
375
|
+
const coerced = coerceBooleanLike(envRaw);
|
|
376
|
+
if (coerced === undefined) {
|
|
377
|
+
throw new Error(
|
|
378
|
+
`REMNIC_EMIT_LEGACY_TOOLS must be a boolean-like value (${ACCEPTED}); got "${envRaw}"`,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
return coerced;
|
|
382
|
+
}
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
|
|
233
386
|
export function isOpenaiApiKeyDisabled(value: unknown): boolean {
|
|
234
387
|
return value === false || (typeof value === "string" && value.trim().toLowerCase() === "false");
|
|
235
388
|
}
|
|
@@ -1379,6 +1532,9 @@ export function parseConfig(raw: unknown): PluginConfig {
|
|
|
1379
1532
|
qmdChunkStrategy: parseQmdChunkStrategy(cfg.qmdChunkStrategy),
|
|
1380
1533
|
qmdCandidateLimit: parsePositiveInteger(cfg.qmdCandidateLimit, "qmdCandidateLimit"),
|
|
1381
1534
|
qmdQueryRerankEnabled: coerceBooleanLike(cfg.qmdQueryRerankEnabled) ?? true,
|
|
1535
|
+
qmdSearchStrategy: parseQmdSearchStrategy(cfg.qmdSearchStrategy),
|
|
1536
|
+
qmdSubprocessStrategy: parseQmdSubprocessStrategy(cfg.qmdSubprocessStrategy),
|
|
1537
|
+
qmdDaemonTimeoutMs: parseQmdDaemonTimeoutMs(cfg.qmdDaemonTimeoutMs),
|
|
1382
1538
|
qmdIndexName: parseOptionalNonEmptyString(cfg.qmdIndexName),
|
|
1383
1539
|
qmdForceCpu: coerceBooleanLike(cfg.qmdForceCpu) ?? false,
|
|
1384
1540
|
qmdGpuBackend: parseQmdGpuBackend(cfg.qmdGpuBackend),
|
|
@@ -2377,6 +2533,7 @@ export function parseConfig(raw: unknown): PluginConfig {
|
|
|
2377
2533
|
typeof cfg.fastGatewayAgentId === "string" && cfg.fastGatewayAgentId.length > 0
|
|
2378
2534
|
? cfg.fastGatewayAgentId
|
|
2379
2535
|
: "",
|
|
2536
|
+
taskModelChain: parseModelChainConfig(cfg.taskModelChain, "taskModelChain"),
|
|
2380
2537
|
|
|
2381
2538
|
// v3.0 namespaces (default off)
|
|
2382
2539
|
namespacesEnabled: cfg.namespacesEnabled === true,
|
|
@@ -2716,20 +2873,30 @@ export function parseConfig(raw: unknown): PluginConfig {
|
|
|
2716
2873
|
.filter((param): param is string => typeof param === "string" && param.trim().length > 0)
|
|
2717
2874
|
: [...DEFAULT_BEHAVIOR_LOOP_PROTECTED_PARAMS],
|
|
2718
2875
|
// v8.0 phase 1
|
|
2719
|
-
|
|
2876
|
+
// All recallPlanner boolean gates coerce boolean-like strings so CLI/env
|
|
2877
|
+
// surfaces (`--config recallPlanner*=true|false`) behave correctly — the
|
|
2878
|
+
// shadow-mode/telemetry/enable flags are documented rollout switches and
|
|
2879
|
+
// must not silently ignore string values (gotcha #36, #1428 review).
|
|
2880
|
+
recallPlannerEnabled: coerceBooleanLike(cfg.recallPlannerEnabled) ?? true,
|
|
2881
|
+
// Issue #1367 / Option C: LLM-based recall planning is opt-in so the
|
|
2882
|
+
// default recall path stays heuristic (no added latency / LLM call unless
|
|
2883
|
+
// the operator asks for it — gotcha #30). Coerce boolean-like strings so
|
|
2884
|
+
// CLI/env surfaces (`--config recallPlannerLlmEnabled=true`) actually
|
|
2885
|
+
// enable it (gotcha #36); defaults off.
|
|
2886
|
+
recallPlannerLlmEnabled: coerceBooleanLike(cfg.recallPlannerLlmEnabled) ?? false,
|
|
2720
2887
|
recallPlannerModel:
|
|
2721
2888
|
typeof cfg.recallPlannerModel === "string" && cfg.recallPlannerModel.trim().length > 0
|
|
2722
2889
|
? cfg.recallPlannerModel.trim()
|
|
2723
2890
|
: DEFAULT_REASONING_MODEL,
|
|
2724
2891
|
recallPlannerTimeoutMs:
|
|
2725
2892
|
typeof cfg.recallPlannerTimeoutMs === "number" ? cfg.recallPlannerTimeoutMs : 1500,
|
|
2726
|
-
recallPlannerUseResponsesApi: cfg.recallPlannerUseResponsesApi
|
|
2893
|
+
recallPlannerUseResponsesApi: coerceBooleanLike(cfg.recallPlannerUseResponsesApi) ?? true,
|
|
2727
2894
|
recallPlannerMaxPromptChars:
|
|
2728
2895
|
typeof cfg.recallPlannerMaxPromptChars === "number" ? cfg.recallPlannerMaxPromptChars : 4000,
|
|
2729
2896
|
recallPlannerMaxMemoryHints:
|
|
2730
2897
|
typeof cfg.recallPlannerMaxMemoryHints === "number" ? cfg.recallPlannerMaxMemoryHints : 24,
|
|
2731
|
-
recallPlannerShadowMode: cfg.recallPlannerShadowMode
|
|
2732
|
-
recallPlannerTelemetryEnabled: cfg.recallPlannerTelemetryEnabled
|
|
2898
|
+
recallPlannerShadowMode: coerceBooleanLike(cfg.recallPlannerShadowMode) ?? false,
|
|
2899
|
+
recallPlannerTelemetryEnabled: coerceBooleanLike(cfg.recallPlannerTelemetryEnabled) ?? true,
|
|
2733
2900
|
recallPlannerMaxQmdResultsMinimal:
|
|
2734
2901
|
typeof cfg.recallPlannerMaxQmdResultsMinimal === "number"
|
|
2735
2902
|
? cfg.recallPlannerMaxQmdResultsMinimal
|
|
@@ -3422,6 +3589,10 @@ export function parseConfig(raw: unknown): PluginConfig {
|
|
|
3422
3589
|
? cfg.binaryLifecycleBackendPath.trim()
|
|
3423
3590
|
: "",
|
|
3424
3591
|
|
|
3592
|
+
// Legacy MCP tool aliases opt-out (issue #1427). Config field wins; then
|
|
3593
|
+
// the REMNIC_/ENGRAM_ env var (gotcha #9); default true for back-compat.
|
|
3594
|
+
// Malformed values fail fast rather than silently defaulting (gotcha #51).
|
|
3595
|
+
emitLegacyTools: resolveEmitLegacyTools(cfg.emitLegacyTools),
|
|
3425
3596
|
// Codex citation parity (issue #379)
|
|
3426
3597
|
citationsEnabled: cfg.citationsEnabled === true,
|
|
3427
3598
|
citationsAutoDetect: cfg.citationsAutoDetect !== false,
|
|
@@ -70,8 +70,9 @@ export async function runCodexMaterialize(
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
// Per-trigger gate: session-end runs must honor codexMaterializeOnSessionEnd.
|
|
73
|
-
//
|
|
74
|
-
//
|
|
73
|
+
// The Codex Stop hook (remnic-codex-hook.cjs, session-end event) passes
|
|
74
|
+
// reason="session_end"; when the user has turned off the session-end trigger
|
|
75
|
+
// we short-circuit here without touching disk.
|
|
75
76
|
if (options.reason === "session_end" && cfg.codexMaterializeOnSessionEnd === false) {
|
|
76
77
|
log.debug(
|
|
77
78
|
`[codex-materialize] skipped — session-end disabled via codexMaterializeOnSessionEnd=false`,
|
|
@@ -199,8 +200,10 @@ function resolveNamespaceDir(
|
|
|
199
200
|
): string {
|
|
200
201
|
if (!cfg.namespacesEnabled) return memoryDir;
|
|
201
202
|
|
|
202
|
-
const
|
|
203
|
-
const
|
|
203
|
+
const configuredDefaultNamespace = (cfg.defaultNamespace ?? "").trim();
|
|
204
|
+
const defaultNamespace =
|
|
205
|
+
configuredDefaultNamespace.length > 0 ? configuredDefaultNamespace : "default";
|
|
206
|
+
const ns = (namespace || defaultNamespace).trim();
|
|
204
207
|
if (!isSafeRouteNamespace(ns)) {
|
|
205
208
|
throw new Error(`invalid materialize namespace: ${ns}`);
|
|
206
209
|
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import path from "node:path";
|
|
21
|
-
import {
|
|
21
|
+
import { lstat, readdir, readFile, realpath, stat } from "node:fs/promises";
|
|
22
22
|
import { constants as fsConstants } from "node:fs";
|
|
23
23
|
import type { StorageManager } from "./storage.js";
|
|
24
24
|
import {
|
|
@@ -499,7 +499,7 @@ export async function runConsolidationProvenanceCheck(options: {
|
|
|
499
499
|
const scanRoots = ["facts", "corrections", "procedures", "reasoning-traces"];
|
|
500
500
|
for (const rootName of scanRoots) {
|
|
501
501
|
const rootPath = path.join(memoryDir, rootName);
|
|
502
|
-
for await (const file of walkMarkdownFiles(rootPath)) {
|
|
502
|
+
for await (const file of walkMarkdownFiles(rootPath, memoryDir)) {
|
|
503
503
|
if (seenPaths.has(file)) continue;
|
|
504
504
|
try {
|
|
505
505
|
const raw = await readFile(file, "utf-8");
|
|
@@ -531,21 +531,40 @@ export async function runConsolidationProvenanceCheck(options: {
|
|
|
531
531
|
/**
|
|
532
532
|
* Recursively yield all `.md` file paths under `root`. Silent on
|
|
533
533
|
* missing directories — the facts/corrections dirs may not exist in
|
|
534
|
-
* fresh installs.
|
|
534
|
+
* fresh installs. Symlinked roots/directories are skipped so the
|
|
535
|
+
* best-effort parse-failure pass cannot escape `memoryDir`.
|
|
535
536
|
*/
|
|
536
|
-
async function* walkMarkdownFiles(root: string): AsyncGenerator<string> {
|
|
537
|
+
async function* walkMarkdownFiles(root: string, memoryDir: string): AsyncGenerator<string> {
|
|
537
538
|
let entries;
|
|
539
|
+
let memoryDirReal: string;
|
|
538
540
|
try {
|
|
541
|
+
const rootStat = await lstat(root);
|
|
542
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) return;
|
|
543
|
+
memoryDirReal = await realpath(memoryDir);
|
|
544
|
+
const rootReal = await realpath(root);
|
|
545
|
+
if (!isPathWithin(rootReal, memoryDirReal)) return;
|
|
539
546
|
entries = await readdir(root, { withFileTypes: true });
|
|
540
547
|
} catch {
|
|
541
548
|
return;
|
|
542
549
|
}
|
|
543
550
|
for (const entry of entries) {
|
|
544
551
|
const full = path.join(root, entry.name);
|
|
552
|
+
if (entry.isSymbolicLink()) continue;
|
|
545
553
|
if (entry.isDirectory()) {
|
|
546
|
-
yield* walkMarkdownFiles(full);
|
|
554
|
+
yield* walkMarkdownFiles(full, memoryDirReal);
|
|
547
555
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
556
|
+
try {
|
|
557
|
+
const fileReal = await realpath(full);
|
|
558
|
+
if (!isPathWithin(fileReal, memoryDirReal)) continue;
|
|
559
|
+
} catch {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
548
562
|
yield full;
|
|
549
563
|
}
|
|
550
564
|
}
|
|
551
565
|
}
|
|
566
|
+
|
|
567
|
+
function isPathWithin(candidate: string, root: string): boolean {
|
|
568
|
+
const relative = path.relative(root, candidate);
|
|
569
|
+
return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative));
|
|
570
|
+
}
|
|
@@ -98,6 +98,28 @@ test("writeConversationChunks keeps distinct raw session keys from overwriting a
|
|
|
98
98
|
}
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
+
test("writeConversationChunks quotes metadata scalars in front matter", async () => {
|
|
102
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-conversation-index-"));
|
|
103
|
+
try {
|
|
104
|
+
const sessionKey = "agent\nkind: other\n---\ncolon: value";
|
|
105
|
+
const written = await writeConversationChunks(root, [
|
|
106
|
+
sampleChunk({
|
|
107
|
+
sessionKey,
|
|
108
|
+
startTs: "2026-05-17T00:00:00.000Z",
|
|
109
|
+
endTs: "2026-05-17T00:01:00.000Z",
|
|
110
|
+
}),
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
const content = await readFile(written[0]!, "utf-8");
|
|
114
|
+
assert.match(content, /^sessionKey: "agent\\nkind: other\\n---\\ncolon: value"$/m);
|
|
115
|
+
assert.match(content, /^startTs: "2026-05-17T00:00:00.000Z"$/m);
|
|
116
|
+
assert.match(content, /^endTs: "2026-05-17T00:01:00.000Z"$/m);
|
|
117
|
+
assert.doesNotMatch(content, /^kind: other$/m);
|
|
118
|
+
} finally {
|
|
119
|
+
await rm(root, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
101
123
|
test("writeConversationChunks rejects invalid chunk timestamps before deriving paths", async () => {
|
|
102
124
|
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-conversation-index-"));
|
|
103
125
|
try {
|
|
@@ -40,6 +40,10 @@ function sanitizeChunkId(id: string): string {
|
|
|
40
40
|
return sanitizePathComponent(id, "chunk");
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function yamlQuotedScalar(value: string): string {
|
|
44
|
+
return JSON.stringify(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
43
47
|
function datePathComponent(startTs: string): string {
|
|
44
48
|
const match = typeof startTs === "string"
|
|
45
49
|
? /^(\d{4})-(\d{2})-(\d{2})T/.exec(startTs)
|
|
@@ -153,9 +157,9 @@ export async function writeConversationChunks(
|
|
|
153
157
|
const content =
|
|
154
158
|
`---\n` +
|
|
155
159
|
`kind: conversation_chunk\n` +
|
|
156
|
-
`sessionKey: ${c.sessionKey}\n` +
|
|
157
|
-
`startTs: ${c.startTs}\n` +
|
|
158
|
-
`endTs: ${c.endTs}\n` +
|
|
160
|
+
`sessionKey: ${yamlQuotedScalar(c.sessionKey)}\n` +
|
|
161
|
+
`startTs: ${yamlQuotedScalar(c.startTs)}\n` +
|
|
162
|
+
`endTs: ${yamlQuotedScalar(c.endTs)}\n` +
|
|
159
163
|
`---\n\n` +
|
|
160
164
|
c.text +
|
|
161
165
|
"\n";
|