@remnic/core 10.0.4 → 10.0.5
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 +46 -46
- package/dist/access-authorization-probe.js +47 -47
- package/dist/access-boundary.js +46 -46
- package/dist/access-cli.d.ts.map +1 -1
- package/dist/access-cli.js +117 -112
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.js +46 -46
- package/dist/access-http-briefing.js +47 -47
- package/dist/access-http-query.js +48 -48
- package/dist/access-http.d.ts.map +1 -1
- package/dist/access-http.js +69 -68
- package/dist/access-identity-continuity-surface.js +46 -46
- package/dist/access-lcm-surface.js +46 -46
- package/dist/access-mcp-correction-tools.d.ts +10 -0
- package/dist/access-mcp-correction-tools.d.ts.map +1 -0
- package/dist/access-mcp-correction-tools.js +8 -0
- package/dist/access-mcp.d.ts.map +1 -1
- package/dist/access-mcp.js +60 -59
- package/dist/access-namespace-preflight.js +46 -46
- package/dist/access-observe-write-category.js +3 -3
- package/dist/access-observe-write-surface.js +46 -46
- package/dist/access-offline-manifest.js +5 -5
- package/dist/access-operations-batch.js +49 -49
- package/dist/access-operations.d.ts +1 -0
- package/dist/access-operations.d.ts.map +1 -1
- package/dist/access-operations.js +56 -56
- package/dist/access-recall-concurrency.js +46 -46
- package/dist/access-recall-diagnostics.js +2 -2
- package/dist/access-recall-response.js +46 -46
- package/dist/access-recall-surface.js +46 -46
- package/dist/access-service.d.ts.map +1 -1
- package/dist/access-service.js +46 -46
- package/dist/access-surface-catalog.js +48 -48
- package/dist/active-recall.js +22 -22
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/auto-sync-FN7E3OTO.js +2 -2
- package/dist/briefing.js +18 -18
- package/dist/buffer.js +1 -1
- package/dist/causal-consolidation.js +18 -18
- package/dist/{chunk-5YDWSWBU.js → chunk-2CP577K6.js} +2 -2
- package/dist/{chunk-ABET5LA5.js → chunk-37HCPM56.js} +23 -23
- package/dist/{chunk-5QHTQMRL.js → chunk-4CWUZBPF.js} +8 -6
- package/dist/chunk-4CWUZBPF.js.map +1 -0
- package/dist/{chunk-H6FA36LC.js → chunk-4K2H4KTR.js} +5 -5
- package/dist/{chunk-TSEUVMJL.js → chunk-52CQKST6.js} +39 -39
- package/dist/{chunk-H7BZBSP6.js → chunk-52II7PB6.js} +4 -4
- package/dist/{chunk-RH6MQ5NG.js → chunk-5BPXMAUP.js} +17 -68
- package/dist/chunk-5BPXMAUP.js.map +1 -0
- package/dist/{chunk-WLC6XA6C.js → chunk-5BY3IA2U.js} +16 -53
- package/dist/chunk-5BY3IA2U.js.map +1 -0
- package/dist/{chunk-ABDTBGI2.js → chunk-5CQO6LVM.js} +2 -2
- package/dist/{chunk-HSZBALHV.js → chunk-5LNPW7NE.js} +2 -2
- package/dist/{chunk-U3LVARGV.js → chunk-5X4ZJESN.js} +2 -2
- package/dist/{chunk-SNGNGH53.js → chunk-63UNVGZW.js} +4 -4
- package/dist/{chunk-2KB7TTAP.js → chunk-64S6X4IU.js} +4 -4
- package/dist/{chunk-3QAIX7TV.js → chunk-6KHF6PAB.js} +2 -2
- package/dist/{chunk-MYBCB4AT.js → chunk-6T42L5FI.js} +2 -2
- package/dist/{chunk-R6EOFKZG.js → chunk-7LYRFI5A.js} +2 -2
- package/dist/{chunk-4NFRYOHT.js → chunk-ANZXWOYD.js} +2 -2
- package/dist/{chunk-QDKABMUE.js → chunk-AXC7UTLM.js} +2 -2
- package/dist/{chunk-5XXTZ4Y7.js → chunk-B657ZGDH.js} +2 -2
- package/dist/{chunk-NSQ3GUVO.js → chunk-CYEK6Z3P.js} +4 -4
- package/dist/{chunk-IRYKRBEX.js → chunk-DJMVSUPK.js} +3 -3
- package/dist/{chunk-GLS3A4SQ.js → chunk-DST7HRSG.js} +2 -2
- package/dist/{chunk-UCCB7RKF.js → chunk-EMVJU7WC.js} +2 -2
- package/dist/{chunk-IY7ZISXV.js → chunk-ENNC3RZ6.js} +2 -2
- package/dist/{chunk-QLK3SL7Q.js → chunk-ETSJD5IV.js} +131 -12
- package/dist/chunk-ETSJD5IV.js.map +1 -0
- package/dist/{chunk-PTFL3INS.js → chunk-FC6RCOIU.js} +2 -2
- package/dist/{chunk-CQQTEDHS.js → chunk-FLV7M7CT.js} +2 -2
- package/dist/{chunk-IXF7TRMQ.js → chunk-FRH2UC7Q.js} +2 -2
- package/dist/{chunk-3SL52JKD.js → chunk-FZVROZKC.js} +5 -5
- package/dist/{chunk-Z2EVKNZR.js → chunk-GEGUBU3D.js} +2 -2
- package/dist/{chunk-LIGP5M2C.js → chunk-GF4H3B3R.js} +8 -8
- package/dist/{chunk-7OOW72QA.js → chunk-GVUW5XK5.js} +305 -136
- package/dist/chunk-GVUW5XK5.js.map +1 -0
- package/dist/{chunk-XTWF5XR6.js → chunk-HPAOMYKM.js} +67 -67
- package/dist/{chunk-ZOGZMAGI.js → chunk-I25RINZK.js} +2 -2
- package/dist/{chunk-FMIVV5SV.js → chunk-I56MTWE7.js} +2 -2
- package/dist/{chunk-EUL3S3LE.js → chunk-J5WI4QRP.js} +4 -4
- package/dist/{chunk-436IRJUD.js → chunk-JA54KIYD.js} +2 -2
- package/dist/{chunk-QWNBQ2T5.js → chunk-JMF5Q4JY.js} +10 -10
- package/dist/{chunk-IDSSDL4W.js → chunk-JXSYWTFK.js} +21 -4
- package/dist/{chunk-IDSSDL4W.js.map → chunk-JXSYWTFK.js.map} +1 -1
- package/dist/chunk-KIUVEMBI.js +49 -0
- package/dist/chunk-KIUVEMBI.js.map +1 -0
- package/dist/{chunk-IINXHPCV.js → chunk-KQI2PKYP.js} +2 -2
- package/dist/{chunk-RGPPNNDJ.js → chunk-KSUOM3YU.js} +9 -9
- package/dist/{chunk-CXG2T7BJ.js → chunk-KZEMFD6E.js} +3 -3
- package/dist/{chunk-O57VUN4U.js → chunk-L2VS27PZ.js} +2 -2
- package/dist/{chunk-IVGJXS5F.js → chunk-L34W4W4I.js} +2 -2
- package/dist/{chunk-MEP3POVW.js → chunk-L3PWXD73.js} +2 -2
- package/dist/{chunk-NQHSXHJP.js → chunk-L6GNS5GB.js} +2 -2
- package/dist/{chunk-YMXCJ7SE.js → chunk-M47Q6566.js} +2 -2
- package/dist/{chunk-DQJCQHII.js → chunk-MQNOIH2C.js} +6 -6
- package/dist/{chunk-IDSZNHIP.js → chunk-MULNBSPP.js} +4 -4
- package/dist/{chunk-4DXVWBDB.js → chunk-NBQIFCJE.js} +2 -2
- package/dist/{chunk-H2XCCUOT.js → chunk-NHSVW7UT.js} +15 -15
- package/dist/{chunk-BDHWKFGM.js → chunk-OFEHITAZ.js} +2 -2
- package/dist/{chunk-RYN4AIRM.js → chunk-OZABSIOV.js} +4 -4
- package/dist/{chunk-Y7CXM7UM.js → chunk-QMFKW4OU.js} +3 -3
- package/dist/{chunk-Z63GU5NN.js → chunk-QWWMS4AE.js} +1 -1
- package/dist/chunk-QWWMS4AE.js.map +1 -0
- package/dist/{chunk-R4O2G6ML.js → chunk-RSMIRLKM.js} +2 -2
- package/dist/{chunk-6AH2DDHI.js → chunk-S5TGRZWY.js} +2 -2
- package/dist/{chunk-TYOBAFCY.js → chunk-SNXBXYUE.js} +3 -3
- package/dist/{chunk-MEMH5NKJ.js → chunk-SUAX5JWY.js} +2 -2
- package/dist/{chunk-IHIP7TJ3.js → chunk-SWRN6U7Q.js} +2 -2
- package/dist/{chunk-DBA2W6VV.js → chunk-SZXPKUMN.js} +6 -6
- package/dist/{chunk-WQUGCOKF.js → chunk-TFA6HPOA.js} +4 -4
- package/dist/{chunk-VWCFR4NR.js → chunk-U6OXJQEH.js} +5 -5
- package/dist/{chunk-DYRXRQHV.js → chunk-V32NHP2R.js} +2 -2
- package/dist/{chunk-ORAL4HJC.js → chunk-WG4TMZJ2.js} +2 -2
- package/dist/{chunk-QVBB2CLG.js → chunk-WRICYGYG.js} +27 -26
- package/dist/chunk-WRICYGYG.js.map +1 -0
- package/dist/{chunk-CV5AMCPX.js → chunk-XPRAOTJY.js} +2 -2
- package/dist/{chunk-RZCRXCLI.js → chunk-XQSJXNWC.js} +2 -2
- package/dist/{chunk-P22M3DX3.js → chunk-YJAOYEWI.js} +2 -2
- package/dist/{chunk-OAFRMLL4.js → chunk-YJEUHGZZ.js} +2 -2
- package/dist/cli.js +89 -88
- package/dist/coding/export-okf-codegraph.js +16 -16
- package/dist/compounding/engine.js +17 -17
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +22 -22
- package/dist/connectors/codex-materialize-runner.js +17 -17
- package/dist/connectors/index.js +17 -17
- package/dist/consolidation-provenance-check.js +2 -2
- package/dist/contradiction/index.js +7 -7
- package/dist/convergence-refresh.js +19 -19
- package/dist/conversation-index/backend.js +2 -2
- package/dist/corpus-watermark.js +19 -19
- package/dist/correction/correction-access-wiring.d.ts +6 -6
- package/dist/correction/correction-access-wiring.d.ts.map +1 -1
- package/dist/correction/correction-classify.d.ts +25 -0
- package/dist/correction/correction-classify.d.ts.map +1 -0
- package/dist/correction/correction-config.d.ts +11 -0
- package/dist/correction/correction-config.d.ts.map +1 -0
- package/dist/correction/correction-contract.d.ts +13 -1
- package/dist/correction/correction-contract.d.ts.map +1 -1
- package/dist/correction/correction-executor.d.ts +2 -0
- package/dist/correction/correction-executor.d.ts.map +1 -1
- package/dist/correction/correction-planner.d.ts +24 -3
- package/dist/correction/correction-planner.d.ts.map +1 -1
- package/dist/entity-retrieval.js +17 -17
- package/dist/event-order-recall.js +2 -2
- package/dist/explicit-capture.js +2 -2
- package/dist/external-wiki-access.js +47 -47
- package/dist/extraction-span-materialize.js +2 -2
- package/dist/extraction.js +22 -22
- package/dist/harmonic-retrieval.js +17 -17
- package/dist/index.js +189 -188
- package/dist/lcm/index.js +3 -3
- package/dist/local-llm.d.ts.map +1 -1
- package/dist/local-llm.js +1 -1
- package/dist/location/index.js +19 -19
- package/dist/maintenance/memory-governance.js +17 -17
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +17 -17
- package/dist/maintenance/rebuild-memory-projection-runner.js +19 -19
- package/dist/maintenance/rebuild-memory-projection.js +18 -18
- package/dist/memory-browse-operations.js +47 -47
- package/dist/memory-browse.js +1 -1
- package/dist/memory-subject.js +1 -1
- package/dist/namespaces/migrate.js +18 -18
- package/dist/namespaces/search.js +6 -6
- package/dist/namespaces/storage.js +17 -17
- package/dist/operator-doctor-corpus.js +20 -20
- package/dist/operator-toolkit.js +32 -32
- package/dist/orchestration/maintenance.js +28 -28
- package/dist/orchestration/session-context.d.ts.map +1 -1
- package/dist/orchestrator.js +113 -112
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-navigate-access.js +2 -2
- package/dist/recall-navigate-disclosure.js +2 -2
- package/dist/recall-navigate.js +2 -2
- package/dist/recall-navigation-operations.js +47 -47
- package/dist/recall-navigation.js +46 -46
- package/dist/recall-why-service.js +2 -2
- 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/reconcile/manifest.js +3 -3
- package/dist/{resolution-RSG4HZTL.js → resolution-TV3F7ZTH.js} +7 -7
- package/dist/resolution-TV3F7ZTH.js.map +1 -0
- package/dist/resume-bundles.js +23 -23
- package/dist/salvage-envelope.js +1 -1
- package/dist/search/factory.js +5 -5
- package/dist/search/index.js +9 -9
- package/dist/semantic-consolidation.js +23 -23
- package/dist/semantic-rule-promotion.js +17 -17
- package/dist/semantic-rule-verifier.js +17 -17
- package/dist/source-agent-qualifier.js +17 -17
- package/dist/standup/build.js +17 -17
- package/dist/storage.js +16 -16
- package/dist/subject-config.js +1 -1
- package/dist/summarizer.js +3 -3
- package/dist/support-passport/index.js +63 -63
- package/dist/transfer/export-okf.js +17 -17
- package/dist/types.d.ts +6 -23
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/verified-recall.js +17 -17
- package/dist/who-knows-cli.js +18 -18
- package/dist/who-knows.js +17 -17
- package/dist/write-envelope.js +1 -1
- package/package.json +2 -2
- package/src/access-cli.ts +4 -0
- package/src/access-http.ts +1 -0
- package/src/access-mcp-correction-tools.ts +56 -0
- package/src/access-mcp.ts +4 -49
- package/src/access-operations.ts +3 -0
- package/src/access-service.ts +12 -21
- package/src/config.test.ts +26 -0
- package/src/config.ts +4 -60
- package/src/correction/correction-access-wiring.ts +56 -16
- package/src/correction/correction-classify.test.ts +185 -0
- package/src/correction/correction-classify.ts +157 -0
- package/src/correction/correction-config.ts +93 -0
- package/src/correction/correction-contract.ts +61 -14
- package/src/correction/correction-executor.test.ts +102 -2
- package/src/correction/correction-executor.ts +19 -1
- package/src/correction/correction-planner.test.ts +141 -0
- package/src/correction/correction-planner.ts +117 -11
- package/src/correction/correction-surfaces.test.ts +22 -1
- package/src/local-llm-qos.test.ts +36 -0
- package/src/local-llm.ts +20 -3
- package/src/orchestration/session-context.ts +0 -15
- package/src/recall-handles-wiring.test.ts +24 -0
- package/src/types.ts +6 -23
- package/dist/chunk-5QHTQMRL.js.map +0 -1
- package/dist/chunk-7OOW72QA.js.map +0 -1
- package/dist/chunk-QLK3SL7Q.js.map +0 -1
- package/dist/chunk-QVBB2CLG.js.map +0 -1
- package/dist/chunk-RH6MQ5NG.js.map +0 -1
- package/dist/chunk-WLC6XA6C.js.map +0 -1
- package/dist/chunk-Z63GU5NN.js.map +0 -1
- /package/dist/{resolution-RSG4HZTL.js.map → access-mcp-correction-tools.js.map} +0 -0
- /package/dist/{chunk-5YDWSWBU.js.map → chunk-2CP577K6.js.map} +0 -0
- /package/dist/{chunk-ABET5LA5.js.map → chunk-37HCPM56.js.map} +0 -0
- /package/dist/{chunk-H6FA36LC.js.map → chunk-4K2H4KTR.js.map} +0 -0
- /package/dist/{chunk-TSEUVMJL.js.map → chunk-52CQKST6.js.map} +0 -0
- /package/dist/{chunk-H7BZBSP6.js.map → chunk-52II7PB6.js.map} +0 -0
- /package/dist/{chunk-ABDTBGI2.js.map → chunk-5CQO6LVM.js.map} +0 -0
- /package/dist/{chunk-HSZBALHV.js.map → chunk-5LNPW7NE.js.map} +0 -0
- /package/dist/{chunk-U3LVARGV.js.map → chunk-5X4ZJESN.js.map} +0 -0
- /package/dist/{chunk-SNGNGH53.js.map → chunk-63UNVGZW.js.map} +0 -0
- /package/dist/{chunk-2KB7TTAP.js.map → chunk-64S6X4IU.js.map} +0 -0
- /package/dist/{chunk-3QAIX7TV.js.map → chunk-6KHF6PAB.js.map} +0 -0
- /package/dist/{chunk-MYBCB4AT.js.map → chunk-6T42L5FI.js.map} +0 -0
- /package/dist/{chunk-R6EOFKZG.js.map → chunk-7LYRFI5A.js.map} +0 -0
- /package/dist/{chunk-4NFRYOHT.js.map → chunk-ANZXWOYD.js.map} +0 -0
- /package/dist/{chunk-QDKABMUE.js.map → chunk-AXC7UTLM.js.map} +0 -0
- /package/dist/{chunk-5XXTZ4Y7.js.map → chunk-B657ZGDH.js.map} +0 -0
- /package/dist/{chunk-NSQ3GUVO.js.map → chunk-CYEK6Z3P.js.map} +0 -0
- /package/dist/{chunk-IRYKRBEX.js.map → chunk-DJMVSUPK.js.map} +0 -0
- /package/dist/{chunk-GLS3A4SQ.js.map → chunk-DST7HRSG.js.map} +0 -0
- /package/dist/{chunk-UCCB7RKF.js.map → chunk-EMVJU7WC.js.map} +0 -0
- /package/dist/{chunk-IY7ZISXV.js.map → chunk-ENNC3RZ6.js.map} +0 -0
- /package/dist/{chunk-PTFL3INS.js.map → chunk-FC6RCOIU.js.map} +0 -0
- /package/dist/{chunk-CQQTEDHS.js.map → chunk-FLV7M7CT.js.map} +0 -0
- /package/dist/{chunk-IXF7TRMQ.js.map → chunk-FRH2UC7Q.js.map} +0 -0
- /package/dist/{chunk-3SL52JKD.js.map → chunk-FZVROZKC.js.map} +0 -0
- /package/dist/{chunk-Z2EVKNZR.js.map → chunk-GEGUBU3D.js.map} +0 -0
- /package/dist/{chunk-LIGP5M2C.js.map → chunk-GF4H3B3R.js.map} +0 -0
- /package/dist/{chunk-XTWF5XR6.js.map → chunk-HPAOMYKM.js.map} +0 -0
- /package/dist/{chunk-ZOGZMAGI.js.map → chunk-I25RINZK.js.map} +0 -0
- /package/dist/{chunk-FMIVV5SV.js.map → chunk-I56MTWE7.js.map} +0 -0
- /package/dist/{chunk-EUL3S3LE.js.map → chunk-J5WI4QRP.js.map} +0 -0
- /package/dist/{chunk-436IRJUD.js.map → chunk-JA54KIYD.js.map} +0 -0
- /package/dist/{chunk-QWNBQ2T5.js.map → chunk-JMF5Q4JY.js.map} +0 -0
- /package/dist/{chunk-IINXHPCV.js.map → chunk-KQI2PKYP.js.map} +0 -0
- /package/dist/{chunk-RGPPNNDJ.js.map → chunk-KSUOM3YU.js.map} +0 -0
- /package/dist/{chunk-CXG2T7BJ.js.map → chunk-KZEMFD6E.js.map} +0 -0
- /package/dist/{chunk-O57VUN4U.js.map → chunk-L2VS27PZ.js.map} +0 -0
- /package/dist/{chunk-IVGJXS5F.js.map → chunk-L34W4W4I.js.map} +0 -0
- /package/dist/{chunk-MEP3POVW.js.map → chunk-L3PWXD73.js.map} +0 -0
- /package/dist/{chunk-NQHSXHJP.js.map → chunk-L6GNS5GB.js.map} +0 -0
- /package/dist/{chunk-YMXCJ7SE.js.map → chunk-M47Q6566.js.map} +0 -0
- /package/dist/{chunk-DQJCQHII.js.map → chunk-MQNOIH2C.js.map} +0 -0
- /package/dist/{chunk-IDSZNHIP.js.map → chunk-MULNBSPP.js.map} +0 -0
- /package/dist/{chunk-4DXVWBDB.js.map → chunk-NBQIFCJE.js.map} +0 -0
- /package/dist/{chunk-H2XCCUOT.js.map → chunk-NHSVW7UT.js.map} +0 -0
- /package/dist/{chunk-BDHWKFGM.js.map → chunk-OFEHITAZ.js.map} +0 -0
- /package/dist/{chunk-RYN4AIRM.js.map → chunk-OZABSIOV.js.map} +0 -0
- /package/dist/{chunk-Y7CXM7UM.js.map → chunk-QMFKW4OU.js.map} +0 -0
- /package/dist/{chunk-R4O2G6ML.js.map → chunk-RSMIRLKM.js.map} +0 -0
- /package/dist/{chunk-6AH2DDHI.js.map → chunk-S5TGRZWY.js.map} +0 -0
- /package/dist/{chunk-TYOBAFCY.js.map → chunk-SNXBXYUE.js.map} +0 -0
- /package/dist/{chunk-MEMH5NKJ.js.map → chunk-SUAX5JWY.js.map} +0 -0
- /package/dist/{chunk-IHIP7TJ3.js.map → chunk-SWRN6U7Q.js.map} +0 -0
- /package/dist/{chunk-DBA2W6VV.js.map → chunk-SZXPKUMN.js.map} +0 -0
- /package/dist/{chunk-WQUGCOKF.js.map → chunk-TFA6HPOA.js.map} +0 -0
- /package/dist/{chunk-VWCFR4NR.js.map → chunk-U6OXJQEH.js.map} +0 -0
- /package/dist/{chunk-DYRXRQHV.js.map → chunk-V32NHP2R.js.map} +0 -0
- /package/dist/{chunk-ORAL4HJC.js.map → chunk-WG4TMZJ2.js.map} +0 -0
- /package/dist/{chunk-CV5AMCPX.js.map → chunk-XPRAOTJY.js.map} +0 -0
- /package/dist/{chunk-RZCRXCLI.js.map → chunk-XQSJXNWC.js.map} +0 -0
- /package/dist/{chunk-P22M3DX3.js.map → chunk-YJAOYEWI.js.map} +0 -0
- /package/dist/{chunk-OAFRMLL4.js.map → chunk-YJEUHGZZ.js.map} +0 -0
|
@@ -68,6 +68,8 @@ export interface ExecutorMemory {
|
|
|
68
68
|
entityRef?: string;
|
|
69
69
|
/** Raw content used for the tombstone hash (rule 23). */
|
|
70
70
|
rawContent: string;
|
|
71
|
+
/** Frontmatter status. Absent is treated as active. */
|
|
72
|
+
status?: string;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
export interface ExecutorDeps {
|
|
@@ -486,13 +488,29 @@ export class CorrectionExecutor {
|
|
|
486
488
|
if (action.replacement && !replacementResult) {
|
|
487
489
|
continue;
|
|
488
490
|
}
|
|
491
|
+
if (action.winnerId && !action.replacement) {
|
|
492
|
+
const winner = await this.deps.getMemory(namespace, action.winnerId, abortSignal);
|
|
493
|
+
const winnerStatus = winner ? (winner.status ?? "active") : undefined;
|
|
494
|
+
if (!winner || winnerStatus !== "active") {
|
|
495
|
+
results.push({
|
|
496
|
+
action,
|
|
497
|
+
status: "failed",
|
|
498
|
+
error: winner
|
|
499
|
+
? `supersede winner is not active: ${action.winnerId} (${winnerStatus})`
|
|
500
|
+
: `supersede winner not found: ${action.winnerId}`,
|
|
501
|
+
});
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
489
505
|
await this.retireAndTombstone(
|
|
490
506
|
namespace,
|
|
491
507
|
action,
|
|
492
508
|
"supersession",
|
|
493
509
|
results,
|
|
494
510
|
appliedTouched,
|
|
495
|
-
{
|
|
511
|
+
{
|
|
512
|
+
supersededBy: replacementResult?.memoryId ?? action.winnerId,
|
|
513
|
+
},
|
|
496
514
|
abortSignal
|
|
497
515
|
);
|
|
498
516
|
if (results.at(-1)?.status === "applied") {
|
|
@@ -38,6 +38,8 @@ interface StubState {
|
|
|
38
38
|
writeReplacementCalls: number;
|
|
39
39
|
propagateCalls: number;
|
|
40
40
|
retireCalls: number;
|
|
41
|
+
statusById?: Map<string, string>;
|
|
42
|
+
classifyCalls?: number;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
function makeCandidate(over: Partial<PlannerCandidate> & { memoryId: string }): PlannerCandidate {
|
|
@@ -72,12 +74,18 @@ function makeDeps(stateDir: string, state: StubState, opts: { maxAffected?: numb
|
|
|
72
74
|
}
|
|
73
75
|
return out;
|
|
74
76
|
},
|
|
77
|
+
inspectMemory: async ({ memoryId }) => {
|
|
78
|
+
const candidate = state.candidatesById.get(memoryId);
|
|
79
|
+
if (!candidate) return null;
|
|
80
|
+
return { candidate, status: state.statusById?.get(memoryId) ?? "active" };
|
|
81
|
+
},
|
|
75
82
|
expandNeighbors: async ({ seedIds }) => {
|
|
76
83
|
// No neighbors in the stub; return the seeds themselves so neighbor
|
|
77
84
|
// expansion is exercised but produces no surprises.
|
|
78
85
|
return seedIds.map((id) => state.candidatesById.get(id)).filter((c): c is PlannerCandidate => c !== undefined);
|
|
79
86
|
},
|
|
80
87
|
classifyAndDraft: async ({ text, candidates }) => {
|
|
88
|
+
state.classifyCalls = (state.classifyCalls ?? 0) + 1;
|
|
81
89
|
return typeof state.llmResult === "function"
|
|
82
90
|
? state.llmResult(text, candidates)
|
|
83
91
|
: state.llmResult;
|
|
@@ -787,3 +795,136 @@ test("#1713: recoverStaleApplyingPlans recovers pre-fix plans immediately at exp
|
|
|
787
795
|
assert.equal(recovered.length, 1, "pre-fix plan at expiry must be recovered immediately");
|
|
788
796
|
});
|
|
789
797
|
});
|
|
798
|
+
|
|
799
|
+
test("duplicateOf skips the LLM and drafts one recoverable supersede linked to the winner", async () => {
|
|
800
|
+
await withTempDir(async (dir) => {
|
|
801
|
+
const state: StubState = {
|
|
802
|
+
candidatesById: new Map([
|
|
803
|
+
["mem-loser", makeCandidate({ memoryId: "mem-loser", content: "duplicate fact" })],
|
|
804
|
+
["mem-winner", makeCandidate({ memoryId: "mem-winner", content: "canonical fact" })],
|
|
805
|
+
]),
|
|
806
|
+
llmResult: () => {
|
|
807
|
+
throw new Error("classifyAndDraft must not run in duplicate mode");
|
|
808
|
+
},
|
|
809
|
+
writeReplacementCalls: 0,
|
|
810
|
+
propagateCalls: 0,
|
|
811
|
+
retireCalls: 0,
|
|
812
|
+
};
|
|
813
|
+
const plan = await new CorrectionPlanner(makeDeps(dir, state)).plan(
|
|
814
|
+
{ text: "mem-loser duplicates mem-winner", targetIds: ["mem-loser"], duplicateOf: "mem-winner" },
|
|
815
|
+
["default"],
|
|
816
|
+
);
|
|
817
|
+
assert.equal(state.classifyCalls ?? 0, 0);
|
|
818
|
+
assert.equal(plan.actions.length, 1);
|
|
819
|
+
assert.deepEqual(plan.actions[0], { kind: "supersede", loserId: "mem-loser", winnerId: "mem-winner" });
|
|
820
|
+
assert.equal(plan.classification, "outdated");
|
|
821
|
+
assert.equal(plan.confidence, 1);
|
|
822
|
+
assert.equal(plan.affected.length, 1);
|
|
823
|
+
assert.equal(plan.affected[0]?.memoryId, "mem-loser");
|
|
824
|
+
assert.equal(plan.request.duplicateOf, "mem-winner");
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
test("a model-selected winnerId outside the located set is dropped", async () => {
|
|
829
|
+
await withTempDir(async (dir) => {
|
|
830
|
+
const state: StubState = {
|
|
831
|
+
candidatesById: new Map([
|
|
832
|
+
["mem-loser", makeCandidate({ memoryId: "mem-loser", content: "duplicate fact" })],
|
|
833
|
+
["mem-winner", makeCandidate({ memoryId: "mem-winner", content: "canonical fact" })],
|
|
834
|
+
]),
|
|
835
|
+
llmResult: {
|
|
836
|
+
classification: "outdated",
|
|
837
|
+
confidence: 0.9,
|
|
838
|
+
actions: [{ kind: "supersede", loserId: "mem-loser", winnerId: "mem-not-located" }],
|
|
839
|
+
relevance: [{ memoryId: "mem-loser", why: "duplicate" }],
|
|
840
|
+
warnings: [],
|
|
841
|
+
},
|
|
842
|
+
writeReplacementCalls: 0,
|
|
843
|
+
propagateCalls: 0,
|
|
844
|
+
retireCalls: 0,
|
|
845
|
+
};
|
|
846
|
+
const plan = await new CorrectionPlanner(makeDeps(dir, state)).plan(
|
|
847
|
+
{ text: "retire the duplicate", targetIds: ["mem-loser"] },
|
|
848
|
+
["default"],
|
|
849
|
+
);
|
|
850
|
+
assert.equal(plan.actions.length, 0);
|
|
851
|
+
assert.match(plan.warnings.join(" "), /outside the located candidate set/);
|
|
852
|
+
});
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
test("a model-selected winnerId inside the located set is kept", async () => {
|
|
856
|
+
await withTempDir(async (dir) => {
|
|
857
|
+
const state: StubState = {
|
|
858
|
+
candidatesById: new Map([
|
|
859
|
+
["mem-loser", makeCandidate({ memoryId: "mem-loser", content: "duplicate fact" })],
|
|
860
|
+
["mem-winner", makeCandidate({ memoryId: "mem-winner", content: "canonical fact" })],
|
|
861
|
+
]),
|
|
862
|
+
llmResult: {
|
|
863
|
+
classification: "outdated",
|
|
864
|
+
confidence: 0.9,
|
|
865
|
+
actions: [{ kind: "supersede", loserId: "mem-loser", winnerId: "mem-winner" }],
|
|
866
|
+
relevance: [{ memoryId: "mem-loser", why: "duplicate" }],
|
|
867
|
+
warnings: [],
|
|
868
|
+
},
|
|
869
|
+
writeReplacementCalls: 0,
|
|
870
|
+
propagateCalls: 0,
|
|
871
|
+
retireCalls: 0,
|
|
872
|
+
};
|
|
873
|
+
const plan = await new CorrectionPlanner(makeDeps(dir, state)).plan(
|
|
874
|
+
{ text: "retire the duplicate", targetIds: ["mem-loser", "mem-winner"] },
|
|
875
|
+
["default"],
|
|
876
|
+
);
|
|
877
|
+
assert.deepEqual(plan.actions, [{ kind: "supersede", loserId: "mem-loser", winnerId: "mem-winner" }]);
|
|
878
|
+
});
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
test("duplicateOf rejects same id, missing memories, inactive memories, and a target list that is not exactly one", async () => {
|
|
882
|
+
await withTempDir(async (dir) => {
|
|
883
|
+
const state: StubState = {
|
|
884
|
+
candidatesById: new Map([
|
|
885
|
+
["mem-loser", makeCandidate({ memoryId: "mem-loser" })],
|
|
886
|
+
["mem-winner", makeCandidate({ memoryId: "mem-winner" })],
|
|
887
|
+
["mem-old", makeCandidate({ memoryId: "mem-old" })],
|
|
888
|
+
]),
|
|
889
|
+
statusById: new Map([["mem-old", "superseded"]]),
|
|
890
|
+
llmResult: { classification: "outdated", confidence: 1, actions: [], relevance: [], warnings: [] },
|
|
891
|
+
writeReplacementCalls: 0,
|
|
892
|
+
propagateCalls: 0,
|
|
893
|
+
retireCalls: 0,
|
|
894
|
+
};
|
|
895
|
+
const planner = new CorrectionPlanner(makeDeps(dir, state));
|
|
896
|
+
await assert.rejects(
|
|
897
|
+
() => planner.plan({ text: "same", targetIds: ["mem-loser"], duplicateOf: "mem-loser" }, ["default"]),
|
|
898
|
+
/different memory from targetIds/,
|
|
899
|
+
);
|
|
900
|
+
await assert.rejects(
|
|
901
|
+
() => planner.plan({ text: "missing loser", targetIds: ["missing"], duplicateOf: "mem-winner" }, ["default"]),
|
|
902
|
+
/target memory not found: missing/,
|
|
903
|
+
);
|
|
904
|
+
await assert.rejects(
|
|
905
|
+
() => planner.plan({ text: "missing winner", targetIds: ["mem-loser"], duplicateOf: "missing-winner" }, ["default"]),
|
|
906
|
+
/duplicateOf memory not found: missing-winner/,
|
|
907
|
+
);
|
|
908
|
+
await assert.rejects(
|
|
909
|
+
() => planner.plan({ text: "inactive loser", targetIds: ["mem-old"], duplicateOf: "mem-winner" }, ["default"]),
|
|
910
|
+
/target memory is not active: mem-old \(superseded\)/,
|
|
911
|
+
);
|
|
912
|
+
state.statusById = new Map([["mem-winner", "forgotten"]]);
|
|
913
|
+
await assert.rejects(
|
|
914
|
+
() => planner.plan({ text: "inactive winner", targetIds: ["mem-loser"], duplicateOf: "mem-winner" }, ["default"]),
|
|
915
|
+
/duplicateOf memory is not active: mem-winner \(forgotten\)/,
|
|
916
|
+
);
|
|
917
|
+
await assert.rejects(
|
|
918
|
+
() => planner.plan({ text: "no target", duplicateOf: "mem-winner" }, ["default"]),
|
|
919
|
+
/exactly one targetIds entry/,
|
|
920
|
+
);
|
|
921
|
+
await assert.rejects(
|
|
922
|
+
() => planner.plan(
|
|
923
|
+
{ text: "two targets", targetIds: ["mem-loser", "mem-winner"], duplicateOf: "mem-winner" },
|
|
924
|
+
["default"],
|
|
925
|
+
),
|
|
926
|
+
/exactly one targetIds entry/,
|
|
927
|
+
);
|
|
928
|
+
assert.equal(state.classifyCalls ?? 0, 0);
|
|
929
|
+
});
|
|
930
|
+
});
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* error, rule 34). Else: search via the injected search path scoped to
|
|
8
8
|
* the caller's readable namespaces, plus 1-hop entity-graph neighbors of
|
|
9
9
|
* top hits. Cap candidates at `maxAffected`.
|
|
10
|
-
* 2. CLASSIFY + DRAFT via one LLM call (Responses API only, gotcha 1)
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* 2. CLASSIFY + DRAFT via one LLM call (Responses API only, gotcha 1),
|
|
11
|
+
* unless `duplicateOf` is set — that path skips the LLM and drafts one
|
|
12
|
+
* recoverable supersede. On LLM failure → deterministic fallback plan
|
|
13
|
+
* (rule 13): classification `outdated`, `confidence: 0`, empty actions.
|
|
14
|
+
* A caller abort throws instead of falling back.
|
|
13
15
|
* 3. RENDER DIFF by materializing what each action would do, using the
|
|
14
16
|
* injected diff renderer (page-versioning — reuse, don't fork).
|
|
15
17
|
* 4. PERSIST the plan atomically under
|
|
@@ -81,6 +83,16 @@ export interface PlannerDeps {
|
|
|
81
83
|
targetIds: readonly string[];
|
|
82
84
|
namespaces: readonly string[];
|
|
83
85
|
}): Promise<PlannerCandidate[]>;
|
|
86
|
+
/**
|
|
87
|
+
* Inspect one memory for duplicate-mode validation. Absent means "not
|
|
88
|
+
* found" (including outside the readable namespaces). A missing method
|
|
89
|
+
* falls back to {@link resolveTargets}, which reports inactive memories
|
|
90
|
+
* as not found.
|
|
91
|
+
*/
|
|
92
|
+
inspectMemory?(request: {
|
|
93
|
+
memoryId: string;
|
|
94
|
+
namespaces: readonly string[];
|
|
95
|
+
}): Promise<PlannerMemoryInspection | null>;
|
|
84
96
|
/**
|
|
85
97
|
* One-hop entity-graph neighbors of the supplied candidates. Returns the
|
|
86
98
|
* union (deduped by memoryId); neighbors not already in `seedIds` only.
|
|
@@ -99,6 +111,7 @@ export interface PlannerDeps {
|
|
|
99
111
|
classifyAndDraft(request: {
|
|
100
112
|
text: string;
|
|
101
113
|
candidates: PlannerCandidate[];
|
|
114
|
+
abortSignal?: AbortSignal;
|
|
102
115
|
}): Promise<LlmClassificationResult>;
|
|
103
116
|
/**
|
|
104
117
|
* Render a human-readable diff preview for the planned actions, using
|
|
@@ -120,6 +133,13 @@ export interface PlannerDeps {
|
|
|
120
133
|
now(): Date;
|
|
121
134
|
}
|
|
122
135
|
|
|
136
|
+
/** One memory inspected for an explicit duplicate supersede. */
|
|
137
|
+
export interface PlannerMemoryInspection {
|
|
138
|
+
candidate: PlannerCandidate;
|
|
139
|
+
/** Omitted status is treated as active (older files leave it unset). */
|
|
140
|
+
status?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
123
143
|
/** Result of the single LLM classify+draft call. */
|
|
124
144
|
export interface LlmClassificationResult {
|
|
125
145
|
classification: CorrectionClassification;
|
|
@@ -177,10 +197,15 @@ export class CorrectionPlanner {
|
|
|
177
197
|
);
|
|
178
198
|
}
|
|
179
199
|
|
|
180
|
-
// 1. LOCATE
|
|
181
|
-
const
|
|
200
|
+
// 1. LOCATE — duplicateOf skips search and the classifier.
|
|
201
|
+
const explicitDuplicate = cleaned.duplicateOf
|
|
202
|
+
? await this.planExplicitDuplicate(cleaned, namespaces, abortSignal)
|
|
203
|
+
: null;
|
|
204
|
+
const located = explicitDuplicate
|
|
205
|
+
? { candidates: [explicitDuplicate.loser] }
|
|
206
|
+
: await this.locateCandidates(cleaned, namespaces, abortSignal);
|
|
182
207
|
throwIfAborted(abortSignal, "correction planning aborted");
|
|
183
|
-
if (located.candidates.length === 0) {
|
|
208
|
+
if (!explicitDuplicate && located.candidates.length === 0) {
|
|
184
209
|
// No candidates AND no explicit targets → empty plan is valid (the user
|
|
185
210
|
// can still discard). But explicit-target-not-found was already raised
|
|
186
211
|
// inside locateCandidates (rule 34).
|
|
@@ -191,10 +216,13 @@ export class CorrectionPlanner {
|
|
|
191
216
|
}
|
|
192
217
|
|
|
193
218
|
// 2. CLASSIFY + DRAFT
|
|
194
|
-
const llm =
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
219
|
+
const llm = explicitDuplicate
|
|
220
|
+
? explicitDuplicate.llm
|
|
221
|
+
: await this.deps.classifyAndDraft({
|
|
222
|
+
text: cleaned.text,
|
|
223
|
+
candidates: located.candidates,
|
|
224
|
+
abortSignal,
|
|
225
|
+
});
|
|
198
226
|
throwIfAborted(abortSignal, "correction planning aborted");
|
|
199
227
|
|
|
200
228
|
// Validate every action shape before persisting — the adapter is trusted
|
|
@@ -235,6 +263,7 @@ export class CorrectionPlanner {
|
|
|
235
263
|
// absent from the candidate set and warn.
|
|
236
264
|
const candidateIds = new Set(located.candidates.map((c) => c.memoryId));
|
|
237
265
|
if (llm.actions.length > 0 && candidateIds.size > 0) {
|
|
266
|
+
const explicitWinner = cleaned.duplicateOf;
|
|
238
267
|
const filtered = llm.actions.filter((action) => {
|
|
239
268
|
const id =
|
|
240
269
|
action.kind === "supersede"
|
|
@@ -242,7 +271,19 @@ export class CorrectionPlanner {
|
|
|
242
271
|
: action.kind === "edit" || action.kind === "retract" || action.kind === "rescope"
|
|
243
272
|
? action.memoryId
|
|
244
273
|
: null;
|
|
245
|
-
|
|
274
|
+
if (id !== null && !candidateIds.has(id)) return false;
|
|
275
|
+
// A model-picked winner must be a located candidate. duplicateOf is
|
|
276
|
+
// the exception: that winner was validated as an active memory and is
|
|
277
|
+
// intentionally not part of the mutated set.
|
|
278
|
+
if (
|
|
279
|
+
action.kind === "supersede" &&
|
|
280
|
+
action.winnerId &&
|
|
281
|
+
action.winnerId !== explicitWinner &&
|
|
282
|
+
!candidateIds.has(action.winnerId)
|
|
283
|
+
) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
return true;
|
|
246
287
|
});
|
|
247
288
|
if (filtered.length < llm.actions.length) {
|
|
248
289
|
const dropped = llm.actions.length - filtered.length;
|
|
@@ -290,6 +331,71 @@ export class CorrectionPlanner {
|
|
|
290
331
|
return this.persist(plan, abortSignal);
|
|
291
332
|
}
|
|
292
333
|
|
|
334
|
+
private async planExplicitDuplicate(
|
|
335
|
+
request: CorrectionRequest,
|
|
336
|
+
namespaces: readonly string[],
|
|
337
|
+
abortSignal?: AbortSignal,
|
|
338
|
+
): Promise<{ loser: PlannerCandidate; llm: LlmClassificationResult }> {
|
|
339
|
+
const winnerId = request.duplicateOf;
|
|
340
|
+
if (!winnerId || !request.targetIds || request.targetIds.length !== 1) {
|
|
341
|
+
throw new CorrectionContractError(
|
|
342
|
+
"CorrectionRequest.duplicateOf requires exactly one targetIds entry (the duplicate memory).",
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
if (request.targetIds[0] === winnerId) {
|
|
346
|
+
throw new CorrectionContractError(
|
|
347
|
+
"CorrectionRequest.duplicateOf must be a different memory from targetIds.",
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
const loserId = request.targetIds[0];
|
|
351
|
+
const loser = await this.inspectDuplicateParty(loserId, namespaces, "target");
|
|
352
|
+
throwIfAborted(abortSignal, "correction planning aborted");
|
|
353
|
+
await this.inspectDuplicateParty(winnerId, namespaces, "duplicateOf");
|
|
354
|
+
throwIfAborted(abortSignal, "correction planning aborted");
|
|
355
|
+
return {
|
|
356
|
+
loser,
|
|
357
|
+
llm: {
|
|
358
|
+
classification: "outdated",
|
|
359
|
+
confidence: 1,
|
|
360
|
+
actions: [{ kind: "supersede", loserId, winnerId }],
|
|
361
|
+
relevance: [{
|
|
362
|
+
memoryId: loserId,
|
|
363
|
+
why: `duplicate of ${winnerId}; recoverable supersede`,
|
|
364
|
+
}],
|
|
365
|
+
warnings: [],
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private async inspectDuplicateParty(
|
|
371
|
+
memoryId: string,
|
|
372
|
+
namespaces: readonly string[],
|
|
373
|
+
role: "target" | "duplicateOf",
|
|
374
|
+
): Promise<PlannerCandidate> {
|
|
375
|
+
const notFound = role === "target"
|
|
376
|
+
? `target memory not found: ${memoryId}`
|
|
377
|
+
: `duplicateOf memory not found: ${memoryId}`;
|
|
378
|
+
const inactive = (status: string) => role === "target"
|
|
379
|
+
? `target memory is not active: ${memoryId} (${status})`
|
|
380
|
+
: `duplicateOf memory is not active: ${memoryId} (${status})`;
|
|
381
|
+
if (this.deps.inspectMemory) {
|
|
382
|
+
const found = await this.deps.inspectMemory({ memoryId, namespaces });
|
|
383
|
+
if (!found) throw new CorrectionContractError(notFound);
|
|
384
|
+
const status = found.status && found.status.length > 0 ? found.status : "active";
|
|
385
|
+
if (status !== "active") throw new CorrectionContractError(inactive(status));
|
|
386
|
+
return found.candidate;
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
const resolved = await this.deps.resolveTargets({ targetIds: [memoryId], namespaces });
|
|
390
|
+
const candidate = resolved.find((item) => item.memoryId === memoryId) ?? resolved[0];
|
|
391
|
+
if (!candidate) throw new CorrectionContractError(notFound);
|
|
392
|
+
return candidate;
|
|
393
|
+
} catch (err) {
|
|
394
|
+
if (err instanceof CorrectionContractError) throw new CorrectionContractError(notFound);
|
|
395
|
+
throw err;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
293
399
|
private async locateCandidates(
|
|
294
400
|
request: CorrectionRequest,
|
|
295
401
|
namespaces: readonly string[],
|
|
@@ -39,7 +39,7 @@ function extractToolNames(response: unknown): Set<string> {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
interface MockCalls {
|
|
42
|
-
planRequests: Array<{ text: string; targetIds?: string[]; principal?: string; abortSignal?: AbortSignal }>;
|
|
42
|
+
planRequests: Array<{ text: string; targetIds?: string[]; duplicateOf?: string; principal?: string; abortSignal?: AbortSignal }>;
|
|
43
43
|
applyRequests: Array<{ planId: string; confirm: boolean; principal?: string; abortSignal?: AbortSignal }>;
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -54,6 +54,7 @@ function makeMockService(
|
|
|
54
54
|
req: {
|
|
55
55
|
text: string;
|
|
56
56
|
targetIds?: string[];
|
|
57
|
+
duplicateOf?: string;
|
|
57
58
|
principal?: string;
|
|
58
59
|
},
|
|
59
60
|
opts?: { abortSignal?: AbortSignal },
|
|
@@ -128,6 +129,26 @@ test("tools/list gate: correction tools present when correctionVisible on (defau
|
|
|
128
129
|
assert.equal(names.has("engram.memory_correct_apply"), true, "apply tool must be present by default");
|
|
129
130
|
assert.equal(names.has("remnic_memory_correct_plan"), true, "plan alias must be present");
|
|
130
131
|
assert.equal(names.has("remnic_memory_correct_apply"), true, "apply alias must be present");
|
|
132
|
+
const tools = (response as { result?: { tools?: Array<{ name?: string; inputSchema?: { properties?: Record<string, unknown> } }> } }).result?.tools ?? [];
|
|
133
|
+
const planTool = tools.find((tool) => tool.name === "engram.memory_correct_plan");
|
|
134
|
+
assert.ok(planTool?.inputSchema?.properties?.duplicateOf, "plan schema must advertise duplicateOf");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("MCP surface: memory_correct_plan forwards duplicateOf", async () => {
|
|
138
|
+
const { service, calls } = makeMockService(STUB_PLAN, STUB_OUTCOME);
|
|
139
|
+
const server = new EngramMcpServer(service, { emitLegacyTools: true, correctionVisible: true });
|
|
140
|
+
await server.handleRequest({
|
|
141
|
+
jsonrpc: "2.0",
|
|
142
|
+
id: 2,
|
|
143
|
+
method: "tools/call",
|
|
144
|
+
params: {
|
|
145
|
+
name: "engram.memory_correct_plan",
|
|
146
|
+
arguments: { text: "duplicate", targetIds: ["mem-loser"], duplicateOf: "mem-winner" },
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
assert.equal(calls.planRequests.length, 1);
|
|
150
|
+
assert.deepEqual(calls.planRequests[0]?.targetIds, ["mem-loser"]);
|
|
151
|
+
assert.equal(calls.planRequests[0]?.duplicateOf, "mem-winner");
|
|
131
152
|
});
|
|
132
153
|
|
|
133
154
|
// ---------------------------------------------------------------------------
|
|
@@ -188,6 +188,42 @@ test("LocalLlmClient allows recall-critical work to run while background work is
|
|
|
188
188
|
}
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
+
test("LocalLlmClient drops a queued background request when its signal aborts", async () => {
|
|
192
|
+
const client = new LocalLlmClient(createConfig());
|
|
193
|
+
let runs = 0;
|
|
194
|
+
let releaseHeld: (() => void) | undefined;
|
|
195
|
+
(client as any).runChatCompletionRequest = async () => {
|
|
196
|
+
runs += 1;
|
|
197
|
+
await new Promise<void>((resolve) => {
|
|
198
|
+
releaseHeld = resolve;
|
|
199
|
+
});
|
|
200
|
+
return {
|
|
201
|
+
content: "held",
|
|
202
|
+
usage: { promptTokens: 1, completionTokens: 1, totalTokens: 2 },
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const held = client.chatCompletion(
|
|
207
|
+
[{ role: "user", content: "hold" }],
|
|
208
|
+
{ operation: "hold", priority: "background" },
|
|
209
|
+
);
|
|
210
|
+
await tick();
|
|
211
|
+
assert.equal(runs, 1);
|
|
212
|
+
|
|
213
|
+
const controller = new AbortController();
|
|
214
|
+
const queued = client.chatCompletion(
|
|
215
|
+
[{ role: "user", content: "queued" }],
|
|
216
|
+
{ operation: "queued", priority: "background", signal: controller.signal },
|
|
217
|
+
);
|
|
218
|
+
controller.abort();
|
|
219
|
+
assert.equal(await queued, null);
|
|
220
|
+
assert.equal(runs, 1);
|
|
221
|
+
assert.equal((client as any).requestQueues.background.length, 0);
|
|
222
|
+
|
|
223
|
+
releaseHeld?.();
|
|
224
|
+
assert.equal((await held)?.content, "held");
|
|
225
|
+
});
|
|
226
|
+
|
|
191
227
|
test("LocalLlmClient skips queued work once a cooldown is activated", async () => {
|
|
192
228
|
const client = new LocalLlmClient(createConfig());
|
|
193
229
|
const executionOrder: string[] = [];
|
package/src/local-llm.ts
CHANGED
|
@@ -1351,13 +1351,30 @@ export class LocalLlmClient {
|
|
|
1351
1351
|
if (options.priority) {
|
|
1352
1352
|
const priority = options.priority;
|
|
1353
1353
|
return await new Promise<LocalLlmChatCompletionResult | null>((resolve) => {
|
|
1354
|
-
|
|
1354
|
+
let settled = false;
|
|
1355
|
+
let onAbort: (() => void) | undefined;
|
|
1356
|
+
const finish = (value: LocalLlmChatCompletionResult | null): void => {
|
|
1357
|
+
if (settled) return;
|
|
1358
|
+
settled = true;
|
|
1359
|
+
if (onAbort) options.signal?.removeEventListener("abort", onAbort);
|
|
1360
|
+
resolve(value);
|
|
1361
|
+
};
|
|
1362
|
+
const queued: LocalLlmQueuedRequest = {
|
|
1355
1363
|
messages,
|
|
1356
1364
|
options,
|
|
1357
1365
|
priority,
|
|
1358
1366
|
enqueuedAtMs: Date.now(),
|
|
1359
|
-
resolve,
|
|
1360
|
-
}
|
|
1367
|
+
resolve: finish,
|
|
1368
|
+
};
|
|
1369
|
+
onAbort = () => {
|
|
1370
|
+
const queue = this.requestQueues[priority];
|
|
1371
|
+
const index = queue.indexOf(queued);
|
|
1372
|
+
if (index >= 0) queue.splice(index, 1);
|
|
1373
|
+
finish(null);
|
|
1374
|
+
};
|
|
1375
|
+
this.requestQueues[priority].push(queued);
|
|
1376
|
+
if (options.signal?.aborted) onAbort();
|
|
1377
|
+
else if (options.signal) options.signal.addEventListener("abort", onAbort, { once: true });
|
|
1361
1378
|
this.scheduleQueueDrain();
|
|
1362
1379
|
});
|
|
1363
1380
|
}
|
|
@@ -632,21 +632,6 @@ export class SessionContextCoordinator {
|
|
|
632
632
|
const principal = req.principal || resolvePrincipal(req.sessionKey, this.deps.config);
|
|
633
633
|
return canWriteNamespace(principal, req.namespace, this.deps.config);
|
|
634
634
|
},
|
|
635
|
-
llmComplete: async ({ system, user }) => {
|
|
636
|
-
const llmResult = await this.deps.localLlm.chatCompletion(
|
|
637
|
-
[
|
|
638
|
-
{ role: "system", content: system },
|
|
639
|
-
{ role: "user", content: user },
|
|
640
|
-
],
|
|
641
|
-
{ operation: "correction-classify", priority: "background" },
|
|
642
|
-
);
|
|
643
|
-
if (!llmResult) {
|
|
644
|
-
throw new Error(
|
|
645
|
-
"passive correction classify+draft: local LLM unavailable (disabled or in cooldown)",
|
|
646
|
-
);
|
|
647
|
-
}
|
|
648
|
-
return llmResult.content;
|
|
649
|
-
},
|
|
650
635
|
});
|
|
651
636
|
return this.deps._passiveCorrectionService;
|
|
652
637
|
}
|
|
@@ -397,6 +397,30 @@ test("correctionPlan: a handle in targetIds is resolved before planning; an unkn
|
|
|
397
397
|
}
|
|
398
398
|
});
|
|
399
399
|
|
|
400
|
+
test("correctionPlan: a handle in duplicateOf is resolved before planning", async () => {
|
|
401
|
+
const { service, memoryDir } = await makeHarness({
|
|
402
|
+
recallMemoryHandles: true,
|
|
403
|
+
correctionEnabled: true,
|
|
404
|
+
});
|
|
405
|
+
try {
|
|
406
|
+
await assert.rejects(
|
|
407
|
+
() =>
|
|
408
|
+
service.correctionPlan({
|
|
409
|
+
text: "that memory is a duplicate",
|
|
410
|
+
targetIds: ["fact-loser"],
|
|
411
|
+
duplicateOf: "[m:dead]",
|
|
412
|
+
sessionKey: SESSION,
|
|
413
|
+
}),
|
|
414
|
+
(err: unknown) =>
|
|
415
|
+
err instanceof EngramAccessInputError &&
|
|
416
|
+
/not found/i.test((err as Error).message) &&
|
|
417
|
+
(err as Error).message.includes("[m:dead]"),
|
|
418
|
+
);
|
|
419
|
+
} finally {
|
|
420
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
400
424
|
test("correctionPlan: a resolved handle targetIds set reaches the planner as concrete ids (round-trip)", async () => {
|
|
401
425
|
const { orchestrator, service, memoryDir } = await makeHarness({
|
|
402
426
|
recallMemoryHandles: true,
|
package/src/types.ts
CHANGED
|
@@ -916,33 +916,16 @@ export interface PluginConfig
|
|
|
916
916
|
* deployments prefer the older (always-inject) behavior. Default `false`.
|
|
917
917
|
*/
|
|
918
918
|
temporalExpiredInInjection: boolean;
|
|
919
|
-
/**
|
|
920
|
-
* Correction Contract (issue #1580) — one plan/apply pipeline for memory
|
|
921
|
-
* corrections. Master visibility + enforcement gate for the
|
|
922
|
-
* `memory_correct_plan` / `memory_correct_apply` tools. Default `true`:
|
|
923
|
-
* planning is read-only (locates candidates + drafts a pending plan file)
|
|
924
|
-
* and safe to ship on; `false` hides the tools and rejects runtime calls so
|
|
925
|
-
* the surface cannot drift out of sync. Parsed from `correction.enabled`
|
|
926
|
-
* (nested) or `correctionEnabled` (flat legacy).
|
|
927
|
-
*/
|
|
919
|
+
/** Correction Contract (#1580). `correction.enabled` / `correctionEnabled`. Default true. */
|
|
928
920
|
correctionEnabled: boolean;
|
|
929
|
-
/**
|
|
930
|
-
* Whether `memory_correct_apply` requires an explicit `confirm: true` to
|
|
931
|
-
* mutate memories. Default `true` (least-privileged). Parsed from
|
|
932
|
-
* `correction.applyRequiresConfirm` / `correctionApplyRequiresConfirm`.
|
|
933
|
-
*/
|
|
921
|
+
/** Apply requires `confirm: true`. `correction.applyRequiresConfirm`. Default true. */
|
|
934
922
|
correctionApplyRequiresConfirm: boolean;
|
|
935
|
-
/**
|
|
936
|
-
* Max memories a single correction plan may touch; over-limit plans are
|
|
937
|
-
* rejected, never silently truncated (review thread). Default `10`. Parsed
|
|
938
|
-
* from `correction.maxAffected` / `correctionMaxAffected`.
|
|
939
|
-
*/
|
|
923
|
+
/** Max memories one plan may touch. `correction.maxAffected`. Default 10. */
|
|
940
924
|
correctionMaxAffected: number;
|
|
941
|
-
/**
|
|
942
|
-
* Plan time-to-live in hours; expired plans are rejected at apply. Default
|
|
943
|
-
* `24`. Parsed from `correction.planTtlHours` / `correctionPlanTtlHours`.
|
|
944
|
-
*/
|
|
925
|
+
/** Plan TTL hours. `correction.planTtlHours`. Default 24. */
|
|
945
926
|
correctionPlanTtlHours: number;
|
|
927
|
+
/** Per-attempt classify timeout (ms, 1..300000). `correction.classifyTimeoutMs`. Default 45000. */
|
|
928
|
+
correctionClassifyTimeoutMs: number;
|
|
946
929
|
/**
|
|
947
930
|
* Passive correction capture (issue #1581) — detects corrections expressed
|
|
948
931
|
* passively in conversation turns during extraction and routes them to the
|