@remnic/core 9.7.14 → 9.8.0
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.d.ts +13 -6
- package/dist/access-admin-ops-surface.js +88 -74
- package/dist/access-audit.js +2 -2
- package/dist/access-boundary.d.ts +35 -17
- package/dist/access-boundary.js +89 -77
- package/dist/access-citation.d.ts +28 -0
- package/dist/access-citation.js +9 -0
- package/dist/access-cli.js +197 -181
- package/dist/access-cli.js.map +1 -1
- package/dist/access-errors.d.ts +16 -1
- package/dist/access-errors.js +7 -3
- package/dist/access-http.d.ts +25 -19
- package/dist/access-http.js +103 -90
- package/dist/access-identity-continuity-surface.d.ts +13 -8
- package/dist/access-identity-continuity-surface.js +88 -74
- package/dist/access-lcm-surface.d.ts +16 -9
- package/dist/access-lcm-surface.js +88 -74
- package/dist/access-mcp.d.ts +23 -16
- package/dist/access-mcp.js +99 -87
- package/dist/access-memory-search-fanout.d.ts +93 -0
- package/dist/access-memory-search-fanout.js +18 -0
- package/dist/access-observe-write-surface.d.ts +26 -8
- package/dist/access-observe-write-surface.js +88 -74
- package/dist/access-operations-batch.js +95 -83
- package/dist/access-operations.d.ts +26 -19
- package/dist/access-operations.js +97 -85
- package/dist/access-recall-concurrency.d.ts +250 -0
- package/dist/access-recall-concurrency.js +157 -0
- package/dist/access-recall-response.d.ts +130 -0
- package/dist/access-recall-response.js +147 -0
- package/dist/access-recall-surface.d.ts +15 -7
- package/dist/access-recall-surface.js +88 -74
- package/dist/access-schema.d.ts +34 -34
- package/dist/access-schema.js +8 -6
- package/dist/{access-service-CPhSnLOH.d.ts → access-service-DEL4qCrc.d.ts} +25 -32
- package/dist/access-service.d.ts +13 -6
- package/dist/access-service.js +92 -75
- package/dist/access-surface-catalog.d.ts +23 -16
- package/dist/access-surface-catalog.js +1 -0
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/access-token-capabilities.d.ts +2 -1
- package/dist/access-token-capabilities.js +4 -2
- package/dist/action-confidence.d.ts +3 -1
- package/dist/active-memory-bridge.d.ts +3 -1
- package/dist/active-memory-bridge.js +4 -4
- package/dist/active-recall.d.ts +4 -1
- package/dist/active-recall.js +23 -13
- package/dist/active-recall.js.map +1 -1
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/{auto-sync-AAMTVZMK.js → auto-sync-UZG3XQZ5.js} +14 -8
- package/dist/{auto-sync-AAMTVZMK.js.map → auto-sync-UZG3XQZ5.js.map} +1 -1
- package/dist/behavior-learner.d.ts +3 -1
- package/dist/behavior-signals.d.ts +3 -1
- package/dist/bootstrap.d.ts +12 -7
- package/dist/bootstrap.js +2 -2
- package/dist/bounded-jsonl-state.d.ts +54 -0
- package/dist/bounded-jsonl-state.js +10 -0
- package/dist/boxes.js +2 -2
- package/dist/briefing.d.ts +5 -2
- package/dist/briefing.js +37 -27
- package/dist/buffer-surprise-report.d.ts +3 -1
- package/dist/buffer.d.ts +60 -2
- package/dist/buffer.js +3 -3
- package/dist/bulk-import/index.d.ts +5 -3
- package/dist/calibration.d.ts +3 -1
- package/dist/calibration.js +6 -6
- package/dist/capabilities.d.ts +7 -7
- package/dist/capabilities.js +1 -1
- package/dist/{capsule-crypto-CZJSLEFG.js → capsule-crypto-YO5QJ6L3.js} +3 -3
- package/dist/capsule-crypto-YO5QJ6L3.js.map +1 -0
- package/dist/{capsule-merge-Z2HUO6IK.js → capsule-merge-5KKJQBWQ.js} +4 -4
- package/dist/{catalog-uX9sT9ws.d.ts → catalog-BWqHb5HT.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +3 -1
- package/dist/causal-behavior.js +6 -6
- package/dist/causal-chain.js +4 -4
- package/dist/causal-consolidation.d.ts +3 -1
- package/dist/causal-consolidation.js +46 -37
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/causal-retrieval.js +6 -6
- package/dist/causal-trajectory-graph.d.ts +3 -1
- package/dist/causal-trajectory-graph.js +1 -1
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-A3GOMRJG.js → chunk-23L3IXFD.js} +2 -2
- package/dist/chunk-2CGLBUW3.js +15 -0
- package/dist/chunk-2CGLBUW3.js.map +1 -0
- package/dist/{chunk-MRHFJV44.js → chunk-2DQ2XREU.js} +2 -2
- package/dist/{chunk-ASKEM4AR.js → chunk-2EJOEICD.js} +1823 -1483
- package/dist/chunk-2EJOEICD.js.map +1 -0
- package/dist/{chunk-L2B3EE7B.js → chunk-2QMKMVHH.js} +2 -2
- package/dist/{chunk-6CALL5VN.js → chunk-2UFO7X4Q.js} +7 -7
- package/dist/{chunk-2MR3MFQB.js → chunk-2VEOKUPP.js} +3 -3
- package/dist/chunk-2Z24E5W2.js +22 -0
- package/dist/chunk-2Z24E5W2.js.map +1 -0
- package/dist/{chunk-RHOZ5LLO.js → chunk-347WD2LE.js} +2 -2
- package/dist/{chunk-5S7Z5SHA.js → chunk-34RC5RT7.js} +2 -2
- package/dist/{chunk-KGWXHWAH.js → chunk-3QAH2S33.js} +3 -3
- package/dist/{chunk-37CTGERM.js → chunk-3WCASNQZ.js} +12 -1
- package/dist/chunk-3WCASNQZ.js.map +1 -0
- package/dist/{chunk-ZIDSD6VC.js → chunk-3Y224NYC.js} +3 -3
- package/dist/{chunk-RBQLJSJ7.js → chunk-44XZ75OQ.js} +46 -10
- package/dist/chunk-44XZ75OQ.js.map +1 -0
- package/dist/{chunk-VFSP73LP.js → chunk-45GF7Y3V.js} +2 -2
- package/dist/{chunk-7ICYQUOL.js → chunk-4BJQODZF.js} +2514 -1526
- package/dist/chunk-4BJQODZF.js.map +1 -0
- package/dist/{chunk-JMBVCAR4.js → chunk-57OWKFUQ.js} +17 -6
- package/dist/chunk-57OWKFUQ.js.map +1 -0
- package/dist/{chunk-OEHCMALU.js → chunk-5I4M26Q7.js} +5 -5
- package/dist/{chunk-2E6G7FCE.js → chunk-5Q7EVV4X.js} +7 -7
- package/dist/{chunk-LC7OODWZ.js → chunk-5SB7SC2G.js} +80 -49
- package/dist/chunk-5SB7SC2G.js.map +1 -0
- package/dist/{chunk-VZ5OJE4X.js → chunk-5VQJBH2G.js} +2 -2
- package/dist/{chunk-PUBTHW5O.js → chunk-6DRPPQEJ.js} +2 -2
- package/dist/{chunk-NWTGD4AN.js → chunk-6KIQT7RG.js} +10 -9
- package/dist/chunk-6KIQT7RG.js.map +1 -0
- package/dist/{chunk-4LEF6XWJ.js → chunk-6O22TZSB.js} +3 -3
- package/dist/{chunk-ZQ7UBUZ6.js → chunk-6QYST5XN.js} +6 -6
- package/dist/{chunk-W4PGTAS2.js → chunk-6WAFYNVT.js} +3 -3
- package/dist/chunk-6ZMAMSHS.js +20 -0
- package/dist/chunk-6ZMAMSHS.js.map +1 -0
- package/dist/{chunk-R5VAVVHP.js → chunk-72A4L5JJ.js} +2 -2
- package/dist/{chunk-X64YLC24.js → chunk-72VAXGEI.js} +5 -13
- package/dist/chunk-72VAXGEI.js.map +1 -0
- package/dist/{chunk-IKQTFQMI.js → chunk-755Q3DGF.js} +22 -3
- package/dist/chunk-755Q3DGF.js.map +1 -0
- package/dist/{chunk-F7AU34FC.js → chunk-76GLCM57.js} +160 -127
- package/dist/chunk-76GLCM57.js.map +1 -0
- package/dist/{chunk-D7FOKIY5.js → chunk-7AMFUATK.js} +12 -9
- package/dist/{chunk-D7FOKIY5.js.map → chunk-7AMFUATK.js.map} +1 -1
- package/dist/{chunk-BFVEEB7W.js → chunk-7ON2JW73.js} +57 -22
- package/dist/chunk-7ON2JW73.js.map +1 -0
- package/dist/chunk-7RNBVZZO.js +270 -0
- package/dist/chunk-7RNBVZZO.js.map +1 -0
- package/dist/{chunk-3FAMU5TX.js → chunk-A6USBZTO.js} +2 -2
- package/dist/{chunk-TXVD7PLT.js → chunk-A72SWTWW.js} +7 -7
- package/dist/{chunk-FC77VUX5.js → chunk-A7Q43EUZ.js} +2 -2
- package/dist/{chunk-UJBESW6X.js → chunk-AE7WMV4V.js} +7 -7
- package/dist/{chunk-J2HSAU72.js → chunk-AIK5W3IW.js} +29 -1
- package/dist/chunk-AIK5W3IW.js.map +1 -0
- package/dist/{chunk-OKUEZOGW.js → chunk-AITCS2RC.js} +2 -2
- package/dist/{chunk-OC35KQIN.js → chunk-AJ7QCSIS.js} +4 -4
- package/dist/{chunk-FNMFJWBY.js → chunk-ANEUPF6O.js} +5 -5
- package/dist/chunk-AREGMX2J.js +50 -0
- package/dist/chunk-AREGMX2J.js.map +1 -0
- package/dist/chunk-AVPXXV6J.js +448 -0
- package/dist/chunk-AVPXXV6J.js.map +1 -0
- package/dist/{chunk-2A3OXAP2.js → chunk-B7RWBZXW.js} +2 -2
- package/dist/{chunk-GWTFLF2P.js → chunk-BJMBJZ2Y.js} +59 -59
- package/dist/chunk-BJMBJZ2Y.js.map +1 -0
- package/dist/{chunk-N3JDIRBW.js → chunk-BLAKS3CP.js} +4 -4
- package/dist/{chunk-M7VGG3ID.js → chunk-BV5WMSIJ.js} +9 -9
- package/dist/{chunk-2BFLDH5F.js → chunk-BXLOS5AJ.js} +2 -2
- package/dist/{chunk-UXKMMNJD.js → chunk-C23BYTJV.js} +6 -6
- package/dist/{chunk-JZHHREBL.js → chunk-C3AKM55W.js} +2 -2
- package/dist/{chunk-RP7YXA67.js → chunk-C5JF6EZ7.js} +2 -2
- package/dist/{chunk-FBNWOMCE.js → chunk-C6XBRYVD.js} +16 -15
- package/dist/chunk-C6XBRYVD.js.map +1 -0
- package/dist/{chunk-FTYDWKO6.js → chunk-CEN6DW6X.js} +12 -2
- package/dist/chunk-CEN6DW6X.js.map +1 -0
- package/dist/chunk-CGASUJVP.js +421 -0
- package/dist/chunk-CGASUJVP.js.map +1 -0
- package/dist/{chunk-DDGV7NAX.js → chunk-CQMEM5CC.js} +2 -2
- package/dist/{chunk-A7B4RMQG.js → chunk-CXZQOVIE.js} +12 -12
- package/dist/chunk-D3V3UMET.js +44 -0
- package/dist/chunk-D3V3UMET.js.map +1 -0
- package/dist/{chunk-7N2RUBAP.js → chunk-DDA47TES.js} +2 -2
- package/dist/{chunk-64SE4X5G.js → chunk-DEBHXHJR.js} +3 -3
- package/dist/{chunk-EHK6KZHK.js → chunk-DNHVPZOD.js} +2 -2
- package/dist/{chunk-437S3G37.js → chunk-DQEMWVMT.js} +2 -2
- package/dist/{chunk-LZZNTPLR.js → chunk-DX3XRGLH.js} +2 -2
- package/dist/chunk-EA2H5LML.js +72 -0
- package/dist/chunk-EA2H5LML.js.map +1 -0
- package/dist/{chunk-IFJ23BHU.js → chunk-EEC7UJPX.js} +112 -106
- package/dist/chunk-EEC7UJPX.js.map +1 -0
- package/dist/{chunk-KSNVDPDF.js → chunk-EKBNDKDY.js} +21 -20
- package/dist/chunk-EKBNDKDY.js.map +1 -0
- package/dist/{chunk-YF6FMLBT.js → chunk-EQWEIIKQ.js} +64 -34
- package/dist/chunk-EQWEIIKQ.js.map +1 -0
- package/dist/{chunk-7MW3CVLD.js → chunk-ES36HTSC.js} +4 -4
- package/dist/{chunk-TN7THW5U.js → chunk-ET356KSB.js} +2 -2
- package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
- package/dist/{chunk-7LFXDGLH.js → chunk-F7GAIWFA.js} +20 -20
- package/dist/{chunk-IKSELOIV.js → chunk-FFPJINJE.js} +7 -7
- package/dist/chunk-FMLKOL2Z.js +636 -0
- package/dist/chunk-FMLKOL2Z.js.map +1 -0
- package/dist/chunk-FQC3DHH2.js +20 -0
- package/dist/chunk-FQC3DHH2.js.map +1 -0
- package/dist/{chunk-V53GJUSL.js → chunk-FUTF72MJ.js} +15 -15
- package/dist/chunk-GCASGYIO.js +71 -0
- package/dist/chunk-GCASGYIO.js.map +1 -0
- package/dist/{chunk-5UNWK7KU.js → chunk-GFPZS3F5.js} +8 -8
- package/dist/{chunk-BCDKBDSY.js → chunk-GNVZQ5JN.js} +99 -41
- package/dist/chunk-GNVZQ5JN.js.map +1 -0
- package/dist/{chunk-G6NKALRN.js → chunk-H4WG3UQZ.js} +2 -2
- package/dist/{chunk-HLTAPHZZ.js → chunk-HNZKVLAN.js} +2 -2
- package/dist/{chunk-5EZXHU36.js → chunk-HYQJQGD3.js} +2 -2
- package/dist/{chunk-ANADNYWK.js → chunk-IDZFBSHN.js} +8 -6
- package/dist/{chunk-ANADNYWK.js.map → chunk-IDZFBSHN.js.map} +1 -1
- package/dist/{chunk-XVLEGMJI.js → chunk-IFH6LWTO.js} +50 -8
- package/dist/chunk-IFH6LWTO.js.map +1 -0
- package/dist/{chunk-RULE4VG5.js → chunk-ILGC6JH6.js} +21 -6
- package/dist/chunk-ILGC6JH6.js.map +1 -0
- package/dist/chunk-IUM3VSHI.js +113 -0
- package/dist/chunk-IUM3VSHI.js.map +1 -0
- package/dist/{chunk-ND5M64JJ.js → chunk-IYNGIMVG.js} +101 -91
- package/dist/chunk-IYNGIMVG.js.map +1 -0
- package/dist/{chunk-TRWPCR6P.js → chunk-JECBQBEX.js} +3 -3
- package/dist/{chunk-HG32URJJ.js → chunk-JYIFGGEM.js} +2 -2
- package/dist/{chunk-VA4WSW54.js → chunk-JYIJNNN3.js} +3 -3
- package/dist/{chunk-L5HJQC36.js → chunk-K2TDK7GG.js} +3 -3
- package/dist/{chunk-MKDRQAA3.js → chunk-KAVBJSBQ.js} +4 -4
- package/dist/{chunk-EEARRVZ2.js → chunk-KF5EUVKQ.js} +16 -6
- package/dist/chunk-KF5EUVKQ.js.map +1 -0
- package/dist/chunk-KFZY2OFH.js +42 -0
- package/dist/chunk-KFZY2OFH.js.map +1 -0
- package/dist/{chunk-4OKLXWDZ.js → chunk-KNB7IBBU.js} +2 -2
- package/dist/{chunk-QLULYKWZ.js → chunk-KSXJOC4F.js} +2 -2
- package/dist/{chunk-OBMR4VIX.js → chunk-L2DPF4DF.js} +186 -15
- package/dist/chunk-L2DPF4DF.js.map +1 -0
- package/dist/{chunk-JI5AVXYO.js → chunk-LEPOP46W.js} +6 -6
- package/dist/{chunk-O7BBFJLP.js → chunk-LTAVSOZW.js} +2 -2
- package/dist/{chunk-E3VODCC3.js → chunk-LUDXM7MD.js} +14 -2
- package/dist/{chunk-E3VODCC3.js.map → chunk-LUDXM7MD.js.map} +1 -1
- package/dist/{chunk-2IZAGAJR.js → chunk-LULR4IUW.js} +3 -3
- package/dist/{chunk-NUWYOKS6.js → chunk-MC5Q3J7S.js} +398 -7
- package/dist/chunk-MC5Q3J7S.js.map +1 -0
- package/dist/{chunk-WMWVO45V.js → chunk-MC6XO7T4.js} +2 -2
- package/dist/chunk-MKQBXQKU.js +107 -0
- package/dist/chunk-MKQBXQKU.js.map +1 -0
- package/dist/{chunk-7PND4PUC.js → chunk-MMGGVZ4Q.js} +29 -9
- package/dist/{chunk-7PND4PUC.js.map → chunk-MMGGVZ4Q.js.map} +1 -1
- package/dist/{chunk-D6RIFG4O.js → chunk-MNB5RMAY.js} +2 -2
- package/dist/{chunk-67ZCJLJY.js → chunk-MYFQ3ED4.js} +8 -8
- package/dist/{chunk-V3UE5I5F.js → chunk-MYVPLJOO.js} +2 -2
- package/dist/chunk-NINSR6LX.js +83 -0
- package/dist/chunk-NINSR6LX.js.map +1 -0
- package/dist/{chunk-MTWKDOVO.js → chunk-NLE6TSLG.js} +2 -2
- package/dist/{chunk-DBRYEIGY.js → chunk-OJP3N7UK.js} +2 -2
- package/dist/{chunk-AD6AMF2N.js → chunk-OQJIB32H.js} +28 -23
- package/dist/chunk-OQJIB32H.js.map +1 -0
- package/dist/{chunk-SMX5267P.js → chunk-OZKOPQCM.js} +2 -2
- package/dist/{chunk-JCK7V6SN.js → chunk-P65YJEEL.js} +2 -2
- package/dist/{chunk-MTR7BZQN.js → chunk-PBGWYM3H.js} +3 -3
- package/dist/chunk-PIG4WJFQ.js +87 -0
- package/dist/chunk-PIG4WJFQ.js.map +1 -0
- package/dist/chunk-PQVCH2AE.js +76 -0
- package/dist/chunk-PQVCH2AE.js.map +1 -0
- package/dist/{chunk-AXTXN4T6.js → chunk-PWFCHOB3.js} +2 -2
- package/dist/{chunk-FDPZVY6D.js → chunk-QFN4U7AG.js} +2 -2
- package/dist/{chunk-K2ZLHESQ.js → chunk-QIEUU4UK.js} +2 -2
- package/dist/{chunk-AHLPZ2SF.js → chunk-QJAQNXWI.js} +44 -6
- package/dist/chunk-QJAQNXWI.js.map +1 -0
- package/dist/{chunk-3D6AFGQR.js → chunk-QVT3E4CE.js} +7 -7
- package/dist/{chunk-VYOFVVNK.js → chunk-R26ZMXR2.js} +24 -24
- package/dist/{chunk-VYOFVVNK.js.map → chunk-R26ZMXR2.js.map} +1 -1
- package/dist/chunk-R3DJVOSM.js +34 -0
- package/dist/chunk-R3DJVOSM.js.map +1 -0
- package/dist/{chunk-SFQ6QNL7.js → chunk-R6YJI3HE.js} +2 -1
- package/dist/{chunk-5CJVLG3H.js → chunk-RFDD3B7W.js} +5 -5
- package/dist/{chunk-JBPKEARU.js → chunk-RKNJBZ55.js} +4 -4
- package/dist/chunk-ROI7BK7H.js +88 -0
- package/dist/chunk-ROI7BK7H.js.map +1 -0
- package/dist/{chunk-UTRJFDP2.js → chunk-S2BBISHU.js} +4 -4
- package/dist/{chunk-IALLX7EE.js → chunk-S2FSBU4H.js} +3 -3
- package/dist/{chunk-BFPETA3S.js → chunk-SCCO46KZ.js} +3 -2
- package/dist/chunk-SCCO46KZ.js.map +1 -0
- package/dist/chunk-SICXL4MK.js +165 -0
- package/dist/chunk-SICXL4MK.js.map +1 -0
- package/dist/chunk-SK7AMF2J.js +168 -0
- package/dist/chunk-SK7AMF2J.js.map +1 -0
- package/dist/{chunk-GMSDQUCR.js → chunk-TDTXKOAJ.js} +6475 -5748
- package/dist/chunk-TDTXKOAJ.js.map +1 -0
- package/dist/{chunk-KSBEEMQF.js → chunk-TG6IHVE2.js} +151 -6
- package/dist/chunk-TG6IHVE2.js.map +1 -0
- package/dist/{chunk-7K5Q6COX.js → chunk-TVVEYCNW.js} +4 -4
- package/dist/{chunk-PK5XBDA6.js → chunk-U3ZAVRP3.js} +2 -2
- package/dist/{chunk-ODPLEWB6.js → chunk-UFGFMWAD.js} +47 -17
- package/dist/chunk-UFGFMWAD.js.map +1 -0
- package/dist/{chunk-4KJLYTLS.js → chunk-V57NLO5P.js} +2 -2
- package/dist/{chunk-FLVL4L3L.js → chunk-VDX2J7OX.js} +2 -2
- package/dist/{chunk-7XGSIUP5.js → chunk-W4FBGYGK.js} +1 -1
- package/dist/{chunk-TXLGHDHS.js → chunk-W4XJFLAL.js} +53 -32
- package/dist/chunk-W4XJFLAL.js.map +1 -0
- package/dist/{chunk-6SXVCD7W.js → chunk-W7IGLX4G.js} +2 -2
- package/dist/{chunk-KQDNAXOM.js → chunk-WG5HSQ4P.js} +19 -6
- package/dist/chunk-WG5HSQ4P.js.map +1 -0
- package/dist/{chunk-LTJAMRGI.js → chunk-WL5EBRDH.js} +17 -3
- package/dist/chunk-WL5EBRDH.js.map +1 -0
- package/dist/{chunk-KAWTOYAW.js → chunk-WLZYGLJ4.js} +2 -2
- package/dist/{chunk-W5NKDU24.js → chunk-WON4APQY.js} +4 -4
- package/dist/{chunk-NKCPUHCI.js → chunk-WZFAPHIK.js} +6 -6
- package/dist/{chunk-KQPJLK3X.js → chunk-XL24QQ63.js} +2 -2
- package/dist/{chunk-X47LWMKA.js → chunk-XMMESCPD.js} +26 -1
- package/dist/chunk-XMMESCPD.js.map +1 -0
- package/dist/{chunk-4B4MUS5D.js → chunk-YROFNEG5.js} +7 -7
- package/dist/{chunk-WU46YPY6.js → chunk-YV32N67O.js} +1 -1
- package/dist/chunk-YV32N67O.js.map +1 -0
- package/dist/{chunk-B3F4CWQR.js → chunk-YVKF3HEW.js} +1 -1
- package/dist/chunk-YVKF3HEW.js.map +1 -0
- package/dist/{chunk-VRAD2FGO.js → chunk-Z54EM5YZ.js} +12 -12
- package/dist/{chunk-2VN4JAX7.js → chunk-ZOKZIR5K.js} +95 -33
- package/dist/chunk-ZOKZIR5K.js.map +1 -0
- package/dist/{cli-_Y2Bmt6A.d.ts → cli-C_pqqTCw.d.ts} +28 -12
- package/dist/cli.d.ts +16 -9
- package/dist/cli.js +145 -130
- package/dist/compounding/engine.d.ts +5 -2
- package/dist/compounding/engine.js +39 -29
- package/dist/compounding/preference-consolidator.d.ts +3 -1
- package/dist/compounding/preference-consolidator.js +1 -1
- package/dist/compression-optimizer.d.ts +3 -1
- package/dist/config-qmd.d.ts +8 -0
- package/dist/config-qmd.js +20 -0
- package/dist/config-qmd.js.map +1 -0
- package/dist/config.d.ts +3 -1
- package/dist/config.js +15 -10
- package/dist/connectors/codex-materialize-runner.d.ts +3 -1
- package/dist/connectors/codex-materialize-runner.js +39 -29
- package/dist/connectors/codex-materialize.d.ts +3 -1
- package/dist/connectors/codex-materialize.js +2 -2
- package/dist/connectors/index.d.ts +3 -1
- package/dist/connectors/index.js +44 -35
- package/dist/consolidation-provenance-check.d.ts +5 -2
- package/dist/consolidation-provenance-check.js +4 -4
- package/dist/consolidation-undo.d.ts +5 -2
- package/dist/consolidation-undo.js +3 -3
- package/dist/contradiction/index.d.ts +5 -2
- package/dist/contradiction/index.js +10 -4
- package/dist/{contradiction-scan-J57T5QV7.js → contradiction-scan-RJBABNRH.js} +4 -4
- package/dist/contradiction-scan-RJBABNRH.js.map +1 -0
- package/dist/conversation-index/backend.d.ts +3 -1
- package/dist/conversation-index/backend.js +5 -5
- package/dist/conversation-index/chunker.d.ts +3 -1
- package/dist/conversation-index/cleanup.js +2 -2
- package/dist/conversation-index/faiss-adapter.d.ts +3 -1
- package/dist/conversation-index/faiss-adapter.js +2 -2
- package/dist/conversation-index/indexer.d.ts +3 -1
- package/dist/conversation-index/indexer.js +2 -2
- package/dist/conversation-index/search.d.ts +3 -1
- package/dist/conversation-index/search.js +2 -2
- package/dist/cross-namespace-budget.d.ts +45 -1
- package/dist/cross-namespace-budget.js +5 -3
- package/dist/dashboard-runtime.js +2 -2
- package/dist/day-summary.d.ts +3 -1
- package/dist/day-summary.js +3 -3
- package/dist/delinearize.d.ts +3 -1
- package/dist/direct-answer-wiring.d.ts +3 -1
- package/dist/direct-answer.d.ts +3 -1
- package/dist/{dreams-ledger-6UODQOX6.js → dreams-ledger-567VKVOU.js} +4 -2
- package/dist/dreams-ledger-567VKVOU.js.map +1 -0
- package/dist/embedding-fallback.d.ts +3 -1
- package/dist/embedding-fallback.js +3 -3
- package/dist/enrichment/index.d.ts +3 -1
- package/dist/entity-retrieval.d.ts +5 -2
- package/dist/entity-retrieval.js +38 -29
- package/dist/entity-schema.d.ts +3 -1
- package/dist/explicit-capture.d.ts +13 -8
- package/dist/explicit-capture.js +7 -3
- package/dist/extraction-faithfulness.d.ts +3 -1
- package/dist/extraction-faithfulness.js +5 -5
- package/dist/extraction-judge-telemetry.d.ts +3 -1
- package/dist/extraction-judge-telemetry.js +2 -2
- package/dist/extraction-judge-training.d.ts +3 -1
- package/dist/extraction-judge-training.js +2 -2
- package/dist/extraction-judge.d.ts +3 -1
- package/dist/extraction-judge.js +5 -5
- package/dist/extraction.d.ts +3 -1
- package/dist/extraction.js +13 -12
- package/dist/faithfulness-config.js +3 -2
- package/dist/fallback-llm.d.ts +3 -1
- package/dist/fallback-llm.js +4 -4
- package/dist/{first-start-migration-TI67YMLG.js → first-start-migration-CSRHCJIQ.js} +2 -2
- package/dist/{forget-CQCLB5LB.js → forget-HRPLVB3W.js} +5 -5
- package/dist/graph-dashboard-diff.d.ts +3 -1
- package/dist/graph-dashboard-key.d.ts +3 -1
- package/dist/graph-dashboard-parser.d.ts +3 -1
- package/dist/{graph-edge-decay-FO7AM4UR.js → graph-edge-decay-Z6ENALU2.js} +3 -3
- package/dist/graph-edge-decay-Z6ENALU2.js.map +1 -0
- package/dist/graph-edge-reinforcement.d.ts +3 -1
- package/dist/graph-snapshot.d.ts +3 -1
- package/dist/graph-snapshot.js +2 -2
- package/dist/graph.d.ts +57 -10
- package/dist/graph.js +1 -1
- package/dist/identity-continuity.d.ts +1 -2
- package/dist/importance.d.ts +3 -1
- package/dist/importers/index.d.ts +3 -1
- package/dist/in-flight-reads.d.ts +3 -1
- package/dist/index.d.ts +101 -92
- package/dist/index.js +290 -242
- package/dist/intent.d.ts +3 -1
- package/dist/lcm/archive.js +2 -2
- package/dist/lcm/engine.d.ts +3 -1
- package/dist/lcm/engine.js +6 -6
- package/dist/lcm/index.d.ts +3 -1
- package/dist/lcm/index.js +13 -13
- package/dist/lcm/schema.js +2 -2
- package/dist/lcm/summarizer.js +3 -3
- package/dist/lcm/tools.d.ts +3 -1
- package/dist/lifecycle.d.ts +3 -1
- package/dist/live-connectors-runner.d.ts +3 -1
- package/dist/local-llm.d.ts +5 -2
- package/dist/local-llm.js +3 -3
- package/dist/local-model-endpoint.d.ts +3 -1
- package/dist/logger.d.ts +3 -1
- package/dist/logger.js +7 -3
- package/dist/maintenance/archive-observations.js +2 -2
- package/dist/maintenance/memory-governance.d.ts +3 -1
- package/dist/maintenance/memory-governance.js +38 -28
- package/dist/maintenance/migrate-observations.js +3 -3
- package/dist/maintenance/observation-ledger-utils.js +2 -2
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +102 -1
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +40 -28
- package/dist/maintenance/rebuild-memory-projection.js +41 -31
- package/dist/maintenance/rebuild-observations.js +3 -3
- package/dist/mcp-memory-inspector-app.d.ts +22 -15
- package/dist/memory-action-policy.d.ts +3 -1
- package/dist/memory-cache.d.ts +46 -2
- package/dist/memory-cache.js +7 -1
- package/dist/memory-corpus-version.js +2 -2
- package/dist/memory-lifecycle-ledger-utils.d.ts +25 -2
- package/dist/memory-lifecycle-ledger-utils.js +11 -1
- package/dist/memory-projection-store.d.ts +3 -1
- package/dist/memory-projection-store.js +2 -2
- package/dist/memory-provenance.d.ts +3 -1
- package/dist/memory-worth-outcomes.d.ts +5 -2
- package/dist/model-registry.js +2 -2
- package/dist/models-json.d.ts +3 -1
- package/dist/models-json.js +2 -2
- package/dist/namespaces/migrate.d.ts +6 -3
- package/dist/namespaces/migrate.js +54 -45
- package/dist/namespaces/principal.d.ts +27 -2
- package/dist/namespaces/principal.js +6 -2
- package/dist/namespaces/search.d.ts +16 -2
- package/dist/namespaces/search.js +25 -21
- package/dist/namespaces/storage.d.ts +24 -3
- package/dist/namespaces/storage.js +38 -30
- package/dist/native-knowledge.d.ts +3 -1
- package/dist/native-knowledge.js +2 -2
- package/dist/negative.js +2 -2
- package/dist/offline-sync-exclude-globs.d.ts +13 -0
- package/dist/offline-sync-exclude-globs.js +10 -0
- package/dist/offline-sync-exclude-globs.js.map +1 -0
- package/dist/offline-sync-impression-drain.d.ts +78 -0
- package/dist/offline-sync-impression-drain.js +37 -0
- package/dist/offline-sync-impression-drain.js.map +1 -0
- package/dist/offline-sync.js +4 -2
- package/dist/operator-toolkit.d.ts +5 -2
- package/dist/operator-toolkit.js +73 -58
- package/dist/orchestration/compression-guideline-coordinator.d.ts +5 -2
- package/dist/orchestration/compression-guideline-coordinator.js +3 -3
- package/dist/orchestration/maintenance.d.ts +121 -2
- package/dist/orchestration/maintenance.js +47 -34
- package/dist/{orchestrator-BbZ_dQSU.d.ts → orchestrator-CQJ8U7En.d.ts} +85 -79
- package/dist/orchestrator.d.ts +13 -8
- package/dist/orchestrator.js +197 -180
- package/dist/page-versioning.js +3 -3
- package/dist/patterns-cli.d.ts +3 -1
- package/dist/{pipeline-FRMDb1fn.d.ts → pipeline-Cn-kvYeV.d.ts} +1 -1
- package/dist/policy-runtime.d.ts +3 -1
- package/dist/prioritized-embed.d.ts +19 -0
- package/dist/prioritized-embed.js +8 -0
- package/dist/prioritized-embed.js.map +1 -0
- package/dist/proactive-contention.d.ts +28 -0
- package/dist/proactive-contention.js +8 -0
- package/dist/proactive-contention.js.map +1 -0
- package/dist/profiling.js +2 -2
- package/dist/provenance.d.ts +3 -1
- package/dist/provenance.js +3 -2
- package/dist/{purge-NDWV6WYD.js → purge-IRF47ZTX.js} +3 -3
- package/dist/{qmd-xJRrHFbA.d.ts → qmd-oh9VXz1d.d.ts} +11 -1
- package/dist/qmd-preflight.d.ts +5 -2
- package/dist/qmd-recall-cache.d.ts +3 -1
- package/dist/qmd-status.d.ts +57 -0
- package/dist/qmd-status.js +18 -0
- package/dist/qmd-status.js.map +1 -0
- package/dist/qmd.d.ts +5 -2
- package/dist/qmd.js +8 -4
- package/dist/recall-concurrency-config.d.ts +16 -0
- package/dist/recall-concurrency-config.js +10 -0
- package/dist/recall-concurrency-config.js.map +1 -0
- package/dist/recall-disclosure-escalation.d.ts +3 -1
- package/dist/recall-explain-renderer.d.ts +4 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-memory-map.d.ts +26 -0
- package/dist/recall-memory-map.js +18 -0
- package/dist/recall-memory-map.js.map +1 -0
- package/dist/recall-planner-llm.d.ts +3 -1
- package/dist/recall-planner-llm.js +4 -4
- package/dist/recall-qos.js +2 -2
- package/dist/recall-state.d.ts +163 -2
- package/dist/recall-state.js +6 -3
- package/dist/recall-tag-filter.d.ts +3 -1
- package/dist/recall-timings.d.ts +11 -2
- package/dist/recall-timings.js +3 -1
- package/dist/recall-xray-cli.d.ts +3 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +3 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +3 -1
- package/dist/recall-xray.js +2 -2
- package/dist/relevance.js +3 -3
- package/dist/replay/normalizers/chatgpt.d.ts +3 -1
- package/dist/replay/normalizers/claude.d.ts +3 -1
- package/dist/replay/normalizers/openclaw.d.ts +3 -1
- package/dist/replay/normalizers/shared.d.ts +3 -1
- package/dist/replay/runner.d.ts +3 -1
- package/dist/replay/types.d.ts +3 -1
- package/dist/rerank.d.ts +8 -1
- package/dist/rerank.js +3 -1
- package/dist/resolution-3K27T4VO.js +18 -0
- package/dist/resolution-3K27T4VO.js.map +1 -0
- package/dist/resolve-auth-token.d.ts +3 -1
- package/dist/resolve-provider-secret.js +2 -2
- package/dist/resume-bundles.js +19 -14
- package/dist/retrieval-agents.d.ts +19 -16
- package/dist/retrieval-agents.js +3 -3
- package/dist/retrieval-tiers.d.ts +3 -1
- package/dist/routing/engine.d.ts +3 -1
- package/dist/routing/store.d.ts +3 -1
- package/dist/routing/store.js +2 -2
- package/dist/salvage-envelope.d.ts +18 -0
- package/dist/salvage-envelope.js +15 -0
- package/dist/salvage-envelope.js.map +1 -0
- package/dist/schemas.d.ts +24 -24
- package/dist/scope-profiles-EpJG7yKP.d.ts +30 -0
- package/dist/search/document-scanner.js +3 -2
- package/dist/search/embed-helper.d.ts +3 -1
- package/dist/search/embed-helper.js +3 -3
- package/dist/search/factory.d.ts +3 -1
- package/dist/search/factory.js +19 -17
- package/dist/search/index.d.ts +3 -1
- package/dist/search/index.js +23 -21
- package/dist/search/lancedb-backend.d.ts +3 -1
- package/dist/search/lancedb-backend.js +6 -5
- package/dist/search/meilisearch-backend.d.ts +3 -1
- package/dist/search/meilisearch-backend.js +6 -5
- package/dist/search/noop-backend.d.ts +3 -1
- package/dist/search/orama-backend.d.ts +3 -1
- package/dist/search/orama-backend.js +6 -5
- package/dist/search/port.d.ts +27 -2
- package/dist/search/port.js +1 -1
- package/dist/search/remote-backend.d.ts +3 -1
- package/dist/search/remote-backend.js +3 -3
- package/dist/secure-store/index.d.ts +12 -1
- package/dist/secure-store/index.js +6 -3
- package/dist/{semantic-consolidation-fvdZRa5Z.d.ts → semantic-consolidation-DILZEQq3.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +4 -2
- package/dist/semantic-consolidation.js +41 -32
- package/dist/semantic-rule-promotion.js +37 -27
- package/dist/semantic-rule-verifier.d.ts +3 -1
- package/dist/semantic-rule-verifier.js +38 -28
- package/dist/session-observer-bands.d.ts +3 -1
- package/dist/session-observer-state.d.ts +3 -1
- package/dist/session-observer-state.js +2 -2
- package/dist/session-transcript-migration.js +2 -2
- package/dist/shared-context/manager.d.ts +3 -1
- package/dist/shared-context/manager.js +3 -3
- package/dist/signal.d.ts +3 -1
- package/dist/{storage-CrvzaM9C.d.ts → storage-NIyKYD8g.d.ts} +505 -110
- package/dist/storage-guards.js +2 -2
- package/dist/storage.d.ts +6 -3
- package/dist/storage.js +41 -29
- package/dist/structured-attributes.d.ts +23 -1
- package/dist/structured-attributes.js +7 -1
- package/dist/summarizer.d.ts +3 -1
- package/dist/summarizer.js +11 -11
- package/dist/summary-snapshot.d.ts +3 -1
- package/dist/summary-snapshot.js +2 -2
- package/dist/temporal-index-batch.d.ts +1 -0
- package/dist/temporal-index-batch.js +10 -0
- package/dist/temporal-index-batch.js.map +1 -0
- package/dist/temporal-index-compat.d.ts +279 -1
- package/dist/temporal-index-compat.js +1 -1
- package/dist/temporal-index.d.ts +1 -225
- package/dist/temporal-index.js +35 -3
- package/dist/temporal-supersession.d.ts +5 -2
- package/dist/temporal-supersession.js +2 -2
- package/dist/temporal-timeline-recall.d.ts +3 -1
- package/dist/temporal-validity.d.ts +3 -1
- package/dist/threading.d.ts +3 -1
- package/dist/threading.js +2 -2
- package/dist/tier-migration.d.ts +5 -2
- package/dist/tier-routing.d.ts +3 -1
- package/dist/{tier-stats-KV7IWCJR.js → tier-stats-QEJSPFV2.js} +2 -2
- package/dist/tokens.js +3 -3
- package/dist/topics.d.ts +3 -1
- package/dist/transcript.d.ts +3 -1
- package/dist/transcript.js +2 -2
- package/dist/transfer/backup.js +3 -3
- package/dist/transfer/capsule-export.js +3 -3
- package/dist/transfer/capsule-import.js +6 -6
- package/dist/transfer/types.d.ts +24 -24
- package/dist/trust-score-stage.d.ts +75 -38
- package/dist/trust-score-stage.js +11 -3
- package/dist/trust-score.d.ts +3 -1
- package/dist/{types-Bl4BWDM8.d.ts → types-D_fG2ix8.d.ts} +49 -63
- package/dist/types-continuity.d.ts +52 -0
- package/dist/types-continuity.js +1 -0
- package/dist/types-continuity.js.map +1 -0
- package/dist/types.d.ts +3 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +3 -1
- package/dist/utils/serialize-mutations.d.ts +31 -2
- package/dist/utils/serialize-mutations.js +1 -1
- package/dist/verified-recall.js +39 -29
- package/dist/write-envelope.d.ts +282 -0
- package/dist/write-envelope.js +35 -0
- package/dist/write-envelope.js.map +1 -0
- package/dist/write-quarantine.d.ts +79 -0
- package/dist/write-quarantine.js +11 -0
- package/dist/write-quarantine.js.map +1 -0
- package/dist/write-rate-limiter.d.ts +45 -0
- package/dist/write-rate-limiter.js +9 -0
- package/dist/write-rate-limiter.js.map +1 -0
- package/package.json +37 -2
- package/src/access-admin-ops-surface.ts +17 -7
- package/src/access-boundary.test.ts +28 -0
- package/src/access-boundary.ts +31 -2
- package/src/access-citation.test.ts +72 -0
- package/src/access-citation.ts +139 -0
- package/src/access-errors.ts +21 -0
- package/src/access-fingerprint-parity.test.ts +211 -0
- package/src/access-http-cancellation.test.ts +59 -21
- package/src/access-http.test.ts +90 -0
- package/src/access-http.ts +79 -79
- package/src/access-mcp-cancellation.test.ts +16 -7
- package/src/access-memory-search-fanout.test.ts +220 -0
- package/src/access-memory-search-fanout.ts +168 -0
- package/src/access-observe-quarantine.test.ts +120 -0
- package/src/access-observe-write-surface.ts +130 -11
- package/src/access-operations-batch.ts +2 -0
- package/src/access-recall-concurrency.ts +773 -0
- package/src/access-recall-response.ts +307 -0
- package/src/access-recall-surface.ts +100 -252
- package/src/access-service-coding-write.test.ts +22 -4
- package/src/access-service-health.test.ts +49 -0
- package/src/access-service-namespace.test.ts +486 -8
- package/src/access-service-observe-scope.test.ts +4 -0
- package/src/access-service-recall-concurrency.test.ts +1990 -0
- package/src/access-service.ts +182 -175
- package/src/access-surface-catalog.ts +1 -0
- package/src/access-token-capabilities.ts +16 -9
- package/src/active-recall.test.ts +35 -0
- package/src/active-recall.ts +7 -0
- package/src/bounded-jsonl-state.ts +93 -0
- package/src/buffer-session.test.ts +422 -5
- package/src/buffer-surprise-trigger.test.ts +41 -0
- package/src/buffer.ts +218 -3
- package/src/capabilities-cluster-b.test.ts +1 -0
- package/src/capabilities.ts +5 -6
- package/src/cli.ts +21 -22
- package/src/coding/architecture-surfaces.test.ts +6 -3
- package/src/coding/architecture-surfaces.ts +36 -25
- package/src/coding/decision-surfaces.ts +42 -34
- package/src/compounding/engine.test.ts +5 -1
- package/src/compounding/engine.ts +7 -7
- package/src/config-qmd.ts +89 -0
- package/src/config.test.ts +136 -0
- package/src/config.ts +122 -118
- package/src/connector-provenance-lifecycle.test.ts +4 -4
- package/src/connectors/coerce.test.ts +131 -0
- package/src/connectors/coerce.ts +80 -7
- package/src/contradiction/contradiction.test.ts +10 -6
- package/src/contradiction/resolution.ts +17 -4
- package/src/correction/correction-access-wiring.ts +62 -18
- package/src/cross-namespace-budget.test.ts +35 -0
- package/src/cross-namespace-budget.ts +86 -19
- package/src/explicit-capture.ts +54 -17
- package/src/extraction.ts +2 -3
- package/src/faithfulness-graph-guard.test.ts +16 -69
- package/src/graph-recall.test.ts +31 -0
- package/src/graph.test.ts +137 -1
- package/src/graph.ts +274 -17
- package/src/index.ts +22 -2
- package/src/local-llm.ts +4 -6
- package/src/logger.ts +16 -0
- package/src/maintenance/dreams-ledger.ts +100 -49
- package/src/maintenance/rebuild-memory-lifecycle-ledger-cli.ts +77 -0
- package/src/maintenance/rebuild-memory-lifecycle-ledger.ts +373 -9
- package/src/memory-cache.ts +70 -0
- package/src/memory-lifecycle-ledger-utils.ts +51 -5
- package/src/namespaces/principal.test.ts +37 -1
- package/src/namespaces/principal.ts +48 -0
- package/src/namespaces/search.test.ts +79 -5
- package/src/namespaces/search.ts +59 -6
- package/src/namespaces/storage.ts +45 -1
- package/src/offline-sync-exclude-globs.ts +58 -0
- package/src/offline-sync-impression-drain.test.ts +316 -0
- package/src/offline-sync-impression-drain.ts +260 -0
- package/src/offline-sync.test.ts +32 -5
- package/src/offline-sync.ts +1 -14
- package/src/operator-toolkit.ts +1 -1
- package/src/orchestration/batch-deindex-flush.test.ts +146 -0
- package/src/orchestration/consolidation-run.ts +96 -85
- package/src/orchestration/extraction-envelope.ts +94 -0
- package/src/orchestration/extraction-persist-promotion.ts +134 -0
- package/src/orchestration/extraction-persist.ts +325 -260
- package/src/orchestration/extraction-run.test.ts +1 -1
- package/src/orchestration/extraction-run.ts +2 -2
- package/src/orchestration/graph-recall-coordinator.ts +18 -7
- package/src/orchestration/lifecycle-policy-coordinator.ts +63 -48
- package/src/orchestration/maintenance.test.ts +2103 -3
- package/src/orchestration/maintenance.ts +581 -2
- package/src/orchestration/namespace-read-fanout.ts +14 -8
- package/src/orchestration/orchestrator-helpers.ts +8 -1
- package/src/orchestration/orchestrator-init.ts +22 -6
- package/src/orchestration/orchestrator-namespace-scope.ts +71 -0
- package/src/orchestration/persistence-index.ts +64 -9
- package/src/orchestration/qmd-result-resolver.ts +27 -0
- package/src/orchestration/recall-entry.ts +34 -19
- package/src/orchestration/recall-internal.ts +207 -79
- package/src/orchestration/recall-rerank-coordinator.ts +229 -82
- package/src/orchestration/recall-result-formatter.ts +76 -20
- package/src/orchestration/recall-search-pipeline.test.ts +70 -0
- package/src/orchestration/recall-search-pipeline.ts +102 -129
- package/src/orchestration/recall-section-coordinator.ts +315 -85
- package/src/orchestration/semantic-consolidation-coordinator.ts +69 -37
- package/src/orchestration/session-context.ts +21 -0
- package/src/orchestration/workspace-ops.test.ts +348 -0
- package/src/orchestration/workspace-ops.ts +172 -34
- package/src/orchestrator-chunked-hash-durability.test.ts +126 -0
- package/src/orchestrator-connector-dedup.test.ts +1061 -40
- package/src/orchestrator-flush.test.ts +155 -39
- package/src/orchestrator-nonchunked-hash-durability.test.ts +121 -0
- package/src/orchestrator-source-attribution.test.ts +9 -9
- package/src/orchestrator.ts +166 -150
- package/src/prioritized-embed.ts +123 -0
- package/src/proactive-contention.test.ts +80 -0
- package/src/proactive-contention.ts +39 -0
- package/src/procedural/procedure-miner.test.ts +19 -14
- package/src/procedural/procedure-miner.ts +44 -13
- package/src/qmd-client.test.ts +111 -1
- package/src/qmd-status.ts +186 -0
- package/src/qmd.test.ts +22 -0
- package/src/qmd.ts +60 -48
- package/src/recall-budget-config.test.ts +30 -0
- package/src/recall-cold-deadline.test.ts +149 -0
- package/src/recall-concurrency-config.ts +28 -0
- package/src/recall-memory-map.ts +74 -0
- package/src/recall-rerank-coordinator.test.ts +508 -0
- package/src/recall-state.test.ts +825 -1
- package/src/recall-state.ts +406 -7
- package/src/recall-timings-access.test.ts +41 -0
- package/src/recall-timings.ts +16 -0
- package/src/relay/mission-access.test.ts +5 -5
- package/src/rerank.ts +22 -0
- package/src/retrieval-agents.test.ts +79 -0
- package/src/retrieval-agents.ts +71 -19
- package/src/salvage-envelope.ts +28 -0
- package/src/scopes/scope-plan.test.ts +42 -0
- package/src/scopes/scope-plan.ts +45 -0
- package/src/search/port.ts +24 -0
- package/src/secure-store/index.ts +1 -0
- package/src/secure-store/secure-fs.ts +48 -0
- package/src/secure-store/secure-store.test.ts +40 -0
- package/src/semantic-rule-promotion.ts +13 -4
- package/src/storage/content-hash-index.ts +655 -0
- package/src/storage/entity-timeline.ts +473 -0
- package/src/storage/memory-lifecycle-ledger-access.ts +603 -0
- package/src/storage/secure-line-reader.test.ts +197 -0
- package/src/storage/secure-line-reader.ts +181 -3
- package/src/storage-append-header-sniff.test.ts +195 -0
- package/src/storage-behavior-signals-cache.test.ts +237 -0
- package/src/storage-contract/cache-coherence.test.ts +96 -1
- package/src/storage-contract/content-hash.test.ts +47 -2
- package/src/storage-fact-hash-index-defer.test.ts +1200 -0
- package/src/storage-memory-lifecycle-ledger.test.ts +1146 -3
- package/src/storage.ts +942 -903
- package/src/structured-attributes.ts +52 -0
- package/src/temporal-index-batch.ts +112 -0
- package/src/temporal-index.test.ts +193 -0
- package/src/temporal-index.ts +49 -53
- package/src/testing/lifecycle-matrix.test.ts +212 -0
- package/src/testing/lifecycle-matrix.ts +271 -0
- package/src/testing/orchestrator-lite.ts +236 -0
- package/src/testing/subjects/dreams-ledger.test.ts +67 -0
- package/src/testing/subjects/extraction-lifecycle.test.ts +442 -0
- package/src/testing/subjects/lifecycle-ledger.test.ts +93 -0
- package/src/testing/subjects/recall-budget.test.ts +461 -0
- package/src/testing/subjects/serialized-write-chain.test.ts +224 -0
- package/src/trust-score-stage.test.ts +154 -0
- package/src/trust-score-stage.ts +216 -53
- package/src/types-continuity.ts +57 -0
- package/src/types.ts +66 -77
- package/src/utils/path-containment.test.ts +75 -0
- package/src/utils/path-containment.ts +76 -0
- package/src/utils/serialize-mutations.test.ts +122 -0
- package/src/utils/serialize-mutations.ts +68 -3
- package/src/wearables/memory-gen.test.ts +31 -9
- package/src/wearables/memory-gen.ts +95 -55
- package/src/wearables/pipeline.test.ts +10 -7
- package/src/wearables/service.test.ts +6 -8
- package/src/wearables/service.ts +2 -2
- package/src/write-envelope.extension.test.ts +111 -0
- package/src/write-envelope.test.ts +683 -0
- package/src/write-envelope.ts +902 -0
- package/src/write-quarantine.test.ts +193 -0
- package/src/write-quarantine.ts +241 -0
- package/src/write-rate-limiter.test.ts +105 -0
- package/src/write-rate-limiter.ts +135 -0
- package/src/write-sealed-parity.test.ts +204 -0
- package/dist/chunk-2VN4JAX7.js.map +0 -1
- package/dist/chunk-37CTGERM.js.map +0 -1
- package/dist/chunk-7ICYQUOL.js.map +0 -1
- package/dist/chunk-AD6AMF2N.js.map +0 -1
- package/dist/chunk-AHLPZ2SF.js.map +0 -1
- package/dist/chunk-ASKEM4AR.js.map +0 -1
- package/dist/chunk-B3F4CWQR.js.map +0 -1
- package/dist/chunk-BCDKBDSY.js.map +0 -1
- package/dist/chunk-BFPETA3S.js.map +0 -1
- package/dist/chunk-BFVEEB7W.js.map +0 -1
- package/dist/chunk-E5BK6CGV.js +0 -335
- package/dist/chunk-E5BK6CGV.js.map +0 -1
- package/dist/chunk-EEARRVZ2.js.map +0 -1
- package/dist/chunk-F7AU34FC.js.map +0 -1
- package/dist/chunk-FBNWOMCE.js.map +0 -1
- package/dist/chunk-FTYDWKO6.js.map +0 -1
- package/dist/chunk-GMSDQUCR.js.map +0 -1
- package/dist/chunk-GWTFLF2P.js.map +0 -1
- package/dist/chunk-IFJ23BHU.js.map +0 -1
- package/dist/chunk-IKQTFQMI.js.map +0 -1
- package/dist/chunk-J2HSAU72.js.map +0 -1
- package/dist/chunk-JMBVCAR4.js.map +0 -1
- package/dist/chunk-KQDNAXOM.js.map +0 -1
- package/dist/chunk-KSBEEMQF.js.map +0 -1
- package/dist/chunk-KSNVDPDF.js.map +0 -1
- package/dist/chunk-LC7OODWZ.js.map +0 -1
- package/dist/chunk-LTJAMRGI.js.map +0 -1
- package/dist/chunk-ND5M64JJ.js.map +0 -1
- package/dist/chunk-NUWYOKS6.js.map +0 -1
- package/dist/chunk-NWTGD4AN.js.map +0 -1
- package/dist/chunk-OBMR4VIX.js.map +0 -1
- package/dist/chunk-ODPLEWB6.js.map +0 -1
- package/dist/chunk-PHK3HARR.js +0 -32
- package/dist/chunk-PHK3HARR.js.map +0 -1
- package/dist/chunk-RANHFI3P.js +0 -8
- package/dist/chunk-RANHFI3P.js.map +0 -1
- package/dist/chunk-RBQLJSJ7.js.map +0 -1
- package/dist/chunk-RULE4VG5.js.map +0 -1
- package/dist/chunk-SVOZFLIQ.js +0 -16
- package/dist/chunk-SVOZFLIQ.js.map +0 -1
- package/dist/chunk-TXLGHDHS.js.map +0 -1
- package/dist/chunk-UN3CH3CO.js +0 -118
- package/dist/chunk-UN3CH3CO.js.map +0 -1
- package/dist/chunk-VSMRDBFT.js +0 -17
- package/dist/chunk-VSMRDBFT.js.map +0 -1
- package/dist/chunk-WU46YPY6.js.map +0 -1
- package/dist/chunk-X47LWMKA.js.map +0 -1
- package/dist/chunk-X64YLC24.js.map +0 -1
- package/dist/chunk-XVLEGMJI.js.map +0 -1
- package/dist/chunk-YF6FMLBT.js.map +0 -1
- package/dist/resolution-VH4NGENZ.js +0 -12
- /package/dist/{capsule-crypto-CZJSLEFG.js.map → access-citation.js.map} +0 -0
- /package/dist/{contradiction-scan-J57T5QV7.js.map → access-memory-search-fanout.js.map} +0 -0
- /package/dist/{dreams-ledger-6UODQOX6.js.map → access-recall-concurrency.js.map} +0 -0
- /package/dist/{graph-edge-decay-FO7AM4UR.js.map → access-recall-response.js.map} +0 -0
- /package/dist/{resolution-VH4NGENZ.js.map → bounded-jsonl-state.js.map} +0 -0
- /package/dist/{capsule-merge-Z2HUO6IK.js.map → capsule-merge-5KKJQBWQ.js.map} +0 -0
- /package/dist/{chunk-A3GOMRJG.js.map → chunk-23L3IXFD.js.map} +0 -0
- /package/dist/{chunk-MRHFJV44.js.map → chunk-2DQ2XREU.js.map} +0 -0
- /package/dist/{chunk-L2B3EE7B.js.map → chunk-2QMKMVHH.js.map} +0 -0
- /package/dist/{chunk-6CALL5VN.js.map → chunk-2UFO7X4Q.js.map} +0 -0
- /package/dist/{chunk-2MR3MFQB.js.map → chunk-2VEOKUPP.js.map} +0 -0
- /package/dist/{chunk-RHOZ5LLO.js.map → chunk-347WD2LE.js.map} +0 -0
- /package/dist/{chunk-5S7Z5SHA.js.map → chunk-34RC5RT7.js.map} +0 -0
- /package/dist/{chunk-KGWXHWAH.js.map → chunk-3QAH2S33.js.map} +0 -0
- /package/dist/{chunk-ZIDSD6VC.js.map → chunk-3Y224NYC.js.map} +0 -0
- /package/dist/{chunk-VFSP73LP.js.map → chunk-45GF7Y3V.js.map} +0 -0
- /package/dist/{chunk-OEHCMALU.js.map → chunk-5I4M26Q7.js.map} +0 -0
- /package/dist/{chunk-2E6G7FCE.js.map → chunk-5Q7EVV4X.js.map} +0 -0
- /package/dist/{chunk-VZ5OJE4X.js.map → chunk-5VQJBH2G.js.map} +0 -0
- /package/dist/{chunk-PUBTHW5O.js.map → chunk-6DRPPQEJ.js.map} +0 -0
- /package/dist/{chunk-4LEF6XWJ.js.map → chunk-6O22TZSB.js.map} +0 -0
- /package/dist/{chunk-ZQ7UBUZ6.js.map → chunk-6QYST5XN.js.map} +0 -0
- /package/dist/{chunk-W4PGTAS2.js.map → chunk-6WAFYNVT.js.map} +0 -0
- /package/dist/{chunk-R5VAVVHP.js.map → chunk-72A4L5JJ.js.map} +0 -0
- /package/dist/{chunk-3FAMU5TX.js.map → chunk-A6USBZTO.js.map} +0 -0
- /package/dist/{chunk-TXVD7PLT.js.map → chunk-A72SWTWW.js.map} +0 -0
- /package/dist/{chunk-FC77VUX5.js.map → chunk-A7Q43EUZ.js.map} +0 -0
- /package/dist/{chunk-UJBESW6X.js.map → chunk-AE7WMV4V.js.map} +0 -0
- /package/dist/{chunk-OKUEZOGW.js.map → chunk-AITCS2RC.js.map} +0 -0
- /package/dist/{chunk-OC35KQIN.js.map → chunk-AJ7QCSIS.js.map} +0 -0
- /package/dist/{chunk-FNMFJWBY.js.map → chunk-ANEUPF6O.js.map} +0 -0
- /package/dist/{chunk-2A3OXAP2.js.map → chunk-B7RWBZXW.js.map} +0 -0
- /package/dist/{chunk-N3JDIRBW.js.map → chunk-BLAKS3CP.js.map} +0 -0
- /package/dist/{chunk-M7VGG3ID.js.map → chunk-BV5WMSIJ.js.map} +0 -0
- /package/dist/{chunk-2BFLDH5F.js.map → chunk-BXLOS5AJ.js.map} +0 -0
- /package/dist/{chunk-UXKMMNJD.js.map → chunk-C23BYTJV.js.map} +0 -0
- /package/dist/{chunk-JZHHREBL.js.map → chunk-C3AKM55W.js.map} +0 -0
- /package/dist/{chunk-RP7YXA67.js.map → chunk-C5JF6EZ7.js.map} +0 -0
- /package/dist/{chunk-DDGV7NAX.js.map → chunk-CQMEM5CC.js.map} +0 -0
- /package/dist/{chunk-A7B4RMQG.js.map → chunk-CXZQOVIE.js.map} +0 -0
- /package/dist/{chunk-7N2RUBAP.js.map → chunk-DDA47TES.js.map} +0 -0
- /package/dist/{chunk-64SE4X5G.js.map → chunk-DEBHXHJR.js.map} +0 -0
- /package/dist/{chunk-EHK6KZHK.js.map → chunk-DNHVPZOD.js.map} +0 -0
- /package/dist/{chunk-437S3G37.js.map → chunk-DQEMWVMT.js.map} +0 -0
- /package/dist/{chunk-LZZNTPLR.js.map → chunk-DX3XRGLH.js.map} +0 -0
- /package/dist/{chunk-7MW3CVLD.js.map → chunk-ES36HTSC.js.map} +0 -0
- /package/dist/{chunk-TN7THW5U.js.map → chunk-ET356KSB.js.map} +0 -0
- /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
- /package/dist/{chunk-7LFXDGLH.js.map → chunk-F7GAIWFA.js.map} +0 -0
- /package/dist/{chunk-IKSELOIV.js.map → chunk-FFPJINJE.js.map} +0 -0
- /package/dist/{chunk-V53GJUSL.js.map → chunk-FUTF72MJ.js.map} +0 -0
- /package/dist/{chunk-5UNWK7KU.js.map → chunk-GFPZS3F5.js.map} +0 -0
- /package/dist/{chunk-G6NKALRN.js.map → chunk-H4WG3UQZ.js.map} +0 -0
- /package/dist/{chunk-HLTAPHZZ.js.map → chunk-HNZKVLAN.js.map} +0 -0
- /package/dist/{chunk-5EZXHU36.js.map → chunk-HYQJQGD3.js.map} +0 -0
- /package/dist/{chunk-TRWPCR6P.js.map → chunk-JECBQBEX.js.map} +0 -0
- /package/dist/{chunk-HG32URJJ.js.map → chunk-JYIFGGEM.js.map} +0 -0
- /package/dist/{chunk-VA4WSW54.js.map → chunk-JYIJNNN3.js.map} +0 -0
- /package/dist/{chunk-L5HJQC36.js.map → chunk-K2TDK7GG.js.map} +0 -0
- /package/dist/{chunk-MKDRQAA3.js.map → chunk-KAVBJSBQ.js.map} +0 -0
- /package/dist/{chunk-4OKLXWDZ.js.map → chunk-KNB7IBBU.js.map} +0 -0
- /package/dist/{chunk-QLULYKWZ.js.map → chunk-KSXJOC4F.js.map} +0 -0
- /package/dist/{chunk-JI5AVXYO.js.map → chunk-LEPOP46W.js.map} +0 -0
- /package/dist/{chunk-O7BBFJLP.js.map → chunk-LTAVSOZW.js.map} +0 -0
- /package/dist/{chunk-2IZAGAJR.js.map → chunk-LULR4IUW.js.map} +0 -0
- /package/dist/{chunk-WMWVO45V.js.map → chunk-MC6XO7T4.js.map} +0 -0
- /package/dist/{chunk-D6RIFG4O.js.map → chunk-MNB5RMAY.js.map} +0 -0
- /package/dist/{chunk-67ZCJLJY.js.map → chunk-MYFQ3ED4.js.map} +0 -0
- /package/dist/{chunk-V3UE5I5F.js.map → chunk-MYVPLJOO.js.map} +0 -0
- /package/dist/{chunk-MTWKDOVO.js.map → chunk-NLE6TSLG.js.map} +0 -0
- /package/dist/{chunk-DBRYEIGY.js.map → chunk-OJP3N7UK.js.map} +0 -0
- /package/dist/{chunk-SMX5267P.js.map → chunk-OZKOPQCM.js.map} +0 -0
- /package/dist/{chunk-JCK7V6SN.js.map → chunk-P65YJEEL.js.map} +0 -0
- /package/dist/{chunk-MTR7BZQN.js.map → chunk-PBGWYM3H.js.map} +0 -0
- /package/dist/{chunk-AXTXN4T6.js.map → chunk-PWFCHOB3.js.map} +0 -0
- /package/dist/{chunk-FDPZVY6D.js.map → chunk-QFN4U7AG.js.map} +0 -0
- /package/dist/{chunk-K2ZLHESQ.js.map → chunk-QIEUU4UK.js.map} +0 -0
- /package/dist/{chunk-3D6AFGQR.js.map → chunk-QVT3E4CE.js.map} +0 -0
- /package/dist/{chunk-SFQ6QNL7.js.map → chunk-R6YJI3HE.js.map} +0 -0
- /package/dist/{chunk-5CJVLG3H.js.map → chunk-RFDD3B7W.js.map} +0 -0
- /package/dist/{chunk-JBPKEARU.js.map → chunk-RKNJBZ55.js.map} +0 -0
- /package/dist/{chunk-UTRJFDP2.js.map → chunk-S2BBISHU.js.map} +0 -0
- /package/dist/{chunk-IALLX7EE.js.map → chunk-S2FSBU4H.js.map} +0 -0
- /package/dist/{chunk-7K5Q6COX.js.map → chunk-TVVEYCNW.js.map} +0 -0
- /package/dist/{chunk-PK5XBDA6.js.map → chunk-U3ZAVRP3.js.map} +0 -0
- /package/dist/{chunk-4KJLYTLS.js.map → chunk-V57NLO5P.js.map} +0 -0
- /package/dist/{chunk-FLVL4L3L.js.map → chunk-VDX2J7OX.js.map} +0 -0
- /package/dist/{chunk-7XGSIUP5.js.map → chunk-W4FBGYGK.js.map} +0 -0
- /package/dist/{chunk-6SXVCD7W.js.map → chunk-W7IGLX4G.js.map} +0 -0
- /package/dist/{chunk-KAWTOYAW.js.map → chunk-WLZYGLJ4.js.map} +0 -0
- /package/dist/{chunk-W5NKDU24.js.map → chunk-WON4APQY.js.map} +0 -0
- /package/dist/{chunk-NKCPUHCI.js.map → chunk-WZFAPHIK.js.map} +0 -0
- /package/dist/{chunk-KQPJLK3X.js.map → chunk-XL24QQ63.js.map} +0 -0
- /package/dist/{chunk-4B4MUS5D.js.map → chunk-YROFNEG5.js.map} +0 -0
- /package/dist/{chunk-VRAD2FGO.js.map → chunk-Z54EM5YZ.js.map} +0 -0
- /package/dist/{first-start-migration-TI67YMLG.js.map → first-start-migration-CSRHCJIQ.js.map} +0 -0
- /package/dist/{forget-CQCLB5LB.js.map → forget-HRPLVB3W.js.map} +0 -0
- /package/dist/{purge-NDWV6WYD.js.map → purge-IRF47ZTX.js.map} +0 -0
- /package/dist/{tier-stats-KV7IWCJR.js.map → tier-stats-QEJSPFV2.js.map} +0 -0
|
@@ -0,0 +1,1990 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
EngramAccessInputError,
|
|
6
|
+
EngramAccessService,
|
|
7
|
+
type EngramAccessRecallRequest,
|
|
8
|
+
type EngramAccessRecallResponse,
|
|
9
|
+
} from "./access-service.js";
|
|
10
|
+
import type { BudgetReservation } from "./cross-namespace-budget.js";
|
|
11
|
+
import {
|
|
12
|
+
type RecallCoordinatorHost,
|
|
13
|
+
consumeFlight,
|
|
14
|
+
createAndStartFlight,
|
|
15
|
+
leadRecallFlight,
|
|
16
|
+
raceAbort,
|
|
17
|
+
withRecallConcurrency,
|
|
18
|
+
} from "./access-recall-concurrency.js";
|
|
19
|
+
|
|
20
|
+
// Issue #1906 (+ review): the per-principal recall lock was a width-1 FIFO
|
|
21
|
+
// mutex held for the whole ~30s recall. Its replacement is a per-principal
|
|
22
|
+
// concurrency semaphore (recallMaxConcurrentPerPrincipal) plus query-level
|
|
23
|
+
// single-flight coalescing (recallSingleFlightEnabled). These tests exercise:
|
|
24
|
+
// - concurrency (distinct recalls overlap; cap honored; queueWaitMs)
|
|
25
|
+
// - single-flight (coalesce to one pipeline; exactly-once even at cap < N;
|
|
26
|
+
// followers join without a slot)
|
|
27
|
+
// - atomic budget admission (no over-admission; per-caller record; deny msg;
|
|
28
|
+
// rollback on failure)
|
|
29
|
+
// - flight abort semantics (own refcounted controller: leader abort keeps the
|
|
30
|
+
// shared pipeline for followers; all-callers-abort cancels it; queued abort
|
|
31
|
+
// immediate; no lane poisoning) and abort-listener cleanup.
|
|
32
|
+
//
|
|
33
|
+
// Coalescing is deterministic: the leader registers its flight synchronously in
|
|
34
|
+
// the same turn recall() sees the miss, so every later identical arrival joins
|
|
35
|
+
// by map lookup — no wall-clock waits.
|
|
36
|
+
|
|
37
|
+
function deferred<T = void>(): {
|
|
38
|
+
promise: Promise<T>;
|
|
39
|
+
resolve: (value: T | PromiseLike<T>) => void;
|
|
40
|
+
reject: (reason?: unknown) => void;
|
|
41
|
+
} {
|
|
42
|
+
let resolve!: (value: T | PromiseLike<T>) => void;
|
|
43
|
+
let reject!: (reason?: unknown) => void;
|
|
44
|
+
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
|
|
45
|
+
resolve = resolvePromise;
|
|
46
|
+
reject = rejectPromise;
|
|
47
|
+
});
|
|
48
|
+
return { promise, resolve, reject };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Yield past all pending microtasks (a single macrotask hop). Deterministic and
|
|
52
|
+
// count-free: lets a coalesced follower fully consume and reach its persistence
|
|
53
|
+
// await before the test observes its state.
|
|
54
|
+
function flushMacrotasks(): Promise<void> {
|
|
55
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
56
|
+
setImmediate(resolve);
|
|
57
|
+
return promise;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// A recall response shaped only as this suite's stubs need it. Assigned via a
|
|
61
|
+
// named helper (not an inline cast into a member access): the pipeline is
|
|
62
|
+
// stubbed, so the real envelope shape is irrelevant — we only assert
|
|
63
|
+
// structuredClone independence of the `results` array.
|
|
64
|
+
function stubResponse(results: unknown[]): EngramAccessRecallResponse {
|
|
65
|
+
return { results } as unknown as EngramAccessRecallResponse;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readResults(response: EngramAccessRecallResponse): unknown[] {
|
|
69
|
+
assert.ok(response && typeof response === "object" && "results" in response);
|
|
70
|
+
const { results } = response as { results: unknown[] };
|
|
71
|
+
return results;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type BudgetDecision = {
|
|
75
|
+
allowed: boolean;
|
|
76
|
+
reason?: string;
|
|
77
|
+
count?: number;
|
|
78
|
+
limit?: { hardLimit: number; windowMs: number; softLimit?: number };
|
|
79
|
+
reservation?: BudgetReservation;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
interface Harness {
|
|
83
|
+
service: EngramAccessService;
|
|
84
|
+
recallSemaphores: Map<string, unknown>;
|
|
85
|
+
recallInFlight: Map<string, unknown>;
|
|
86
|
+
/** Net live budget reservations (record minus release) under the default
|
|
87
|
+
* budget model — used to assert exact-entry rollback on failures. */
|
|
88
|
+
liveBudget: () => number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build an EngramAccessService whose recall pipeline is a stub. `pipeline` runs
|
|
93
|
+
* once per leader execution (its call count == number of real pipeline runs).
|
|
94
|
+
* When `crossNamespace` is set, executeRecall models the real atomic budget
|
|
95
|
+
* admission: it RESERVES (budget.record, returning a token) BEFORE running the
|
|
96
|
+
* pipeline and rolls back (budget.release(token)) if the pipeline fails —
|
|
97
|
+
* exactly like access-recall-surface.executeRecall.
|
|
98
|
+
*/
|
|
99
|
+
function makeService(opts: {
|
|
100
|
+
limit?: number;
|
|
101
|
+
singleFlight?: boolean;
|
|
102
|
+
pipeline: (request: EngramAccessRecallRequest) => Promise<EngramAccessRecallResponse>;
|
|
103
|
+
crossNamespace?: boolean;
|
|
104
|
+
budgetRecord?: (principal: string) => BudgetDecision;
|
|
105
|
+
budgetRelease?: (reservation: BudgetReservation | undefined) => void;
|
|
106
|
+
/** Fired by the default budget model each time a reservation is released —
|
|
107
|
+
* lets tests await release deterministically instead of polling microtasks. */
|
|
108
|
+
onBudgetRelease?: () => void;
|
|
109
|
+
/** Fired by the default budget model each time a reservation is recorded. */
|
|
110
|
+
onBudgetRecord?: () => void;
|
|
111
|
+
}): Harness {
|
|
112
|
+
const service = Object.create(EngramAccessService.prototype) as EngramAccessService;
|
|
113
|
+
const recallSemaphores = new Map<string, unknown>();
|
|
114
|
+
const recallInFlight = new Map<string, unknown>();
|
|
115
|
+
// Default budget model: token-tracking so tests can assert exact-entry
|
|
116
|
+
// rollback. Custom budgetRecord (deny/count tests) bypasses the live set.
|
|
117
|
+
let reservationSeq = 0;
|
|
118
|
+
const liveReservations = new Set<number>();
|
|
119
|
+
const defaultRecord = (principal: string): BudgetDecision => {
|
|
120
|
+
const id = ++reservationSeq;
|
|
121
|
+
liveReservations.add(id);
|
|
122
|
+
opts.onBudgetRecord?.();
|
|
123
|
+
return { allowed: true, reservation: { principal, id } };
|
|
124
|
+
};
|
|
125
|
+
const defaultRelease = (reservation: BudgetReservation | undefined): void => {
|
|
126
|
+
if (reservation) liveReservations.delete(reservation.id);
|
|
127
|
+
opts.onBudgetRelease?.();
|
|
128
|
+
};
|
|
129
|
+
const budgetRecord = opts.budgetRecord ?? defaultRecord;
|
|
130
|
+
const budgetRelease = opts.budgetRelease ?? defaultRelease;
|
|
131
|
+
const host = service as unknown as {
|
|
132
|
+
recallSemaphores: Map<string, unknown>;
|
|
133
|
+
recallInFlight: Map<string, unknown>;
|
|
134
|
+
orchestrator: { config: Record<string, unknown> };
|
|
135
|
+
resolveRequestPrincipal: () => string;
|
|
136
|
+
executeRecall: (
|
|
137
|
+
request: EngramAccessRecallRequest,
|
|
138
|
+
) => Promise<{
|
|
139
|
+
response: EngramAccessRecallResponse;
|
|
140
|
+
budgetRecordPrincipal: string | null;
|
|
141
|
+
reservation?: BudgetReservation;
|
|
142
|
+
}>;
|
|
143
|
+
budget: {
|
|
144
|
+
record: (principal: string) => BudgetDecision;
|
|
145
|
+
release: (reservation: BudgetReservation | undefined) => void;
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
host.recallSemaphores = recallSemaphores;
|
|
149
|
+
host.recallInFlight = recallInFlight;
|
|
150
|
+
host.orchestrator = {
|
|
151
|
+
config: {
|
|
152
|
+
recallMaxConcurrentPerPrincipal: opts.limit ?? 4,
|
|
153
|
+
recallSingleFlightEnabled: opts.singleFlight ?? true,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
host.resolveRequestPrincipal = () => "principal";
|
|
157
|
+
host.budget = { record: budgetRecord, release: budgetRelease };
|
|
158
|
+
host.executeRecall = async (request) => {
|
|
159
|
+
let reserved: string | null = null;
|
|
160
|
+
let reservation: BudgetReservation | undefined;
|
|
161
|
+
if (opts.crossNamespace) {
|
|
162
|
+
// Atomic admission reserve BEFORE the pipeline (#1906): a denied reserve
|
|
163
|
+
// throws here, so the pipeline never runs (no over-admission).
|
|
164
|
+
const decision = budgetRecord("principal");
|
|
165
|
+
if (!decision.allowed) {
|
|
166
|
+
throw new EngramAccessInputError(
|
|
167
|
+
`recall denied: cross-namespace budget exceeded (${decision.count}/${decision.limit?.hardLimit} in ${decision.limit?.windowMs}ms window)`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
reserved = "principal";
|
|
171
|
+
reservation = decision.reservation;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const response = await opts.pipeline(request);
|
|
175
|
+
return { response, budgetRecordPrincipal: reserved, reservation };
|
|
176
|
+
} catch (err) {
|
|
177
|
+
budgetRelease(reservation); // roll back the exact entry on pipeline failure
|
|
178
|
+
throw err;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
return {
|
|
182
|
+
service,
|
|
183
|
+
recallSemaphores,
|
|
184
|
+
recallInFlight,
|
|
185
|
+
liveBudget: () => liveReservations.size,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function abortRejects(signal: AbortSignal): Promise<never> {
|
|
190
|
+
return new Promise<never>((_resolve, reject) => {
|
|
191
|
+
if (signal.aborted) {
|
|
192
|
+
reject(new Error("aborted"));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
signal.addEventListener("abort", () => reject(new Error("aborted")), { once: true });
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
test("distinct concurrent recalls for one principal overlap (not serialized)", async () => {
|
|
200
|
+
let started = 0;
|
|
201
|
+
const bothStarted = deferred<void>();
|
|
202
|
+
const release = deferred<void>();
|
|
203
|
+
const { service } = makeService({
|
|
204
|
+
limit: 4,
|
|
205
|
+
pipeline: async () => {
|
|
206
|
+
started += 1;
|
|
207
|
+
if (started === 2) bothStarted.resolve();
|
|
208
|
+
await release.promise;
|
|
209
|
+
return stubResponse([]);
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const a = service.recall({ query: "alpha" });
|
|
214
|
+
const b = service.recall({ query: "beta" });
|
|
215
|
+
|
|
216
|
+
// Both pipelines are in flight simultaneously — under the old width-1 lock
|
|
217
|
+
// the second would not start until the first released.
|
|
218
|
+
await bothStarted.promise;
|
|
219
|
+
assert.equal(started, 2);
|
|
220
|
+
|
|
221
|
+
release.resolve();
|
|
222
|
+
await Promise.all([a, b]);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("identical concurrent recalls coalesce to one pipeline; each caller gets its own response", async () => {
|
|
226
|
+
let pipelineRuns = 0;
|
|
227
|
+
const recordCalls: string[] = [];
|
|
228
|
+
const { service, recallInFlight, recallSemaphores } = makeService({
|
|
229
|
+
limit: 0,
|
|
230
|
+
singleFlight: true,
|
|
231
|
+
crossNamespace: true,
|
|
232
|
+
pipeline: async () => {
|
|
233
|
+
pipelineRuns += 1;
|
|
234
|
+
return stubResponse([{ id: "m1" }]);
|
|
235
|
+
},
|
|
236
|
+
budgetRecord: (p) => {
|
|
237
|
+
recordCalls.push(p);
|
|
238
|
+
return { allowed: true };
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const req: EngramAccessRecallRequest = { query: "same", sessionKey: "s1" };
|
|
243
|
+
const responses = await Promise.all(
|
|
244
|
+
Array.from({ length: 5 }, () => service.recall({ ...req })),
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
assert.equal(pipelineRuns, 1, "the pipeline runs exactly once for 5 identical recalls");
|
|
248
|
+
assert.equal(responses.length, 5);
|
|
249
|
+
for (const r of responses) assert.ok(r);
|
|
250
|
+
|
|
251
|
+
// Independent objects: mutating one caller's response must not affect others.
|
|
252
|
+
readResults(responses[0]).push({ id: "mutated" });
|
|
253
|
+
assert.equal(readResults(responses[1]).length, 1);
|
|
254
|
+
|
|
255
|
+
// Budget recorded once per caller: leader reserved in executeRecall + 4
|
|
256
|
+
// followers each record their own event in followRecallFlight.
|
|
257
|
+
assert.equal(recordCalls.length, 5);
|
|
258
|
+
|
|
259
|
+
// No leaks after all coalesced callers settle.
|
|
260
|
+
assert.equal(recallInFlight.size, 0);
|
|
261
|
+
assert.equal(recallSemaphores.size, 0);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("exactly-once coalescing even when the concurrency cap is below the caller count", async () => {
|
|
265
|
+
let pipelineRuns = 0;
|
|
266
|
+
const release = deferred<void>();
|
|
267
|
+
const { service, recallInFlight } = makeService({
|
|
268
|
+
limit: 2, // cap 2 < 5 callers
|
|
269
|
+
singleFlight: true,
|
|
270
|
+
pipeline: async () => {
|
|
271
|
+
pipelineRuns += 1;
|
|
272
|
+
await release.promise;
|
|
273
|
+
return stubResponse([]);
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const calls = Array.from({ length: 5 }, () => service.recall({ query: "same" }));
|
|
278
|
+
// Followers join the synchronously-registered flight without taking a slot.
|
|
279
|
+
release.resolve();
|
|
280
|
+
const responses = await Promise.all(calls);
|
|
281
|
+
|
|
282
|
+
assert.equal(pipelineRuns, 1, "cap < N must still coalesce to one pipeline run");
|
|
283
|
+
assert.equal(responses.length, 5);
|
|
284
|
+
assert.equal(recallInFlight.size, 0);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("single-flight disabled runs the pipeline once per identical request", async () => {
|
|
288
|
+
let pipelineRuns = 0;
|
|
289
|
+
const { service } = makeService({
|
|
290
|
+
limit: 0,
|
|
291
|
+
singleFlight: false,
|
|
292
|
+
pipeline: async () => {
|
|
293
|
+
pipelineRuns += 1;
|
|
294
|
+
return stubResponse([]);
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await Promise.all(Array.from({ length: 5 }, () => service.recall({ query: "same" })));
|
|
299
|
+
assert.equal(pipelineRuns, 5);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("budget accounting stays per-caller under coalescing and preserves the deny message", async () => {
|
|
303
|
+
let recordCount = 0;
|
|
304
|
+
const { service } = makeService({
|
|
305
|
+
limit: 0,
|
|
306
|
+
singleFlight: true,
|
|
307
|
+
crossNamespace: true,
|
|
308
|
+
pipeline: async () => stubResponse([]),
|
|
309
|
+
budgetRecord: () => {
|
|
310
|
+
recordCount += 1;
|
|
311
|
+
// First two callers allowed; the third crosses the hard limit.
|
|
312
|
+
if (recordCount <= 2) return { allowed: true };
|
|
313
|
+
return { allowed: false, count: 3, limit: { hardLimit: 2, windowMs: 60_000 } };
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const settled = await Promise.allSettled(
|
|
318
|
+
Array.from({ length: 3 }, () => service.recall({ query: "same" })),
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
assert.equal(recordCount, 3, "budget.record is invoked once per coalesced caller");
|
|
322
|
+
assert.equal(settled.filter((s) => s.status === "fulfilled").length, 2);
|
|
323
|
+
const rejected = settled.filter(
|
|
324
|
+
(s): s is PromiseRejectedResult => s.status === "rejected",
|
|
325
|
+
);
|
|
326
|
+
assert.equal(rejected.length, 1);
|
|
327
|
+
const err = rejected[0].reason;
|
|
328
|
+
assert.ok(err instanceof Error);
|
|
329
|
+
assert.equal(
|
|
330
|
+
err.message,
|
|
331
|
+
"recall denied: cross-namespace budget exceeded (3/2 in 60000ms window)",
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("budget hard-limit holds under N concurrent distinct recalls (no over-admission)", async () => {
|
|
336
|
+
let pipelineRuns = 0;
|
|
337
|
+
let recordCount = 0;
|
|
338
|
+
const release = deferred<void>();
|
|
339
|
+
const { service } = makeService({
|
|
340
|
+
limit: 0, // unlimited concurrency: all N reach admission together
|
|
341
|
+
singleFlight: true,
|
|
342
|
+
crossNamespace: true,
|
|
343
|
+
pipeline: async () => {
|
|
344
|
+
pipelineRuns += 1;
|
|
345
|
+
await release.promise;
|
|
346
|
+
return stubResponse([]);
|
|
347
|
+
},
|
|
348
|
+
budgetRecord: () => {
|
|
349
|
+
recordCount += 1;
|
|
350
|
+
// hard limit 2: the 3rd+ reserve is denied AT ADMISSION, before its
|
|
351
|
+
// pipeline can run.
|
|
352
|
+
if (recordCount <= 2) return { allowed: true };
|
|
353
|
+
return { allowed: false, count: 3, limit: { hardLimit: 2, windowMs: 60_000 } };
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Distinct queries => 5 independent leaders, each reserves before its own
|
|
358
|
+
// pipeline. Fire them, then release the two admitted pipelines.
|
|
359
|
+
const calls = Array.from({ length: 5 }, (_, i) => service.recall({ query: `q${i}` }));
|
|
360
|
+
// Let all 5 reach the reserve (each is a leader; reserves are synchronous
|
|
361
|
+
// record() calls sequenced by the event loop).
|
|
362
|
+
await Promise.resolve();
|
|
363
|
+
await Promise.resolve();
|
|
364
|
+
release.resolve();
|
|
365
|
+
const settled = await Promise.allSettled(calls);
|
|
366
|
+
|
|
367
|
+
assert.equal(pipelineRuns, 2, "only 2 pipelines run; the 3rd+ deny before running (no over-admission)");
|
|
368
|
+
assert.equal(settled.filter((s) => s.status === "fulfilled").length, 2);
|
|
369
|
+
assert.equal(settled.filter((s) => s.status === "rejected").length, 3);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("a failed pipeline rolls back its budget reservation", async () => {
|
|
373
|
+
let recordCount = 0;
|
|
374
|
+
let releaseCount = 0;
|
|
375
|
+
const { service } = makeService({
|
|
376
|
+
limit: 0,
|
|
377
|
+
singleFlight: true,
|
|
378
|
+
crossNamespace: true,
|
|
379
|
+
pipeline: async () => {
|
|
380
|
+
throw new Error("pipeline boom");
|
|
381
|
+
},
|
|
382
|
+
budgetRecord: () => {
|
|
383
|
+
recordCount += 1;
|
|
384
|
+
return { allowed: true };
|
|
385
|
+
},
|
|
386
|
+
budgetRelease: () => {
|
|
387
|
+
releaseCount += 1;
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
await assert.rejects(service.recall({ query: "same" }), /pipeline boom/);
|
|
392
|
+
assert.equal(recordCount, 1, "reserved once at admission");
|
|
393
|
+
assert.equal(releaseCount, 1, "rolled back once on pipeline failure");
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("a queued recall rejects immediately on abort — before the holder releases", async () => {
|
|
397
|
+
let pipelineRuns = 0;
|
|
398
|
+
const leaderStarted = deferred<void>();
|
|
399
|
+
const releaseLeader = deferred<void>();
|
|
400
|
+
const { service, recallSemaphores } = makeService({
|
|
401
|
+
limit: 1,
|
|
402
|
+
pipeline: async (request) => {
|
|
403
|
+
pipelineRuns += 1;
|
|
404
|
+
if (request.query === "leader") {
|
|
405
|
+
leaderStarted.resolve();
|
|
406
|
+
await releaseLeader.promise;
|
|
407
|
+
}
|
|
408
|
+
return stubResponse([]);
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const leader = service.recall({ query: "leader" });
|
|
413
|
+
await leaderStarted.promise;
|
|
414
|
+
|
|
415
|
+
const controller = new AbortController();
|
|
416
|
+
const queued = service.recall({ query: "queued", abortSignal: controller.signal });
|
|
417
|
+
controller.abort();
|
|
418
|
+
|
|
419
|
+
// Rejects on abort while the leader is still held (pipelineRuns stays 1).
|
|
420
|
+
await assert.rejects(queued, (error: Error) => error.name === "AbortError");
|
|
421
|
+
assert.equal(pipelineRuns, 1, "the aborted queued recall never started its pipeline");
|
|
422
|
+
|
|
423
|
+
releaseLeader.resolve();
|
|
424
|
+
await leader;
|
|
425
|
+
assert.equal(recallSemaphores.size, 0, "semaphore self-cleans after settle");
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("an aborted queued recall does not poison the per-principal lane", async () => {
|
|
429
|
+
let thirdRan = false;
|
|
430
|
+
const leaderStarted = deferred<void>();
|
|
431
|
+
const releaseLeader = deferred<void>();
|
|
432
|
+
const { service } = makeService({
|
|
433
|
+
limit: 1,
|
|
434
|
+
pipeline: async (request) => {
|
|
435
|
+
if (request.query === "leader") {
|
|
436
|
+
leaderStarted.resolve();
|
|
437
|
+
await releaseLeader.promise;
|
|
438
|
+
} else if (request.query === "third") {
|
|
439
|
+
thirdRan = true;
|
|
440
|
+
}
|
|
441
|
+
return stubResponse([]);
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const leader = service.recall({ query: "leader" });
|
|
446
|
+
await leaderStarted.promise;
|
|
447
|
+
|
|
448
|
+
const controller = new AbortController();
|
|
449
|
+
const second = service.recall({ query: "second", abortSignal: controller.signal });
|
|
450
|
+
const third = service.recall({ query: "third" });
|
|
451
|
+
controller.abort();
|
|
452
|
+
releaseLeader.resolve();
|
|
453
|
+
|
|
454
|
+
await leader;
|
|
455
|
+
await assert.rejects(second, (error: Error) => error.name === "AbortError");
|
|
456
|
+
await third;
|
|
457
|
+
assert.equal(thirdRan, true);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("a leader abort does not reject still-connected followers (flight completes for them)", async () => {
|
|
461
|
+
let pipelineRuns = 0;
|
|
462
|
+
const started = deferred<void>();
|
|
463
|
+
const release = deferred<void>();
|
|
464
|
+
const { service } = makeService({
|
|
465
|
+
limit: 0,
|
|
466
|
+
singleFlight: true,
|
|
467
|
+
pipeline: async () => {
|
|
468
|
+
pipelineRuns += 1;
|
|
469
|
+
started.resolve();
|
|
470
|
+
await release.promise;
|
|
471
|
+
return stubResponse([{ id: "shared" }]);
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
const leaderController = new AbortController();
|
|
476
|
+
const leader = service.recall({ query: "same", abortSignal: leaderController.signal });
|
|
477
|
+
const follower = service.recall({ query: "same" }); // no signal
|
|
478
|
+
await started.promise;
|
|
479
|
+
|
|
480
|
+
// Leader disconnects mid-pipeline; the follower must still get the result.
|
|
481
|
+
leaderController.abort();
|
|
482
|
+
release.resolve();
|
|
483
|
+
|
|
484
|
+
await assert.rejects(leader, (error: Error) => error.name === "AbortError");
|
|
485
|
+
const result = await follower;
|
|
486
|
+
assert.ok(result);
|
|
487
|
+
assert.equal(pipelineRuns, 1, "the shared pipeline was not restarted for the follower");
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test("the shared pipeline is cancelled only when every attached caller aborts", async () => {
|
|
491
|
+
let pipelineCancelled = false;
|
|
492
|
+
const started = deferred<void>();
|
|
493
|
+
const { service } = makeService({
|
|
494
|
+
limit: 0,
|
|
495
|
+
singleFlight: true,
|
|
496
|
+
pipeline: async (request) => {
|
|
497
|
+
started.resolve();
|
|
498
|
+
const flightSignal = request.abortSignal;
|
|
499
|
+
assert.ok(flightSignal, "the shared pipeline runs on the flight's own signal");
|
|
500
|
+
try {
|
|
501
|
+
// Honor the flight's own abort signal (not any single caller's).
|
|
502
|
+
await abortRejects(flightSignal);
|
|
503
|
+
return stubResponse([]);
|
|
504
|
+
} catch {
|
|
505
|
+
pipelineCancelled = true;
|
|
506
|
+
throw Object.assign(new Error("operation aborted"), { name: "AbortError" });
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
const cA = new AbortController();
|
|
512
|
+
const cB = new AbortController();
|
|
513
|
+
const a = service.recall({ query: "same", abortSignal: cA.signal });
|
|
514
|
+
const b = service.recall({ query: "same", abortSignal: cB.signal });
|
|
515
|
+
await started.promise;
|
|
516
|
+
|
|
517
|
+
cA.abort(); // one caller aborts — pipeline must keep running
|
|
518
|
+
await Promise.resolve();
|
|
519
|
+
assert.equal(pipelineCancelled, false, "one caller aborting must not cancel the shared pipeline");
|
|
520
|
+
|
|
521
|
+
cB.abort(); // last caller aborts — now cancel the flight
|
|
522
|
+
await assert.rejects(a, (error: Error) => error.name === "AbortError");
|
|
523
|
+
await assert.rejects(b, (error: Error) => error.name === "AbortError");
|
|
524
|
+
assert.equal(pipelineCancelled, true, "flight cancelled once all attached callers aborted");
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test("abort listeners on a caller's signal return to baseline after the recall settles", async () => {
|
|
528
|
+
const controller = new AbortController();
|
|
529
|
+
const signal = controller.signal;
|
|
530
|
+
let liveAbortListeners = 0;
|
|
531
|
+
const origAdd = signal.addEventListener.bind(signal);
|
|
532
|
+
const origRemove = signal.removeEventListener.bind(signal);
|
|
533
|
+
// Count only "abort" listeners that are explicitly removed (the recall path
|
|
534
|
+
// never fires abort here, so raceAbort's .finally and attachFlightAbort's
|
|
535
|
+
// detach must remove every listener they added).
|
|
536
|
+
type AddArgs = Parameters<typeof signal.addEventListener>;
|
|
537
|
+
type RemoveArgs = Parameters<typeof signal.removeEventListener>;
|
|
538
|
+
const patched = signal as unknown as {
|
|
539
|
+
addEventListener: (...args: AddArgs) => void;
|
|
540
|
+
removeEventListener: (...args: RemoveArgs) => void;
|
|
541
|
+
};
|
|
542
|
+
patched.addEventListener = (...args: AddArgs) => {
|
|
543
|
+
if (args[0] === "abort") liveAbortListeners += 1;
|
|
544
|
+
origAdd(...args);
|
|
545
|
+
};
|
|
546
|
+
patched.removeEventListener = (...args: RemoveArgs) => {
|
|
547
|
+
if (args[0] === "abort") liveAbortListeners -= 1;
|
|
548
|
+
origRemove(...args);
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
const { service } = makeService({
|
|
552
|
+
limit: 0,
|
|
553
|
+
singleFlight: true,
|
|
554
|
+
pipeline: async () => stubResponse([]),
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
await service.recall({ query: "same", abortSignal: signal });
|
|
558
|
+
assert.equal(liveAbortListeners, 0, "every abort listener added during the recall was removed");
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
async function measurePeakConcurrency(limit: number): Promise<number> {
|
|
562
|
+
let active = 0;
|
|
563
|
+
let peak = 0;
|
|
564
|
+
let startedCount = 0;
|
|
565
|
+
const expectedStart = limit === 0 ? 3 : Math.min(limit, 3);
|
|
566
|
+
const reachedExpected = deferred<void>();
|
|
567
|
+
const gates: Array<() => void> = [];
|
|
568
|
+
const { service } = makeService({
|
|
569
|
+
limit,
|
|
570
|
+
singleFlight: false, // distinct queries anyway; keep coalescing out of the way
|
|
571
|
+
pipeline: async () => {
|
|
572
|
+
active += 1;
|
|
573
|
+
peak = Math.max(peak, active);
|
|
574
|
+
startedCount += 1;
|
|
575
|
+
if (startedCount === expectedStart) reachedExpected.resolve();
|
|
576
|
+
const gate = deferred<void>();
|
|
577
|
+
gates.push(gate.resolve);
|
|
578
|
+
await gate.promise;
|
|
579
|
+
active -= 1;
|
|
580
|
+
return stubResponse([]);
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
const calls = [
|
|
585
|
+
service.recall({ query: "q1" }),
|
|
586
|
+
service.recall({ query: "q2" }),
|
|
587
|
+
service.recall({ query: "q3" }),
|
|
588
|
+
];
|
|
589
|
+
|
|
590
|
+
// Wait on a real signal: exactly `expectedStart` recalls have entered the
|
|
591
|
+
// pipeline together. The semaphore holds any surplus in its FIFO queue, so
|
|
592
|
+
// peak concurrency is already captured at this instant.
|
|
593
|
+
await reachedExpected.promise;
|
|
594
|
+
const capturedPeak = peak;
|
|
595
|
+
|
|
596
|
+
// Drain to completion: resolve gates as they appear until all three calls
|
|
597
|
+
// settle. Bounded by call completion, not by wall-clock time.
|
|
598
|
+
const all = Promise.all(calls);
|
|
599
|
+
let settled = false;
|
|
600
|
+
void all.then(
|
|
601
|
+
() => {
|
|
602
|
+
settled = true;
|
|
603
|
+
},
|
|
604
|
+
() => {
|
|
605
|
+
settled = true;
|
|
606
|
+
},
|
|
607
|
+
);
|
|
608
|
+
while (!settled) {
|
|
609
|
+
while (gates.length > 0) {
|
|
610
|
+
const next = gates.shift();
|
|
611
|
+
if (next) next();
|
|
612
|
+
}
|
|
613
|
+
await Promise.resolve();
|
|
614
|
+
}
|
|
615
|
+
await all;
|
|
616
|
+
return capturedPeak;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
test("semaphore honors recallMaxConcurrentPerPrincipal (cap = 2, 1, unlimited)", async () => {
|
|
620
|
+
assert.equal(await measurePeakConcurrency(2), 2, "limit=2 caps peak concurrency at 2");
|
|
621
|
+
assert.equal(await measurePeakConcurrency(1), 1, "limit=1 restores exact serialization");
|
|
622
|
+
assert.equal(await measurePeakConcurrency(0), 3, "limit=0 (unlimited) runs all 3 at once");
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
test("queueWaitMs is threaded to the pipeline: 0 when a slot is free, >0 when queued", async () => {
|
|
626
|
+
// Drive Date.now deterministically so the queued recall records a positive
|
|
627
|
+
// wait without depending on wall-clock timing.
|
|
628
|
+
const realNow = Date.now;
|
|
629
|
+
let clock = 1_000;
|
|
630
|
+
Date.now = () => clock;
|
|
631
|
+
try {
|
|
632
|
+
const seen: Array<number | undefined> = [];
|
|
633
|
+
const leaderStarted = deferred<void>();
|
|
634
|
+
const releaseLeader = deferred<void>();
|
|
635
|
+
const { service } = makeService({
|
|
636
|
+
limit: 1,
|
|
637
|
+
singleFlight: false,
|
|
638
|
+
pipeline: async (request) => {
|
|
639
|
+
seen.push(request.queueWaitMs);
|
|
640
|
+
if (request.query === "leader") {
|
|
641
|
+
leaderStarted.resolve();
|
|
642
|
+
await releaseLeader.promise;
|
|
643
|
+
}
|
|
644
|
+
return stubResponse([]);
|
|
645
|
+
},
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
const leader = service.recall({ query: "leader" });
|
|
649
|
+
await leaderStarted.promise;
|
|
650
|
+
// Queued recall captures startedWaiting = 1000 now, then blocks on the slot.
|
|
651
|
+
const queued = service.recall({ query: "queued" });
|
|
652
|
+
// Advance the clock while it waits, then release the holder.
|
|
653
|
+
clock = 1_050;
|
|
654
|
+
releaseLeader.resolve();
|
|
655
|
+
await Promise.all([leader, queued]);
|
|
656
|
+
|
|
657
|
+
assert.equal(seen.length, 2);
|
|
658
|
+
assert.equal(seen[0], 0, "leader acquired its slot immediately (queueWaitMs 0)");
|
|
659
|
+
assert.equal(seen[1], 50, "queued recall accrued a measurable queue wait");
|
|
660
|
+
} finally {
|
|
661
|
+
Date.now = realNow;
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
test("leader failure does not poison followers permanently — a later identical recall succeeds", async () => {
|
|
666
|
+
let pipelineRuns = 0;
|
|
667
|
+
const { service, recallInFlight, recallSemaphores } = makeService({
|
|
668
|
+
limit: 0,
|
|
669
|
+
singleFlight: true,
|
|
670
|
+
pipeline: async () => {
|
|
671
|
+
pipelineRuns += 1;
|
|
672
|
+
if (pipelineRuns === 1) throw new Error("leader boom");
|
|
673
|
+
return stubResponse([]);
|
|
674
|
+
},
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
const settled = await Promise.allSettled(
|
|
678
|
+
Array.from({ length: 3 }, () => service.recall({ query: "same" })),
|
|
679
|
+
);
|
|
680
|
+
assert.equal(
|
|
681
|
+
settled.every((s) => s.status === "rejected"),
|
|
682
|
+
true,
|
|
683
|
+
"all coalesced callers see the leader failure",
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
// The in-flight entry must be cleared so a fresh identical recall re-executes.
|
|
687
|
+
assert.equal(recallInFlight.size, 0, "failed leader does not leave a poisoned entry");
|
|
688
|
+
const recovered = await service.recall({ query: "same" });
|
|
689
|
+
assert.ok(recovered);
|
|
690
|
+
assert.equal(pipelineRuns, 2, "the recovery recall ran a fresh pipeline");
|
|
691
|
+
assert.equal(recallInFlight.size, 0);
|
|
692
|
+
assert.equal(recallSemaphores.size, 0);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
test("a leader aborted while the flight is queued for a slot still completes for a live follower", async () => {
|
|
696
|
+
let sameRuns = 0;
|
|
697
|
+
const occStarted = deferred<void>();
|
|
698
|
+
const occGate = deferred<void>();
|
|
699
|
+
const { service, recallInFlight } = makeService({
|
|
700
|
+
limit: 1, // one slot: the occupier holds it while the "same" flight queues
|
|
701
|
+
singleFlight: true,
|
|
702
|
+
pipeline: async (request) => {
|
|
703
|
+
if (request.query === "occupier") {
|
|
704
|
+
occStarted.resolve();
|
|
705
|
+
await occGate.promise;
|
|
706
|
+
return stubResponse([]);
|
|
707
|
+
}
|
|
708
|
+
sameRuns += 1;
|
|
709
|
+
return stubResponse([{ id: "shared" }]);
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
// Occupy the only slot.
|
|
714
|
+
const occupier = service.recall({ query: "occupier" });
|
|
715
|
+
await occStarted.promise;
|
|
716
|
+
const baselineFlights = recallInFlight.size;
|
|
717
|
+
|
|
718
|
+
// Leader for "same": registers a flight whose pipeline QUEUES for the slot.
|
|
719
|
+
const leaderController = new AbortController();
|
|
720
|
+
const leader = service.recall({ query: "same", abortSignal: leaderController.signal });
|
|
721
|
+
// Deterministically wait (microtask spins, no wall clock) for the leader to
|
|
722
|
+
// register its flight.
|
|
723
|
+
for (let i = 0; i < 50 && recallInFlight.size <= baselineFlights; i++) {
|
|
724
|
+
await Promise.resolve();
|
|
725
|
+
}
|
|
726
|
+
assert.ok(recallInFlight.size > baselineFlights, "leader registered its flight");
|
|
727
|
+
|
|
728
|
+
// Follower joins the queued flight (no slot).
|
|
729
|
+
const follower = service.recall({ query: "same" });
|
|
730
|
+
await Promise.resolve();
|
|
731
|
+
|
|
732
|
+
// Leader disconnects WHILE the flight is still queued for a slot.
|
|
733
|
+
leaderController.abort();
|
|
734
|
+
await assert.rejects(leader, (error: Error) => error.name === "AbortError");
|
|
735
|
+
assert.equal(sameRuns, 0, "the queued pipeline has not run yet");
|
|
736
|
+
|
|
737
|
+
// Free the slot: the flight (still wanted by the follower) now runs.
|
|
738
|
+
occGate.resolve();
|
|
739
|
+
const result = await follower;
|
|
740
|
+
assert.ok(result);
|
|
741
|
+
assert.equal(sameRuns, 1, "the shared pipeline ran once for the surviving follower");
|
|
742
|
+
await occupier;
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
test("a response clone failure on a successful pipeline releases the reservation (flight path, round 3 #1)", async () => {
|
|
746
|
+
const h = makeService({
|
|
747
|
+
limit: 0,
|
|
748
|
+
singleFlight: true,
|
|
749
|
+
crossNamespace: true,
|
|
750
|
+
// A function in the response is not structuredClone-able => clone throws
|
|
751
|
+
// AFTER the pipeline (and its reserve) succeeded.
|
|
752
|
+
pipeline: async () => stubResponse([() => {}]),
|
|
753
|
+
});
|
|
754
|
+
await assert.rejects(h.service.recall({ query: "same" }));
|
|
755
|
+
assert.equal(h.liveBudget(), 0, "clone failure released the admission reservation");
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
test("a response clone failure on a successful pipeline releases the reservation (direct path, round 3 #1)", async () => {
|
|
759
|
+
const h = makeService({
|
|
760
|
+
limit: 0,
|
|
761
|
+
singleFlight: false,
|
|
762
|
+
crossNamespace: true,
|
|
763
|
+
pipeline: async () => stubResponse([() => {}]),
|
|
764
|
+
});
|
|
765
|
+
await assert.rejects(h.service.recall({ query: "same" }));
|
|
766
|
+
assert.equal(h.liveBudget(), 0, "clone failure released the admission reservation");
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
test("a new caller after all callers abort runs a fresh pipeline (does not join the cancelled flight, round 3 #2)", async () => {
|
|
770
|
+
let sameRuns = 0;
|
|
771
|
+
const firstStarted = deferred<void>();
|
|
772
|
+
const { service, recallInFlight } = makeService({
|
|
773
|
+
limit: 0,
|
|
774
|
+
singleFlight: true,
|
|
775
|
+
pipeline: async (request) => {
|
|
776
|
+
sameRuns += 1;
|
|
777
|
+
const myRun = sameRuns;
|
|
778
|
+
if (myRun === 1) {
|
|
779
|
+
// The first (to-be-cancelled) run blocks on the flight's own signal.
|
|
780
|
+
firstStarted.resolve();
|
|
781
|
+
const flightSignal = request.abortSignal;
|
|
782
|
+
assert.ok(flightSignal);
|
|
783
|
+
await abortRejects(flightSignal);
|
|
784
|
+
throw Object.assign(new Error("operation aborted"), { name: "AbortError" });
|
|
785
|
+
}
|
|
786
|
+
return stubResponse([{ run: myRun }]);
|
|
787
|
+
},
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
const cA = new AbortController();
|
|
791
|
+
const cB = new AbortController();
|
|
792
|
+
const a = service.recall({ query: "same", abortSignal: cA.signal });
|
|
793
|
+
const b = service.recall({ query: "same", abortSignal: cB.signal });
|
|
794
|
+
await firstStarted.promise;
|
|
795
|
+
|
|
796
|
+
// Every attached caller aborts => the flight is cancelled AND unregistered.
|
|
797
|
+
cA.abort();
|
|
798
|
+
cB.abort();
|
|
799
|
+
assert.equal(recallInFlight.size, 0, "cancelled flight unregisters immediately");
|
|
800
|
+
|
|
801
|
+
// A new caller arriving now must lead a FRESH pipeline, not inherit the
|
|
802
|
+
// cancelled flight's AbortError.
|
|
803
|
+
const fresh = service.recall({ query: "same" });
|
|
804
|
+
await assert.rejects(a, (error: Error) => error.name === "AbortError");
|
|
805
|
+
await assert.rejects(b, (error: Error) => error.name === "AbortError");
|
|
806
|
+
const result = await fresh;
|
|
807
|
+
assert.ok(result);
|
|
808
|
+
assert.equal(sameRuns, 2, "the new caller ran a second, fresh pipeline");
|
|
809
|
+
assert.equal(recallInFlight.size, 0);
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
test("an idempotency put failure after the leader reserves releases the reservation (round 3 #3)", async () => {
|
|
813
|
+
const h = makeService({
|
|
814
|
+
limit: 0,
|
|
815
|
+
singleFlight: true,
|
|
816
|
+
crossNamespace: true,
|
|
817
|
+
pipeline: async () => stubResponse([]),
|
|
818
|
+
});
|
|
819
|
+
// Simulate handleIdempotentRead's store put failing AFTER execute() (and thus
|
|
820
|
+
// the leader's budget reserve) succeeded.
|
|
821
|
+
const host = h.service as unknown as {
|
|
822
|
+
handleIdempotentRead: (options: {
|
|
823
|
+
execute: () => Promise<EngramAccessRecallResponse>;
|
|
824
|
+
}) => Promise<EngramAccessRecallResponse>;
|
|
825
|
+
};
|
|
826
|
+
host.handleIdempotentRead = async (options) => {
|
|
827
|
+
await options.execute();
|
|
828
|
+
throw new Error("idempotency put failed");
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
await assert.rejects(
|
|
832
|
+
h.service.recall({ query: "same", idempotencyKey: "put-fail" }),
|
|
833
|
+
/idempotency put failed/,
|
|
834
|
+
);
|
|
835
|
+
assert.equal(h.liveBudget(), 0, "the leader's reservation was released on put failure");
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
test("an idempotency put failure after a direct-path reserve releases the reservation (round 4 #1)", async () => {
|
|
839
|
+
const h = makeService({
|
|
840
|
+
limit: 0,
|
|
841
|
+
singleFlight: false, // direct path (runRecallDirect)
|
|
842
|
+
crossNamespace: true,
|
|
843
|
+
pipeline: async () => stubResponse([]),
|
|
844
|
+
});
|
|
845
|
+
// Simulate handleIdempotentRead's store put failing AFTER execute() (and thus
|
|
846
|
+
// the reserve) succeeded.
|
|
847
|
+
const host = h.service as unknown as {
|
|
848
|
+
handleIdempotentRead: (options: {
|
|
849
|
+
execute: () => Promise<EngramAccessRecallResponse>;
|
|
850
|
+
}) => Promise<EngramAccessRecallResponse>;
|
|
851
|
+
};
|
|
852
|
+
host.handleIdempotentRead = async (options) => {
|
|
853
|
+
await options.execute();
|
|
854
|
+
throw new Error("idempotency put failed");
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
await assert.rejects(
|
|
858
|
+
h.service.recall({ query: "same", idempotencyKey: "put-fail-direct" }),
|
|
859
|
+
/idempotency put failed/,
|
|
860
|
+
);
|
|
861
|
+
assert.equal(h.liveBudget(), 0, "the direct-path reservation was released on put failure");
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
test("a coalesced follower's response carries its OWN soft-limit warning, not the leader's (round 4 #2)", async () => {
|
|
865
|
+
let count = 0;
|
|
866
|
+
const release = deferred<void>();
|
|
867
|
+
const { service } = makeService({
|
|
868
|
+
limit: 0,
|
|
869
|
+
singleFlight: true,
|
|
870
|
+
crossNamespace: true,
|
|
871
|
+
pipeline: async () => {
|
|
872
|
+
await release.promise;
|
|
873
|
+
return stubResponse([]);
|
|
874
|
+
},
|
|
875
|
+
// soft=1: count 1 => under soft (no warning), count 2 => over soft (warning).
|
|
876
|
+
budgetRecord: () => {
|
|
877
|
+
count += 1;
|
|
878
|
+
const reason = count > 1 ? "warn-over-soft" : "allowed-under-soft";
|
|
879
|
+
return {
|
|
880
|
+
allowed: true,
|
|
881
|
+
reason,
|
|
882
|
+
count,
|
|
883
|
+
limit: { hardLimit: 10, softLimit: 1, windowMs: 60_000 },
|
|
884
|
+
reservation: { principal: "principal", id: count },
|
|
885
|
+
};
|
|
886
|
+
},
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
const leader = service.recall({ query: "same" });
|
|
890
|
+
const follower = service.recall({ query: "same" });
|
|
891
|
+
await Promise.resolve();
|
|
892
|
+
release.resolve();
|
|
893
|
+
const [leaderResp, followerResp] = await Promise.all([leader, follower]);
|
|
894
|
+
|
|
895
|
+
// Leader reserved count 1 (under soft) — its stub response has no warning.
|
|
896
|
+
assert.equal(leaderResp.budgetWarning, undefined, "leader (count 1) carries no warning");
|
|
897
|
+
// Follower recorded count 2 (over soft) — it must see ITS OWN warning, not the
|
|
898
|
+
// leader's absent one.
|
|
899
|
+
assert.ok(followerResp.budgetWarning, "follower (count 2) carries its own warning");
|
|
900
|
+
assert.equal(followerResp.budgetWarning?.reason, "warn-over-soft");
|
|
901
|
+
assert.equal(followerResp.budgetWarning?.count, 2);
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
test("single-flight off: a caller abort while the underlying pipeline still resolves does not leak budget (round 5 #2)", async () => {
|
|
905
|
+
const started = deferred<void>();
|
|
906
|
+
const release = deferred<void>();
|
|
907
|
+
const released = deferred<void>();
|
|
908
|
+
const h = makeService({
|
|
909
|
+
limit: 0,
|
|
910
|
+
singleFlight: false, // direct path (runRecallDirect)
|
|
911
|
+
crossNamespace: true,
|
|
912
|
+
// The pipeline resolves (with a reservation) even though the caller aborted.
|
|
913
|
+
pipeline: async () => {
|
|
914
|
+
started.resolve();
|
|
915
|
+
await release.promise;
|
|
916
|
+
return stubResponse([]);
|
|
917
|
+
},
|
|
918
|
+
onBudgetRelease: () => released.resolve(),
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
const controller = new AbortController();
|
|
922
|
+
const p = h.service.recall({ query: "same", abortSignal: controller.signal });
|
|
923
|
+
// Let the pipeline actually START (reserve budget) before aborting: the
|
|
924
|
+
// post-slot-grant abort re-check (round 14 #2) short-circuits an abort that
|
|
925
|
+
// lands BEFORE execution, so this round-5 #2 orphan-release path is only
|
|
926
|
+
// reachable once the pipeline is in flight.
|
|
927
|
+
await started.promise;
|
|
928
|
+
controller.abort();
|
|
929
|
+
await assert.rejects(p, (error: Error) => error.name === "AbortError");
|
|
930
|
+
|
|
931
|
+
// The underlying recall still resolves with a reservation nobody observed; the
|
|
932
|
+
// orphan-release fires the release hook deterministically.
|
|
933
|
+
release.resolve();
|
|
934
|
+
await released.promise;
|
|
935
|
+
assert.equal(h.liveBudget(), 0, "the orphaned reservation from the cancelled recall was released");
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
test("the flight stays registered until its consumers finish (cleanup on idle, not pipeline settle) (round 6 #1)", async () => {
|
|
939
|
+
let pipelineRuns = 0;
|
|
940
|
+
const gate = deferred<void>();
|
|
941
|
+
const { service, recallInFlight } = makeService({
|
|
942
|
+
limit: 0,
|
|
943
|
+
singleFlight: true,
|
|
944
|
+
pipeline: async () => {
|
|
945
|
+
pipelineRuns += 1;
|
|
946
|
+
await gate.promise;
|
|
947
|
+
return stubResponse([]);
|
|
948
|
+
},
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
const leader = service.recall({ query: "same" });
|
|
952
|
+
// The leader registers its flight synchronously within recall() (before its
|
|
953
|
+
// first await), so it is present immediately.
|
|
954
|
+
assert.equal(recallInFlight.size, 1, "flight registered while the leader is attached");
|
|
955
|
+
|
|
956
|
+
// A joiner arrives and coalesces onto the live flight.
|
|
957
|
+
const joiner = service.recall({ query: "same" });
|
|
958
|
+
gate.resolve();
|
|
959
|
+
await Promise.all([leader, joiner]);
|
|
960
|
+
assert.equal(pipelineRuns, 1, "joiner coalesced onto the shared pipeline");
|
|
961
|
+
// Only after ALL consumers finished is the flight unregistered.
|
|
962
|
+
assert.equal(recallInFlight.size, 0, "flight unregistered once idle");
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
test("round 6 #2 (direct path): abort between the result and the final check releases the reservation", async () => {
|
|
966
|
+
const hookGate = deferred<void>();
|
|
967
|
+
const executeDone = deferred<void>();
|
|
968
|
+
const h = makeService({
|
|
969
|
+
limit: 0,
|
|
970
|
+
singleFlight: false,
|
|
971
|
+
crossNamespace: true,
|
|
972
|
+
pipeline: async () => stubResponse([]),
|
|
973
|
+
});
|
|
974
|
+
// Inject a controllable async step AFTER execute() (models the real gap
|
|
975
|
+
// between the produced result and the final abort check).
|
|
976
|
+
const host = h.service as unknown as {
|
|
977
|
+
handleIdempotentRead: (options: {
|
|
978
|
+
execute: () => Promise<EngramAccessRecallResponse>;
|
|
979
|
+
}) => Promise<EngramAccessRecallResponse>;
|
|
980
|
+
};
|
|
981
|
+
host.handleIdempotentRead = async (options) => {
|
|
982
|
+
const response = await options.execute();
|
|
983
|
+
executeDone.resolve();
|
|
984
|
+
await hookGate.promise;
|
|
985
|
+
return response;
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
const controller = new AbortController();
|
|
989
|
+
const p = h.service.recall({ query: "same", abortSignal: controller.signal });
|
|
990
|
+
// execute() completed (pipeline done, reservation captured); now parked.
|
|
991
|
+
await executeDone.promise;
|
|
992
|
+
assert.equal(h.liveBudget(), 1, "reservation held while the result is in hand");
|
|
993
|
+
// Abort lands in the gap, then the parked step completes.
|
|
994
|
+
controller.abort();
|
|
995
|
+
hookGate.resolve();
|
|
996
|
+
await assert.rejects(p, (error: Error) => error.name === "AbortError");
|
|
997
|
+
assert.equal(h.liveBudget(), 0, "the completed-but-undelivered recall released its reservation");
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
test("round 6 #2 (flight path): abort between the result and the final check releases the reservation", async () => {
|
|
1001
|
+
const hookGate = deferred<void>();
|
|
1002
|
+
const executeDone = deferred<void>();
|
|
1003
|
+
const h = makeService({
|
|
1004
|
+
limit: 0,
|
|
1005
|
+
singleFlight: true,
|
|
1006
|
+
crossNamespace: true,
|
|
1007
|
+
pipeline: async () => stubResponse([]),
|
|
1008
|
+
});
|
|
1009
|
+
const host = h.service as unknown as {
|
|
1010
|
+
handleIdempotentRead: (options: {
|
|
1011
|
+
execute: () => Promise<EngramAccessRecallResponse>;
|
|
1012
|
+
}) => Promise<EngramAccessRecallResponse>;
|
|
1013
|
+
};
|
|
1014
|
+
host.handleIdempotentRead = async (options) => {
|
|
1015
|
+
const response = await options.execute();
|
|
1016
|
+
executeDone.resolve();
|
|
1017
|
+
await hookGate.promise;
|
|
1018
|
+
return response;
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
const controller = new AbortController();
|
|
1022
|
+
const p = h.service.recall({ query: "same", abortSignal: controller.signal });
|
|
1023
|
+
await executeDone.promise;
|
|
1024
|
+
assert.equal(h.liveBudget(), 1, "reservation held while the result is in hand");
|
|
1025
|
+
controller.abort();
|
|
1026
|
+
hookGate.resolve();
|
|
1027
|
+
await assert.rejects(p, (error: Error) => error.name === "AbortError");
|
|
1028
|
+
assert.equal(h.liveBudget(), 0, "the completed-but-undelivered recall released its reservation");
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
test("an unkeyed cross-namespace follower that disconnects after reserving releases its reservation (round 8 #1)", async () => {
|
|
1032
|
+
const gate = deferred<void>();
|
|
1033
|
+
const controller = new AbortController();
|
|
1034
|
+
let records = 0;
|
|
1035
|
+
const h = makeService({
|
|
1036
|
+
limit: 0,
|
|
1037
|
+
singleFlight: true,
|
|
1038
|
+
crossNamespace: true,
|
|
1039
|
+
pipeline: async () => {
|
|
1040
|
+
await gate.promise;
|
|
1041
|
+
return stubResponse([]);
|
|
1042
|
+
},
|
|
1043
|
+
// record #1 = leader's pipeline reserve, record #2 = the follower's own
|
|
1044
|
+
// per-caller reserve — abort the follower the instant it reserves so the
|
|
1045
|
+
// disconnect lands after the reserve but before delivery.
|
|
1046
|
+
onBudgetRecord: () => {
|
|
1047
|
+
records += 1;
|
|
1048
|
+
if (records === 2) controller.abort();
|
|
1049
|
+
},
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
const leader = h.service.recall({ query: "same" });
|
|
1053
|
+
const follower = h.service.recall({ query: "same", abortSignal: controller.signal });
|
|
1054
|
+
gate.resolve();
|
|
1055
|
+
|
|
1056
|
+
const leaderResp = await leader;
|
|
1057
|
+
assert.ok(leaderResp, "the leader delivered");
|
|
1058
|
+
await assert.rejects(follower, (error: Error) => error.name === "AbortError");
|
|
1059
|
+
assert.equal(
|
|
1060
|
+
h.liveBudget(),
|
|
1061
|
+
1,
|
|
1062
|
+
"the follower's post-consume reservation was released; only the leader's delivered one stands",
|
|
1063
|
+
);
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
test("a keyed leader whose signal is already aborted at admission rejects without starting a pipeline or reserving budget (round 9)", async () => {
|
|
1067
|
+
let pipelineRuns = 0;
|
|
1068
|
+
const h = makeService({
|
|
1069
|
+
limit: 0,
|
|
1070
|
+
singleFlight: true,
|
|
1071
|
+
crossNamespace: true,
|
|
1072
|
+
pipeline: async () => {
|
|
1073
|
+
pipelineRuns += 1;
|
|
1074
|
+
return stubResponse([]);
|
|
1075
|
+
},
|
|
1076
|
+
});
|
|
1077
|
+
// A keyed leader on a cache MISS runs execute() (the admission path under test).
|
|
1078
|
+
// Stub handleIdempotentRead to that miss path (the harness has no real store).
|
|
1079
|
+
const host = h.service as unknown as {
|
|
1080
|
+
handleIdempotentRead: (options: {
|
|
1081
|
+
execute: () => Promise<EngramAccessRecallResponse>;
|
|
1082
|
+
}) => Promise<EngramAccessRecallResponse>;
|
|
1083
|
+
};
|
|
1084
|
+
host.handleIdempotentRead = async (options) => options.execute();
|
|
1085
|
+
const controller = new AbortController();
|
|
1086
|
+
controller.abort(); // client disconnected BEFORE admission
|
|
1087
|
+
|
|
1088
|
+
await assert.rejects(
|
|
1089
|
+
h.service.recall({ query: "gone", idempotencyKey: "k", abortSignal: controller.signal }),
|
|
1090
|
+
(error: Error) => error.name === "AbortError",
|
|
1091
|
+
);
|
|
1092
|
+
// Drain any deferred work so a stray pipeline/reserve would surface.
|
|
1093
|
+
await flushMacrotasks();
|
|
1094
|
+
|
|
1095
|
+
assert.equal(pipelineRuns, 0, "no pipeline ran for a caller that left before admission");
|
|
1096
|
+
assert.equal(h.liveBudget(), 0, "no cross-namespace budget event was reserved");
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
test("a keyed caller aborted at admission does NOT join a live coalesced flight — records no budget (round 10 #1)", async () => {
|
|
1100
|
+
const pipelineGate = deferred<void>();
|
|
1101
|
+
let recordCount = 0;
|
|
1102
|
+
const h = makeService({
|
|
1103
|
+
limit: 0,
|
|
1104
|
+
singleFlight: true,
|
|
1105
|
+
crossNamespace: true,
|
|
1106
|
+
pipeline: async () => {
|
|
1107
|
+
await pipelineGate.promise;
|
|
1108
|
+
return stubResponse([]);
|
|
1109
|
+
},
|
|
1110
|
+
onBudgetRecord: () => {
|
|
1111
|
+
recordCount += 1;
|
|
1112
|
+
},
|
|
1113
|
+
});
|
|
1114
|
+
const host = h.service as unknown as {
|
|
1115
|
+
handleIdempotentRead: (options: {
|
|
1116
|
+
execute: () => Promise<EngramAccessRecallResponse>;
|
|
1117
|
+
}) => Promise<EngramAccessRecallResponse>;
|
|
1118
|
+
};
|
|
1119
|
+
// A keyed leader on a cache MISS runs execute() (the harness has no real store).
|
|
1120
|
+
host.handleIdempotentRead = async (options) => options.execute();
|
|
1121
|
+
const svc = h.service as unknown as RecallCoordinatorHost;
|
|
1122
|
+
|
|
1123
|
+
const flightKey = "principal\u0000same";
|
|
1124
|
+
const req = { query: "same", idempotencyKey: "k" } as EngramAccessRecallRequest;
|
|
1125
|
+
// A LIVE coalesced flight kept registered by a real consumer.
|
|
1126
|
+
const flight = createAndStartFlight(svc, req, flightKey, "principal", true);
|
|
1127
|
+
const liveConsumer = consumeFlight(svc, flight, req, false, false);
|
|
1128
|
+
|
|
1129
|
+
// A keyed caller whose signal is ALREADY aborted reaches leadRecallFlight and
|
|
1130
|
+
// finds the existing flight. It must reject at admission WITHOUT joining — a
|
|
1131
|
+
// keyed join is non-racing (race=false), so it would record its OWN budget
|
|
1132
|
+
// event (and run a keyed put) before the late abort check.
|
|
1133
|
+
const controller = new AbortController();
|
|
1134
|
+
controller.abort();
|
|
1135
|
+
const abortedReq = {
|
|
1136
|
+
query: "same",
|
|
1137
|
+
idempotencyKey: "k",
|
|
1138
|
+
abortSignal: controller.signal,
|
|
1139
|
+
} as EngramAccessRecallRequest;
|
|
1140
|
+
const aborted = leadRecallFlight(svc, abortedReq, abortedReq, {}, flightKey, "principal");
|
|
1141
|
+
pipelineGate.resolve();
|
|
1142
|
+
await assert.rejects(aborted, (error: Error) => error.name === "AbortError");
|
|
1143
|
+
|
|
1144
|
+
const consumed = await liveConsumer;
|
|
1145
|
+
assert.ok(consumed.response, "the live flight still completed for its other consumer");
|
|
1146
|
+
// Exactly ONE budget event: the shared pipeline's reserve. The aborted caller
|
|
1147
|
+
// joined nothing (with the bug it records a SECOND, keyed event that survives).
|
|
1148
|
+
assert.equal(recordCount, 1, "the aborted caller recorded no budget of its own");
|
|
1149
|
+
assert.equal(h.liveBudget(), 1, "only the shared pipeline's reservation is live");
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
test("a response's budgetWarning never carries the internal reservation token / principal id (round 10 #2)", async () => {
|
|
1153
|
+
let count = 0;
|
|
1154
|
+
const release = deferred<void>();
|
|
1155
|
+
const { service } = makeService({
|
|
1156
|
+
limit: 0,
|
|
1157
|
+
singleFlight: true,
|
|
1158
|
+
crossNamespace: true,
|
|
1159
|
+
pipeline: async () => {
|
|
1160
|
+
await release.promise;
|
|
1161
|
+
return stubResponse([]);
|
|
1162
|
+
},
|
|
1163
|
+
// soft=1: count 1 => under soft (no warning), count 2 => over soft (warning).
|
|
1164
|
+
// The reservation carries a distinctive principal id that must NOT leak.
|
|
1165
|
+
budgetRecord: () => {
|
|
1166
|
+
count += 1;
|
|
1167
|
+
const reason = count > 1 ? "warn-over-soft" : "allowed-under-soft";
|
|
1168
|
+
return {
|
|
1169
|
+
allowed: true,
|
|
1170
|
+
reason,
|
|
1171
|
+
count,
|
|
1172
|
+
limit: { hardLimit: 10, softLimit: 1, windowMs: 60_000 },
|
|
1173
|
+
reservation: { principal: "tenant-secret-42", id: count },
|
|
1174
|
+
};
|
|
1175
|
+
},
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
const leader = service.recall({ query: "same" });
|
|
1179
|
+
const follower = service.recall({ query: "same" });
|
|
1180
|
+
await Promise.resolve();
|
|
1181
|
+
release.resolve();
|
|
1182
|
+
const [, followerResp] = await Promise.all([leader, follower]);
|
|
1183
|
+
|
|
1184
|
+
assert.ok(followerResp.budgetWarning, "follower (count 2) carries its own soft-limit warning");
|
|
1185
|
+
assert.equal(
|
|
1186
|
+
(followerResp.budgetWarning as { reservation?: unknown }).reservation,
|
|
1187
|
+
undefined,
|
|
1188
|
+
"the server-only reservation rollback token must not ride along on the response",
|
|
1189
|
+
);
|
|
1190
|
+
const serialized = JSON.stringify(followerResp.budgetWarning);
|
|
1191
|
+
assert.ok(
|
|
1192
|
+
!serialized.includes("tenant-secret-42"),
|
|
1193
|
+
"the principal id must not leak into the serialized warning",
|
|
1194
|
+
);
|
|
1195
|
+
assert.ok(
|
|
1196
|
+
!serialized.includes("reservation"),
|
|
1197
|
+
"no reservation field in the serialized warning",
|
|
1198
|
+
);
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
test("identical recalls with DISTINCT idempotency keys coalesce onto one pipeline (round 11)", async () => {
|
|
1202
|
+
const gate = deferred<void>();
|
|
1203
|
+
let pipelineRuns = 0;
|
|
1204
|
+
const h = makeService({
|
|
1205
|
+
limit: 0,
|
|
1206
|
+
singleFlight: true,
|
|
1207
|
+
crossNamespace: true,
|
|
1208
|
+
pipeline: async () => {
|
|
1209
|
+
pipelineRuns += 1;
|
|
1210
|
+
await gate.promise;
|
|
1211
|
+
return stubResponse([]);
|
|
1212
|
+
},
|
|
1213
|
+
});
|
|
1214
|
+
// The leader (miss path) runs execute() so it registers its flight
|
|
1215
|
+
// synchronously; the follower's fast-path then finds it. The follower joins
|
|
1216
|
+
// via followRecallFlight and never touches handleIdempotentRead.
|
|
1217
|
+
const host = h.service as unknown as {
|
|
1218
|
+
handleIdempotentRead: (options: {
|
|
1219
|
+
execute: () => Promise<EngramAccessRecallResponse>;
|
|
1220
|
+
}) => Promise<EngramAccessRecallResponse>;
|
|
1221
|
+
};
|
|
1222
|
+
host.handleIdempotentRead = async (options) => options.execute();
|
|
1223
|
+
|
|
1224
|
+
// Same recall WORK (query + options), DISTINCT per-request idempotency keys —
|
|
1225
|
+
// a common transport-retry pattern. They must share ONE pipeline + slot.
|
|
1226
|
+
const leader = h.service.recall({ query: "same", idempotencyKey: "key-A" });
|
|
1227
|
+
const follower = h.service.recall({ query: "same", idempotencyKey: "key-B" });
|
|
1228
|
+
gate.resolve();
|
|
1229
|
+
const [leaderResp, followerResp] = await Promise.all([leader, follower]);
|
|
1230
|
+
|
|
1231
|
+
assert.ok(leaderResp);
|
|
1232
|
+
assert.ok(followerResp);
|
|
1233
|
+
assert.equal(
|
|
1234
|
+
pipelineRuns,
|
|
1235
|
+
1,
|
|
1236
|
+
"distinct-key identical recalls coalesced onto ONE pipeline (idempotencyKey is not in the flight key)",
|
|
1237
|
+
);
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
test("an identical arrival during a keyed leader's slow idempotency.put coalesces onto the one pipeline (round 12 #2)", async () => {
|
|
1241
|
+
const putGate = deferred<void>();
|
|
1242
|
+
const executeDone = deferred<void>();
|
|
1243
|
+
let pipelineRuns = 0;
|
|
1244
|
+
const h = makeService({
|
|
1245
|
+
limit: 0,
|
|
1246
|
+
singleFlight: true,
|
|
1247
|
+
crossNamespace: true,
|
|
1248
|
+
pipeline: async () => {
|
|
1249
|
+
pipelineRuns += 1;
|
|
1250
|
+
return stubResponse([]);
|
|
1251
|
+
},
|
|
1252
|
+
});
|
|
1253
|
+
// Only the KEYED leader enters handleIdempotentRead; gate its put so the flight
|
|
1254
|
+
// sits in the persistence window (leader consumed + detached, put pending).
|
|
1255
|
+
const host = h.service as unknown as {
|
|
1256
|
+
handleIdempotentRead: (options: {
|
|
1257
|
+
execute: () => Promise<EngramAccessRecallResponse>;
|
|
1258
|
+
}) => Promise<EngramAccessRecallResponse>;
|
|
1259
|
+
};
|
|
1260
|
+
host.handleIdempotentRead = async (options) => {
|
|
1261
|
+
const response = await options.execute();
|
|
1262
|
+
executeDone.resolve();
|
|
1263
|
+
await putGate.promise; // SLOW put
|
|
1264
|
+
return response;
|
|
1265
|
+
};
|
|
1266
|
+
|
|
1267
|
+
const leader = h.service.recall({ query: "same", idempotencyKey: "leader" });
|
|
1268
|
+
await executeDone.promise; // leader detached; flight kept alive by the persisted gate
|
|
1269
|
+
|
|
1270
|
+
// An identical UNKEYED arrival during the put window must find the still-
|
|
1271
|
+
// registered flight and coalesce onto it (not start a second pipeline).
|
|
1272
|
+
const arrival = h.service.recall({ query: "same" });
|
|
1273
|
+
await flushMacrotasks();
|
|
1274
|
+
putGate.resolve();
|
|
1275
|
+
|
|
1276
|
+
const [leaderResp, arrivalResp] = await Promise.all([leader, arrival]);
|
|
1277
|
+
assert.ok(leaderResp);
|
|
1278
|
+
assert.ok(arrivalResp);
|
|
1279
|
+
assert.equal(
|
|
1280
|
+
pipelineRuns,
|
|
1281
|
+
1,
|
|
1282
|
+
"the arrival coalesced onto the leader's pipeline through the slow persistence window",
|
|
1283
|
+
);
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
test("a KEYED distinct-key arrival during a keyed leader's slow idempotency.put coalesces via the existing-join (round 12 #2, keyed follower)", async () => {
|
|
1287
|
+
// Companion to the round 12 #2 unkeyed-arrival test: the codex finding was
|
|
1288
|
+
// specifically about a KEYED caller routing through leadRecallFlight's
|
|
1289
|
+
// existing-join during the leader's persistence window. A keyed follower must
|
|
1290
|
+
// still find the registered flight and coalesce onto the one pipeline.
|
|
1291
|
+
const putGate = deferred<void>();
|
|
1292
|
+
const leaderExecuted = deferred<void>();
|
|
1293
|
+
let pipelineRuns = 0;
|
|
1294
|
+
const h = makeService({
|
|
1295
|
+
limit: 0,
|
|
1296
|
+
singleFlight: true,
|
|
1297
|
+
crossNamespace: true,
|
|
1298
|
+
pipeline: async () => {
|
|
1299
|
+
pipelineRuns += 1;
|
|
1300
|
+
return stubResponse([]);
|
|
1301
|
+
},
|
|
1302
|
+
});
|
|
1303
|
+
// Both callers are keyed, so both enter handleIdempotentRead. The stub runs
|
|
1304
|
+
// execute() (store MISS for every distinct key), then gates the leader in its
|
|
1305
|
+
// put so the flight sits in the persistence window while the second key
|
|
1306
|
+
// arrives. `race`=false for keyed callers, so a coalescing follower awaits the
|
|
1307
|
+
// (already-resolved) pipeline immediately.
|
|
1308
|
+
const host = h.service as unknown as {
|
|
1309
|
+
handleIdempotentRead: (options: {
|
|
1310
|
+
execute: () => Promise<EngramAccessRecallResponse>;
|
|
1311
|
+
}) => Promise<EngramAccessRecallResponse>;
|
|
1312
|
+
};
|
|
1313
|
+
host.handleIdempotentRead = async (options) => {
|
|
1314
|
+
const response = await options.execute();
|
|
1315
|
+
leaderExecuted.resolve();
|
|
1316
|
+
await putGate.promise; // SLOW put for whichever caller is inside
|
|
1317
|
+
return response;
|
|
1318
|
+
};
|
|
1319
|
+
|
|
1320
|
+
const leader = h.service.recall({ query: "same", idempotencyKey: "key-A" });
|
|
1321
|
+
await leaderExecuted.promise; // leader detached; flight kept alive by persisted gate
|
|
1322
|
+
|
|
1323
|
+
// A DISTINCT idempotency key for the SAME work arrives during the put window.
|
|
1324
|
+
// It shares the flight key (idempotencyKey is excluded from it), so it must
|
|
1325
|
+
// coalesce onto the still-registered flight rather than start a new pipeline.
|
|
1326
|
+
const follower = h.service.recall({ query: "same", idempotencyKey: "key-B" });
|
|
1327
|
+
await flushMacrotasks();
|
|
1328
|
+
putGate.resolve();
|
|
1329
|
+
|
|
1330
|
+
const [leaderResp, followerResp] = await Promise.all([leader, follower]);
|
|
1331
|
+
assert.ok(leaderResp);
|
|
1332
|
+
assert.ok(followerResp);
|
|
1333
|
+
assert.equal(
|
|
1334
|
+
pipelineRuns,
|
|
1335
|
+
1,
|
|
1336
|
+
"the keyed distinct-key follower coalesced onto the leader's pipeline through the persistence window",
|
|
1337
|
+
);
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
test("an identical arrival during a keyed leader's FAILING idempotency.put still coalesces onto one pipeline; only the leader rejects (round 12 #2, put failure)", async () => {
|
|
1341
|
+
// The flight must stay registered through the persistence window regardless of
|
|
1342
|
+
// the put OUTCOME. A slow put that ultimately FAILS must not (a) start a second
|
|
1343
|
+
// pipeline for an arrival during the window, nor (b) fail that arrival: an
|
|
1344
|
+
// unkeyed follower consumed the shared result and never depends on the leader's
|
|
1345
|
+
// put. The leader's put failure is isolated to the leader.
|
|
1346
|
+
const putGate = deferred<void>();
|
|
1347
|
+
const leaderExecuted = deferred<void>();
|
|
1348
|
+
const putError = new Error("idempotency store write failed");
|
|
1349
|
+
let pipelineRuns = 0;
|
|
1350
|
+
const h = makeService({
|
|
1351
|
+
limit: 0,
|
|
1352
|
+
singleFlight: true,
|
|
1353
|
+
crossNamespace: true,
|
|
1354
|
+
pipeline: async () => {
|
|
1355
|
+
pipelineRuns += 1;
|
|
1356
|
+
return stubResponse([]);
|
|
1357
|
+
},
|
|
1358
|
+
});
|
|
1359
|
+
const host = h.service as unknown as {
|
|
1360
|
+
handleIdempotentRead: (options: {
|
|
1361
|
+
execute: () => Promise<EngramAccessRecallResponse>;
|
|
1362
|
+
}) => Promise<EngramAccessRecallResponse>;
|
|
1363
|
+
};
|
|
1364
|
+
host.handleIdempotentRead = async (options) => {
|
|
1365
|
+
const response = await options.execute();
|
|
1366
|
+
leaderExecuted.resolve();
|
|
1367
|
+
await putGate.promise; // SLOW put
|
|
1368
|
+
throw putError; // ...that ultimately FAILS
|
|
1369
|
+
};
|
|
1370
|
+
|
|
1371
|
+
const leader = h.service.recall({ query: "same", idempotencyKey: "leader" });
|
|
1372
|
+
await leaderExecuted.promise; // leader detached; flight kept alive by persisted gate
|
|
1373
|
+
|
|
1374
|
+
// An identical UNKEYED arrival during the FAILING put window coalesces onto the
|
|
1375
|
+
// still-registered flight and succeeds from the shared result before the put
|
|
1376
|
+
// fails — it never awaits the leader's persistence (round 12: followRecallFlight
|
|
1377
|
+
// is unkeyed-only and does not gate on `persisted`).
|
|
1378
|
+
const arrival = h.service.recall({ query: "same" });
|
|
1379
|
+
const arrivalResp = await arrival;
|
|
1380
|
+
assert.ok(arrivalResp, "the arrival coalesced and succeeded independent of the leader's put");
|
|
1381
|
+
|
|
1382
|
+
putGate.resolve();
|
|
1383
|
+
await assert.rejects(leader, (err: unknown) => err === putError);
|
|
1384
|
+
assert.equal(
|
|
1385
|
+
pipelineRuns,
|
|
1386
|
+
1,
|
|
1387
|
+
"the arrival coalesced onto the one pipeline through the FAILING persistence window",
|
|
1388
|
+
);
|
|
1389
|
+
// The failed keyed leader released its reservation; the surviving follower
|
|
1390
|
+
// recorded its own event, so exactly one live reservation remains.
|
|
1391
|
+
assert.equal(h.liveBudget(), 1, "leader's reservation released on put failure; follower's stands");
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
test("a keyed follower coalescing via the existing-join awaits the leader's persisted gate before succeeding (round 13 #1, success)", async () => {
|
|
1395
|
+
// Finding 1: a keyed caller that coalesces onto a live flight through
|
|
1396
|
+
// leadRecallFlight's existing-join must not report success (nor run its OWN
|
|
1397
|
+
// idempotency.put) before the LEADER's put settled. It awaits the joined
|
|
1398
|
+
// flight's persisted gate; on the leader's put SUCCESS it then returns.
|
|
1399
|
+
const putGate = deferred<void>();
|
|
1400
|
+
const leaderExecuted = deferred<void>();
|
|
1401
|
+
let pipelineRuns = 0;
|
|
1402
|
+
const h = makeService({
|
|
1403
|
+
limit: 0,
|
|
1404
|
+
singleFlight: true,
|
|
1405
|
+
crossNamespace: true,
|
|
1406
|
+
pipeline: async () => {
|
|
1407
|
+
pipelineRuns += 1;
|
|
1408
|
+
return stubResponse([]);
|
|
1409
|
+
},
|
|
1410
|
+
});
|
|
1411
|
+
// Both callers are keyed and enter handleIdempotentRead (store MISS for each
|
|
1412
|
+
// distinct key). Only the LEADER (key-A) holds a slow put; the follower's
|
|
1413
|
+
// execute() parks on the leader's persisted gate, so it never reaches its own
|
|
1414
|
+
// put until the leader's put resolves.
|
|
1415
|
+
const host = h.service as unknown as {
|
|
1416
|
+
handleIdempotentRead: (options: {
|
|
1417
|
+
idempotencyKey?: string;
|
|
1418
|
+
execute: () => Promise<EngramAccessRecallResponse>;
|
|
1419
|
+
}) => Promise<EngramAccessRecallResponse>;
|
|
1420
|
+
};
|
|
1421
|
+
host.handleIdempotentRead = async (options) => {
|
|
1422
|
+
const response = await options.execute();
|
|
1423
|
+
if (options.idempotencyKey === "key-A") {
|
|
1424
|
+
leaderExecuted.resolve();
|
|
1425
|
+
await putGate.promise; // leader's slow put
|
|
1426
|
+
}
|
|
1427
|
+
return response;
|
|
1428
|
+
};
|
|
1429
|
+
|
|
1430
|
+
const leader = h.service.recall({ query: "same", idempotencyKey: "key-A" });
|
|
1431
|
+
await leaderExecuted.promise; // leader consumed + detached; flight in the put window
|
|
1432
|
+
|
|
1433
|
+
let followerSettled = false;
|
|
1434
|
+
const follower = h.service
|
|
1435
|
+
.recall({ query: "same", idempotencyKey: "key-B" })
|
|
1436
|
+
.then((r) => {
|
|
1437
|
+
followerSettled = true;
|
|
1438
|
+
return r;
|
|
1439
|
+
});
|
|
1440
|
+
await flushMacrotasks();
|
|
1441
|
+
|
|
1442
|
+
assert.equal(pipelineRuns, 1, "the keyed follower coalesced onto the one pipeline");
|
|
1443
|
+
assert.equal(
|
|
1444
|
+
followerSettled,
|
|
1445
|
+
false,
|
|
1446
|
+
"the keyed follower is pending on the leader's persisted gate, not yet successful",
|
|
1447
|
+
);
|
|
1448
|
+
|
|
1449
|
+
putGate.resolve();
|
|
1450
|
+
const [leaderResp, followerResp] = await Promise.all([leader, follower]);
|
|
1451
|
+
assert.ok(leaderResp);
|
|
1452
|
+
assert.ok(followerResp);
|
|
1453
|
+
assert.equal(pipelineRuns, 1, "still exactly one pipeline");
|
|
1454
|
+
assert.equal(
|
|
1455
|
+
h.liveBudget(),
|
|
1456
|
+
2,
|
|
1457
|
+
"both keyed reservations stand on success (leader's pipeline reserve + follower's own event)",
|
|
1458
|
+
);
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
test("a keyed follower coalescing via the existing-join inherits the leader's put FAILURE (round 13 #1, failure)", async () => {
|
|
1462
|
+
// Finding 1: when the leader's idempotency.put FAILS, a keyed follower that
|
|
1463
|
+
// joined its flight must reject with the SAME error and release its own
|
|
1464
|
+
// reservation — never a phantom success that a same-key retry would diverge
|
|
1465
|
+
// from.
|
|
1466
|
+
const putGate = deferred<void>();
|
|
1467
|
+
const leaderExecuted = deferred<void>();
|
|
1468
|
+
const putError = new Error("idempotency store write failed");
|
|
1469
|
+
let pipelineRuns = 0;
|
|
1470
|
+
const h = makeService({
|
|
1471
|
+
limit: 0,
|
|
1472
|
+
singleFlight: true,
|
|
1473
|
+
crossNamespace: true,
|
|
1474
|
+
pipeline: async () => {
|
|
1475
|
+
pipelineRuns += 1;
|
|
1476
|
+
return stubResponse([]);
|
|
1477
|
+
},
|
|
1478
|
+
});
|
|
1479
|
+
const host = h.service as unknown as {
|
|
1480
|
+
handleIdempotentRead: (options: {
|
|
1481
|
+
idempotencyKey?: string;
|
|
1482
|
+
execute: () => Promise<EngramAccessRecallResponse>;
|
|
1483
|
+
}) => Promise<EngramAccessRecallResponse>;
|
|
1484
|
+
};
|
|
1485
|
+
host.handleIdempotentRead = async (options) => {
|
|
1486
|
+
// The follower (key-B) never returns from execute(): it rejects on the
|
|
1487
|
+
// leader's persisted gate, so control never reaches its own put.
|
|
1488
|
+
const response = await options.execute();
|
|
1489
|
+
if (options.idempotencyKey === "key-A") {
|
|
1490
|
+
leaderExecuted.resolve();
|
|
1491
|
+
await putGate.promise; // leader's slow put...
|
|
1492
|
+
throw putError; // ...that FAILS
|
|
1493
|
+
}
|
|
1494
|
+
return response;
|
|
1495
|
+
};
|
|
1496
|
+
|
|
1497
|
+
const leader = h.service.recall({ query: "same", idempotencyKey: "key-A" });
|
|
1498
|
+
await leaderExecuted.promise; // leader in the put window
|
|
1499
|
+
|
|
1500
|
+
const follower = h.service.recall({ query: "same", idempotencyKey: "key-B" });
|
|
1501
|
+
await flushMacrotasks(); // the follower parks on the leader's persisted gate
|
|
1502
|
+
|
|
1503
|
+
// Attach the rejection handlers BEFORE resolving so the follower's inherited
|
|
1504
|
+
// rejection is never momentarily unhandled.
|
|
1505
|
+
const leaderRej = assert.rejects(leader, (err: unknown) => err === putError);
|
|
1506
|
+
const followerRej = assert.rejects(follower, (err: unknown) => err === putError);
|
|
1507
|
+
putGate.resolve();
|
|
1508
|
+
await leaderRej;
|
|
1509
|
+
await followerRej;
|
|
1510
|
+
|
|
1511
|
+
assert.equal(pipelineRuns, 1, "the follower coalesced onto the one pipeline");
|
|
1512
|
+
assert.equal(
|
|
1513
|
+
h.liveBudget(),
|
|
1514
|
+
0,
|
|
1515
|
+
"both reservations released: the leader's on put failure and the follower's on the inherited failure",
|
|
1516
|
+
);
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
test("an unkeyed follower during a keyed leader's slow put succeeds WITHOUT awaiting the persisted gate (round 13 #1, unkeyed unchanged)", async () => {
|
|
1520
|
+
// Finding 1 scope guard: unkeyed followers keep existing behavior. They
|
|
1521
|
+
// consumed the shared result and never depend on another caller's
|
|
1522
|
+
// persistence, so they must return success while the leader's put is still
|
|
1523
|
+
// pending — never blocking on the persisted gate.
|
|
1524
|
+
const putGate = deferred<void>();
|
|
1525
|
+
const leaderExecuted = deferred<void>();
|
|
1526
|
+
let pipelineRuns = 0;
|
|
1527
|
+
const h = makeService({
|
|
1528
|
+
limit: 0,
|
|
1529
|
+
singleFlight: true,
|
|
1530
|
+
crossNamespace: true,
|
|
1531
|
+
pipeline: async () => {
|
|
1532
|
+
pipelineRuns += 1;
|
|
1533
|
+
return stubResponse([]);
|
|
1534
|
+
},
|
|
1535
|
+
});
|
|
1536
|
+
const host = h.service as unknown as {
|
|
1537
|
+
handleIdempotentRead: (options: {
|
|
1538
|
+
idempotencyKey?: string;
|
|
1539
|
+
execute: () => Promise<EngramAccessRecallResponse>;
|
|
1540
|
+
}) => Promise<EngramAccessRecallResponse>;
|
|
1541
|
+
};
|
|
1542
|
+
host.handleIdempotentRead = async (options) => {
|
|
1543
|
+
const response = await options.execute();
|
|
1544
|
+
if (options.idempotencyKey === "key-A") {
|
|
1545
|
+
leaderExecuted.resolve();
|
|
1546
|
+
await putGate.promise; // leader's put still pending while the arrival runs
|
|
1547
|
+
}
|
|
1548
|
+
return response;
|
|
1549
|
+
};
|
|
1550
|
+
|
|
1551
|
+
const leader = h.service.recall({ query: "same", idempotencyKey: "key-A" });
|
|
1552
|
+
await leaderExecuted.promise; // leader in the put window, flight registered
|
|
1553
|
+
|
|
1554
|
+
// The UNKEYED arrival takes the fast-path follower and returns the shared
|
|
1555
|
+
// result while the leader's put is still pending.
|
|
1556
|
+
const arrivalResp = await h.service.recall({ query: "same" });
|
|
1557
|
+
assert.ok(arrivalResp, "the unkeyed follower succeeded before the leader's put settled");
|
|
1558
|
+
assert.equal(pipelineRuns, 1, "coalesced onto the one pipeline");
|
|
1559
|
+
|
|
1560
|
+
putGate.resolve();
|
|
1561
|
+
await leader;
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
// ── round 14 review findings ────────────────────────────────────────────────
|
|
1565
|
+
|
|
1566
|
+
test("a recall aborted after its concurrency slot is granted rejects before the pipeline runs and releases the slot (round 14 #2)", async () => {
|
|
1567
|
+
// The queued waiter's abort listener is removed by `take()` the instant a slot
|
|
1568
|
+
// is granted. If the signal fires in the microtask gap between the grant and
|
|
1569
|
+
// the recall function running, withRecallConcurrency must re-check the signal
|
|
1570
|
+
// and release the slot instead of starting work for a disconnected caller.
|
|
1571
|
+
const h = makeService({
|
|
1572
|
+
limit: 1,
|
|
1573
|
+
singleFlight: false,
|
|
1574
|
+
pipeline: async () => stubResponse([]),
|
|
1575
|
+
});
|
|
1576
|
+
const svc = h.service as unknown as RecallCoordinatorHost;
|
|
1577
|
+
|
|
1578
|
+
// Slot is free: acquireRecallSlot resolves synchronously, so the fn runs in a
|
|
1579
|
+
// microtask. Aborting right after the call lands in exactly that gap — after
|
|
1580
|
+
// the (synchronous) slot grant, before the fn continuation.
|
|
1581
|
+
const controller = new AbortController();
|
|
1582
|
+
let fnRan = false;
|
|
1583
|
+
const pending = withRecallConcurrency(svc, "principal", controller.signal, async () => {
|
|
1584
|
+
fnRan = true;
|
|
1585
|
+
return "ok";
|
|
1586
|
+
});
|
|
1587
|
+
controller.abort();
|
|
1588
|
+
|
|
1589
|
+
await assert.rejects(pending, (error: Error) => error.name === "AbortError");
|
|
1590
|
+
assert.equal(fnRan, false, "the recall function never ran after the post-grant abort");
|
|
1591
|
+
assert.equal(
|
|
1592
|
+
h.recallSemaphores.size,
|
|
1593
|
+
0,
|
|
1594
|
+
"the granted slot was released so the semaphore did not leak",
|
|
1595
|
+
);
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
test("a queued keyed leader whose only caller aborts cancels before executing (round 14 #3)", async () => {
|
|
1599
|
+
// A keyed single-flight leader queued behind the concurrency cap runs its
|
|
1600
|
+
// pipeline on the FLIGHT controller with race=false, so its caller's abort was
|
|
1601
|
+
// previously never observed: the flight would still execute + reserve + persist
|
|
1602
|
+
// once a slot freed, even though its only caller had disconnected while queued.
|
|
1603
|
+
const holderGate = deferred<void>();
|
|
1604
|
+
let pipelineRuns = 0;
|
|
1605
|
+
const h = makeService({
|
|
1606
|
+
limit: 1,
|
|
1607
|
+
singleFlight: true,
|
|
1608
|
+
crossNamespace: true,
|
|
1609
|
+
pipeline: async (request) => {
|
|
1610
|
+
pipelineRuns += 1;
|
|
1611
|
+
if (request.query === "holder") await holderGate.promise;
|
|
1612
|
+
return stubResponse([{ id: "m1" }]);
|
|
1613
|
+
},
|
|
1614
|
+
});
|
|
1615
|
+
const host = h.service as unknown as {
|
|
1616
|
+
handleIdempotentRead: (options: {
|
|
1617
|
+
execute: () => Promise<EngramAccessRecallResponse>;
|
|
1618
|
+
}) => Promise<EngramAccessRecallResponse>;
|
|
1619
|
+
};
|
|
1620
|
+
host.handleIdempotentRead = async (options) => options.execute();
|
|
1621
|
+
|
|
1622
|
+
// Occupy the single slot with an unrelated (different-work) recall.
|
|
1623
|
+
const holder = h.service.recall({ query: "holder" });
|
|
1624
|
+
await flushMacrotasks();
|
|
1625
|
+
|
|
1626
|
+
// A keyed leader for DIFFERENT work registers its flight and queues for a slot.
|
|
1627
|
+
const controller = new AbortController();
|
|
1628
|
+
const leader = h.service.recall({
|
|
1629
|
+
query: "keyed",
|
|
1630
|
+
idempotencyKey: "k",
|
|
1631
|
+
abortSignal: controller.signal,
|
|
1632
|
+
});
|
|
1633
|
+
await flushMacrotasks();
|
|
1634
|
+
assert.equal(h.recallInFlight.size, 2, "both flights are registered while queued");
|
|
1635
|
+
|
|
1636
|
+
// The keyed leader's only caller disconnects while its flight is still queued.
|
|
1637
|
+
controller.abort();
|
|
1638
|
+
await assert.rejects(leader, (error: Error) => error.name === "AbortError");
|
|
1639
|
+
|
|
1640
|
+
// Free the slot; a still-alive queued flight would now execute + reserve.
|
|
1641
|
+
holderGate.resolve();
|
|
1642
|
+
await holder;
|
|
1643
|
+
await flushMacrotasks();
|
|
1644
|
+
|
|
1645
|
+
assert.equal(pipelineRuns, 1, "only the holder ran — the cancelled queued keyed flight never executed");
|
|
1646
|
+
assert.equal(h.liveBudget(), 1, "the cancelled queued keyed flight reserved no budget (only the holder's stands)");
|
|
1647
|
+
assert.equal(h.recallInFlight.size, 0, "the cancelled keyed flight unregistered");
|
|
1648
|
+
assert.equal(h.recallSemaphores.size, 0, "the semaphore did not leak");
|
|
1649
|
+
});
|
|
1650
|
+
|
|
1651
|
+
test("a queued keyed flight with an additional live caller survives its leader's abort (round 14 #3)", async () => {
|
|
1652
|
+
// Shared-flight semantics for ADDITIONAL callers: when a second caller has
|
|
1653
|
+
// joined, one caller's abort must not cancel the shared pipeline.
|
|
1654
|
+
const holderGate = deferred<void>();
|
|
1655
|
+
let pipelineRuns = 0;
|
|
1656
|
+
const h = makeService({
|
|
1657
|
+
limit: 1,
|
|
1658
|
+
singleFlight: true,
|
|
1659
|
+
crossNamespace: true,
|
|
1660
|
+
pipeline: async (request) => {
|
|
1661
|
+
pipelineRuns += 1;
|
|
1662
|
+
if (request.query === "holder") await holderGate.promise;
|
|
1663
|
+
return stubResponse([{ id: "m1" }]);
|
|
1664
|
+
},
|
|
1665
|
+
});
|
|
1666
|
+
const host = h.service as unknown as {
|
|
1667
|
+
handleIdempotentRead: (options: {
|
|
1668
|
+
idempotencyKey?: string;
|
|
1669
|
+
execute: () => Promise<EngramAccessRecallResponse>;
|
|
1670
|
+
}) => Promise<EngramAccessRecallResponse>;
|
|
1671
|
+
};
|
|
1672
|
+
host.handleIdempotentRead = async (options) => options.execute();
|
|
1673
|
+
|
|
1674
|
+
const holder = h.service.recall({ query: "holder" });
|
|
1675
|
+
await flushMacrotasks();
|
|
1676
|
+
|
|
1677
|
+
// Leader (key-A) registers its keyed flight and queues behind the cap.
|
|
1678
|
+
const leaderCtl = new AbortController();
|
|
1679
|
+
const leader = h.service.recall({
|
|
1680
|
+
query: "same",
|
|
1681
|
+
idempotencyKey: "key-A",
|
|
1682
|
+
abortSignal: leaderCtl.signal,
|
|
1683
|
+
});
|
|
1684
|
+
await flushMacrotasks();
|
|
1685
|
+
// A second keyed caller (same work, distinct key) joins the queued flight.
|
|
1686
|
+
const follower = h.service.recall({ query: "same", idempotencyKey: "key-B" });
|
|
1687
|
+
await flushMacrotasks();
|
|
1688
|
+
|
|
1689
|
+
// The leader disconnects; a keyed leader with race=false does NOT reject on
|
|
1690
|
+
// its own abort — it stays committed and the follower keeps the flight alive.
|
|
1691
|
+
leaderCtl.abort();
|
|
1692
|
+
|
|
1693
|
+
// Free the slot; the shared flight must still run once for the follower.
|
|
1694
|
+
holderGate.resolve();
|
|
1695
|
+
await holder;
|
|
1696
|
+
const followerResp = await follower;
|
|
1697
|
+
// The leader still rejects with AbortError, but only AFTER the shared pipeline
|
|
1698
|
+
// completed and it persisted (round 5 #1 commit semantics preserved).
|
|
1699
|
+
await assert.rejects(leader, (error: Error) => error.name === "AbortError");
|
|
1700
|
+
|
|
1701
|
+
assert.ok(followerResp, "the additional caller received the shared result");
|
|
1702
|
+
assert.equal(
|
|
1703
|
+
pipelineRuns,
|
|
1704
|
+
2,
|
|
1705
|
+
"holder + the one shared keyed pipeline — the leader's abort did not cancel it",
|
|
1706
|
+
);
|
|
1707
|
+
});
|
|
1708
|
+
|
|
1709
|
+
test("a keyed follower joining via the existing-join returns its own AbortError promptly when it disconnects during the leader's put (round 14 #4)", async () => {
|
|
1710
|
+
// A keyed follower coalescing through leadRecallFlight's existing-join awaits
|
|
1711
|
+
// the leader's `persisted` gate. That await must RACE the follower's own abort
|
|
1712
|
+
// signal: a client that disconnects while the leader's put is pending must get
|
|
1713
|
+
// AbortError promptly — not block until the put settles.
|
|
1714
|
+
const putGate = deferred<void>();
|
|
1715
|
+
const leaderExecuted = deferred<void>();
|
|
1716
|
+
let pipelineRuns = 0;
|
|
1717
|
+
let leaderPersisted = false;
|
|
1718
|
+
const h = makeService({
|
|
1719
|
+
limit: 0,
|
|
1720
|
+
singleFlight: true,
|
|
1721
|
+
crossNamespace: true,
|
|
1722
|
+
pipeline: async () => {
|
|
1723
|
+
pipelineRuns += 1;
|
|
1724
|
+
return stubResponse([{ id: "m1" }]);
|
|
1725
|
+
},
|
|
1726
|
+
});
|
|
1727
|
+
const host = h.service as unknown as {
|
|
1728
|
+
handleIdempotentRead: (options: {
|
|
1729
|
+
idempotencyKey?: string;
|
|
1730
|
+
execute: () => Promise<EngramAccessRecallResponse>;
|
|
1731
|
+
}) => Promise<EngramAccessRecallResponse>;
|
|
1732
|
+
};
|
|
1733
|
+
host.handleIdempotentRead = async (options) => {
|
|
1734
|
+
const response = await options.execute();
|
|
1735
|
+
if (options.idempotencyKey === "key-A") {
|
|
1736
|
+
leaderExecuted.resolve();
|
|
1737
|
+
await putGate.promise; // leader's slow put
|
|
1738
|
+
leaderPersisted = true;
|
|
1739
|
+
}
|
|
1740
|
+
return response;
|
|
1741
|
+
};
|
|
1742
|
+
|
|
1743
|
+
const leader = h.service.recall({ query: "same", idempotencyKey: "key-A" });
|
|
1744
|
+
await leaderExecuted.promise; // leader consumed + detached; flight in the put window
|
|
1745
|
+
|
|
1746
|
+
const followerCtl = new AbortController();
|
|
1747
|
+
const follower = h.service.recall({
|
|
1748
|
+
query: "same",
|
|
1749
|
+
idempotencyKey: "key-B",
|
|
1750
|
+
abortSignal: followerCtl.signal,
|
|
1751
|
+
});
|
|
1752
|
+
await flushMacrotasks(); // follower consumed the shared result; parked on the persisted gate
|
|
1753
|
+
|
|
1754
|
+
// The follower's client disconnects while the leader's put is STILL pending.
|
|
1755
|
+
followerCtl.abort();
|
|
1756
|
+
await assert.rejects(
|
|
1757
|
+
follower,
|
|
1758
|
+
(error: Error) => error.name === "AbortError",
|
|
1759
|
+
"the keyed follower rejected with its own AbortError before the leader's put settled",
|
|
1760
|
+
);
|
|
1761
|
+
assert.equal(leaderPersisted, false, "the leader's put was still pending when the follower rejected");
|
|
1762
|
+
|
|
1763
|
+
// The leader's persistence continues to completion despite the follower's abort.
|
|
1764
|
+
putGate.resolve();
|
|
1765
|
+
const leaderResp = await leader;
|
|
1766
|
+
assert.ok(leaderResp, "the leader still completed and persisted");
|
|
1767
|
+
assert.equal(leaderPersisted, true, "the leader's persistence continued after the follower aborted");
|
|
1768
|
+
assert.equal(pipelineRuns, 1, "one shared pipeline for both callers");
|
|
1769
|
+
});
|
|
1770
|
+
|
|
1771
|
+
test("a keyed follower aborting during the leader's put releases ONLY its own reservation, leaving the leader's intact (round 14 #1)", async () => {
|
|
1772
|
+
// Companion to the promptness test: the abort must release exactly the
|
|
1773
|
+
// follower's OWN cross-namespace reservation and never touch the leader's,
|
|
1774
|
+
// whose event legitimately stands because it persisted.
|
|
1775
|
+
const putGate = deferred<void>();
|
|
1776
|
+
const leaderExecuted = deferred<void>();
|
|
1777
|
+
const h = makeService({
|
|
1778
|
+
limit: 0,
|
|
1779
|
+
singleFlight: true,
|
|
1780
|
+
crossNamespace: true,
|
|
1781
|
+
pipeline: async () => stubResponse([{ id: "m1" }]),
|
|
1782
|
+
});
|
|
1783
|
+
const host = h.service as unknown as {
|
|
1784
|
+
handleIdempotentRead: (options: {
|
|
1785
|
+
idempotencyKey?: string;
|
|
1786
|
+
execute: () => Promise<EngramAccessRecallResponse>;
|
|
1787
|
+
}) => Promise<EngramAccessRecallResponse>;
|
|
1788
|
+
};
|
|
1789
|
+
host.handleIdempotentRead = async (options) => {
|
|
1790
|
+
const response = await options.execute();
|
|
1791
|
+
if (options.idempotencyKey === "key-A") {
|
|
1792
|
+
leaderExecuted.resolve();
|
|
1793
|
+
await putGate.promise;
|
|
1794
|
+
}
|
|
1795
|
+
return response;
|
|
1796
|
+
};
|
|
1797
|
+
|
|
1798
|
+
const leader = h.service.recall({ query: "same", idempotencyKey: "key-A" });
|
|
1799
|
+
await leaderExecuted.promise;
|
|
1800
|
+
|
|
1801
|
+
const followerCtl = new AbortController();
|
|
1802
|
+
const follower = h.service.recall({
|
|
1803
|
+
query: "same",
|
|
1804
|
+
idempotencyKey: "key-B",
|
|
1805
|
+
abortSignal: followerCtl.signal,
|
|
1806
|
+
});
|
|
1807
|
+
await flushMacrotasks();
|
|
1808
|
+
|
|
1809
|
+
// Leader's reservation + follower's own event are both live in the window.
|
|
1810
|
+
assert.equal(h.liveBudget(), 2, "leader and follower each reserved their own event");
|
|
1811
|
+
|
|
1812
|
+
followerCtl.abort();
|
|
1813
|
+
await assert.rejects(follower, (error: Error) => error.name === "AbortError");
|
|
1814
|
+
assert.equal(
|
|
1815
|
+
h.liveBudget(),
|
|
1816
|
+
1,
|
|
1817
|
+
"only the follower's reservation was released; the leader's stands",
|
|
1818
|
+
);
|
|
1819
|
+
|
|
1820
|
+
putGate.resolve();
|
|
1821
|
+
await leader;
|
|
1822
|
+
assert.equal(h.liveBudget(), 1, "the leader's reservation remains after it persisted");
|
|
1823
|
+
});
|
|
1824
|
+
|
|
1825
|
+
test("a queued keyed flight cancels once its leader AND its existing-join follower both abort (round 15 #2)", async () => {
|
|
1826
|
+
// A keyed leader queued behind the concurrency cap runs race=false; a keyed
|
|
1827
|
+
// existing-join follower is also non-racing. The follower's abort must still
|
|
1828
|
+
// be COUNTED in the flight's live refcount while the flight is only queued:
|
|
1829
|
+
// otherwise, when both callers disconnect before a slot frees, the leader's
|
|
1830
|
+
// abort drops live 2 -> 1 but the follower's is never counted, so the flight
|
|
1831
|
+
// executes + reserves + persists with no caller connected.
|
|
1832
|
+
const holderGate = deferred<void>();
|
|
1833
|
+
let pipelineRuns = 0;
|
|
1834
|
+
const h = makeService({
|
|
1835
|
+
limit: 1,
|
|
1836
|
+
singleFlight: true,
|
|
1837
|
+
crossNamespace: true,
|
|
1838
|
+
pipeline: async (request) => {
|
|
1839
|
+
pipelineRuns += 1;
|
|
1840
|
+
if (request.query === "holder") await holderGate.promise;
|
|
1841
|
+
return stubResponse([{ id: "m1" }]);
|
|
1842
|
+
},
|
|
1843
|
+
});
|
|
1844
|
+
const host = h.service as unknown as {
|
|
1845
|
+
handleIdempotentRead: (options: {
|
|
1846
|
+
idempotencyKey?: string;
|
|
1847
|
+
execute: () => Promise<EngramAccessRecallResponse>;
|
|
1848
|
+
}) => Promise<EngramAccessRecallResponse>;
|
|
1849
|
+
};
|
|
1850
|
+
host.handleIdempotentRead = async (options) => options.execute();
|
|
1851
|
+
|
|
1852
|
+
// Occupy the single slot with unrelated work.
|
|
1853
|
+
const holder = h.service.recall({ query: "holder" });
|
|
1854
|
+
await flushMacrotasks();
|
|
1855
|
+
|
|
1856
|
+
// Leader (key-A) registers the keyed flight and queues behind the cap.
|
|
1857
|
+
const leaderCtl = new AbortController();
|
|
1858
|
+
const leader = h.service.recall({
|
|
1859
|
+
query: "same",
|
|
1860
|
+
idempotencyKey: "key-A",
|
|
1861
|
+
abortSignal: leaderCtl.signal,
|
|
1862
|
+
});
|
|
1863
|
+
await flushMacrotasks();
|
|
1864
|
+
// A second keyed caller (same work, distinct key) joins via the existing-join
|
|
1865
|
+
// and parks on the queued flight.
|
|
1866
|
+
const followerCtl = new AbortController();
|
|
1867
|
+
const follower = h.service.recall({
|
|
1868
|
+
query: "same",
|
|
1869
|
+
idempotencyKey: "key-B",
|
|
1870
|
+
abortSignal: followerCtl.signal,
|
|
1871
|
+
});
|
|
1872
|
+
await flushMacrotasks();
|
|
1873
|
+
assert.equal(h.recallInFlight.size, 2, "holder + the one shared keyed flight registered");
|
|
1874
|
+
|
|
1875
|
+
// Both callers disconnect while the flight is still queued. Only when BOTH
|
|
1876
|
+
// are counted does live reach 0 and cancel the not-yet-committed flight.
|
|
1877
|
+
leaderCtl.abort();
|
|
1878
|
+
followerCtl.abort();
|
|
1879
|
+
await assert.rejects(leader, (error: Error) => error.name === "AbortError");
|
|
1880
|
+
await assert.rejects(follower, (error: Error) => error.name === "AbortError");
|
|
1881
|
+
|
|
1882
|
+
// Free the slot; a still-alive queued flight would now execute + reserve.
|
|
1883
|
+
holderGate.resolve();
|
|
1884
|
+
await holder;
|
|
1885
|
+
await flushMacrotasks();
|
|
1886
|
+
|
|
1887
|
+
assert.equal(pipelineRuns, 1, "only the holder ran — the fully-abandoned queued keyed flight never executed");
|
|
1888
|
+
assert.equal(h.liveBudget(), 1, "the cancelled queued keyed flight reserved no budget (only the holder's stands)");
|
|
1889
|
+
assert.equal(h.recallInFlight.size, 0, "the cancelled keyed flight unregistered");
|
|
1890
|
+
assert.equal(h.recallSemaphores.size, 0, "the semaphore did not leak");
|
|
1891
|
+
});
|
|
1892
|
+
|
|
1893
|
+
test("raceAbort rejects for an already-aborted signal even when p is already fulfilled (round 15 #1)", async () => {
|
|
1894
|
+
// Promise.race settles already-resolved members in array order, so a fulfilled
|
|
1895
|
+
// p (index 0) would beat a synchronously-rejected abort racer (index 1). Without
|
|
1896
|
+
// a pre-check, a caller that aborted before raceAbort is called (e.g. the keyed
|
|
1897
|
+
// follower persistence wait) would proceed past the helper. raceAbort must
|
|
1898
|
+
// reject synchronously for an already-aborted signal.
|
|
1899
|
+
makeService({ pipeline: async () => stubResponse([]) });
|
|
1900
|
+
|
|
1901
|
+
const aborted = new AbortController();
|
|
1902
|
+
aborted.abort();
|
|
1903
|
+
await assert.rejects(
|
|
1904
|
+
raceAbort(Promise.resolve("value"), aborted.signal),
|
|
1905
|
+
(error: Error) => error.name === "AbortError",
|
|
1906
|
+
"an already-aborted signal wins over an already-fulfilled p",
|
|
1907
|
+
);
|
|
1908
|
+
|
|
1909
|
+
// The non-aborted branch is unchanged: p resolves through the race.
|
|
1910
|
+
const live = new AbortController();
|
|
1911
|
+
assert.equal(
|
|
1912
|
+
await raceAbort(Promise.resolve("value"), live.signal),
|
|
1913
|
+
"value",
|
|
1914
|
+
"a live signal lets the fulfilled p win the race",
|
|
1915
|
+
);
|
|
1916
|
+
});
|
|
1917
|
+
|
|
1918
|
+
test("a keyed follower joining an UNKEYED leader's flight rejects with AbortError (releasing its budget, skipping its put) when it disconnects during the shared pipeline (round 16 #1)", async () => {
|
|
1919
|
+
// A keyed caller can coalesce onto a flight started by an UNKEYED leader
|
|
1920
|
+
// (the idempotencyKey is excluded from the flight key). That flight carries
|
|
1921
|
+
// NO `persisted` gate, so the keyed follower's existing-join could not await
|
|
1922
|
+
// it. Because a keyed join is non-racing (race=false), the follower's abort
|
|
1923
|
+
// is never observed during the shared-pipeline wait, so without a recheck the
|
|
1924
|
+
// follower would return success — persisting its idempotency key and keeping
|
|
1925
|
+
// its own cross-namespace budget event — even though its client disconnected.
|
|
1926
|
+
// The existing-join must recheck the abort before returning: the follower must
|
|
1927
|
+
// reject with AbortError, release its reservation, and never run its own put.
|
|
1928
|
+
const pipelineGate = deferred<void>();
|
|
1929
|
+
const leaderRunning = deferred<void>();
|
|
1930
|
+
let pipelineRuns = 0;
|
|
1931
|
+
let followerPutRan = false;
|
|
1932
|
+
const h = makeService({
|
|
1933
|
+
limit: 0,
|
|
1934
|
+
singleFlight: true,
|
|
1935
|
+
crossNamespace: true,
|
|
1936
|
+
pipeline: async () => {
|
|
1937
|
+
pipelineRuns += 1;
|
|
1938
|
+
leaderRunning.resolve();
|
|
1939
|
+
await pipelineGate.promise; // keep the shared pipeline running while the follower joins + aborts
|
|
1940
|
+
return stubResponse([{ id: "m1" }]);
|
|
1941
|
+
},
|
|
1942
|
+
});
|
|
1943
|
+
const host = h.service as unknown as {
|
|
1944
|
+
handleIdempotentRead: (options: {
|
|
1945
|
+
idempotencyKey?: string;
|
|
1946
|
+
execute: () => Promise<EngramAccessRecallResponse>;
|
|
1947
|
+
}) => Promise<EngramAccessRecallResponse>;
|
|
1948
|
+
};
|
|
1949
|
+
host.handleIdempotentRead = async (options) => {
|
|
1950
|
+
const response = await options.execute();
|
|
1951
|
+
// Only a KEYED caller performs a put. With the fix, execute() throws for the
|
|
1952
|
+
// aborted keyed follower, so this line never runs for it.
|
|
1953
|
+
if (options.idempotencyKey) followerPutRan = true;
|
|
1954
|
+
return response;
|
|
1955
|
+
};
|
|
1956
|
+
|
|
1957
|
+
// UNKEYED leader registers the flight and runs the one gated pipeline.
|
|
1958
|
+
const leader = h.service.recall({ query: "same" });
|
|
1959
|
+
await leaderRunning.promise;
|
|
1960
|
+
|
|
1961
|
+
// KEYED follower coalesces onto the unkeyed leader's flight (no persisted gate)
|
|
1962
|
+
// and parks on the shared pipeline via a non-racing consume.
|
|
1963
|
+
const followerCtl = new AbortController();
|
|
1964
|
+
const follower = h.service.recall({
|
|
1965
|
+
query: "same",
|
|
1966
|
+
idempotencyKey: "key-B",
|
|
1967
|
+
abortSignal: followerCtl.signal,
|
|
1968
|
+
});
|
|
1969
|
+
await flushMacrotasks();
|
|
1970
|
+
// Only the leader's pipeline reservation is live yet: a non-racing keyed join
|
|
1971
|
+
// records its OWN event only AFTER the shared pipeline resolves, so the
|
|
1972
|
+
// follower has not recorded while the pipeline is still gated.
|
|
1973
|
+
assert.equal(h.liveBudget(), 1, "leader's pipeline reservation is live; the parked follower has not recorded yet");
|
|
1974
|
+
|
|
1975
|
+
// The follower's client disconnects while the shared pipeline is still running.
|
|
1976
|
+
followerCtl.abort();
|
|
1977
|
+
pipelineGate.resolve();
|
|
1978
|
+
|
|
1979
|
+
await assert.rejects(
|
|
1980
|
+
follower,
|
|
1981
|
+
(error: Error) => error.name === "AbortError",
|
|
1982
|
+
"the keyed follower on an unkeyed flight rejected with its own AbortError",
|
|
1983
|
+
);
|
|
1984
|
+
assert.equal(followerPutRan, false, "the aborted keyed follower never ran its own idempotency.put");
|
|
1985
|
+
|
|
1986
|
+
const leaderResp = await leader;
|
|
1987
|
+
assert.ok(leaderResp, "the unkeyed leader still completed for its own connected client");
|
|
1988
|
+
assert.equal(pipelineRuns, 1, "both callers shared one pipeline");
|
|
1989
|
+
assert.equal(h.liveBudget(), 1, "the aborted follower's reservation was released; only the leader's stands");
|
|
1990
|
+
});
|