@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
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recall-recognition-search.test.ts — issue #2975 recall-slice wiring.
|
|
3
|
+
*
|
|
4
|
+
* Pins the fetchQmdMemoryResultsWithArtifactTopUp seam:
|
|
5
|
+
* - gate off: original fetch only (zero extra I/O, same options object);
|
|
6
|
+
* - under-threshold index: recognition pass, no vector fetch for that ns;
|
|
7
|
+
* - above-threshold index: vector fetch, no recognizer call.
|
|
8
|
+
*/
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
|
|
15
|
+
import { saveRecognitionIndex, type RecognitionIndex } from "../recall-recognition-tier.js";
|
|
16
|
+
import {
|
|
17
|
+
fetchQmdMemoryResultsWithRecognitionSwap,
|
|
18
|
+
type RecognitionSearchDeps,
|
|
19
|
+
} from "./recall-recognition-search.js";
|
|
20
|
+
import type { QmdSearchResult } from "../types.js";
|
|
21
|
+
|
|
22
|
+
function vec(namespace: string, filePath: string, score = 1): QmdSearchResult {
|
|
23
|
+
return { docid: `${namespace}:${filePath}`, namespace, path: filePath, score, snippet: filePath };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function indexOf(ids: string[]): RecognitionIndex {
|
|
27
|
+
return {
|
|
28
|
+
version: 1,
|
|
29
|
+
entries: ids.map((id) => ({ id, description: `trigger for ${id}` })),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function tmpDir(): Promise<string> {
|
|
34
|
+
return mkdtemp(path.join(os.tmpdir(), "remnic-recognition-search-"));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function makeDeps(args: {
|
|
38
|
+
recallRecognitionTier: boolean;
|
|
39
|
+
recognitionIndexMaxEntries?: number;
|
|
40
|
+
storages: Record<string, { dir: string; pathsById: Record<string, string> }>;
|
|
41
|
+
vectorResults?: QmdSearchResult[];
|
|
42
|
+
recognize?: (prompt: string) => Promise<string>;
|
|
43
|
+
recognizeError?: Error;
|
|
44
|
+
}) {
|
|
45
|
+
const fetchCalls: Array<{
|
|
46
|
+
prompt: string;
|
|
47
|
+
qmdFetchLimit: number;
|
|
48
|
+
qmdHybridFetchLimit: number;
|
|
49
|
+
options: unknown;
|
|
50
|
+
}> = [];
|
|
51
|
+
const recognizePrompts: string[] = [];
|
|
52
|
+
const storageForCalls: string[] = [];
|
|
53
|
+
|
|
54
|
+
const deps = {
|
|
55
|
+
config: {
|
|
56
|
+
recallRecognitionTier: args.recallRecognitionTier,
|
|
57
|
+
recognitionIndexMaxEntries: args.recognitionIndexMaxEntries ?? 500,
|
|
58
|
+
},
|
|
59
|
+
storageRouter: {
|
|
60
|
+
storageFor: async (namespace: string) => {
|
|
61
|
+
storageForCalls.push(namespace);
|
|
62
|
+
const storage = args.storages[namespace];
|
|
63
|
+
if (!storage) throw new Error(`unknown namespace ${namespace}`);
|
|
64
|
+
return {
|
|
65
|
+
dir: storage.dir,
|
|
66
|
+
findExistingMemoryPaths: async (ids: string[]) => {
|
|
67
|
+
const found = new Map<string, string[]>();
|
|
68
|
+
for (const id of ids) {
|
|
69
|
+
const filePath = storage.pathsById[id];
|
|
70
|
+
if (filePath) found.set(id, [filePath]);
|
|
71
|
+
}
|
|
72
|
+
return found;
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
fastLlmForRerank: {
|
|
78
|
+
chatCompletion: async (messages: Array<{ role: string; content: string }>) => {
|
|
79
|
+
if (args.recognizeError) throw args.recognizeError;
|
|
80
|
+
const prompt = messages[0]?.content ?? "";
|
|
81
|
+
recognizePrompts.push(prompt);
|
|
82
|
+
return { content: await (args.recognize ?? (async () => ""))(prompt) };
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
fetchQmdMemoryResultsWithArtifactTopUp: async (
|
|
86
|
+
prompt: string,
|
|
87
|
+
qmdFetchLimit: number,
|
|
88
|
+
qmdHybridFetchLimit: number,
|
|
89
|
+
options: unknown,
|
|
90
|
+
) => {
|
|
91
|
+
fetchCalls.push({ prompt, qmdFetchLimit, qmdHybridFetchLimit, options });
|
|
92
|
+
return args.vectorResults ?? [];
|
|
93
|
+
},
|
|
94
|
+
} as unknown as RecognitionSearchDeps;
|
|
95
|
+
|
|
96
|
+
return { deps, fetchCalls, recognizePrompts, storageForCalls };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const baseOptions = {
|
|
100
|
+
namespacesEnabled: true,
|
|
101
|
+
recallNamespaces: ["alpha"],
|
|
102
|
+
resolveNamespace: (filePath: string) => (filePath.startsWith("beta/") ? "beta" : "alpha"),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
test("off-path: gate off calls original fetch once with the same options object", async () => {
|
|
106
|
+
const options = { ...baseOptions };
|
|
107
|
+
const { deps, fetchCalls, recognizePrompts, storageForCalls } = makeDeps({
|
|
108
|
+
recallRecognitionTier: false,
|
|
109
|
+
storages: {},
|
|
110
|
+
vectorResults: [vec("alpha", "facts/from-vector.md")],
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const out = await fetchQmdMemoryResultsWithRecognitionSwap(deps, "q", 7, 9, options);
|
|
114
|
+
|
|
115
|
+
assert.equal(fetchCalls.length, 1);
|
|
116
|
+
assert.equal(fetchCalls[0].prompt, "q");
|
|
117
|
+
assert.equal(fetchCalls[0].qmdFetchLimit, 7);
|
|
118
|
+
assert.equal(fetchCalls[0].qmdHybridFetchLimit, 9);
|
|
119
|
+
assert.equal(fetchCalls[0].options, options);
|
|
120
|
+
assert.equal(recognizePrompts.length, 0);
|
|
121
|
+
assert.equal(storageForCalls.length, 0);
|
|
122
|
+
assert.deepEqual(out.map((r) => r.path), ["facts/from-vector.md"]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("acceptance: under-threshold namespace uses recognition, not vector search", async () => {
|
|
126
|
+
const dir = await tmpDir();
|
|
127
|
+
try {
|
|
128
|
+
await saveRecognitionIndex(dir, indexOf(["m-001", "m-002", "m-003"]));
|
|
129
|
+
const { deps, fetchCalls, recognizePrompts } = makeDeps({
|
|
130
|
+
recallRecognitionTier: true,
|
|
131
|
+
recognitionIndexMaxEntries: 10,
|
|
132
|
+
storages: {
|
|
133
|
+
alpha: {
|
|
134
|
+
dir,
|
|
135
|
+
pathsById: {
|
|
136
|
+
"m-001": path.join(dir, "facts/m-001.md"),
|
|
137
|
+
"m-002": path.join(dir, "facts/m-002.md"),
|
|
138
|
+
"m-003": path.join(dir, "facts/m-003.md"),
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
recognize: async () => "m-002, m-003",
|
|
143
|
+
vectorResults: [vec("alpha", "facts/must-not-appear.md")],
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const out = await fetchQmdMemoryResultsWithRecognitionSwap(deps, "which memories?", 5, 5, {
|
|
147
|
+
...baseOptions,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
assert.equal(fetchCalls.length, 0, "vector search must not run on the recognition tier");
|
|
151
|
+
assert.equal(recognizePrompts.length, 1);
|
|
152
|
+
assert.ok(recognizePrompts[0].includes("m-001: trigger for m-001"));
|
|
153
|
+
assert.ok(recognizePrompts[0].includes("m-002: trigger for m-002"));
|
|
154
|
+
assert.ok(recognizePrompts[0].includes("m-003: trigger for m-003"));
|
|
155
|
+
assert.deepEqual(
|
|
156
|
+
out.map((r) => r.path),
|
|
157
|
+
["facts/m-002.md", "facts/m-003.md"],
|
|
158
|
+
);
|
|
159
|
+
assert.deepEqual(
|
|
160
|
+
out.map((r) => r.namespace),
|
|
161
|
+
["alpha", "alpha"],
|
|
162
|
+
);
|
|
163
|
+
} finally {
|
|
164
|
+
await rm(dir, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("acceptance: above-threshold namespace uses vector search, not the recognizer", async () => {
|
|
169
|
+
const dir = await tmpDir();
|
|
170
|
+
try {
|
|
171
|
+
await saveRecognitionIndex(dir, indexOf(["a", "b", "c", "d", "e"]));
|
|
172
|
+
const options = { ...baseOptions };
|
|
173
|
+
const { deps, fetchCalls, recognizePrompts } = makeDeps({
|
|
174
|
+
recallRecognitionTier: true,
|
|
175
|
+
recognitionIndexMaxEntries: 4,
|
|
176
|
+
storages: { alpha: { dir, pathsById: {} } },
|
|
177
|
+
vectorResults: [vec("alpha", "facts/vector-hit.md")],
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const out = await fetchQmdMemoryResultsWithRecognitionSwap(deps, "q", 3, 3, options);
|
|
181
|
+
|
|
182
|
+
assert.equal(recognizePrompts.length, 0);
|
|
183
|
+
assert.equal(fetchCalls.length, 1);
|
|
184
|
+
assert.equal(fetchCalls[0].options, options);
|
|
185
|
+
assert.deepEqual(out.map((r) => r.path), ["facts/vector-hit.md"]);
|
|
186
|
+
} finally {
|
|
187
|
+
await rm(dir, { recursive: true, force: true });
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("acceptance: missing index falls back to vector search with original options", async () => {
|
|
192
|
+
const dir = await tmpDir();
|
|
193
|
+
try {
|
|
194
|
+
const options = { ...baseOptions };
|
|
195
|
+
const { deps, fetchCalls, recognizePrompts } = makeDeps({
|
|
196
|
+
recallRecognitionTier: true,
|
|
197
|
+
storages: { alpha: { dir, pathsById: {} } },
|
|
198
|
+
vectorResults: [vec("alpha", "facts/fallback.md")],
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const out = await fetchQmdMemoryResultsWithRecognitionSwap(deps, "q", 2, 2, options);
|
|
202
|
+
|
|
203
|
+
assert.equal(recognizePrompts.length, 0);
|
|
204
|
+
assert.equal(fetchCalls.length, 1);
|
|
205
|
+
assert.equal(fetchCalls[0].options, options);
|
|
206
|
+
assert.deepEqual(out.map((r) => r.path), ["facts/fallback.md"]);
|
|
207
|
+
} finally {
|
|
208
|
+
await rm(dir, { recursive: true, force: true });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("mixed namespaces: small ns recognizes; large ns stays on vector search", async () => {
|
|
213
|
+
const small = await tmpDir();
|
|
214
|
+
const large = await tmpDir();
|
|
215
|
+
try {
|
|
216
|
+
await saveRecognitionIndex(small, indexOf(["s-1", "s-2"]));
|
|
217
|
+
await saveRecognitionIndex(large, indexOf(["l-1", "l-2", "l-3"]));
|
|
218
|
+
const { deps, fetchCalls, recognizePrompts } = makeDeps({
|
|
219
|
+
recallRecognitionTier: true,
|
|
220
|
+
recognitionIndexMaxEntries: 2,
|
|
221
|
+
storages: {
|
|
222
|
+
alpha: {
|
|
223
|
+
dir: small,
|
|
224
|
+
pathsById: { "s-1": path.join(small, "facts/s-1.md") },
|
|
225
|
+
},
|
|
226
|
+
beta: { dir: large, pathsById: {} },
|
|
227
|
+
},
|
|
228
|
+
recognize: async () => "s-1",
|
|
229
|
+
vectorResults: [vec("beta", "facts/large-hit.md")],
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const out = await fetchQmdMemoryResultsWithRecognitionSwap(deps, "q", 10, 10, {
|
|
233
|
+
namespacesEnabled: true,
|
|
234
|
+
recallNamespaces: ["alpha", "beta"],
|
|
235
|
+
resolveNamespace: (filePath) => (filePath.includes("large") ? "beta" : "alpha"),
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
assert.equal(recognizePrompts.length, 1);
|
|
239
|
+
assert.equal(fetchCalls.length, 1);
|
|
240
|
+
assert.deepEqual(
|
|
241
|
+
(fetchCalls[0].options as { recallNamespaces: string[] }).recallNamespaces,
|
|
242
|
+
["beta"],
|
|
243
|
+
);
|
|
244
|
+
assert.deepEqual(
|
|
245
|
+
out.map((r) => `${r.namespace}:${r.path}`),
|
|
246
|
+
["alpha:facts/s-1.md", "beta:facts/large-hit.md"],
|
|
247
|
+
);
|
|
248
|
+
} finally {
|
|
249
|
+
await rm(small, { recursive: true, force: true });
|
|
250
|
+
await rm(large, { recursive: true, force: true });
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("loud degradation: recognizer throw routes that namespace back to vector search", async () => {
|
|
255
|
+
const dir = await tmpDir();
|
|
256
|
+
try {
|
|
257
|
+
await saveRecognitionIndex(dir, indexOf(["m-001"]));
|
|
258
|
+
const { deps, fetchCalls, recognizePrompts } = makeDeps({
|
|
259
|
+
recallRecognitionTier: true,
|
|
260
|
+
storages: { alpha: { dir, pathsById: { "m-001": path.join(dir, "facts/m-001.md") } } },
|
|
261
|
+
recognizeError: new Error("model down"),
|
|
262
|
+
vectorResults: [vec("alpha", "facts/degraded.md")],
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const out = await fetchQmdMemoryResultsWithRecognitionSwap(deps, "q", 4, 4, { ...baseOptions });
|
|
266
|
+
|
|
267
|
+
assert.equal(recognizePrompts.length, 0);
|
|
268
|
+
assert.equal(fetchCalls.length, 1);
|
|
269
|
+
assert.deepEqual(out.map((r) => r.path), ["facts/degraded.md"]);
|
|
270
|
+
} finally {
|
|
271
|
+
await rm(dir, { recursive: true, force: true });
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("namespaces disabled: gate on still calls original fetch with the same options object", async () => {
|
|
276
|
+
const options = { ...baseOptions, namespacesEnabled: false };
|
|
277
|
+
const { deps, fetchCalls, recognizePrompts, storageForCalls } = makeDeps({
|
|
278
|
+
recallRecognitionTier: true,
|
|
279
|
+
storages: {},
|
|
280
|
+
vectorResults: [vec("alpha", "facts/from-vector.md")],
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const out = await fetchQmdMemoryResultsWithRecognitionSwap(deps, "q", 2, 2, options);
|
|
284
|
+
|
|
285
|
+
assert.equal(fetchCalls.length, 1);
|
|
286
|
+
assert.equal(fetchCalls[0].options, options);
|
|
287
|
+
assert.equal(recognizePrompts.length, 0);
|
|
288
|
+
assert.equal(storageForCalls.length, 0);
|
|
289
|
+
assert.deepEqual(out.map((r) => r.path), ["facts/from-vector.md"]);
|
|
290
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recognition-tier swap at fetchQmdMemoryResultsWithArtifactTopUp (issue #2975).
|
|
3
|
+
*
|
|
4
|
+
* Off-path (recallRecognitionTier !== true): original fetch only — no index
|
|
5
|
+
* load, no model call. On-path: namespaces whose index is at or under
|
|
6
|
+
* recognitionIndexMaxEntries go through one recognizer call; the rest keep
|
|
7
|
+
* vector search. A failing recognizer degrades that namespace to vector search.
|
|
8
|
+
*/
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
|
|
11
|
+
import { log } from "../logger.js";
|
|
12
|
+
import { dedupeResultsByNamespace } from "../recall-memory-map.js";
|
|
13
|
+
import {
|
|
14
|
+
decideRecognitionTier,
|
|
15
|
+
loadRecognitionIndex,
|
|
16
|
+
runRecognitionTier,
|
|
17
|
+
type RecognitionIndex,
|
|
18
|
+
} from "../recall-recognition-tier.js";
|
|
19
|
+
import type { QmdSearchResult } from "../types.js";
|
|
20
|
+
import { throwIfRecallAborted } from "./orchestrator-helpers.js";
|
|
21
|
+
import type { RecallInternalDeps } from "./recall-internal-deps.js";
|
|
22
|
+
|
|
23
|
+
export type RecognitionSearchDeps = Pick<
|
|
24
|
+
RecallInternalDeps,
|
|
25
|
+
"config" | "storageRouter" | "fastLlmForRerank" | "fetchQmdMemoryResultsWithArtifactTopUp"
|
|
26
|
+
>;
|
|
27
|
+
|
|
28
|
+
type ArtifactTopUpOptions = Parameters<
|
|
29
|
+
RecognitionSearchDeps["fetchQmdMemoryResultsWithArtifactTopUp"]
|
|
30
|
+
>[3];
|
|
31
|
+
|
|
32
|
+
type RecognitionStorage = {
|
|
33
|
+
dir: string;
|
|
34
|
+
findExistingMemoryPaths(ids: string[]): Promise<Map<string, string[]>>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export async function fetchQmdMemoryResultsWithRecognitionSwap(
|
|
38
|
+
deps: RecognitionSearchDeps,
|
|
39
|
+
prompt: string,
|
|
40
|
+
qmdFetchLimit: number,
|
|
41
|
+
qmdHybridFetchLimit: number,
|
|
42
|
+
options: ArtifactTopUpOptions,
|
|
43
|
+
): Promise<QmdSearchResult[]> {
|
|
44
|
+
if (
|
|
45
|
+
deps.config.recallRecognitionTier !== true ||
|
|
46
|
+
options.collection ||
|
|
47
|
+
options.namespacesEnabled !== true
|
|
48
|
+
) {
|
|
49
|
+
return deps.fetchQmdMemoryResultsWithArtifactTopUp(
|
|
50
|
+
prompt,
|
|
51
|
+
qmdFetchLimit,
|
|
52
|
+
qmdHybridFetchLimit,
|
|
53
|
+
options,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const namespaces = options.recallNamespaces.filter(Boolean);
|
|
58
|
+
if (namespaces.length === 0) {
|
|
59
|
+
return deps.fetchQmdMemoryResultsWithArtifactTopUp(
|
|
60
|
+
prompt,
|
|
61
|
+
qmdFetchLimit,
|
|
62
|
+
qmdHybridFetchLimit,
|
|
63
|
+
options,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
throwIfRecallAborted(options.abortSignal);
|
|
68
|
+
const maxEntries = deps.config.recognitionIndexMaxEntries;
|
|
69
|
+
const recognitionNs: Array<{ namespace: string; storage: RecognitionStorage; index: RecognitionIndex }> = [];
|
|
70
|
+
const vectorNs: string[] = [];
|
|
71
|
+
|
|
72
|
+
for (const namespace of namespaces) {
|
|
73
|
+
throwIfRecallAborted(options.abortSignal);
|
|
74
|
+
const storage = (await deps.storageRouter.storageFor(namespace)) as RecognitionStorage;
|
|
75
|
+
const index = await loadRecognitionIndex(storage.dir);
|
|
76
|
+
const decision = decideRecognitionTier(index, { maxEntries });
|
|
77
|
+
if (index !== null && decision.tier === "recognition") {
|
|
78
|
+
recognitionNs.push({ namespace, storage, index });
|
|
79
|
+
} else {
|
|
80
|
+
vectorNs.push(namespace);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (recognitionNs.length === 0) {
|
|
85
|
+
return deps.fetchQmdMemoryResultsWithArtifactTopUp(
|
|
86
|
+
prompt,
|
|
87
|
+
qmdFetchLimit,
|
|
88
|
+
qmdHybridFetchLimit,
|
|
89
|
+
options,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const recognize = async (recognitionPrompt: string): Promise<string | null | undefined> => {
|
|
94
|
+
const reply = await deps.fastLlmForRerank.chatCompletion(
|
|
95
|
+
[{ role: "user", content: recognitionPrompt }],
|
|
96
|
+
{ operation: "recognition-tier", priority: "recall-critical" },
|
|
97
|
+
);
|
|
98
|
+
return reply?.content;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const recognized: QmdSearchResult[] = [];
|
|
102
|
+
for (const { namespace, storage, index } of recognitionNs) {
|
|
103
|
+
throwIfRecallAborted(options.abortSignal);
|
|
104
|
+
try {
|
|
105
|
+
const run = await runRecognitionTier(prompt, index.entries, recognize);
|
|
106
|
+
recognized.push(
|
|
107
|
+
...(await hitsFromIds(namespace, storage, index, run.ids)),
|
|
108
|
+
);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
log.warn(
|
|
111
|
+
`recognition tier degraded to vector search: recognizer failed${
|
|
112
|
+
err instanceof Error ? ` (${err.message})` : ""
|
|
113
|
+
}`,
|
|
114
|
+
);
|
|
115
|
+
vectorNs.push(namespace);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const vectorResults =
|
|
120
|
+
vectorNs.length === 0
|
|
121
|
+
? []
|
|
122
|
+
: await deps.fetchQmdMemoryResultsWithArtifactTopUp(
|
|
123
|
+
prompt,
|
|
124
|
+
qmdFetchLimit,
|
|
125
|
+
qmdHybridFetchLimit,
|
|
126
|
+
{ ...options, recallNamespaces: vectorNs },
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
return dedupeResultsByNamespace(
|
|
130
|
+
[...recognized, ...vectorResults],
|
|
131
|
+
options.resolveNamespace,
|
|
132
|
+
qmdFetchLimit,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function hitsFromIds(
|
|
137
|
+
namespace: string,
|
|
138
|
+
storage: RecognitionStorage,
|
|
139
|
+
index: RecognitionIndex,
|
|
140
|
+
ids: readonly string[],
|
|
141
|
+
): Promise<QmdSearchResult[]> {
|
|
142
|
+
if (ids.length === 0) return [];
|
|
143
|
+
const pathsById = await storage.findExistingMemoryPaths([...ids]);
|
|
144
|
+
const descriptionById = new Map(index.entries.map((entry) => [entry.id, entry.description]));
|
|
145
|
+
const hits: QmdSearchResult[] = [];
|
|
146
|
+
for (const [offset, id] of ids.entries()) {
|
|
147
|
+
const absPath = pathsById.get(id)?.[0];
|
|
148
|
+
if (!absPath) continue;
|
|
149
|
+
const rel = path.relative(storage.dir, absPath).split(path.sep).join("/");
|
|
150
|
+
if (!rel || rel.startsWith("..")) continue;
|
|
151
|
+
hits.push({
|
|
152
|
+
docid: `${namespace}:${rel}`,
|
|
153
|
+
namespace,
|
|
154
|
+
path: rel,
|
|
155
|
+
score: ids.length - offset,
|
|
156
|
+
snippet: descriptionById.get(id) ?? id,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return hits;
|
|
160
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Intent-gated recall for active procedure memories (issue #519).
|
|
3
|
+
* Session-end experience episodes (issue #2979) compete here after promotion.
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
import type { MemoryFile, PluginConfig } from "../types.js";
|
|
@@ -9,6 +10,10 @@ import { isActiveMemoryStatus } from "../memory-lifecycle-ledger-utils.js";
|
|
|
9
10
|
import { canRecallToolScopedMemory } from "../tool-scoped-memory.js";
|
|
10
11
|
import { resolveSecurityCapabilities } from "../capabilities.js";
|
|
11
12
|
import { renderAuthorityBoundContent } from "../recall-context-composition.js";
|
|
13
|
+
import {
|
|
14
|
+
renderSessionExperiencePreview,
|
|
15
|
+
scoreSessionExperienceForPrompt,
|
|
16
|
+
} from "../experience/session-experience-recall.js";
|
|
12
17
|
|
|
13
18
|
function tokenOverlapScore(prompt: string, memoryText: string): number {
|
|
14
19
|
const norm = (s: string) =>
|
|
@@ -31,12 +36,16 @@ function scoreProcedureForPrompt(
|
|
|
31
36
|
m: MemoryFile,
|
|
32
37
|
prompt: string,
|
|
33
38
|
queryIntent: ReturnType<typeof inferIntentFromText>,
|
|
39
|
+
experienceEnabled: boolean,
|
|
34
40
|
): number {
|
|
35
41
|
const memText = `${m.content}\n${(m.frontmatter.tags ?? []).join(" ")}`;
|
|
36
42
|
const jaccard = tokenOverlapScore(prompt, memText);
|
|
37
43
|
const memIntent = inferIntentFromText(m.content.slice(0, 2000));
|
|
38
44
|
const intentScore = intentCompatibilityScore(queryIntent, memIntent);
|
|
39
|
-
|
|
45
|
+
const base = jaccard * 0.55 + intentScore * 0.45;
|
|
46
|
+
if (!experienceEnabled) return base;
|
|
47
|
+
const experience = scoreSessionExperienceForPrompt(m, prompt);
|
|
48
|
+
return experience === null ? base : Math.max(base, experience);
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
/**
|
|
@@ -83,6 +92,7 @@ export async function buildProcedureRecallSection(
|
|
|
83
92
|
);
|
|
84
93
|
|
|
85
94
|
const all = await storage.readAllMemories();
|
|
95
|
+
const experienceEnabled = config.sessionExperience?.enabled === true;
|
|
86
96
|
const scored = all
|
|
87
97
|
.filter(
|
|
88
98
|
(m) =>
|
|
@@ -91,7 +101,7 @@ export async function buildProcedureRecallSection(
|
|
|
91
101
|
(options?.partitionToolScoped !== true ||
|
|
92
102
|
canRecallToolScopedMemory(m.frontmatter, options.requestingConnector)),
|
|
93
103
|
)
|
|
94
|
-
.map((m) => ({ m, score: scoreProcedureForPrompt(m, trimmed, queryIntent) }))
|
|
104
|
+
.map((m) => ({ m, score: scoreProcedureForPrompt(m, trimmed, queryIntent, experienceEnabled) }))
|
|
95
105
|
.filter((x) => x.score > 0.04)
|
|
96
106
|
.sort((a, b) => b.score - a.score)
|
|
97
107
|
.slice(0, maxN);
|
|
@@ -110,7 +120,8 @@ export async function buildProcedureRecallSection(
|
|
|
110
120
|
const security = resolveSecurityCapabilities(config);
|
|
111
121
|
const blocks = scored.map(({ m, score }) => {
|
|
112
122
|
const id = m.frontmatter.id;
|
|
113
|
-
const
|
|
123
|
+
const labeled = experienceEnabled ? renderSessionExperiencePreview(m) : null;
|
|
124
|
+
const flat = (labeled ?? m.content).replace(/\s+/g, " ").trim();
|
|
114
125
|
const preview = flat.slice(0, 320);
|
|
115
126
|
const suffix = flat.length > 320 ? "…" : "";
|
|
116
127
|
const body = renderAuthorityBoundContent(`${preview}${suffix}`, m.frontmatter.origin, {
|
package/src/recall-audit.test.ts
CHANGED
|
@@ -49,3 +49,37 @@ test("pruneRecallAuditEntries removes day directories older than retention", asy
|
|
|
49
49
|
assert.equal((await stat(keepDir)).isDirectory(), true);
|
|
50
50
|
await assert.rejects(stat(deleteDir));
|
|
51
51
|
});
|
|
52
|
+
|
|
53
|
+
test("#2972 audit entry persists degradation when present and omits it otherwise", async () => {
|
|
54
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-recall-audit-deg-"));
|
|
55
|
+
const healthy = {
|
|
56
|
+
ts: "2026-04-12T14:30:12.445Z",
|
|
57
|
+
sessionKey: "agent/main:session/healthy",
|
|
58
|
+
agentId: "main",
|
|
59
|
+
trigger: "access-surface",
|
|
60
|
+
queryText: "How did the CI outage resolve?",
|
|
61
|
+
candidateMemoryIds: ["mem_1"],
|
|
62
|
+
summary: "CI recovered.",
|
|
63
|
+
injectedChars: 12,
|
|
64
|
+
toggleState: "enabled" as const,
|
|
65
|
+
};
|
|
66
|
+
const degraded = {
|
|
67
|
+
...healthy,
|
|
68
|
+
sessionKey: "agent/main:session/degraded",
|
|
69
|
+
degradation: {
|
|
70
|
+
state: "degraded" as const,
|
|
71
|
+
reason: "budget-compacted" as const,
|
|
72
|
+
budget: { contextBudget: 80, fullChars: 200, deliveredChars: 80 },
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
await appendRecallAuditEntry(root, healthy);
|
|
77
|
+
await appendRecallAuditEntry(root, degraded);
|
|
78
|
+
|
|
79
|
+
const healthyPath = buildRecallAuditPath(root, healthy.ts, healthy.sessionKey);
|
|
80
|
+
const degradedPath = buildRecallAuditPath(root, degraded.ts, degraded.sessionKey);
|
|
81
|
+
assert.deepEqual(JSON.parse((await readFile(healthyPath, "utf8")).trim()), healthy);
|
|
82
|
+
assert.equal("degradation" in JSON.parse((await readFile(healthyPath, "utf8")).trim()), false);
|
|
83
|
+
assert.deepEqual(JSON.parse((await readFile(degradedPath, "utf8")).trim()), degraded);
|
|
84
|
+
});
|
|
85
|
+
|
package/src/recall-audit.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { Dirent } from "node:fs";
|
|
|
2
2
|
import { appendFile, mkdir, readdir, rm } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
+
import type { RecallContextDegradation } from "./recall-context-composition.js";
|
|
6
|
+
|
|
5
7
|
export interface RecallAuditEntry {
|
|
6
8
|
ts: string;
|
|
7
9
|
sessionKey: string;
|
|
@@ -16,6 +18,8 @@ export interface RecallAuditEntry {
|
|
|
16
18
|
plannerMode?: string;
|
|
17
19
|
requestedMode?: string;
|
|
18
20
|
fallbackUsed?: boolean;
|
|
21
|
+
/** Injected-context degradation (#2972). Absent on a healthy recall. */
|
|
22
|
+
degradation?: RecallContextDegradation;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
function formatIsoDate(ts: string): string {
|