@remnic/core 9.69.50 → 9.69.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-admin-ops-surface.js +30 -30
- package/dist/access-audit.d.ts +1 -0
- package/dist/access-audit.js +2 -2
- package/dist/access-authorization-probe.js +31 -31
- package/dist/access-boundary.js +30 -30
- package/dist/access-cli.js +84 -83
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.js +30 -30
- package/dist/access-http-briefing.js +31 -31
- package/dist/access-http-query.js +32 -32
- package/dist/access-http.js +50 -50
- package/dist/access-identity-continuity-surface.js +30 -30
- package/dist/access-lcm-surface.js +30 -30
- package/dist/access-mcp-output-schemas.js +1 -1
- package/dist/access-mcp.js +41 -41
- package/dist/access-namespace-preflight.js +30 -30
- package/dist/access-observe-write-category.js +5 -5
- package/dist/access-observe-write-surface.js +30 -30
- package/dist/access-operations-batch.js +32 -32
- package/dist/access-operations.js +39 -39
- package/dist/access-recall-concurrency.js +30 -30
- package/dist/access-recall-response.js +30 -30
- package/dist/access-recall-surface.d.ts +1 -1
- package/dist/access-recall-surface.js +30 -30
- package/dist/access-schema.js +4 -4
- package/dist/access-service.js +30 -30
- package/dist/access-surface-catalog.js +32 -32
- package/dist/active-recall.js +2 -2
- package/dist/briefing.js +5 -5
- package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-YO5QJ6L3.js} +2 -2
- package/dist/causal-consolidation.js +6 -6
- package/dist/{chunk-BHI5ILU5.js → chunk-25HSEIEM.js} +2 -2
- package/dist/{chunk-3RZB7TYL.js → chunk-2EICZYIG.js} +2 -2
- package/dist/{chunk-2IVHOQRY.js → chunk-2ERLN25A.js} +2 -2
- package/dist/{chunk-OQZYHCWM.js → chunk-2G2CVN2T.js} +27 -26
- package/dist/{chunk-OQZYHCWM.js.map → chunk-2G2CVN2T.js.map} +1 -1
- package/dist/{chunk-PPNFKQ6D.js → chunk-2SZBCM2S.js} +2 -2
- package/dist/{chunk-E73SVGJA.js → chunk-33SX6K3N.js} +2 -2
- package/dist/{chunk-D6WX5FEF.js → chunk-3QZRSEAM.js} +2 -2
- package/dist/{chunk-LBLXEFWK.js → chunk-4MN27537.js} +1 -1
- package/dist/chunk-4MN27537.js.map +1 -0
- package/dist/{chunk-LR7KUQ2W.js → chunk-6HCJWCA4.js} +5 -5
- package/dist/chunk-6VITOSPL.js +69 -0
- package/dist/chunk-6VITOSPL.js.map +1 -0
- package/dist/{chunk-EZV43XBD.js → chunk-6Z4OLQUO.js} +2 -2
- package/dist/{chunk-JTPAXDSX.js → chunk-6ZMIASYT.js} +2 -2
- package/dist/{chunk-UD6W67JM.js → chunk-72AV2H3R.js} +76 -25
- package/dist/chunk-72AV2H3R.js.map +1 -0
- package/dist/{chunk-DNO6QWCB.js → chunk-7TTOX6NW.js} +2 -2
- package/dist/{chunk-OP5IBPHP.js → chunk-A33DLYNY.js} +2 -2
- package/dist/{chunk-3MIZ2EKQ.js → chunk-AH3OIMS7.js} +2 -2
- package/dist/{chunk-TZ44E732.js → chunk-AIVRD66I.js} +2 -2
- package/dist/{chunk-IST6MBYC.js → chunk-BMAYH7F4.js} +473 -329
- package/dist/chunk-BMAYH7F4.js.map +1 -0
- package/dist/{chunk-XTMABIFW.js → chunk-BULK6DGB.js} +2 -2
- package/dist/{chunk-2NLLXCJG.js → chunk-BXLOS5AJ.js} +2 -2
- package/dist/{chunk-PMLFPVYJ.js → chunk-CKBA2MWS.js} +2 -2
- package/dist/{chunk-TVVEYCNW.js → chunk-DAOMWGO2.js} +2 -2
- package/dist/{chunk-E64HBW7M.js → chunk-DLXBMXIB.js} +2 -2
- package/dist/{chunk-X7Y7WX73.js → chunk-DQEMWVMT.js} +1 -1
- package/dist/{chunk-MQMZO23A.js → chunk-DQSVGTKB.js} +2 -2
- package/dist/{chunk-X6B2QWXW.js → chunk-DX63RFDJ.js} +2 -2
- package/dist/{chunk-OLJYDGGP.js → chunk-EBJL6HGR.js} +4 -4
- package/dist/{chunk-KOEDSRF5.js → chunk-ETNC6TT6.js} +2 -2
- package/dist/{chunk-SRMBMMTK.js → chunk-HEKWNWNL.js} +3 -3
- package/dist/{chunk-UI6R2YUB.js → chunk-IGDEKECC.js} +1 -1
- package/dist/{chunk-MECKA6G2.js → chunk-ISA5U5AP.js} +2 -2
- package/dist/{chunk-ZUNYFDI4.js → chunk-JGPG5UHM.js} +2 -2
- package/dist/{chunk-C3PKRFZ4.js → chunk-JXSOB3SA.js} +2 -2
- package/dist/{chunk-56C7OO3N.js → chunk-KAPYPTPS.js} +15 -15
- package/dist/{chunk-W6V7JN2E.js → chunk-KBHDV7P3.js} +3 -3
- package/dist/{chunk-F5V7KRQS.js → chunk-LIAKZGOY.js} +2 -2
- package/dist/{chunk-A7GQBJP5.js → chunk-LVFI5LG7.js} +7 -7
- package/dist/{chunk-QSP4Y73Q.js → chunk-LX7KIJQF.js} +2 -2
- package/dist/{chunk-ECLY4NZA.js → chunk-LZI6FWZW.js} +7 -1
- package/dist/chunk-LZI6FWZW.js.map +1 -0
- package/dist/{chunk-4PXFLDMJ.js → chunk-MNF6PTNL.js} +2 -2
- package/dist/{chunk-BOTSR5S3.js → chunk-MYDA2GA2.js} +2 -2
- package/dist/{chunk-4SDO74N2.js → chunk-NROI5XMG.js} +2 -2
- package/dist/{chunk-IYHHCSF6.js → chunk-NVW5ISEJ.js} +32 -32
- package/dist/{chunk-AL7OLBL7.js → chunk-NXSNFPYI.js} +2 -2
- package/dist/{chunk-D7QX26FF.js → chunk-O4JFBEUL.js} +2 -2
- package/dist/{chunk-PA5EOTRR.js → chunk-OIK3YL2P.js} +3 -3
- package/dist/{chunk-JOP7W24J.js → chunk-PLJ2MQFT.js} +4 -4
- package/dist/{chunk-HRCWDO6S.js → chunk-PYTSGD5M.js} +2 -2
- package/dist/{chunk-VBRS7Z3O.js → chunk-Q6OC4UUT.js} +2 -2
- package/dist/{chunk-4FWNMN5R.js → chunk-QRSC37NC.js} +5 -5
- package/dist/{chunk-D2XUSZ24.js → chunk-RIP2AOZW.js} +2 -2
- package/dist/{chunk-ZBAQMSEY.js → chunk-SCM4VYJW.js} +2 -2
- package/dist/{chunk-6525YIYL.js → chunk-SMI2G3MA.js} +3 -3
- package/dist/{chunk-DH536WT2.js → chunk-ST7FDKZT.js} +12 -2
- package/dist/chunk-ST7FDKZT.js.map +1 -0
- package/dist/{chunk-AYGWJ2UK.js → chunk-SZ75IB2T.js} +2 -2
- package/dist/{chunk-42BV46XS.js → chunk-T73ZC7C5.js} +2 -2
- package/dist/{chunk-3SI3FFTW.js → chunk-TNFDWJRS.js} +2 -2
- package/dist/{chunk-3UQIS2GC.js → chunk-TTFJQ5B5.js} +2 -2
- package/dist/{chunk-QZDUNK6V.js → chunk-UP3226JC.js} +6 -6
- package/dist/{chunk-IPLYGWQF.js → chunk-VDX2J7OX.js} +6 -6
- package/dist/{chunk-QXQCPNKA.js → chunk-VLR4S5GF.js} +7 -7
- package/dist/{chunk-ZHXSOMYH.js → chunk-VVX5PZFS.js} +2 -2
- package/dist/{chunk-HS75MCQF.js → chunk-WUUGE7QC.js} +4 -4
- package/dist/{chunk-MVMNEU65.js → chunk-X25NTN3W.js} +2 -2
- package/dist/{chunk-IC3PM3YO.js → chunk-XFXVIXDI.js} +2 -2
- package/dist/{chunk-ML5FW2RO.js → chunk-XHDQQ2TT.js} +2 -2
- package/dist/{chunk-XUKUMBP3.js → chunk-XRUOO7OT.js} +2 -2
- package/dist/{chunk-WVI43D54.js → chunk-YEIKOBOW.js} +15 -2
- package/dist/chunk-YEIKOBOW.js.map +1 -0
- package/dist/{chunk-AYP2FZFA.js → chunk-YIIW2OH2.js} +2 -2
- package/dist/{chunk-FO6IK4IF.js → chunk-YJJMJH6J.js} +8 -8
- package/dist/cli.js +68 -68
- package/dist/coding/export-okf-codegraph.js +4 -4
- package/dist/compounding/engine.js +5 -5
- package/dist/config.js +2 -2
- package/dist/connectors/codex-materialize-runner.js +5 -5
- package/dist/connectors/index.js +5 -5
- package/dist/convergence-refresh.js +7 -7
- package/dist/corpus-watermark.js +7 -7
- package/dist/deep-recall-http-glue.js +5 -5
- package/dist/deep-recall.js +2 -2
- package/dist/directory-sidecars.d.ts +28 -2
- package/dist/directory-sidecars.js +9 -1
- package/dist/entity-retrieval.js +5 -5
- package/dist/external-wiki-access.js +31 -31
- package/dist/extraction.js +5 -5
- package/dist/harmonic-retrieval.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +98 -88
- package/dist/lcm/engine.js +3 -3
- package/dist/lcm/index.js +8 -8
- package/dist/location/index.js +7 -7
- package/dist/maintenance/memory-governance.js +5 -5
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +5 -5
- package/dist/maintenance/rebuild-memory-projection.js +6 -6
- package/dist/memory-browse-operations.js +31 -31
- package/dist/memory-browse.js +3 -3
- package/dist/namespaces/migrate.js +6 -6
- package/dist/namespaces/search.js +3 -3
- package/dist/namespaces/storage.js +5 -5
- package/dist/operator-doctor-corpus.js +8 -8
- package/dist/operator-toolkit.js +15 -15
- package/dist/orchestration/maintenance.js +13 -13
- package/dist/orchestrator.js +84 -83
- package/dist/recall-audit-anomaly.d.ts +1 -0
- package/dist/recall-audit.d.ts +4 -0
- package/dist/recall-audit.js +1 -1
- package/dist/recall-composition-decision.d.ts +41 -0
- package/dist/recall-composition-decision.js +17 -0
- package/dist/recall-composition-decision.js.map +1 -0
- package/dist/recall-explain-renderer.d.ts +1 -0
- package/dist/recall-explain-renderer.js +2 -2
- package/dist/recall-navigation-operations.js +31 -31
- package/dist/recall-navigation-renderer.d.ts +1 -0
- package/dist/recall-navigation.d.ts +1 -0
- package/dist/recall-navigation.js +30 -30
- package/dist/recall-state-view-admission.js +3 -3
- package/dist/recall-tag-filter.d.ts +1 -0
- package/dist/recall-xray-cli.d.ts +1 -0
- package/dist/recall-xray-cli.js +3 -3
- package/dist/recall-xray-renderer.d.ts +1 -0
- package/dist/recall-xray-renderer.js +2 -2
- package/dist/recall-xray.d.ts +9 -0
- package/dist/recall-xray.js +1 -1
- package/dist/resume-bundles.js +3 -3
- package/dist/search/factory.js +2 -2
- package/dist/search/index.js +4 -4
- package/dist/semantic-consolidation.js +6 -6
- package/dist/semantic-rule-promotion.js +5 -5
- package/dist/semantic-rule-verifier.js +5 -5
- package/dist/source-agent-qualifier.js +5 -5
- package/dist/standup/build.js +5 -5
- package/dist/storage.js +4 -4
- package/dist/support-passport/index.js +33 -33
- package/dist/transfer/autodetect.js +1 -1
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +3 -3
- package/dist/transfer/capsule-import.js +2 -2
- package/dist/transfer/export-okf.js +5 -5
- package/dist/verified-recall.js +5 -5
- package/dist/who-knows-cli.js +6 -6
- package/dist/who-knows.js +5 -5
- package/package.json +2 -2
- package/src/access-mcp-output-schemas.ts +14 -1
- package/src/access-recall-response.ts +3 -0
- package/src/directory-sidecars.test.ts +119 -0
- package/src/directory-sidecars.ts +110 -25
- package/src/experience/session-experience-recall.test.ts +228 -0
- package/src/experience/session-experience-recall.ts +60 -0
- package/src/index.ts +1 -0
- package/src/orchestration/recall-entry.ts +16 -2
- package/src/orchestration/recall-internal.ts +21 -18
- package/src/orchestration/recall-recognition-search.test.ts +290 -0
- package/src/orchestration/recall-recognition-search.ts +160 -0
- package/src/procedural/procedure-recall.ts +14 -3
- package/src/recall-audit.test.ts +34 -0
- package/src/recall-audit.ts +4 -0
- package/src/recall-composition-decision.test.ts +134 -0
- package/src/recall-composition-decision.ts +113 -0
- package/src/recall-xray-renderer.test.ts +17 -0
- package/src/recall-xray-renderer.ts +10 -0
- package/src/recall-xray.test.ts +28 -0
- package/src/recall-xray.ts +15 -0
- package/src/testing/subjects/recall-recognition-tier.test.ts +106 -0
- package/dist/chunk-DH536WT2.js.map +0 -1
- package/dist/chunk-ECLY4NZA.js.map +0 -1
- package/dist/chunk-IST6MBYC.js.map +0 -1
- package/dist/chunk-LBLXEFWK.js.map +0 -1
- package/dist/chunk-UD6W67JM.js.map +0 -1
- package/dist/chunk-WVI43D54.js.map +0 -1
- /package/dist/{capsule-crypto-7FJQINUR.js.map → capsule-crypto-YO5QJ6L3.js.map} +0 -0
- /package/dist/{chunk-BHI5ILU5.js.map → chunk-25HSEIEM.js.map} +0 -0
- /package/dist/{chunk-3RZB7TYL.js.map → chunk-2EICZYIG.js.map} +0 -0
- /package/dist/{chunk-2IVHOQRY.js.map → chunk-2ERLN25A.js.map} +0 -0
- /package/dist/{chunk-PPNFKQ6D.js.map → chunk-2SZBCM2S.js.map} +0 -0
- /package/dist/{chunk-E73SVGJA.js.map → chunk-33SX6K3N.js.map} +0 -0
- /package/dist/{chunk-D6WX5FEF.js.map → chunk-3QZRSEAM.js.map} +0 -0
- /package/dist/{chunk-LR7KUQ2W.js.map → chunk-6HCJWCA4.js.map} +0 -0
- /package/dist/{chunk-EZV43XBD.js.map → chunk-6Z4OLQUO.js.map} +0 -0
- /package/dist/{chunk-JTPAXDSX.js.map → chunk-6ZMIASYT.js.map} +0 -0
- /package/dist/{chunk-DNO6QWCB.js.map → chunk-7TTOX6NW.js.map} +0 -0
- /package/dist/{chunk-OP5IBPHP.js.map → chunk-A33DLYNY.js.map} +0 -0
- /package/dist/{chunk-3MIZ2EKQ.js.map → chunk-AH3OIMS7.js.map} +0 -0
- /package/dist/{chunk-TZ44E732.js.map → chunk-AIVRD66I.js.map} +0 -0
- /package/dist/{chunk-XTMABIFW.js.map → chunk-BULK6DGB.js.map} +0 -0
- /package/dist/{chunk-2NLLXCJG.js.map → chunk-BXLOS5AJ.js.map} +0 -0
- /package/dist/{chunk-PMLFPVYJ.js.map → chunk-CKBA2MWS.js.map} +0 -0
- /package/dist/{chunk-TVVEYCNW.js.map → chunk-DAOMWGO2.js.map} +0 -0
- /package/dist/{chunk-E64HBW7M.js.map → chunk-DLXBMXIB.js.map} +0 -0
- /package/dist/{chunk-X7Y7WX73.js.map → chunk-DQEMWVMT.js.map} +0 -0
- /package/dist/{chunk-MQMZO23A.js.map → chunk-DQSVGTKB.js.map} +0 -0
- /package/dist/{chunk-X6B2QWXW.js.map → chunk-DX63RFDJ.js.map} +0 -0
- /package/dist/{chunk-OLJYDGGP.js.map → chunk-EBJL6HGR.js.map} +0 -0
- /package/dist/{chunk-KOEDSRF5.js.map → chunk-ETNC6TT6.js.map} +0 -0
- /package/dist/{chunk-SRMBMMTK.js.map → chunk-HEKWNWNL.js.map} +0 -0
- /package/dist/{chunk-UI6R2YUB.js.map → chunk-IGDEKECC.js.map} +0 -0
- /package/dist/{chunk-MECKA6G2.js.map → chunk-ISA5U5AP.js.map} +0 -0
- /package/dist/{chunk-ZUNYFDI4.js.map → chunk-JGPG5UHM.js.map} +0 -0
- /package/dist/{chunk-C3PKRFZ4.js.map → chunk-JXSOB3SA.js.map} +0 -0
- /package/dist/{chunk-56C7OO3N.js.map → chunk-KAPYPTPS.js.map} +0 -0
- /package/dist/{chunk-W6V7JN2E.js.map → chunk-KBHDV7P3.js.map} +0 -0
- /package/dist/{chunk-F5V7KRQS.js.map → chunk-LIAKZGOY.js.map} +0 -0
- /package/dist/{chunk-A7GQBJP5.js.map → chunk-LVFI5LG7.js.map} +0 -0
- /package/dist/{chunk-QSP4Y73Q.js.map → chunk-LX7KIJQF.js.map} +0 -0
- /package/dist/{chunk-4PXFLDMJ.js.map → chunk-MNF6PTNL.js.map} +0 -0
- /package/dist/{chunk-BOTSR5S3.js.map → chunk-MYDA2GA2.js.map} +0 -0
- /package/dist/{chunk-4SDO74N2.js.map → chunk-NROI5XMG.js.map} +0 -0
- /package/dist/{chunk-IYHHCSF6.js.map → chunk-NVW5ISEJ.js.map} +0 -0
- /package/dist/{chunk-AL7OLBL7.js.map → chunk-NXSNFPYI.js.map} +0 -0
- /package/dist/{chunk-D7QX26FF.js.map → chunk-O4JFBEUL.js.map} +0 -0
- /package/dist/{chunk-PA5EOTRR.js.map → chunk-OIK3YL2P.js.map} +0 -0
- /package/dist/{chunk-JOP7W24J.js.map → chunk-PLJ2MQFT.js.map} +0 -0
- /package/dist/{chunk-HRCWDO6S.js.map → chunk-PYTSGD5M.js.map} +0 -0
- /package/dist/{chunk-VBRS7Z3O.js.map → chunk-Q6OC4UUT.js.map} +0 -0
- /package/dist/{chunk-4FWNMN5R.js.map → chunk-QRSC37NC.js.map} +0 -0
- /package/dist/{chunk-D2XUSZ24.js.map → chunk-RIP2AOZW.js.map} +0 -0
- /package/dist/{chunk-ZBAQMSEY.js.map → chunk-SCM4VYJW.js.map} +0 -0
- /package/dist/{chunk-6525YIYL.js.map → chunk-SMI2G3MA.js.map} +0 -0
- /package/dist/{chunk-AYGWJ2UK.js.map → chunk-SZ75IB2T.js.map} +0 -0
- /package/dist/{chunk-42BV46XS.js.map → chunk-T73ZC7C5.js.map} +0 -0
- /package/dist/{chunk-3SI3FFTW.js.map → chunk-TNFDWJRS.js.map} +0 -0
- /package/dist/{chunk-3UQIS2GC.js.map → chunk-TTFJQ5B5.js.map} +0 -0
- /package/dist/{chunk-QZDUNK6V.js.map → chunk-UP3226JC.js.map} +0 -0
- /package/dist/{chunk-IPLYGWQF.js.map → chunk-VDX2J7OX.js.map} +0 -0
- /package/dist/{chunk-QXQCPNKA.js.map → chunk-VLR4S5GF.js.map} +0 -0
- /package/dist/{chunk-ZHXSOMYH.js.map → chunk-VVX5PZFS.js.map} +0 -0
- /package/dist/{chunk-HS75MCQF.js.map → chunk-WUUGE7QC.js.map} +0 -0
- /package/dist/{chunk-MVMNEU65.js.map → chunk-X25NTN3W.js.map} +0 -0
- /package/dist/{chunk-IC3PM3YO.js.map → chunk-XFXVIXDI.js.map} +0 -0
- /package/dist/{chunk-ML5FW2RO.js.map → chunk-XHDQQ2TT.js.map} +0 -0
- /package/dist/{chunk-XUKUMBP3.js.map → chunk-XRUOO7OT.js.map} +0 -0
- /package/dist/{chunk-AYP2FZFA.js.map → chunk-YIIW2OH2.js.map} +0 -0
- /package/dist/{chunk-FO6IK4IF.js.map → chunk-YJJMJH6J.js.map} +0 -0
|
@@ -26,12 +26,20 @@
|
|
|
26
26
|
* for `index.md`/`log.md`) while staying full-text searchable. Files at the
|
|
27
27
|
* sidecar path WITHOUT the marker are user content: never overwritten, never
|
|
28
28
|
* removed.
|
|
29
|
+
*
|
|
30
|
+
* Layer 2 (this slice): {@link refreshDirectorySidecarsAfterWrite} refreshes
|
|
31
|
+
* only the changed directory and its category ancestors, so a memory write
|
|
32
|
+
* populates sidecars without walking the rest of the store. `enabled: false`
|
|
33
|
+
* is a proven no-op (no reads, no writes). Config parsing lives here;
|
|
34
|
+
* `parseConfig` wiring, retrieval drill-down, xray, and LLM summarization
|
|
35
|
+
* remain later layers on #2977 (`config.ts` is at its file-size ratchet).
|
|
29
36
|
*/
|
|
30
37
|
|
|
31
38
|
import { createHash } from "node:crypto";
|
|
32
39
|
import { lstatSync, readFileSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
33
40
|
import path from "node:path";
|
|
34
41
|
|
|
42
|
+
import { coerceBooleanLike } from "./connectors/coerce.js";
|
|
35
43
|
import { writeFileAtomically } from "./maintenance/atomic-file.js";
|
|
36
44
|
import { normalizeProjectionPreview } from "./memory-projection-format.js";
|
|
37
45
|
import { OKF_RESERVED_BASENAMES } from "./okf/type-mapping.js";
|
|
@@ -43,6 +51,18 @@ export const DIRECTORY_SIDECAR_MARKER = "<!-- remnic-directory-sidecar -->";
|
|
|
43
51
|
const ABSTRACT_MAX_CHARS = 256;
|
|
44
52
|
const OVERVIEW_MAX_CHARS = 4000;
|
|
45
53
|
const DEFAULT_QUERY_LIMIT = 5;
|
|
54
|
+
const CATEGORY_ROOTS: ReadonlySet<string> = new Set(RECALL_FALLBACK_DIRS);
|
|
55
|
+
|
|
56
|
+
/** Config fields this slice owns. Mix into `PluginConfig` in a later layer. */
|
|
57
|
+
export interface DirectorySidecarSettings {
|
|
58
|
+
/** Populate sidecars on memory writes. Default false. */
|
|
59
|
+
directorySidecarsEnabled: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Strict off-default: only an explicit true/"true"/1 opts in. */
|
|
63
|
+
export function parseDirectorySidecarsEnabled(raw: unknown): boolean {
|
|
64
|
+
return coerceBooleanLike(raw, "directorySidecarsEnabled") === true;
|
|
65
|
+
}
|
|
46
66
|
|
|
47
67
|
/** Basenames never treated as child memories (ours plus OKF's reserved ones). */
|
|
48
68
|
const RESERVED_CHILD_BASENAMES: Readonly<Record<string, true>> = Object.freeze({
|
|
@@ -238,8 +258,77 @@ function collectSidecarDirs(scopeRoot: string): string[] {
|
|
|
238
258
|
return dirs;
|
|
239
259
|
}
|
|
240
260
|
|
|
261
|
+
async function syncSidecarDir(
|
|
262
|
+
dir: string,
|
|
263
|
+
enabled: boolean,
|
|
264
|
+
childAbstracts: Map<string, string>,
|
|
265
|
+
written: string[],
|
|
266
|
+
removed: string[],
|
|
267
|
+
): Promise<void> {
|
|
268
|
+
const target = sidecarPath(dir);
|
|
269
|
+
const raw = readFileOrNull(target);
|
|
270
|
+
const existing = raw !== null && raw.startsWith(DIRECTORY_SIDECAR_MARKER) ? raw : null;
|
|
271
|
+
if (!enabled) {
|
|
272
|
+
if (existing !== null) {
|
|
273
|
+
unlinkSync(target);
|
|
274
|
+
removed.push(target);
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const next = renderSidecar(dir, directoryFingerprint(dir), childAbstracts);
|
|
279
|
+
if (next === null) {
|
|
280
|
+
if (existing !== null) {
|
|
281
|
+
unlinkSync(target);
|
|
282
|
+
removed.push(target);
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const parsedNext = parseSidecar(next);
|
|
287
|
+
if (parsedNext) childAbstracts.set(dir, parsedNext.abstract);
|
|
288
|
+
if (raw === next) return;
|
|
289
|
+
if (existing === null && raw !== null) return; // user-authored file: hands off
|
|
290
|
+
await writeFileAtomically(target, next);
|
|
291
|
+
written.push(target);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function childAbstractsFromDisk(dir: string): Map<string, string> {
|
|
295
|
+
const abstracts = new Map<string, string>();
|
|
296
|
+
for (const name of directoryChildren(dir)) {
|
|
297
|
+
const childDir = path.join(dir, name);
|
|
298
|
+
const parsed = parseSidecar(readFileOrNull(sidecarPath(childDir)) ?? "");
|
|
299
|
+
if (parsed) abstracts.set(childDir, parsed.abstract);
|
|
300
|
+
}
|
|
301
|
+
return abstracts;
|
|
302
|
+
}
|
|
303
|
+
|
|
241
304
|
/**
|
|
242
|
-
*
|
|
305
|
+
* Category-root ancestry of a changed memory path, deepest first.
|
|
306
|
+
* Paths outside a recall category return `[]` so writes to `state/` or
|
|
307
|
+
* `namespaces/<ns>/` itself never mint a sidecar.
|
|
308
|
+
*/
|
|
309
|
+
export function directorySidecarAncestry(memoryDir: string, changedPath: string): string[] {
|
|
310
|
+
const root = path.resolve(memoryDir);
|
|
311
|
+
const abs = path.isAbsolute(changedPath) ? path.resolve(changedPath) : path.resolve(root, changedPath);
|
|
312
|
+
const rel = path.relative(root, abs);
|
|
313
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) return [];
|
|
314
|
+
const parts = rel.split(path.sep).filter(Boolean);
|
|
315
|
+
const dirParts = isRealDirectory(abs) ? parts : parts.slice(0, -1);
|
|
316
|
+
let categoryAt = -1;
|
|
317
|
+
if (dirParts[0] === "namespaces" && dirParts.length >= 3 && CATEGORY_ROOTS.has(dirParts[2] ?? "")) {
|
|
318
|
+
categoryAt = 2;
|
|
319
|
+
} else if (CATEGORY_ROOTS.has(dirParts[0] ?? "")) {
|
|
320
|
+
categoryAt = 0;
|
|
321
|
+
}
|
|
322
|
+
if (categoryAt === -1) return [];
|
|
323
|
+
const dirs: string[] = [];
|
|
324
|
+
for (let i = dirParts.length; i > categoryAt; i--) {
|
|
325
|
+
dirs.push(path.join(root, ...dirParts.slice(0, i)));
|
|
326
|
+
}
|
|
327
|
+
return dirs;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Regenerates directory sidecars incrementally. Unchanged sidecars are left
|
|
243
332
|
* untouched; emptied directories lose theirs; user files at the sidecar path
|
|
244
333
|
* (no marker) are never written or removed. With `enabled: false` every
|
|
245
334
|
* generated sidecar is removed — the OKF index-maintenance contract.
|
|
@@ -253,35 +342,31 @@ export async function runDirectorySidecarMaintenance(
|
|
|
253
342
|
for (const scope of scopeRoots(memoryDir)) {
|
|
254
343
|
const childAbstracts = new Map<string, string>();
|
|
255
344
|
for (const dir of collectSidecarDirs(scope)) {
|
|
256
|
-
|
|
257
|
-
const raw = readFileOrNull(target);
|
|
258
|
-
const existing = raw !== null && raw.startsWith(DIRECTORY_SIDECAR_MARKER) ? raw : null;
|
|
259
|
-
if (!enabled) {
|
|
260
|
-
if (existing !== null) {
|
|
261
|
-
unlinkSync(target);
|
|
262
|
-
removed.push(target);
|
|
263
|
-
}
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
const next = renderSidecar(dir, directoryFingerprint(dir), childAbstracts);
|
|
267
|
-
if (next === null) {
|
|
268
|
-
if (existing !== null) {
|
|
269
|
-
unlinkSync(target);
|
|
270
|
-
removed.push(target);
|
|
271
|
-
}
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
const parsedNext = parseSidecar(next);
|
|
275
|
-
if (parsedNext) childAbstracts.set(dir, parsedNext.abstract);
|
|
276
|
-
if (raw === next) continue;
|
|
277
|
-
if (existing === null && raw !== null) continue; // user-authored file: hands off
|
|
278
|
-
await writeFileAtomically(target, next);
|
|
279
|
-
written.push(target);
|
|
345
|
+
await syncSidecarDir(dir, enabled, childAbstracts, written, removed);
|
|
280
346
|
}
|
|
281
347
|
}
|
|
282
348
|
return { written, removed };
|
|
283
349
|
}
|
|
284
350
|
|
|
351
|
+
/**
|
|
352
|
+
* Incremental write-path refresh: only the changed directory and its
|
|
353
|
+
* category ancestors, using the same fingerprint/render path as maintenance.
|
|
354
|
+
* `enabled: false` returns immediately — no readdir, no writes.
|
|
355
|
+
*/
|
|
356
|
+
export async function refreshDirectorySidecarsAfterWrite(
|
|
357
|
+
memoryDir: string,
|
|
358
|
+
changedPath: string,
|
|
359
|
+
enabled: boolean,
|
|
360
|
+
): Promise<DirectorySidecarReport> {
|
|
361
|
+
if (!enabled) return { written: [], removed: [] };
|
|
362
|
+
const written: string[] = [];
|
|
363
|
+
const removed: string[] = [];
|
|
364
|
+
for (const dir of directorySidecarAncestry(memoryDir, changedPath)) {
|
|
365
|
+
await syncSidecarDir(dir, true, childAbstractsFromDisk(dir), written, removed);
|
|
366
|
+
}
|
|
367
|
+
return { written, removed };
|
|
368
|
+
}
|
|
369
|
+
|
|
285
370
|
/**
|
|
286
371
|
* Read a directory's sidecar for retrieval. By default a stale sidecar is
|
|
287
372
|
* refreshed in place (child directories first), which is what makes direct
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-end experience recall injection (issue #2979 layer 2).
|
|
3
|
+
*
|
|
4
|
+
* Promoted experience memories compete in the existing procedure-recall
|
|
5
|
+
* slot and share `recallMaxProcedures`. pending_review never injects.
|
|
6
|
+
* Gate off: extraction still makes zero storage calls; recall does not
|
|
7
|
+
* inspect experience attributes (no Experience preview prefix).
|
|
8
|
+
*/
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
|
|
15
|
+
import { parseConfig } from "../config.js";
|
|
16
|
+
import type { BufferTurn, PluginConfig } from "../types.js";
|
|
17
|
+
import type { SealedMemoryEnvelope } from "../write-envelope.js";
|
|
18
|
+
import { StorageManager } from "../storage.js";
|
|
19
|
+
import { buildProcedureRecallSection } from "../procedural/procedure-recall.js";
|
|
20
|
+
import { buildProcedureMarkdownBody } from "../procedural/procedure-types.js";
|
|
21
|
+
import { runSessionExperienceExtraction } from "./session-experience.js";
|
|
22
|
+
import {
|
|
23
|
+
isSessionExperienceMemory,
|
|
24
|
+
renderSessionExperiencePreview,
|
|
25
|
+
scoreSessionExperienceForPrompt,
|
|
26
|
+
} from "./session-experience-recall.js";
|
|
27
|
+
|
|
28
|
+
function turn(role: "user" | "assistant", content: string): BufferTurn {
|
|
29
|
+
return { role, content, timestamp: "2026-01-15T10:00:00.000Z" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function successSession(): BufferTurn[] {
|
|
33
|
+
return [
|
|
34
|
+
turn("user", "Fix the failing integration test in the payments module. The test times out after 30 seconds."),
|
|
35
|
+
turn("assistant", "I will inspect the payments test file and reproduce the timeout locally first."),
|
|
36
|
+
turn("assistant", "The timeout came from an unawaited database call in the test setup. I added the missing await."),
|
|
37
|
+
turn("assistant", "Verified: the payments integration test passes now and the full suite is green."),
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const TASK_PROMPT = "Let's fix the failing integration test in the payments module";
|
|
42
|
+
|
|
43
|
+
function recallConfig(memoryDir: string, experienceEnabled: boolean, recallMaxProcedures = 2): PluginConfig {
|
|
44
|
+
return parseConfig({
|
|
45
|
+
memoryDir,
|
|
46
|
+
workspaceDir: path.join(memoryDir, "ws"),
|
|
47
|
+
openaiApiKey: "test-key",
|
|
48
|
+
procedural: { enabled: true, recallMaxProcedures },
|
|
49
|
+
sessionExperience: { enabled: experienceEnabled },
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function recordingStorage() {
|
|
54
|
+
const calls: { readAllMemories: number; writeSealedMemory: SealedMemoryEnvelope[] } = {
|
|
55
|
+
readAllMemories: 0,
|
|
56
|
+
writeSealedMemory: [],
|
|
57
|
+
};
|
|
58
|
+
const storage = {
|
|
59
|
+
async readAllMemories() {
|
|
60
|
+
calls.readAllMemories += 1;
|
|
61
|
+
return [];
|
|
62
|
+
},
|
|
63
|
+
async writeSealedMemory(envelope: SealedMemoryEnvelope) {
|
|
64
|
+
calls.writeSealedMemory.push(envelope);
|
|
65
|
+
return { id: "should-not-write" };
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
return { storage, calls };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
test("gate off: session-end extraction still performs zero storage calls after recall wiring", async () => {
|
|
72
|
+
const memoryDir = await mkdtemp(path.join(tmpdir(), "remnic-sx-recall-off-"));
|
|
73
|
+
try {
|
|
74
|
+
const config = parseConfig({ memoryDir });
|
|
75
|
+
assert.equal(config.sessionExperience.enabled, false);
|
|
76
|
+
const { storage, calls } = recordingStorage();
|
|
77
|
+
const result = await runSessionExperienceExtraction({
|
|
78
|
+
turns: successSession(),
|
|
79
|
+
sessionKey: "session-recall-off",
|
|
80
|
+
config,
|
|
81
|
+
storage,
|
|
82
|
+
});
|
|
83
|
+
assert.deepEqual(result, { written: false, skippedReason: "session_experience_disabled" });
|
|
84
|
+
assert.equal(calls.readAllMemories, 0);
|
|
85
|
+
assert.equal(calls.writeSealedMemory.length, 0);
|
|
86
|
+
} finally {
|
|
87
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("pending_review experience is not injected into procedure-recall", async () => {
|
|
92
|
+
const memoryDir = await mkdtemp(path.join(tmpdir(), "remnic-sx-recall-pending-"));
|
|
93
|
+
try {
|
|
94
|
+
const storage = new StorageManager(memoryDir);
|
|
95
|
+
await storage.ensureDirectories();
|
|
96
|
+
const config = recallConfig(memoryDir, true);
|
|
97
|
+
const written = await runSessionExperienceExtraction({
|
|
98
|
+
turns: successSession(),
|
|
99
|
+
sessionKey: "session-recall-pending",
|
|
100
|
+
config,
|
|
101
|
+
storage,
|
|
102
|
+
});
|
|
103
|
+
assert.equal(written.written, true);
|
|
104
|
+
const memories = await storage.readAllMemories();
|
|
105
|
+
assert.equal(memories[0]?.frontmatter.status, "pending_review");
|
|
106
|
+
const section = await buildProcedureRecallSection(storage, TASK_PROMPT, config);
|
|
107
|
+
assert.equal(section, null, "pending_review must not occupy a procedure-recall slot");
|
|
108
|
+
} finally {
|
|
109
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("promoted experience injects through procedure-recall with an Experience preview", async () => {
|
|
114
|
+
const memoryDir = await mkdtemp(path.join(tmpdir(), "remnic-sx-recall-on-"));
|
|
115
|
+
try {
|
|
116
|
+
const storage = new StorageManager(memoryDir);
|
|
117
|
+
await storage.ensureDirectories();
|
|
118
|
+
const config = recallConfig(memoryDir, true);
|
|
119
|
+
const written = await runSessionExperienceExtraction({
|
|
120
|
+
turns: successSession(),
|
|
121
|
+
sessionKey: "session-recall-on",
|
|
122
|
+
config,
|
|
123
|
+
storage,
|
|
124
|
+
});
|
|
125
|
+
assert.equal(written.written, true);
|
|
126
|
+
assert.ok(written.memoryId);
|
|
127
|
+
const promoted = await storage.updateMemoryFrontmatter(written.memoryId, { status: "active" });
|
|
128
|
+
assert.equal(promoted, true);
|
|
129
|
+
const section = await buildProcedureRecallSection(storage, TASK_PROMPT, config);
|
|
130
|
+
assert.ok(section);
|
|
131
|
+
assert.match(section, /## Relevant procedures/);
|
|
132
|
+
assert.match(section, new RegExp(written.memoryId));
|
|
133
|
+
assert.match(section, /Experience\. Situation:/);
|
|
134
|
+
assert.match(section, /payments/i);
|
|
135
|
+
} finally {
|
|
136
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("experience and procedure share recallMaxProcedures; situation match wins cap 1", async () => {
|
|
141
|
+
const memoryDir = await mkdtemp(path.join(tmpdir(), "remnic-sx-recall-budget-"));
|
|
142
|
+
try {
|
|
143
|
+
const storage = new StorageManager(memoryDir);
|
|
144
|
+
await storage.ensureDirectories();
|
|
145
|
+
const config = recallConfig(memoryDir, true, 1);
|
|
146
|
+
const written = await runSessionExperienceExtraction({
|
|
147
|
+
turns: successSession(),
|
|
148
|
+
sessionKey: "session-recall-budget",
|
|
149
|
+
config,
|
|
150
|
+
storage,
|
|
151
|
+
});
|
|
152
|
+
assert.equal(written.written, true);
|
|
153
|
+
await storage.updateMemoryFrontmatter(written.memoryId, { status: "active" });
|
|
154
|
+
const body = buildProcedureMarkdownBody([
|
|
155
|
+
{ order: 1, intent: "Run deploy checks for production gateway" },
|
|
156
|
+
{ order: 2, intent: "Push the release tag" },
|
|
157
|
+
]);
|
|
158
|
+
const other = await storage.writeMemory(
|
|
159
|
+
"procedure",
|
|
160
|
+
`When you deploy the gateway\n\n${body}`,
|
|
161
|
+
{ source: "test", tags: ["deploy", "gateway"] },
|
|
162
|
+
);
|
|
163
|
+
const section = await buildProcedureRecallSection(storage, TASK_PROMPT, config);
|
|
164
|
+
assert.ok(section);
|
|
165
|
+
assert.match(section, new RegExp(written.memoryId));
|
|
166
|
+
assert.doesNotMatch(section, new RegExp(other.id));
|
|
167
|
+
} finally {
|
|
168
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("gate off: promoted experience may inject as a procedure but without Experience preview", async () => {
|
|
173
|
+
const memoryDir = await mkdtemp(path.join(tmpdir(), "remnic-sx-recall-generic-"));
|
|
174
|
+
try {
|
|
175
|
+
const storage = new StorageManager(memoryDir);
|
|
176
|
+
await storage.ensureDirectories();
|
|
177
|
+
const enabled = recallConfig(memoryDir, true);
|
|
178
|
+
const written = await runSessionExperienceExtraction({
|
|
179
|
+
turns: successSession(),
|
|
180
|
+
sessionKey: "session-recall-generic",
|
|
181
|
+
config: enabled,
|
|
182
|
+
storage,
|
|
183
|
+
});
|
|
184
|
+
assert.equal(written.written, true);
|
|
185
|
+
await storage.updateMemoryFrontmatter(written.memoryId, { status: "active" });
|
|
186
|
+
const off = recallConfig(memoryDir, false);
|
|
187
|
+
const section = await buildProcedureRecallSection(storage, TASK_PROMPT, off);
|
|
188
|
+
if (section !== null) {
|
|
189
|
+
assert.match(section, new RegExp(written.memoryId));
|
|
190
|
+
assert.doesNotMatch(section, /Experience\. Situation:/);
|
|
191
|
+
}
|
|
192
|
+
} finally {
|
|
193
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("helpers: non-experience memories score/render as null; episodes label Situation", async () => {
|
|
198
|
+
const memoryDir = await mkdtemp(path.join(tmpdir(), "remnic-sx-recall-helpers-"));
|
|
199
|
+
try {
|
|
200
|
+
const storage = new StorageManager(memoryDir);
|
|
201
|
+
await storage.ensureDirectories();
|
|
202
|
+
const other = await storage.writeMemory("procedure", "When you deploy the gateway, run the smoke suite.", {
|
|
203
|
+
source: "test",
|
|
204
|
+
});
|
|
205
|
+
const otherFile = (await storage.readAllMemories()).find((m) => m.frontmatter.id === other.id);
|
|
206
|
+
assert.ok(otherFile);
|
|
207
|
+
assert.equal(isSessionExperienceMemory(otherFile), false);
|
|
208
|
+
assert.equal(scoreSessionExperienceForPrompt(otherFile, TASK_PROMPT), null);
|
|
209
|
+
assert.equal(renderSessionExperiencePreview(otherFile), null);
|
|
210
|
+
|
|
211
|
+
const config = recallConfig(memoryDir, true);
|
|
212
|
+
const written = await runSessionExperienceExtraction({
|
|
213
|
+
turns: successSession(),
|
|
214
|
+
sessionKey: "session-recall-helpers",
|
|
215
|
+
config,
|
|
216
|
+
storage,
|
|
217
|
+
});
|
|
218
|
+
assert.equal(written.written, true);
|
|
219
|
+
const episode = (await storage.readAllMemories()).find((m) => m.frontmatter.id === written.memoryId);
|
|
220
|
+
assert.ok(episode);
|
|
221
|
+
assert.equal(isSessionExperienceMemory(episode), true);
|
|
222
|
+
const score = scoreSessionExperienceForPrompt(episode, TASK_PROMPT);
|
|
223
|
+
assert.ok(score !== null && score > 0.04);
|
|
224
|
+
assert.match(renderSessionExperiencePreview(episode) ?? "", /Experience\. Situation:.*payments/i);
|
|
225
|
+
} finally {
|
|
226
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
227
|
+
}
|
|
228
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-experience recall helpers (issue #2979 layer 2).
|
|
3
|
+
*
|
|
4
|
+
* Promoted experience memories are `category: procedure`, so they already
|
|
5
|
+
* compete in `buildProcedureRecallSection` and share `recallMaxProcedures`.
|
|
6
|
+
* These helpers add situation-overlap scoring and an Experience preview.
|
|
7
|
+
* Call them only when `sessionExperience.enabled` is true — the gate-off
|
|
8
|
+
* path must not inspect experience attributes.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { MemoryFile } from "../types.js";
|
|
12
|
+
|
|
13
|
+
function tokenOverlapScore(prompt: string, memoryText: string): number {
|
|
14
|
+
const norm = (s: string) =>
|
|
15
|
+
s
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.split(/[^a-z0-9]+/)
|
|
18
|
+
.filter((t) => t.length > 2);
|
|
19
|
+
const promptTokens = new Set(norm(prompt));
|
|
20
|
+
const memTokens = new Set(norm(memoryText));
|
|
21
|
+
if (promptTokens.size === 0 || memTokens.size === 0) return 0;
|
|
22
|
+
let inter = 0;
|
|
23
|
+
for (const t of promptTokens) {
|
|
24
|
+
if (memTokens.has(t)) inter++;
|
|
25
|
+
}
|
|
26
|
+
const union = new Set([...promptTokens, ...memTokens]);
|
|
27
|
+
return inter / Math.max(1, union.size);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isSessionExperienceMemory(memory: MemoryFile): boolean {
|
|
31
|
+
const hash = memory.frontmatter.structuredAttributes?.experience_session_hash;
|
|
32
|
+
return typeof hash === "string" && hash.length > 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Situation-overlap score in `[0, 1]`, or `null` when the memory is not a
|
|
37
|
+
* session-experience episode. Caller keeps the generic procedure score.
|
|
38
|
+
*/
|
|
39
|
+
export function scoreSessionExperienceForPrompt(memory: MemoryFile, prompt: string): number | null {
|
|
40
|
+
if (!isSessionExperienceMemory(memory)) return null;
|
|
41
|
+
const situation = memory.frontmatter.structuredAttributes?.experience_situation ?? "";
|
|
42
|
+
const situationScore = tokenOverlapScore(prompt, situation);
|
|
43
|
+
const bodyScore = tokenOverlapScore(prompt, memory.content);
|
|
44
|
+
return Math.min(1, situationScore * 0.75 + bodyScore * 0.25);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Flattened Experience preview, or `null` when the memory is not an episode. */
|
|
48
|
+
export function renderSessionExperiencePreview(memory: MemoryFile): string | null {
|
|
49
|
+
if (!isSessionExperienceMemory(memory)) return null;
|
|
50
|
+
const attrs = memory.frontmatter.structuredAttributes ?? {};
|
|
51
|
+
const situation = (attrs.experience_situation ?? "").trim();
|
|
52
|
+
const approach = (attrs.experience_approach ?? "").trim();
|
|
53
|
+
const reflection = (attrs.experience_reflection ?? "").trim();
|
|
54
|
+
if (situation.length === 0 && approach.length === 0 && reflection.length === 0) {
|
|
55
|
+
return `Experience. ${memory.content.replace(/\s+/g, " ").trim()}`;
|
|
56
|
+
}
|
|
57
|
+
return `Experience. Situation: ${situation} Approach: ${approach} Reflection: ${reflection}`
|
|
58
|
+
.replace(/\s+/g, " ")
|
|
59
|
+
.trim();
|
|
60
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -469,6 +469,7 @@ export {
|
|
|
469
469
|
export type { ResolveSecretRefFn } from "./resolve-auth-token.js";
|
|
470
470
|
export type { SecretRef, AgentAccessAuthToken } from "./types.js";
|
|
471
471
|
export * from "./recall-context-composition.js";
|
|
472
|
+
export * from "./recall-composition-decision.js";
|
|
472
473
|
// Recall X-ray CLI helpers (issue #570). Exported so the standalone
|
|
473
474
|
// `@remnic/cli` binary can wire the `remnic xray` command without
|
|
474
475
|
// reimporting core-internal modules by relative path (CLAUDE.md rule 26).
|
|
@@ -22,7 +22,12 @@ import type { RecallResultFormatter } from "./recall-result-formatter.js";
|
|
|
22
22
|
import type { IdentityInjectionMode, PluginConfig, QmdSearchResult } from "../types.js";
|
|
23
23
|
import { stateViewPacketActive } from "../recall-state-view-anchors.js";
|
|
24
24
|
import { resultStateViewKey, stateViewPacketKeys } from "../recall-state-view.js";
|
|
25
|
-
|
|
25
|
+
import { applyRecallStateViews } from "../recall-state-view-wire.js";
|
|
26
|
+
import { composeRecallContext } from "../recall-context-composition.js";
|
|
27
|
+
import {
|
|
28
|
+
notifyContextComposition,
|
|
29
|
+
recallFailureComposition,
|
|
30
|
+
} from "../recall-composition-decision.js";
|
|
26
31
|
|
|
27
32
|
export interface RecallEntryDeps {
|
|
28
33
|
appendRecallSection(
|
|
@@ -197,7 +202,16 @@ export class RecallEntryCoordinator {
|
|
|
197
202
|
// endTrace() is safe here: if no trace is active (disabled or already
|
|
198
203
|
// closed by recallInternal's try/finally), it returns null immediately.
|
|
199
204
|
this.deps.profiler.endTrace();
|
|
200
|
-
|
|
205
|
+
// Caller-cancelled recalls stay quiet: the failure was the caller's
|
|
206
|
+
// abort, not a recall degradation (issue #2972 contract).
|
|
207
|
+
const missing = options.abortSignal?.aborted
|
|
208
|
+
? null
|
|
209
|
+
: recallFailureComposition(err);
|
|
210
|
+
if (!missing) return "";
|
|
211
|
+
notifyContextComposition(options.onContextComposition, missing, (observerErr) => {
|
|
212
|
+
log.warn("recall: context composition observer failed open", observerErr);
|
|
213
|
+
});
|
|
214
|
+
return composeRecallContext(missing);
|
|
201
215
|
} finally {
|
|
202
216
|
options.abortSignal?.removeEventListener("abort", onAbort);
|
|
203
217
|
}
|
|
@@ -45,9 +45,14 @@ import { buildQmdRecallCacheKey, getCachedQmdRecall, setCachedQmdRecall } from "
|
|
|
45
45
|
import { MEMORY_ID_PATTERN } from "../recall-handles.js";
|
|
46
46
|
import { shouldRecordRecallAuthorityHistory } from "../recall-navigation-config.js";
|
|
47
47
|
import {
|
|
48
|
-
|
|
48
|
+
contextBudgetForFooter,
|
|
49
49
|
formatCuriosityFooter, renderAuthorityBoundContent, selectCuriosityQuestion,
|
|
50
50
|
} from "../recall-context-composition.js";
|
|
51
|
+
import {
|
|
52
|
+
compactRecallContextFromBuckets,
|
|
53
|
+
decideRecallContextComposition,
|
|
54
|
+
notifyContextComposition,
|
|
55
|
+
} from "../recall-composition-decision.js";
|
|
51
56
|
import {
|
|
52
57
|
createBoundedCoreSectionRunner,
|
|
53
58
|
createRecallSectionMetricRecorder,
|
|
@@ -110,6 +115,7 @@ import {
|
|
|
110
115
|
import type { RecallInternalDeps } from "./recall-internal-deps.js";
|
|
111
116
|
import { appendEpisodicContextSection } from "./episodic-context-section.js";
|
|
112
117
|
import { resolveCompositeProfileStorage } from "./recall-profile-storage.js";
|
|
118
|
+
import { fetchQmdMemoryResultsWithRecognitionSwap } from "./recall-recognition-search.js";
|
|
113
119
|
|
|
114
120
|
export class RecallInternalCoordinator {
|
|
115
121
|
private readonly securityCapabilities: SecurityCapabilitySet;
|
|
@@ -2141,7 +2147,8 @@ export class RecallInternalCoordinator {
|
|
|
2141
2147
|
|
|
2142
2148
|
try {
|
|
2143
2149
|
const filteredResults =
|
|
2144
|
-
await
|
|
2150
|
+
await fetchQmdMemoryResultsWithRecognitionSwap(
|
|
2151
|
+
this.deps,
|
|
2145
2152
|
retrievalQuery,
|
|
2146
2153
|
qmdFetchLimit,
|
|
2147
2154
|
qmdHybridFetchLimit,
|
|
@@ -4951,23 +4958,16 @@ export class RecallInternalCoordinator {
|
|
|
4951
4958
|
recalledMemoryNamespaces,
|
|
4952
4959
|
);
|
|
4953
4960
|
const recalledContext = assembledRecall.sections.join("\n\n---\n\n");
|
|
4954
|
-
const composition =
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
"recall: context composition observer rejected", err,
|
|
4963
|
-
));
|
|
4964
|
-
}
|
|
4965
|
-
} catch (err) {
|
|
4961
|
+
const { composition, context, truncated: compositionTruncated } =
|
|
4962
|
+
decideRecallContextComposition({
|
|
4963
|
+
context: recalledContext,
|
|
4964
|
+
compactContext: compactRecallContextFromBuckets(sectionBuckets),
|
|
4965
|
+
footer: curiosityFooter,
|
|
4966
|
+
maxChars: recallBudgetChars,
|
|
4967
|
+
});
|
|
4968
|
+
notifyContextComposition(options.onContextComposition, composition, (err) => {
|
|
4966
4969
|
log.warn("recall: context composition observer failed open", err);
|
|
4967
|
-
}
|
|
4968
|
-
const compositionTruncated = context.length < composeRecallContext({
|
|
4969
|
-
context: recalledContext, footer: curiosityFooter,
|
|
4970
|
-
}).length;
|
|
4970
|
+
});
|
|
4971
4971
|
const includedSections = [...assembledRecall.includedIds];
|
|
4972
4972
|
const omittedSections = [...assembledRecall.omittedIds];
|
|
4973
4973
|
if (composition.footer && !includedSections.includes("questions")) includedSections.push("questions");
|
|
@@ -5202,6 +5202,9 @@ export class RecallInternalCoordinator {
|
|
|
5202
5202
|
chars: this.deps.getRecallBudgetChars(options.budgetCharsOverride),
|
|
5203
5203
|
used: context.length,
|
|
5204
5204
|
},
|
|
5205
|
+
...(composition.degradation
|
|
5206
|
+
? { degradation: composition.degradation }
|
|
5207
|
+
: {}),
|
|
5205
5208
|
sessionKey,
|
|
5206
5209
|
namespace: selfNamespace,
|
|
5207
5210
|
traceId,
|