@remnic/core 10.0.4 → 10.0.6
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 +49 -49
- package/dist/access-authorization-probe.js +50 -50
- package/dist/access-boundary.js +49 -49
- package/dist/access-cli.d.ts.map +1 -1
- package/dist/access-cli.js +120 -115
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.js +49 -49
- package/dist/access-http-briefing.js +50 -50
- package/dist/access-http-query.js +51 -51
- package/dist/access-http.d.ts.map +1 -1
- package/dist/access-http.js +72 -71
- package/dist/access-identity-continuity-surface.js +49 -49
- package/dist/access-lcm-surface.js +49 -49
- 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 +63 -62
- package/dist/access-namespace-preflight.js +49 -49
- package/dist/access-observe-write-category.js +3 -3
- package/dist/access-observe-write-surface.js +49 -49
- package/dist/access-offline-manifest.js +5 -5
- package/dist/access-operations-batch.js +52 -52
- package/dist/access-operations.d.ts +1 -0
- package/dist/access-operations.d.ts.map +1 -1
- package/dist/access-operations.js +59 -59
- package/dist/access-recall-concurrency.js +49 -49
- package/dist/access-recall-diagnostics.js +2 -2
- package/dist/access-recall-response.js +49 -49
- package/dist/access-recall-surface.js +49 -49
- package/dist/access-service.d.ts.map +1 -1
- package/dist/access-service.js +49 -49
- package/dist/access-surface-catalog.js +51 -51
- 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/calibration.d.ts +31 -3
- package/dist/calibration.d.ts.map +1 -1
- package/dist/calibration.js +67 -16
- package/dist/calibration.js.map +1 -1
- package/dist/causal-consolidation.js +18 -18
- package/dist/{chunk-5YDWSWBU.js → chunk-2CP577K6.js} +2 -2
- package/dist/{chunk-QVBB2CLG.js → chunk-2UF42WXV.js} +27 -26
- package/dist/chunk-2UF42WXV.js.map +1 -0
- package/dist/{chunk-ABET5LA5.js → chunk-37HCPM56.js} +23 -23
- package/dist/{chunk-HSZBALHV.js → chunk-4JNTGPBR.js} +2 -2
- package/dist/{chunk-H6FA36LC.js → chunk-4K2H4KTR.js} +5 -5
- 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-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-MEMH5NKJ.js → chunk-6C76FZRM.js} +2 -2
- package/dist/{chunk-3QAIX7TV.js → chunk-6KHF6PAB.js} +2 -2
- package/dist/{chunk-MYBCB4AT.js → chunk-6T42L5FI.js} +2 -2
- package/dist/{chunk-XTWF5XR6.js → chunk-7CZIKCJR.js} +69 -69
- package/dist/{chunk-R6EOFKZG.js → chunk-7LYRFI5A.js} +2 -2
- package/dist/{chunk-4NFRYOHT.js → chunk-ANZXWOYD.js} +2 -2
- package/dist/{chunk-5XXTZ4Y7.js → chunk-B657ZGDH.js} +2 -2
- package/dist/{chunk-5QHTQMRL.js → chunk-CCMXCZLT.js} +8 -6
- package/dist/chunk-CCMXCZLT.js.map +1 -0
- package/dist/{chunk-NSQ3GUVO.js → chunk-CYEK6Z3P.js} +4 -4
- package/dist/{chunk-4DXVWBDB.js → chunk-D6M3VKMY.js} +4 -10
- package/dist/chunk-D6M3VKMY.js.map +1 -0
- package/dist/{chunk-IRYKRBEX.js → chunk-DJMVSUPK.js} +3 -3
- package/dist/{chunk-7OOW72QA.js → chunk-DTXN4NVD.js} +307 -138
- package/dist/chunk-DTXN4NVD.js.map +1 -0
- 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-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-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-ABDTBGI2.js → chunk-KCCZFDJD.js} +2 -2
- 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-ZOGZMAGI.js → chunk-KQT4DRKF.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-QDKABMUE.js → chunk-MH2DXDN6.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-BDHWKFGM.js → chunk-OFEHITAZ.js} +2 -2
- package/dist/{chunk-RYN4AIRM.js → chunk-OZABSIOV.js} +4 -4
- package/dist/{chunk-IXF7TRMQ.js → chunk-Q562GF7J.js} +2 -2
- package/dist/{chunk-H2XCCUOT.js → chunk-QEPL7KIZ.js} +15 -15
- package/dist/{chunk-WLC6XA6C.js → chunk-QGLEM46C.js} +16 -53
- package/dist/chunk-QGLEM46C.js.map +1 -0
- 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-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-DYRXRQHV.js → chunk-TL4PM7PC.js} +2 -2
- package/dist/{chunk-TSEUVMJL.js → chunk-TRBLJU6Y.js} +39 -39
- package/dist/{chunk-VWCFR4NR.js → chunk-U6OXJQEH.js} +5 -5
- package/dist/{chunk-YMXCJ7SE.js → chunk-VUEG3TZ4.js} +2 -2
- package/dist/{chunk-ORAL4HJC.js → chunk-WG4TMZJ2.js} +2 -2
- package/dist/{chunk-CV5AMCPX.js → chunk-XPRAOTJY.js} +2 -2
- package/dist/{chunk-RZCRXCLI.js → chunk-XQSJXNWC.js} +2 -2
- package/dist/{chunk-GLS3A4SQ.js → chunk-YGPXV546.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 +92 -91
- package/dist/coding/export-okf-codegraph.js +16 -16
- package/dist/compounding/engine.d.ts.map +1 -1
- 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 +50 -50
- package/dist/extraction-span-materialize.js +2 -2
- package/dist/extraction.js +25 -25
- package/dist/harmonic-retrieval.js +17 -17
- package/dist/index.js +192 -191
- 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 +50 -50
- 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 +116 -115
- 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 +50 -50
- package/dist/recall-navigation.js +49 -49
- 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 +66 -66
- 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/calibration-local-llm.test.ts +361 -0
- package/src/calibration.ts +109 -26
- package/src/compounding/engine.ts +2 -8
- 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-4DXVWBDB.js.map +0 -1
- 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-HSZBALHV.js.map → chunk-4JNTGPBR.js.map} +0 -0
- /package/dist/{chunk-H6FA36LC.js.map → chunk-4K2H4KTR.js.map} +0 -0
- /package/dist/{chunk-H7BZBSP6.js.map → chunk-52II7PB6.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-MEMH5NKJ.js.map → chunk-6C76FZRM.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-XTWF5XR6.js.map → chunk-7CZIKCJR.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-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-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-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-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-ABDTBGI2.js.map → chunk-KCCZFDJD.js.map} +0 -0
- /package/dist/{chunk-IINXHPCV.js.map → chunk-KQI2PKYP.js.map} +0 -0
- /package/dist/{chunk-ZOGZMAGI.js.map → chunk-KQT4DRKF.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-QDKABMUE.js.map → chunk-MH2DXDN6.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-BDHWKFGM.js.map → chunk-OFEHITAZ.js.map} +0 -0
- /package/dist/{chunk-RYN4AIRM.js.map → chunk-OZABSIOV.js.map} +0 -0
- /package/dist/{chunk-IXF7TRMQ.js.map → chunk-Q562GF7J.js.map} +0 -0
- /package/dist/{chunk-H2XCCUOT.js.map → chunk-QEPL7KIZ.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-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-DYRXRQHV.js.map → chunk-TL4PM7PC.js.map} +0 -0
- /package/dist/{chunk-TSEUVMJL.js.map → chunk-TRBLJU6Y.js.map} +0 -0
- /package/dist/{chunk-VWCFR4NR.js.map → chunk-U6OXJQEH.js.map} +0 -0
- /package/dist/{chunk-YMXCJ7SE.js.map → chunk-VUEG3TZ4.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-GLS3A4SQ.js.map → chunk-YGPXV546.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
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Correction classify completion: short timeout, caller abort, gateway fallback.
|
|
3
|
+
*/
|
|
4
|
+
import { strict as assert } from "node:assert";
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
|
|
7
|
+
import { isAbortError } from "../abort-error.js";
|
|
8
|
+
import type { PluginConfig } from "../types.js";
|
|
9
|
+
import { type CorrectionClassifyDeps, completeCorrectionClassify } from "./correction-classify.js";
|
|
10
|
+
|
|
11
|
+
const MESSAGES = [{ role: "user" as const, content: "the old fact is a duplicate" }];
|
|
12
|
+
|
|
13
|
+
function config(over: Partial<PluginConfig> = {}): PluginConfig {
|
|
14
|
+
return {
|
|
15
|
+
localLlmEnabled: true,
|
|
16
|
+
localLlmFallback: true,
|
|
17
|
+
modelSource: "plugin",
|
|
18
|
+
correctionClassifyTimeoutMs: 40,
|
|
19
|
+
...over,
|
|
20
|
+
} as PluginConfig;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function hangUntilAbort(): CorrectionClassifyDeps["localLlm"] {
|
|
24
|
+
return {
|
|
25
|
+
chatCompletion: (_messages, options) =>
|
|
26
|
+
new Promise((resolve) => {
|
|
27
|
+
const finish = () => resolve(null);
|
|
28
|
+
if (options?.signal?.aborted) {
|
|
29
|
+
finish();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
options?.signal?.addEventListener("abort", finish, { once: true });
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
test("classify falls back after the local attempt times out", async () => {
|
|
38
|
+
let fallbackCalls = 0;
|
|
39
|
+
let localSignal: AbortSignal | undefined;
|
|
40
|
+
const deps: CorrectionClassifyDeps = {
|
|
41
|
+
config: config(),
|
|
42
|
+
localLlm: {
|
|
43
|
+
chatCompletion: (_messages, options) =>
|
|
44
|
+
new Promise((resolve) => {
|
|
45
|
+
localSignal = options?.signal;
|
|
46
|
+
const finish = () => resolve(null);
|
|
47
|
+
if (options?.signal?.aborted) {
|
|
48
|
+
finish();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
options?.signal?.addEventListener("abort", finish, { once: true });
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
54
|
+
fallbackChat: async () => {
|
|
55
|
+
fallbackCalls += 1;
|
|
56
|
+
return { content: '{"classification":"outdated","confidence":0.8,"actions":[]}', modelUsed: "gateway" };
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
const content = await completeCorrectionClassify(MESSAGES, deps);
|
|
60
|
+
assert.equal(fallbackCalls, 1);
|
|
61
|
+
assert.equal(localSignal?.aborted, true);
|
|
62
|
+
assert.match(content ?? "", /outdated/);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("classify cancels on caller abort and does not call the fallback", async () => {
|
|
66
|
+
let fallbackCalls = 0;
|
|
67
|
+
const caller = new AbortController();
|
|
68
|
+
const deps: CorrectionClassifyDeps = {
|
|
69
|
+
config: config({ correctionClassifyTimeoutMs: 30_000 }),
|
|
70
|
+
localLlm: hangUntilAbort(),
|
|
71
|
+
fallbackChat: async () => {
|
|
72
|
+
fallbackCalls += 1;
|
|
73
|
+
return { content: "should-not-run", modelUsed: "gateway" };
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
const pending = completeCorrectionClassify(MESSAGES, deps, caller.signal);
|
|
77
|
+
caller.abort();
|
|
78
|
+
await assert.rejects(pending, (err: unknown) => isAbortError(err));
|
|
79
|
+
assert.equal(fallbackCalls, 0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("classify uses the fallback when the local LLM returns null", async () => {
|
|
83
|
+
let localCalls = 0;
|
|
84
|
+
let fallbackCalls = 0;
|
|
85
|
+
const deps: CorrectionClassifyDeps = {
|
|
86
|
+
config: config(),
|
|
87
|
+
localLlm: {
|
|
88
|
+
chatCompletion: async () => {
|
|
89
|
+
localCalls += 1;
|
|
90
|
+
return null;
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
fallbackChat: async () => {
|
|
94
|
+
fallbackCalls += 1;
|
|
95
|
+
return { content: '{"ok":true}', modelUsed: "gateway" };
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
const content = await completeCorrectionClassify(MESSAGES, deps);
|
|
99
|
+
assert.equal(localCalls, 1);
|
|
100
|
+
assert.equal(fallbackCalls, 1);
|
|
101
|
+
assert.equal(content, '{"ok":true}');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("classify does not call the fallback when the local LLM returns content", async () => {
|
|
105
|
+
let fallbackCalls = 0;
|
|
106
|
+
const deps: CorrectionClassifyDeps = {
|
|
107
|
+
config: config(),
|
|
108
|
+
localLlm: { chatCompletion: async () => ({ content: "local-json" }) },
|
|
109
|
+
fallbackChat: async () => {
|
|
110
|
+
fallbackCalls += 1;
|
|
111
|
+
return { content: "gateway", modelUsed: "gateway" };
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
assert.equal(await completeCorrectionClassify(MESSAGES, deps), "local-json");
|
|
115
|
+
assert.equal(fallbackCalls, 0);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("gateway modelSource skips the local LLM", async () => {
|
|
119
|
+
let localCalls = 0;
|
|
120
|
+
const deps: CorrectionClassifyDeps = {
|
|
121
|
+
config: config({ modelSource: "gateway", localLlmEnabled: true }),
|
|
122
|
+
localLlm: {
|
|
123
|
+
chatCompletion: async () => {
|
|
124
|
+
localCalls += 1;
|
|
125
|
+
return { content: "local" };
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
fallbackChat: async () => ({ content: "gateway-primary", modelUsed: "gateway" }),
|
|
129
|
+
};
|
|
130
|
+
assert.equal(await completeCorrectionClassify(MESSAGES, deps), "gateway-primary");
|
|
131
|
+
assert.equal(localCalls, 0);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("no configured gateway models return null without waiting out the classify timeout", async () => {
|
|
135
|
+
const started = Date.now();
|
|
136
|
+
const content = await completeCorrectionClassify(MESSAGES, {
|
|
137
|
+
config: config({ localLlmEnabled: false }),
|
|
138
|
+
localLlm: {
|
|
139
|
+
chatCompletion: async () => {
|
|
140
|
+
throw new Error("local must not run");
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
assert.equal(content, null);
|
|
145
|
+
assert.ok(Date.now() - started < 1000, `no-models path took ${Date.now() - started}ms`);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("a fallback attempt timeout returns null instead of an abort", async () => {
|
|
149
|
+
const deps: CorrectionClassifyDeps = {
|
|
150
|
+
config: config({ localLlmEnabled: false, correctionClassifyTimeoutMs: 40 }),
|
|
151
|
+
localLlm: {
|
|
152
|
+
chatCompletion: async () => {
|
|
153
|
+
throw new Error("local must not run");
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
fallbackChat: (_messages, options) =>
|
|
157
|
+
new Promise((_resolve, reject) => {
|
|
158
|
+
const fail = () => {
|
|
159
|
+
const err = new Error("fallback attempt aborted");
|
|
160
|
+
err.name = "AbortError";
|
|
161
|
+
reject(err);
|
|
162
|
+
};
|
|
163
|
+
if (options.signal?.aborted) {
|
|
164
|
+
fail();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
options.signal?.addEventListener("abort", fail, { once: true });
|
|
168
|
+
}),
|
|
169
|
+
};
|
|
170
|
+
assert.equal(await completeCorrectionClassify(MESSAGES, deps), null);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("localLlmFallback false does not call the gateway after a local miss", async () => {
|
|
174
|
+
let fallbackCalls = 0;
|
|
175
|
+
const deps: CorrectionClassifyDeps = {
|
|
176
|
+
config: config({ localLlmFallback: false }),
|
|
177
|
+
localLlm: { chatCompletion: async () => null },
|
|
178
|
+
fallbackChat: async () => {
|
|
179
|
+
fallbackCalls += 1;
|
|
180
|
+
return { content: "nope", modelUsed: "gateway" };
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
await assert.rejects(() => completeCorrectionClassify(MESSAGES, deps), /fallback disabled/);
|
|
184
|
+
assert.equal(fallbackCalls, 0);
|
|
185
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Correction classify completion: a short per-attempt budget, caller abort,
|
|
3
|
+
* and the same gateway/fallback client extraction uses when the local LLM
|
|
4
|
+
* is down or `modelSource` is `gateway`.
|
|
5
|
+
*
|
|
6
|
+
* Each attempt (local, then gateway) gets `correctionClassifyTimeoutMs`.
|
|
7
|
+
* A caller abort cancels the in-flight attempt and does not fall through.
|
|
8
|
+
* A local timeout, null, or error falls through when `localLlmFallback` is
|
|
9
|
+
* not false. Gateway mode skips the local client and uses the task chain
|
|
10
|
+
* as the primary path.
|
|
11
|
+
*/
|
|
12
|
+
import { abortError, isAbortError, throwIfAborted } from "../abort-error.js";
|
|
13
|
+
import { resolveLocalLlmCapabilities } from "../capabilities.js";
|
|
14
|
+
import { fallbackLlmRuntimeContextFromConfig } from "../fallback-llm-codex-subscription.js";
|
|
15
|
+
import {
|
|
16
|
+
FallbackLlmClient,
|
|
17
|
+
type FallbackLlmOptions,
|
|
18
|
+
type FallbackLlmResponse,
|
|
19
|
+
gatewayTaskChainOptions,
|
|
20
|
+
} from "../fallback-llm.js";
|
|
21
|
+
import type { LocalLlmChatCompletionOptions } from "../local-llm-helpers.js";
|
|
22
|
+
import type { PluginConfig } from "../types.js";
|
|
23
|
+
import { DEFAULT_CORRECTION_CLASSIFY_TIMEOUT_MS, MAX_CORRECTION_CLASSIFY_TIMEOUT_MS } from "./correction-config.js";
|
|
24
|
+
|
|
25
|
+
export interface CorrectionClassifyMessage {
|
|
26
|
+
role: "system" | "user" | "assistant";
|
|
27
|
+
content: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CorrectionClassifyDeps {
|
|
31
|
+
config: PluginConfig;
|
|
32
|
+
localLlm: {
|
|
33
|
+
chatCompletion(
|
|
34
|
+
messages: Array<{ role: string; content: string }>,
|
|
35
|
+
options?: LocalLlmChatCompletionOptions
|
|
36
|
+
): Promise<{ content: string } | null>;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Test seam. Production builds a {@link FallbackLlmClient} from config.
|
|
40
|
+
* Returning null is the no-models fast path.
|
|
41
|
+
*/
|
|
42
|
+
fallbackChat?(
|
|
43
|
+
messages: CorrectionClassifyMessage[],
|
|
44
|
+
options: FallbackLlmOptions
|
|
45
|
+
): Promise<FallbackLlmResponse | null>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function completeCorrectionClassify(
|
|
49
|
+
messages: CorrectionClassifyMessage[],
|
|
50
|
+
deps: CorrectionClassifyDeps,
|
|
51
|
+
abortSignal?: AbortSignal
|
|
52
|
+
): Promise<string | null> {
|
|
53
|
+
throwIfAborted(abortSignal, "correction classify aborted");
|
|
54
|
+
const timeoutMs = classifyTimeoutMs(deps.config);
|
|
55
|
+
const gatewayPrimary = deps.config.modelSource === "gateway";
|
|
56
|
+
const localEnabled = resolveLocalLlmCapabilities(deps.config).localLlm === true && !gatewayPrimary;
|
|
57
|
+
|
|
58
|
+
if (localEnabled) {
|
|
59
|
+
const localAttempt = startAttempt(abortSignal, timeoutMs);
|
|
60
|
+
try {
|
|
61
|
+
const local = await deps.localLlm.chatCompletion(messages, {
|
|
62
|
+
operation: "correction-classify",
|
|
63
|
+
priority: "background",
|
|
64
|
+
timeoutMs,
|
|
65
|
+
temperature: 0.2,
|
|
66
|
+
signal: localAttempt.signal,
|
|
67
|
+
});
|
|
68
|
+
if (abortSignal?.aborted) throw abortError("correction classify aborted");
|
|
69
|
+
if (local?.content) return local.content;
|
|
70
|
+
if (deps.config.localLlmFallback === false) {
|
|
71
|
+
throw new Error("correction classify: local LLM unavailable and fallback disabled");
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (abortSignal?.aborted) {
|
|
75
|
+
throw isAbortError(err) ? err : abortError("correction classify aborted");
|
|
76
|
+
}
|
|
77
|
+
// A timeout abort belongs to this attempt. Fall through unless the
|
|
78
|
+
// operator turned the gateway path off.
|
|
79
|
+
if (deps.config.localLlmFallback === false) {
|
|
80
|
+
if (isAbortError(err)) {
|
|
81
|
+
throw new Error("correction classify: local LLM timed out and fallback disabled");
|
|
82
|
+
}
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
} finally {
|
|
86
|
+
localAttempt.dispose();
|
|
87
|
+
}
|
|
88
|
+
} else if (!gatewayPrimary && deps.config.localLlmFallback === false) {
|
|
89
|
+
throw new Error("correction classify: local LLM disabled and fallback disabled");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
throwIfAborted(abortSignal, "correction classify aborted");
|
|
93
|
+
const fallbackAttempt = startAttempt(abortSignal, timeoutMs);
|
|
94
|
+
try {
|
|
95
|
+
const fallbackOptions: FallbackLlmOptions = {
|
|
96
|
+
temperature: 0.2,
|
|
97
|
+
timeoutMs,
|
|
98
|
+
signal: fallbackAttempt.signal,
|
|
99
|
+
...gatewayTaskChainOptions(deps.config),
|
|
100
|
+
};
|
|
101
|
+
const fallback = deps.fallbackChat
|
|
102
|
+
? await deps.fallbackChat(messages, fallbackOptions)
|
|
103
|
+
: await new FallbackLlmClient(
|
|
104
|
+
deps.config.gatewayConfig,
|
|
105
|
+
fallbackLlmRuntimeContextFromConfig(deps.config)
|
|
106
|
+
).chatCompletion(messages, fallbackOptions);
|
|
107
|
+
if (abortSignal?.aborted) throw abortError("correction classify aborted");
|
|
108
|
+
return fallback?.content ?? null;
|
|
109
|
+
} catch (err) {
|
|
110
|
+
// The attempt signal aborts on its own budget. That is a finished miss
|
|
111
|
+
// (empty plan), not a caller disconnect — only the caller signal skips
|
|
112
|
+
// the empty-plan path.
|
|
113
|
+
if (abortSignal?.aborted) {
|
|
114
|
+
throw isAbortError(err) ? err : abortError("correction classify aborted");
|
|
115
|
+
}
|
|
116
|
+
if (isAbortError(err)) return null;
|
|
117
|
+
throw err;
|
|
118
|
+
} finally {
|
|
119
|
+
fallbackAttempt.dispose();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function classifyTimeoutMs(config: PluginConfig): number {
|
|
124
|
+
const configured = config.correctionClassifyTimeoutMs;
|
|
125
|
+
if (
|
|
126
|
+
typeof configured === "number" &&
|
|
127
|
+
Number.isInteger(configured) &&
|
|
128
|
+
configured >= 1 &&
|
|
129
|
+
configured <= MAX_CORRECTION_CLASSIFY_TIMEOUT_MS
|
|
130
|
+
) {
|
|
131
|
+
return configured;
|
|
132
|
+
}
|
|
133
|
+
return DEFAULT_CORRECTION_CLASSIFY_TIMEOUT_MS;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function startAttempt(
|
|
137
|
+
caller: AbortSignal | undefined,
|
|
138
|
+
timeoutMs: number
|
|
139
|
+
): { signal: AbortSignal; dispose: () => void } {
|
|
140
|
+
// A ref'd timer, not AbortSignal.timeout: that helper's timer is unref'd,
|
|
141
|
+
// so a classify wait would not keep a short-lived caller alive until the
|
|
142
|
+
// budget elapses.
|
|
143
|
+
const controller = new AbortController();
|
|
144
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
145
|
+
const onCaller = (): void => controller.abort();
|
|
146
|
+
if (caller?.aborted) controller.abort();
|
|
147
|
+
else caller?.addEventListener("abort", onCaller, { once: true });
|
|
148
|
+
let disposed = false;
|
|
149
|
+
const dispose = (): void => {
|
|
150
|
+
if (disposed) return;
|
|
151
|
+
disposed = true;
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
caller?.removeEventListener("abort", onCaller);
|
|
154
|
+
};
|
|
155
|
+
controller.signal.addEventListener("abort", dispose, { once: true });
|
|
156
|
+
return { signal: controller.signal, dispose };
|
|
157
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Correction-contract config (issue #1580 plus classify timeout).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from config.ts so the grandfathered parser file does not grow.
|
|
5
|
+
* Nested `correction.<key>` wins over the flat `correction<Key>` legacy key.
|
|
6
|
+
* Invalid values throw — they are never clamped or silently defaulted.
|
|
7
|
+
*
|
|
8
|
+
* Reads stay inline on `cfg` so the parsed-key extractor records the same
|
|
9
|
+
* paths the plugin schema declares.
|
|
10
|
+
*/
|
|
11
|
+
import { coerceBool, coerceNumber } from "../connectors/coerce.js";
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_CORRECTION_CLASSIFY_TIMEOUT_MS = 45_000;
|
|
14
|
+
export const MAX_CORRECTION_CLASSIFY_TIMEOUT_MS = 300_000;
|
|
15
|
+
|
|
16
|
+
export interface ParsedCorrectionConfig {
|
|
17
|
+
correctionEnabled: boolean;
|
|
18
|
+
correctionApplyRequiresConfirm: boolean;
|
|
19
|
+
correctionMaxAffected: number;
|
|
20
|
+
correctionPlanTtlHours: number;
|
|
21
|
+
correctionClassifyTimeoutMs: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseCorrectionConfig(cfg: Record<string, unknown>): ParsedCorrectionConfig {
|
|
25
|
+
return {
|
|
26
|
+
correctionEnabled: (() => {
|
|
27
|
+
const nested = (cfg.correction as Record<string, unknown> | undefined)?.enabled;
|
|
28
|
+
const raw = nested !== undefined ? nested : cfg.correctionEnabled;
|
|
29
|
+
if (raw === undefined || raw === null) return true;
|
|
30
|
+
const value = coerceBool(raw);
|
|
31
|
+
if (value === undefined) {
|
|
32
|
+
throw new Error(`Invalid correction.enabled: expected a boolean, got ${JSON.stringify(raw)}`);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
})(),
|
|
36
|
+
correctionApplyRequiresConfirm: (() => {
|
|
37
|
+
const nested = (cfg.correction as Record<string, unknown> | undefined)?.applyRequiresConfirm;
|
|
38
|
+
const raw = nested !== undefined ? nested : cfg.correctionApplyRequiresConfirm;
|
|
39
|
+
if (raw === undefined || raw === null) return true;
|
|
40
|
+
const value = coerceBool(raw);
|
|
41
|
+
if (value === undefined) {
|
|
42
|
+
throw new Error(`Invalid correction.applyRequiresConfirm: expected a boolean, got ${JSON.stringify(raw)}`);
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
})(),
|
|
46
|
+
correctionMaxAffected: (() => {
|
|
47
|
+
const rawNested = (cfg.correction as Record<string, unknown> | undefined)?.maxAffected;
|
|
48
|
+
const raw = rawNested !== undefined ? rawNested : cfg.correctionMaxAffected;
|
|
49
|
+
if (raw === undefined || raw === null) return 10;
|
|
50
|
+
const n = coerceNumber(raw);
|
|
51
|
+
if (n === undefined || !Number.isFinite(n) || n < 1 || !Number.isInteger(n)) {
|
|
52
|
+
throw new Error(`Invalid correction.maxAffected: expected an integer >= 1, got ${JSON.stringify(raw)}`);
|
|
53
|
+
}
|
|
54
|
+
return n;
|
|
55
|
+
})(),
|
|
56
|
+
correctionPlanTtlHours: (() => {
|
|
57
|
+
const nested = (cfg.correction as Record<string, unknown> | undefined)?.planTtlHours;
|
|
58
|
+
const fromNested = nested !== undefined ? coerceNumber(nested) : undefined;
|
|
59
|
+
if (fromNested !== undefined && fromNested >= 1) return fromNested;
|
|
60
|
+
if (nested !== undefined && fromNested === undefined) {
|
|
61
|
+
throw new Error(`Invalid correction.planTtlHours: expected a number >= 1, got ${JSON.stringify(nested)}`);
|
|
62
|
+
}
|
|
63
|
+
if (fromNested !== undefined && fromNested < 1) {
|
|
64
|
+
throw new Error(`Invalid correction.planTtlHours: expected a number >= 1, got ${JSON.stringify(nested)}`);
|
|
65
|
+
}
|
|
66
|
+
const fromFlat = coerceNumber(cfg.correctionPlanTtlHours);
|
|
67
|
+
if (fromFlat !== undefined && fromFlat >= 1) return fromFlat;
|
|
68
|
+
if (cfg.correctionPlanTtlHours !== undefined && fromFlat === undefined) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Invalid correction.planTtlHours: expected a number >= 1, got ${JSON.stringify(cfg.correctionPlanTtlHours)}`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (fromFlat !== undefined && fromFlat < 1) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Invalid correction.planTtlHours: expected a number >= 1, got ${JSON.stringify(cfg.correctionPlanTtlHours)}`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return 24;
|
|
79
|
+
})(),
|
|
80
|
+
correctionClassifyTimeoutMs: (() => {
|
|
81
|
+
const nested = (cfg.correction as Record<string, unknown> | undefined)?.classifyTimeoutMs;
|
|
82
|
+
const raw = nested !== undefined ? nested : cfg.correctionClassifyTimeoutMs;
|
|
83
|
+
if (raw === undefined || raw === null) return DEFAULT_CORRECTION_CLASSIFY_TIMEOUT_MS;
|
|
84
|
+
const n = coerceNumber(raw);
|
|
85
|
+
if (n === undefined || !Number.isInteger(n) || n < 1 || n > MAX_CORRECTION_CLASSIFY_TIMEOUT_MS) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Invalid correction.classifyTimeoutMs: expected an integer from 1 to ${MAX_CORRECTION_CLASSIFY_TIMEOUT_MS}, got ${JSON.stringify(raw)}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return n;
|
|
91
|
+
})(),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -51,6 +51,12 @@ export interface CorrectionRequest {
|
|
|
51
51
|
* planner only sees the ALREADY-AUTHORIZED namespace.
|
|
52
52
|
*/
|
|
53
53
|
namespace?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Winner memory id. When set, `targetIds` must be exactly the duplicate
|
|
56
|
+
* (the loser). The planner skips classification and drafts one recoverable
|
|
57
|
+
* supersede — no hard delete, no replacement write.
|
|
58
|
+
*/
|
|
59
|
+
duplicateOf?: string;
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
// ---------------------------------------------------------------------------
|
|
@@ -103,8 +109,11 @@ export const MEMORY_CATEGORIES: readonly MemoryCategory[] = [
|
|
|
103
109
|
];
|
|
104
110
|
|
|
105
111
|
export type CorrectionAction =
|
|
106
|
-
/**
|
|
107
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Outdated: a new fact replaces the loser, or `winnerId` records an
|
|
114
|
+
* existing memory as the replacement without writing a new one.
|
|
115
|
+
*/
|
|
116
|
+
| { kind: "supersede"; loserId: string; replacement?: MemoryDraft; winnerId?: string }
|
|
108
117
|
/** Incomplete / minor-wrong: a versioned edit to an existing memory. */
|
|
109
118
|
| { kind: "edit"; memoryId: string; patch: string }
|
|
110
119
|
/** Wrong: retire + tombstone. */
|
|
@@ -213,6 +222,9 @@ export const CORRECTION_TEXT_MAX = 10_000;
|
|
|
213
222
|
/** Maximum supported redaction pattern length (§34 — bounded patterns). */
|
|
214
223
|
export const REDACTION_PATTERN_MAX = 256;
|
|
215
224
|
|
|
225
|
+
/** Maximum memory-id length for `duplicateOf` / explicit targets. */
|
|
226
|
+
export const CORRECTION_MEMORY_ID_MAX = 512;
|
|
227
|
+
|
|
216
228
|
/**
|
|
217
229
|
* Validate a {@link CorrectionRequest}'s invariants. Returns the cleaned
|
|
218
230
|
* request or throws with a field-specific message (rule 51 — list valid
|
|
@@ -234,25 +246,47 @@ export function validateCorrectionRequest(request: CorrectionRequest): Correctio
|
|
|
234
246
|
const targetIds = Array.isArray(request.targetIds)
|
|
235
247
|
? request.targetIds.filter((id): id is string => typeof id === "string" && id.length > 0)
|
|
236
248
|
: undefined;
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
249
|
+
const normalizedTargets = targetIds !== undefined && targetIds.length === 0 ? undefined : targetIds;
|
|
250
|
+
const duplicateOf = readDuplicateOf(request.duplicateOf);
|
|
251
|
+
if (duplicateOf) {
|
|
252
|
+
if (!normalizedTargets || normalizedTargets.length !== 1) {
|
|
253
|
+
throw new CorrectionContractError(
|
|
254
|
+
"CorrectionRequest.duplicateOf requires exactly one targetIds entry (the duplicate memory).",
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
if (normalizedTargets[0] === duplicateOf) {
|
|
258
|
+
throw new CorrectionContractError(
|
|
259
|
+
"CorrectionRequest.duplicateOf must be a different memory from targetIds.",
|
|
260
|
+
);
|
|
261
|
+
}
|
|
246
262
|
}
|
|
247
263
|
return {
|
|
248
264
|
text,
|
|
249
|
-
...(
|
|
265
|
+
...(normalizedTargets ? { targetIds: normalizedTargets } : {}),
|
|
266
|
+
...(duplicateOf ? { duplicateOf } : {}),
|
|
250
267
|
...(request.sessionKey ? { sessionKey: request.sessionKey } : {}),
|
|
251
268
|
...(request.principal ? { principal: request.principal } : {}),
|
|
252
269
|
...(request.namespace ? { namespace: request.namespace } : {}),
|
|
253
270
|
};
|
|
254
271
|
}
|
|
255
272
|
|
|
273
|
+
function readDuplicateOf(value: unknown): string | undefined {
|
|
274
|
+
if (value === undefined || value === null) return undefined;
|
|
275
|
+
if (typeof value !== "string") {
|
|
276
|
+
throw new CorrectionContractError("CorrectionRequest.duplicateOf must be a memory id string.");
|
|
277
|
+
}
|
|
278
|
+
const id = value.trim();
|
|
279
|
+
if (id.length === 0 || id !== value) {
|
|
280
|
+
throw new CorrectionContractError("CorrectionRequest.duplicateOf must be a non-empty memory id.");
|
|
281
|
+
}
|
|
282
|
+
if (id.length > CORRECTION_MEMORY_ID_MAX) {
|
|
283
|
+
throw new CorrectionContractError(
|
|
284
|
+
`CorrectionRequest.duplicateOf exceeds the ${CORRECTION_MEMORY_ID_MAX}-character limit (${id.length}).`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return id;
|
|
288
|
+
}
|
|
289
|
+
|
|
256
290
|
/**
|
|
257
291
|
* Validate a redaction pattern (§34 — bounded, literal-or-safe-regex; reject
|
|
258
292
|
* catastrophic patterns). Returns the cleaned pattern or throws.
|
|
@@ -377,8 +411,21 @@ export function validateCorrectionAction(action: unknown): asserts action is Cor
|
|
|
377
411
|
if (typeof a.loserId !== "string" || a.loserId.length === 0) {
|
|
378
412
|
throw new CorrectionContractError("supersede.loserId is required.");
|
|
379
413
|
}
|
|
380
|
-
|
|
381
|
-
|
|
414
|
+
{
|
|
415
|
+
const hasReplacement = a.replacement !== undefined && a.replacement !== null;
|
|
416
|
+
const hasWinner = a.winnerId !== undefined && a.winnerId !== null;
|
|
417
|
+
if (hasWinner) {
|
|
418
|
+
if (typeof a.winnerId !== "string" || a.winnerId.trim().length === 0 || a.winnerId !== a.winnerId.trim()) {
|
|
419
|
+
throw new CorrectionContractError("supersede.winnerId must be a non-empty memory id.");
|
|
420
|
+
}
|
|
421
|
+
if (a.winnerId === a.loserId) {
|
|
422
|
+
throw new CorrectionContractError("supersede.winnerId must differ from loserId.");
|
|
423
|
+
}
|
|
424
|
+
if (hasReplacement) {
|
|
425
|
+
throw new CorrectionContractError("supersede.winnerId cannot be combined with a replacement draft.");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (hasReplacement) validateMemoryDraft(a.replacement);
|
|
382
429
|
}
|
|
383
430
|
break;
|
|
384
431
|
case "edit":
|
|
@@ -35,7 +35,7 @@ import { CorrectionExecutor, CORRECTION_ERROR_MAX, sanitizeErrorMessage, type Ex
|
|
|
35
35
|
|
|
36
36
|
interface FakeState {
|
|
37
37
|
/** memoryId → current memory state. */
|
|
38
|
-
memories: Map<string, ExecutorMemory & { status: string; supersededBy?: string; validUntil?: string }>;
|
|
38
|
+
memories: Map<string, ExecutorMemory & { status: string; supersededBy?: string; validUntil?: string; lineage?: string[] }>;
|
|
39
39
|
tombstones: Array<{ reason: string; sourceMemoryId: string; rawContent: string; supersessionKey?: string; supersessionKeys?: string[]; contentHash?: string }>;
|
|
40
40
|
redactionRules: string[];
|
|
41
41
|
auditRecords: Array<{ planId: string; outcome: CorrectionOutcome }>;
|
|
@@ -98,7 +98,7 @@ function makeExecutorDeps(state: FakeState, opts: { biTemporalEnabled?: boolean;
|
|
|
98
98
|
getMemory: async (_ns, memoryId) => {
|
|
99
99
|
const m = state.memories.get(memoryId);
|
|
100
100
|
if (!m) return null;
|
|
101
|
-
const {
|
|
101
|
+
const { supersededBy: _sb, validUntil: _v, ...rest } = m;
|
|
102
102
|
return rest;
|
|
103
103
|
},
|
|
104
104
|
writeReplacement: async (namespace, draft) => {
|
|
@@ -1589,3 +1589,103 @@ test("#1678: sanitizeErrorMessage strips absolute paths and caps length", () =>
|
|
|
1589
1589
|
assert.ok(!atPath.includes("secret.txt"), "path suffix must be consumed: " + atPath);
|
|
1590
1590
|
assert.ok(atPath.includes("<path>"), "must leave <path> placeholder: " + atPath);
|
|
1591
1591
|
});
|
|
1592
|
+
|
|
1593
|
+
test("winnerId supersede leaves the loser recoverable and the winner unchanged", async () => {
|
|
1594
|
+
await withTempDir(async (dir) => {
|
|
1595
|
+
const candidates = new Map<string, PlannerCandidate>([
|
|
1596
|
+
["mem-loser", { memoryId: "mem-loser", path: "facts/mem-loser.md", content: "duplicate", excerpt: "duplicate", score: 1 }],
|
|
1597
|
+
["mem-winner", { memoryId: "mem-winner", path: "facts/mem-winner.md", content: "canonical", excerpt: "canonical", score: 1 }],
|
|
1598
|
+
]);
|
|
1599
|
+
const state: FakeState = {
|
|
1600
|
+
memories: new Map([
|
|
1601
|
+
["mem-loser", fakeMemory({ memoryId: "mem-loser", content: "duplicate", rawContent: "duplicate raw" })],
|
|
1602
|
+
["mem-winner", fakeMemory({ memoryId: "mem-winner", content: "canonical", rawContent: "canonical raw" })],
|
|
1603
|
+
]),
|
|
1604
|
+
tombstones: [],
|
|
1605
|
+
redactionRules: [],
|
|
1606
|
+
auditRecords: [],
|
|
1607
|
+
propagateCalls: 0,
|
|
1608
|
+
writtenReplacements: [],
|
|
1609
|
+
};
|
|
1610
|
+
const planner = new CorrectionPlanner({
|
|
1611
|
+
...makePlannerDeps(dir, candidates),
|
|
1612
|
+
inspectMemory: async ({ memoryId }) => {
|
|
1613
|
+
const candidate = candidates.get(memoryId);
|
|
1614
|
+
if (!candidate) return null;
|
|
1615
|
+
return { candidate, status: "active" };
|
|
1616
|
+
},
|
|
1617
|
+
});
|
|
1618
|
+
const plan = await planner.plan(
|
|
1619
|
+
{ text: "duplicate", targetIds: ["mem-loser"], duplicateOf: "mem-winner" },
|
|
1620
|
+
["default"],
|
|
1621
|
+
);
|
|
1622
|
+
assert.equal(plan.actions.length, 1);
|
|
1623
|
+
const executor = new CorrectionExecutor(makeExecutorDeps(state), planner);
|
|
1624
|
+
const outcome = await executor.apply("default", plan.planId, { confirm: true });
|
|
1625
|
+
assert.equal(outcome.status, "applied");
|
|
1626
|
+
const loser = state.memories.get("mem-loser");
|
|
1627
|
+
const winner = state.memories.get("mem-winner");
|
|
1628
|
+
assert.ok(loser, "loser file remains");
|
|
1629
|
+
assert.equal(loser?.status, "superseded");
|
|
1630
|
+
assert.equal(loser?.supersededBy, "mem-winner");
|
|
1631
|
+
assert.equal(loser?.lineage, undefined);
|
|
1632
|
+
assert.equal(winner?.status, "active");
|
|
1633
|
+
assert.equal(winner?.content, "canonical");
|
|
1634
|
+
assert.equal(state.writtenReplacements.length, 0);
|
|
1635
|
+
assert.equal(state.tombstones.length, 1);
|
|
1636
|
+
assert.equal(state.tombstones[0]?.reason, "supersession");
|
|
1637
|
+
assert.equal(state.tombstones[0]?.sourceMemoryId, "mem-loser");
|
|
1638
|
+
});
|
|
1639
|
+
});
|
|
1640
|
+
|
|
1641
|
+
test("winnerId supersede fails closed when the winner is no longer active", async () => {
|
|
1642
|
+
await withTempDir(async (dir) => {
|
|
1643
|
+
const candidates = new Map<string, PlannerCandidate>([
|
|
1644
|
+
["mem-loser", { memoryId: "mem-loser", path: "facts/mem-loser.md", content: "duplicate", excerpt: "duplicate", score: 1 }],
|
|
1645
|
+
]);
|
|
1646
|
+
const state: FakeState = {
|
|
1647
|
+
memories: new Map([
|
|
1648
|
+
["mem-loser", fakeMemory({ memoryId: "mem-loser", content: "duplicate" })],
|
|
1649
|
+
["mem-winner", fakeMemory({ memoryId: "mem-winner", content: "canonical", status: "superseded" })],
|
|
1650
|
+
]),
|
|
1651
|
+
tombstones: [],
|
|
1652
|
+
redactionRules: [],
|
|
1653
|
+
auditRecords: [],
|
|
1654
|
+
propagateCalls: 0,
|
|
1655
|
+
writtenReplacements: [],
|
|
1656
|
+
};
|
|
1657
|
+
const planner = new CorrectionPlanner({
|
|
1658
|
+
...makePlannerDeps(dir, candidates),
|
|
1659
|
+
inspectMemory: async ({ memoryId }) => {
|
|
1660
|
+
if (memoryId === "mem-winner") {
|
|
1661
|
+
return {
|
|
1662
|
+
candidate: {
|
|
1663
|
+
memoryId,
|
|
1664
|
+
path: "facts/mem-winner.md",
|
|
1665
|
+
content: "canonical",
|
|
1666
|
+
excerpt: "canonical",
|
|
1667
|
+
score: 1,
|
|
1668
|
+
},
|
|
1669
|
+
status: "active",
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
const candidate = candidates.get(memoryId);
|
|
1673
|
+
if (!candidate) return null;
|
|
1674
|
+
return { candidate, status: "active" };
|
|
1675
|
+
},
|
|
1676
|
+
});
|
|
1677
|
+
const plan = await planner.plan(
|
|
1678
|
+
{ text: "duplicate", targetIds: ["mem-loser"], duplicateOf: "mem-winner" },
|
|
1679
|
+
["default"],
|
|
1680
|
+
);
|
|
1681
|
+
const outcome = await new CorrectionExecutor(makeExecutorDeps(state), planner).apply(
|
|
1682
|
+
"default",
|
|
1683
|
+
plan.planId,
|
|
1684
|
+
{ confirm: true },
|
|
1685
|
+
);
|
|
1686
|
+
assert.equal(outcome.results[0]?.status, "failed");
|
|
1687
|
+
assert.match(outcome.results[0]?.error ?? "", /winner is not active/);
|
|
1688
|
+
assert.equal(state.memories.get("mem-loser")?.status, "active");
|
|
1689
|
+
assert.equal(state.tombstones.length, 0);
|
|
1690
|
+
});
|
|
1691
|
+
});
|