@remnic/core 10.0.37 → 10.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-admin-ops-surface.js +76 -76
- package/dist/access-audit.js +2 -2
- package/dist/access-authorization-probe.js +77 -77
- package/dist/access-boundary.js +76 -76
- package/dist/access-citation-namespace.js +2 -2
- package/dist/access-cli.js +165 -164
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.js +76 -76
- package/dist/access-http-briefing.js +77 -77
- package/dist/access-http-query.js +78 -78
- package/dist/access-http.js +101 -101
- package/dist/access-identity-continuity-surface.js +76 -76
- package/dist/access-lcm-surface.js +76 -76
- package/dist/access-mcp.js +90 -90
- package/dist/access-namespace-preflight.js +76 -76
- package/dist/access-observe-write-category.js +15 -15
- package/dist/access-observe-write-surface.js +76 -76
- package/dist/access-offline-manifest.js +12 -12
- package/dist/access-operations-batch.js +80 -80
- package/dist/access-operations.js +87 -87
- package/dist/access-recall-concurrency.js +76 -76
- package/dist/access-recall-diagnostics.js +2 -2
- package/dist/access-recall-response.js +76 -76
- package/dist/access-recall-surface.js +76 -76
- package/dist/access-schema.js +10 -10
- package/dist/access-service.js +76 -76
- package/dist/access-surface-catalog.js +78 -78
- package/dist/active-recall.js +31 -31
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/{auto-sync-R3XGJL5I.js → auto-sync-BNCBMU6H.js} +9 -9
- package/dist/briefing.js +21 -21
- package/dist/buffer-types.d.ts +59 -0
- package/dist/buffer-types.d.ts.map +1 -0
- package/dist/buffer-types.js +1 -0
- package/dist/buffer.d.ts +14 -22
- package/dist/buffer.d.ts.map +1 -1
- package/dist/buffer.js +11 -11
- package/dist/calibration.js +3 -3
- package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-GWVG7LGC.js} +2 -2
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.js +24 -24
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory-graph.js +2 -2
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-MH3LC7KB.js → chunk-22YJQTPN.js} +2 -2
- package/dist/{chunk-MKE5DP3S.js → chunk-23ZMQONL.js} +2 -2
- package/dist/{chunk-MGXFJOUR.js → chunk-2AV2U2SZ.js} +4 -4
- package/dist/{chunk-HJ3UTGIW.js → chunk-2JNBPOZ2.js} +3 -3
- package/dist/{chunk-EEOYF3IB.js → chunk-3RJDYZJE.js} +2 -2
- package/dist/{chunk-FBAHBFT5.js → chunk-4HJ2QK32.js} +2 -2
- package/dist/{chunk-UXXVOA4D.js → chunk-4LLV2N3R.js} +3 -3
- package/dist/{chunk-P5CUEB2Q.js → chunk-4QLKVZH6.js} +8 -8
- package/dist/{chunk-2RJO7VS4.js → chunk-4UT2NUAC.js} +2 -2
- package/dist/{chunk-R6TCM47S.js → chunk-5KZNWERU.js} +156 -143
- package/dist/chunk-5KZNWERU.js.map +1 -0
- package/dist/{chunk-MNDHKZZQ.js → chunk-5Q6CR65T.js} +2 -2
- package/dist/{chunk-XTBEXZFN.js → chunk-5SF7JOJB.js} +4 -4
- package/dist/{chunk-BQ6DGL6A.js → chunk-64WHWIPL.js} +2 -2
- package/dist/{chunk-TVIGN3XZ.js → chunk-6FRFVIKC.js} +10 -10
- package/dist/{chunk-A23DPFFI.js → chunk-6ITNEPJO.js} +2 -2
- package/dist/{chunk-UFBIOF7C.js → chunk-6X3J4NLZ.js} +10 -5
- package/dist/chunk-6X3J4NLZ.js.map +1 -0
- package/dist/{chunk-G6PPA4TH.js → chunk-6Z2ZWBOV.js} +6 -6
- package/dist/{chunk-QEGE5QCL.js → chunk-7IANFJ24.js} +58 -58
- package/dist/{chunk-PSQ2P2T3.js → chunk-7XZ7TOLQ.js} +3 -3
- package/dist/{chunk-Z5AQZRHF.js → chunk-AER3HD3B.js} +2 -2
- package/dist/{chunk-BPBUUGAO.js → chunk-ANV373S5.js} +2 -2
- package/dist/{chunk-GMMZ45C4.js → chunk-AVVCPA7U.js} +6 -8
- package/dist/chunk-AVVCPA7U.js.map +1 -0
- package/dist/{chunk-LSSCMZQ2.js → chunk-B3I2QQU7.js} +3 -3
- package/dist/{chunk-NSQSOLCE.js → chunk-B7VKRCAG.js} +2 -2
- package/dist/{chunk-HXFIILGF.js → chunk-BFPF5N3H.js} +4 -4
- package/dist/{chunk-KQAFEZQX.js → chunk-BUKK5SWA.js} +2 -2
- package/dist/{chunk-RBC4GIZU.js → chunk-BUKZ7MTN.js} +2 -2
- package/dist/{chunk-FO5AFCRI.js → chunk-CR72WZ3M.js} +9 -9
- package/dist/{chunk-YC2C67F7.js → chunk-CRXGQRNA.js} +3 -3
- package/dist/{chunk-WRDFYSUC.js → chunk-CZVNOTLR.js} +2 -2
- package/dist/{chunk-VPO53QMG.js → chunk-D6DOLQYK.js} +2 -2
- package/dist/{chunk-HRT2SJPC.js → chunk-E3NBMTTX.js} +2 -2
- package/dist/{chunk-HIMXDRSQ.js → chunk-E5IEI2MR.js} +2 -2
- package/dist/{chunk-Z2T5BPOZ.js → chunk-EIYJK7OD.js} +2 -2
- package/dist/{chunk-FRAL3EU4.js → chunk-EKPRLTAE.js} +2 -2
- package/dist/{chunk-EVVCE7S2.js → chunk-EKR6OO4P.js} +2 -2
- package/dist/{chunk-TYGLV6ZG.js → chunk-EYGOM6AI.js} +2 -2
- package/dist/{chunk-TYGLV6ZG.js.map → chunk-EYGOM6AI.js.map} +1 -1
- package/dist/{chunk-6R6CU3MR.js → chunk-F2IU6A2L.js} +57 -57
- package/dist/{chunk-FL6SLVAJ.js → chunk-FAKRTS3Y.js} +3 -3
- package/dist/{chunk-64IO6ZEE.js → chunk-FC3ZFEXF.js} +2 -2
- package/dist/{chunk-YGYT7S3G.js → chunk-FJ264QBP.js} +31 -9
- package/dist/chunk-FJ264QBP.js.map +1 -0
- package/dist/{chunk-DGBWPSM6.js → chunk-FLDXEJFX.js} +2 -2
- package/dist/{chunk-JH4FCVWJ.js → chunk-G6OFEGUR.js} +2 -2
- package/dist/{chunk-Z5QWUR3Y.js → chunk-GCW7UT5I.js} +2 -2
- package/dist/{chunk-ULUX6Q7R.js → chunk-GE5ZNDLE.js} +4 -4
- package/dist/{chunk-U4JXQCJ3.js → chunk-GLB6VV3J.js} +2 -2
- package/dist/{chunk-RWWYBE7E.js → chunk-GPVGAHXL.js} +3 -3
- package/dist/{chunk-3BPMYMPX.js → chunk-GSBSGT43.js} +2 -2
- package/dist/{chunk-645WOU5I.js → chunk-H7BZBSP6.js} +4 -4
- package/dist/{chunk-GFYFKXWZ.js → chunk-HUDPXTXW.js} +2 -2
- package/dist/{chunk-L2JQSG5M.js → chunk-I7TR2OCU.js} +2 -2
- package/dist/{chunk-HKEK27M4.js → chunk-IAOW2L6K.js} +2 -2
- package/dist/{chunk-STM4T335.js → chunk-IJH6METJ.js} +2 -2
- package/dist/{chunk-WADPRAZE.js → chunk-IZRNMXZC.js} +2 -2
- package/dist/{chunk-JWTUJVIF.js → chunk-JGS55BAS.js} +53 -2
- package/dist/chunk-JGS55BAS.js.map +1 -0
- package/dist/{chunk-LAQS5HYU.js → chunk-JM6U3SAE.js} +2 -2
- package/dist/{chunk-74AY3S7U.js → chunk-JNLYHJG7.js} +4 -4
- package/dist/{chunk-7ZBANI3M.js → chunk-JOOGPZJE.js} +2 -2
- package/dist/{chunk-V5RJCXLG.js → chunk-JORSMFL6.js} +6 -6
- package/dist/{chunk-VKTP3TZB.js → chunk-JQCP5QW3.js} +4 -4
- package/dist/{chunk-LRJKHXU4.js → chunk-JRTKYFFY.js} +2 -2
- package/dist/{chunk-UMFBHYPW.js → chunk-JT3IXJIL.js} +13 -13
- package/dist/{chunk-S53WZWGL.js → chunk-JZFJZLJY.js} +2 -2
- package/dist/{chunk-6QOL4G76.js → chunk-KQXRVELF.js} +2 -2
- package/dist/{chunk-USOXPN5H.js → chunk-L74NWAD4.js} +2 -2
- package/dist/{chunk-Y6Y3TRBN.js → chunk-LJXBFVI2.js} +4 -4
- package/dist/{chunk-3L5WH6CV.js → chunk-LPD6I2U6.js} +2 -2
- package/dist/{chunk-LDXS746V.js → chunk-LR7RW4T6.js} +3 -3
- package/dist/{chunk-FZVZYU5C.js → chunk-LXN737FX.js} +2 -2
- package/dist/{chunk-5U3NUHMA.js → chunk-MD3W2H3G.js} +301 -98
- package/dist/chunk-MD3W2H3G.js.map +1 -0
- package/dist/{chunk-ARFT6ZBE.js → chunk-MGYZENUY.js} +2 -2
- package/dist/{chunk-YQ6SLFNZ.js → chunk-MPJDQC4G.js} +3 -3
- package/dist/{chunk-5UWP5X3T.js → chunk-NJ2DZYEK.js} +2 -2
- package/dist/{chunk-MBMT5FUQ.js → chunk-O2O4FDAH.js} +2 -2
- package/dist/{chunk-GCVUBVEN.js → chunk-ODJ2HRFW.js} +10 -10
- package/dist/{chunk-2NLLXCJG.js → chunk-OWHERGF2.js} +2 -2
- package/dist/{chunk-I73ZSORJ.js → chunk-OXSTKCYK.js} +4 -4
- package/dist/{chunk-CNQQNYJ2.js → chunk-P3YUEPSQ.js} +2 -2
- package/dist/{chunk-A6PKEU7T.js → chunk-PARM6OYL.js} +1 -1
- package/dist/chunk-PARM6OYL.js.map +1 -0
- package/dist/{chunk-TPKB7T6Z.js → chunk-Q6UZREJK.js} +2 -2
- package/dist/{chunk-UJDTXWMT.js → chunk-QIMGULGN.js} +2 -2
- package/dist/{chunk-E24RUQEN.js → chunk-QNIYL5SK.js} +2 -2
- package/dist/{chunk-KOY25HUB.js → chunk-QYAXMXOB.js} +2 -2
- package/dist/{chunk-CU7WDDPV.js → chunk-RIAKNXZB.js} +4 -4
- package/dist/{chunk-UQWA5QDT.js → chunk-RO2NNWR5.js} +3 -3
- package/dist/{chunk-AYXRK264.js → chunk-RROFIAYI.js} +9 -9
- package/dist/{chunk-L46Y5FPE.js → chunk-RUAIO5UA.js} +2 -2
- package/dist/{chunk-QIC7YZ3D.js → chunk-RWW5KPIY.js} +18 -18
- package/dist/{chunk-E5PVMB3I.js → chunk-SDXBR5QO.js} +2 -2
- package/dist/{chunk-WSLNC5AL.js → chunk-SJHFU52W.js} +8 -8
- package/dist/{chunk-7PG7QBWD.js → chunk-SRPGA6WN.js} +2 -2
- package/dist/{chunk-CUWOSRCS.js → chunk-SUKPM6DV.js} +2 -2
- package/dist/{chunk-BBDASWNT.js → chunk-TJ5YGCPV.js} +81 -76
- package/dist/chunk-TJ5YGCPV.js.map +1 -0
- package/dist/{chunk-G2SRB62M.js → chunk-TTQYX7VU.js} +2 -2
- package/dist/{chunk-TDCR6PQL.js → chunk-TWRPWJJT.js} +2 -2
- package/dist/{chunk-KC5SVCUJ.js → chunk-U66CTIF5.js} +2 -2
- package/dist/{chunk-D2AKUKAQ.js → chunk-UIK3U36O.js} +2 -2
- package/dist/{chunk-XLEP7SS3.js → chunk-UMSH4I3E.js} +2 -2
- package/dist/{chunk-X7Y7WX73.js → chunk-UVYI6VIX.js} +1 -1
- package/dist/{chunk-LHBOFROT.js → chunk-V2XMG7GM.js} +2 -2
- package/dist/chunk-VA3GNAWQ.js +58 -0
- package/dist/chunk-VA3GNAWQ.js.map +1 -0
- package/dist/{chunk-NQOM6PHO.js → chunk-VCOCOBUS.js} +6 -6
- package/dist/{chunk-4P4VLPGB.js → chunk-VECRSYQG.js} +1 -1
- package/dist/{chunk-ID2PLENW.js → chunk-VHBGJAYC.js} +2 -2
- package/dist/{chunk-RVYXSS3Q.js → chunk-VIS23B6O.js} +8 -8
- package/dist/{chunk-NMKETOAX.js → chunk-VSHWPOUB.js} +2 -2
- package/dist/{chunk-UBN5AOLT.js → chunk-W5EO4USJ.js} +2 -2
- package/dist/{chunk-Y7427SL7.js → chunk-WCGJK4NQ.js} +4 -4
- package/dist/{chunk-CCYXCT4B.js → chunk-WQYS47G4.js} +2 -2
- package/dist/{chunk-JH2ALMPR.js → chunk-WR5POHQ3.js} +2 -2
- package/dist/{chunk-DK5VMJL3.js → chunk-WT2VVYBM.js} +2 -2
- package/dist/{chunk-EHIKY2IA.js → chunk-WU5WTCH5.js} +2 -2
- package/dist/{chunk-QKXPOLQD.js → chunk-WUBGMV3G.js} +6 -6
- package/dist/{chunk-O6YJI5VX.js → chunk-WUL6LDJ4.js} +2 -2
- package/dist/{chunk-5EILJ6W2.js → chunk-WXPBDEMW.js} +2 -2
- package/dist/{chunk-U7HJBODU.js → chunk-XKTHJHSQ.js} +5 -5
- package/dist/{chunk-JFI3ATK4.js → chunk-XMYWYJM2.js} +2 -2
- package/dist/{chunk-64S6X4IU.js → chunk-XTN54NML.js} +4 -4
- package/dist/{chunk-VGFNTY3Y.js → chunk-YFXRLRKP.js} +2 -2
- package/dist/{chunk-DAOMWGO2.js → chunk-ZX5N5D6M.js} +4 -4
- package/dist/{chunk-PTRPOJ7U.js → chunk-ZXFKG7VZ.js} +2 -2
- package/dist/cli.js +124 -124
- package/dist/coding/export-okf-codegraph.js +22 -22
- package/dist/coding/pre-action-gate.js +2 -2
- package/dist/compounding/engine.js +23 -23
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +31 -31
- package/dist/connectors/codex-materialize-runner.js +21 -21
- package/dist/connectors/index.js +21 -21
- package/dist/contradiction/index.js +5 -5
- package/dist/convergence-refresh.js +23 -23
- package/dist/corpus-watermark.js +23 -23
- package/dist/deep-recall-http-glue.js +11 -11
- package/dist/deep-recall-policy-llm.js +2 -2
- package/dist/deep-recall.js +2 -2
- package/dist/entity-retrieval.js +22 -22
- package/dist/event-order-recall.js +2 -2
- package/dist/explicit-capture.js +6 -6
- package/dist/external-wiki-access.js +77 -77
- package/dist/extraction-faithfulness.js +2 -2
- package/dist/extraction-judge.d.ts +42 -2
- package/dist/extraction-judge.d.ts.map +1 -1
- package/dist/extraction-judge.js +12 -2
- package/dist/extraction-window-drain.d.ts +48 -0
- package/dist/extraction-window-drain.d.ts.map +1 -0
- package/dist/extraction-window-drain.js +14 -0
- package/dist/extraction.js +29 -29
- package/dist/fallback-llm.d.ts +1 -0
- package/dist/fallback-llm.d.ts.map +1 -1
- package/dist/fallback-llm.js +1 -1
- package/dist/{graph-edge-decay-2BZTY2IM.js → graph-edge-decay-3HVLMT6U.js} +5 -5
- package/dist/graph-edge-decay-3HVLMT6U.js.map +1 -0
- package/dist/graph-jsonl.js +3 -3
- package/dist/graph-snapshot.js +3 -3
- package/dist/graph.js +4 -4
- package/dist/harmonic-retrieval.js +22 -22
- package/dist/harmonic-source-lookup.js +2 -2
- package/dist/index.js +212 -211
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +7 -7
- package/dist/local-llm.js +2 -2
- package/dist/location/index.js +24 -24
- package/dist/maintenance/memory-governance.js +21 -21
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +21 -21
- package/dist/maintenance/rebuild-memory-projection-runner.js +23 -23
- package/dist/maintenance/rebuild-memory-projection.js +22 -22
- package/dist/memory-browse-operations.js +77 -77
- package/dist/memory-browse.js +6 -6
- package/dist/memory-subject.js +3 -3
- package/dist/namespaces/migrate.js +22 -22
- package/dist/namespaces/search.js +7 -7
- package/dist/namespaces/storage.js +21 -21
- package/dist/offline-sync-changeset-generations.js +3 -3
- package/dist/offline-sync-embedding-generation.js +2 -2
- package/dist/offline-sync-file-io.js +2 -2
- package/dist/offline-sync-impression-drain.js +3 -3
- package/dist/offline-sync-snapshot-stream.js +8 -8
- package/dist/offline-sync-upload-staging.js +3 -3
- package/dist/offline-sync.js +9 -9
- package/dist/operator-doctor-corpus.js +24 -24
- package/dist/operator-toolkit.js +42 -42
- package/dist/orchestration/extraction-drain.d.ts +90 -0
- package/dist/orchestration/extraction-drain.d.ts.map +1 -0
- package/dist/orchestration/extraction-run-helpers.d.ts +3 -0
- package/dist/orchestration/extraction-run-helpers.d.ts.map +1 -1
- package/dist/orchestration/extraction-run.d.ts +4 -1
- package/dist/orchestration/extraction-run.d.ts.map +1 -1
- package/dist/orchestration/maintenance.js +35 -35
- package/dist/orchestration/turn-ingestion.d.ts +3 -0
- package/dist/orchestration/turn-ingestion.d.ts.map +1 -1
- package/dist/orchestrator.d.ts +8 -0
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +165 -164
- package/dist/qmd.js +5 -5
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-navigate-access.js +2 -2
- package/dist/recall-navigate-disclosure.js +2 -2
- package/dist/recall-navigate.js +2 -2
- package/dist/recall-navigation-operations.js +77 -77
- package/dist/recall-navigation.js +76 -76
- package/dist/recall-planner-llm.js +1 -1
- package/dist/recall-state-view-admission.js +6 -6
- package/dist/recall-why-service.js +6 -6
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.js +2 -2
- package/dist/reconcile/manifest.js +3 -3
- package/dist/{resolution-4ZEQPXUZ.js → resolution-NNYG37YB.js} +6 -6
- package/dist/resolution-NNYG37YB.js.map +1 -0
- package/dist/resume-bundles.js +32 -32
- package/dist/retrieval-agents.js +2 -2
- package/dist/salvage-envelope.js +5 -5
- package/dist/search/factory.js +6 -6
- package/dist/search/index.js +10 -10
- package/dist/secure-store/index.js +2 -2
- package/dist/semantic-consolidation.js +22 -22
- package/dist/semantic-rule-promotion.js +21 -21
- package/dist/semantic-rule-verifier.js +21 -21
- package/dist/source-agent-qualifier.js +25 -25
- package/dist/standup/build.js +21 -21
- package/dist/storage/dropped-extraction-chunk.d.ts +8 -0
- package/dist/storage/dropped-extraction-chunk.d.ts.map +1 -0
- package/dist/storage.d.ts +1 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +20 -20
- package/dist/subject-config.js +4 -4
- package/dist/summarizer.js +4 -4
- package/dist/support-passport/index.js +79 -79
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +2 -2
- package/dist/transfer/export-okf.js +22 -22
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/verified-recall.js +21 -21
- package/dist/who-knows-cli.js +22 -22
- package/dist/who-knows.js +21 -21
- package/dist/write-envelope.js +4 -4
- package/package.json +2 -2
- package/src/buffer-drop-extraction-chunk.test.ts +196 -0
- package/src/buffer-types.ts +65 -0
- package/src/buffer.ts +60 -62
- package/src/config.test.ts +16 -0
- package/src/config.ts +22 -0
- package/src/extraction-judge-chain.test.ts +298 -2
- package/src/extraction-judge.ts +147 -82
- package/src/extraction-window-drain.test.ts +123 -0
- package/src/extraction-window-drain.ts +110 -0
- package/src/fallback-llm.test.ts +94 -0
- package/src/fallback-llm.ts +7 -7
- package/src/orchestration/extraction-drain.ts +261 -0
- package/src/orchestration/extraction-run-helpers.ts +8 -0
- package/src/orchestration/extraction-run.ts +5 -4
- package/src/orchestration/turn-ingestion.ts +9 -5
- package/src/orchestrator.ts +21 -0
- package/src/secure-store/secure-fs.ts +3 -1
- package/src/storage/dropped-extraction-chunk.ts +27 -0
- package/src/storage-dropped-chunk-filenames.test.ts +30 -0
- package/src/storage.ts +6 -7
- package/src/types.ts +19 -0
- package/dist/chunk-5U3NUHMA.js.map +0 -1
- package/dist/chunk-A6PKEU7T.js.map +0 -1
- package/dist/chunk-BBDASWNT.js.map +0 -1
- package/dist/chunk-GMMZ45C4.js.map +0 -1
- package/dist/chunk-JWTUJVIF.js.map +0 -1
- package/dist/chunk-R6TCM47S.js.map +0 -1
- package/dist/chunk-UFBIOF7C.js.map +0 -1
- package/dist/chunk-YGYT7S3G.js.map +0 -1
- /package/dist/{auto-sync-R3XGJL5I.js.map → auto-sync-BNCBMU6H.js.map} +0 -0
- /package/dist/{capsule-crypto-7FJQINUR.js.map → buffer-types.js.map} +0 -0
- /package/dist/{graph-edge-decay-2BZTY2IM.js.map → capsule-crypto-GWVG7LGC.js.map} +0 -0
- /package/dist/{chunk-MH3LC7KB.js.map → chunk-22YJQTPN.js.map} +0 -0
- /package/dist/{chunk-MKE5DP3S.js.map → chunk-23ZMQONL.js.map} +0 -0
- /package/dist/{chunk-MGXFJOUR.js.map → chunk-2AV2U2SZ.js.map} +0 -0
- /package/dist/{chunk-HJ3UTGIW.js.map → chunk-2JNBPOZ2.js.map} +0 -0
- /package/dist/{chunk-EEOYF3IB.js.map → chunk-3RJDYZJE.js.map} +0 -0
- /package/dist/{chunk-FBAHBFT5.js.map → chunk-4HJ2QK32.js.map} +0 -0
- /package/dist/{chunk-UXXVOA4D.js.map → chunk-4LLV2N3R.js.map} +0 -0
- /package/dist/{chunk-P5CUEB2Q.js.map → chunk-4QLKVZH6.js.map} +0 -0
- /package/dist/{chunk-2RJO7VS4.js.map → chunk-4UT2NUAC.js.map} +0 -0
- /package/dist/{chunk-MNDHKZZQ.js.map → chunk-5Q6CR65T.js.map} +0 -0
- /package/dist/{chunk-XTBEXZFN.js.map → chunk-5SF7JOJB.js.map} +0 -0
- /package/dist/{chunk-BQ6DGL6A.js.map → chunk-64WHWIPL.js.map} +0 -0
- /package/dist/{chunk-TVIGN3XZ.js.map → chunk-6FRFVIKC.js.map} +0 -0
- /package/dist/{chunk-A23DPFFI.js.map → chunk-6ITNEPJO.js.map} +0 -0
- /package/dist/{chunk-G6PPA4TH.js.map → chunk-6Z2ZWBOV.js.map} +0 -0
- /package/dist/{chunk-QEGE5QCL.js.map → chunk-7IANFJ24.js.map} +0 -0
- /package/dist/{chunk-PSQ2P2T3.js.map → chunk-7XZ7TOLQ.js.map} +0 -0
- /package/dist/{chunk-Z5AQZRHF.js.map → chunk-AER3HD3B.js.map} +0 -0
- /package/dist/{chunk-BPBUUGAO.js.map → chunk-ANV373S5.js.map} +0 -0
- /package/dist/{chunk-LSSCMZQ2.js.map → chunk-B3I2QQU7.js.map} +0 -0
- /package/dist/{chunk-NSQSOLCE.js.map → chunk-B7VKRCAG.js.map} +0 -0
- /package/dist/{chunk-HXFIILGF.js.map → chunk-BFPF5N3H.js.map} +0 -0
- /package/dist/{chunk-KQAFEZQX.js.map → chunk-BUKK5SWA.js.map} +0 -0
- /package/dist/{chunk-RBC4GIZU.js.map → chunk-BUKZ7MTN.js.map} +0 -0
- /package/dist/{chunk-FO5AFCRI.js.map → chunk-CR72WZ3M.js.map} +0 -0
- /package/dist/{chunk-YC2C67F7.js.map → chunk-CRXGQRNA.js.map} +0 -0
- /package/dist/{chunk-WRDFYSUC.js.map → chunk-CZVNOTLR.js.map} +0 -0
- /package/dist/{chunk-VPO53QMG.js.map → chunk-D6DOLQYK.js.map} +0 -0
- /package/dist/{chunk-HRT2SJPC.js.map → chunk-E3NBMTTX.js.map} +0 -0
- /package/dist/{chunk-HIMXDRSQ.js.map → chunk-E5IEI2MR.js.map} +0 -0
- /package/dist/{chunk-Z2T5BPOZ.js.map → chunk-EIYJK7OD.js.map} +0 -0
- /package/dist/{chunk-FRAL3EU4.js.map → chunk-EKPRLTAE.js.map} +0 -0
- /package/dist/{chunk-EVVCE7S2.js.map → chunk-EKR6OO4P.js.map} +0 -0
- /package/dist/{chunk-6R6CU3MR.js.map → chunk-F2IU6A2L.js.map} +0 -0
- /package/dist/{chunk-FL6SLVAJ.js.map → chunk-FAKRTS3Y.js.map} +0 -0
- /package/dist/{chunk-64IO6ZEE.js.map → chunk-FC3ZFEXF.js.map} +0 -0
- /package/dist/{chunk-DGBWPSM6.js.map → chunk-FLDXEJFX.js.map} +0 -0
- /package/dist/{chunk-JH4FCVWJ.js.map → chunk-G6OFEGUR.js.map} +0 -0
- /package/dist/{chunk-Z5QWUR3Y.js.map → chunk-GCW7UT5I.js.map} +0 -0
- /package/dist/{chunk-ULUX6Q7R.js.map → chunk-GE5ZNDLE.js.map} +0 -0
- /package/dist/{chunk-U4JXQCJ3.js.map → chunk-GLB6VV3J.js.map} +0 -0
- /package/dist/{chunk-RWWYBE7E.js.map → chunk-GPVGAHXL.js.map} +0 -0
- /package/dist/{chunk-3BPMYMPX.js.map → chunk-GSBSGT43.js.map} +0 -0
- /package/dist/{chunk-645WOU5I.js.map → chunk-H7BZBSP6.js.map} +0 -0
- /package/dist/{chunk-GFYFKXWZ.js.map → chunk-HUDPXTXW.js.map} +0 -0
- /package/dist/{chunk-L2JQSG5M.js.map → chunk-I7TR2OCU.js.map} +0 -0
- /package/dist/{chunk-HKEK27M4.js.map → chunk-IAOW2L6K.js.map} +0 -0
- /package/dist/{chunk-STM4T335.js.map → chunk-IJH6METJ.js.map} +0 -0
- /package/dist/{chunk-WADPRAZE.js.map → chunk-IZRNMXZC.js.map} +0 -0
- /package/dist/{chunk-LAQS5HYU.js.map → chunk-JM6U3SAE.js.map} +0 -0
- /package/dist/{chunk-74AY3S7U.js.map → chunk-JNLYHJG7.js.map} +0 -0
- /package/dist/{chunk-7ZBANI3M.js.map → chunk-JOOGPZJE.js.map} +0 -0
- /package/dist/{chunk-V5RJCXLG.js.map → chunk-JORSMFL6.js.map} +0 -0
- /package/dist/{chunk-VKTP3TZB.js.map → chunk-JQCP5QW3.js.map} +0 -0
- /package/dist/{chunk-LRJKHXU4.js.map → chunk-JRTKYFFY.js.map} +0 -0
- /package/dist/{chunk-UMFBHYPW.js.map → chunk-JT3IXJIL.js.map} +0 -0
- /package/dist/{chunk-S53WZWGL.js.map → chunk-JZFJZLJY.js.map} +0 -0
- /package/dist/{chunk-6QOL4G76.js.map → chunk-KQXRVELF.js.map} +0 -0
- /package/dist/{chunk-USOXPN5H.js.map → chunk-L74NWAD4.js.map} +0 -0
- /package/dist/{chunk-Y6Y3TRBN.js.map → chunk-LJXBFVI2.js.map} +0 -0
- /package/dist/{chunk-3L5WH6CV.js.map → chunk-LPD6I2U6.js.map} +0 -0
- /package/dist/{chunk-LDXS746V.js.map → chunk-LR7RW4T6.js.map} +0 -0
- /package/dist/{chunk-FZVZYU5C.js.map → chunk-LXN737FX.js.map} +0 -0
- /package/dist/{chunk-ARFT6ZBE.js.map → chunk-MGYZENUY.js.map} +0 -0
- /package/dist/{chunk-YQ6SLFNZ.js.map → chunk-MPJDQC4G.js.map} +0 -0
- /package/dist/{chunk-5UWP5X3T.js.map → chunk-NJ2DZYEK.js.map} +0 -0
- /package/dist/{chunk-MBMT5FUQ.js.map → chunk-O2O4FDAH.js.map} +0 -0
- /package/dist/{chunk-GCVUBVEN.js.map → chunk-ODJ2HRFW.js.map} +0 -0
- /package/dist/{chunk-2NLLXCJG.js.map → chunk-OWHERGF2.js.map} +0 -0
- /package/dist/{chunk-I73ZSORJ.js.map → chunk-OXSTKCYK.js.map} +0 -0
- /package/dist/{chunk-CNQQNYJ2.js.map → chunk-P3YUEPSQ.js.map} +0 -0
- /package/dist/{chunk-TPKB7T6Z.js.map → chunk-Q6UZREJK.js.map} +0 -0
- /package/dist/{chunk-UJDTXWMT.js.map → chunk-QIMGULGN.js.map} +0 -0
- /package/dist/{chunk-E24RUQEN.js.map → chunk-QNIYL5SK.js.map} +0 -0
- /package/dist/{chunk-KOY25HUB.js.map → chunk-QYAXMXOB.js.map} +0 -0
- /package/dist/{chunk-CU7WDDPV.js.map → chunk-RIAKNXZB.js.map} +0 -0
- /package/dist/{chunk-UQWA5QDT.js.map → chunk-RO2NNWR5.js.map} +0 -0
- /package/dist/{chunk-AYXRK264.js.map → chunk-RROFIAYI.js.map} +0 -0
- /package/dist/{chunk-L46Y5FPE.js.map → chunk-RUAIO5UA.js.map} +0 -0
- /package/dist/{chunk-QIC7YZ3D.js.map → chunk-RWW5KPIY.js.map} +0 -0
- /package/dist/{chunk-E5PVMB3I.js.map → chunk-SDXBR5QO.js.map} +0 -0
- /package/dist/{chunk-WSLNC5AL.js.map → chunk-SJHFU52W.js.map} +0 -0
- /package/dist/{chunk-7PG7QBWD.js.map → chunk-SRPGA6WN.js.map} +0 -0
- /package/dist/{chunk-CUWOSRCS.js.map → chunk-SUKPM6DV.js.map} +0 -0
- /package/dist/{chunk-G2SRB62M.js.map → chunk-TTQYX7VU.js.map} +0 -0
- /package/dist/{chunk-TDCR6PQL.js.map → chunk-TWRPWJJT.js.map} +0 -0
- /package/dist/{chunk-KC5SVCUJ.js.map → chunk-U66CTIF5.js.map} +0 -0
- /package/dist/{chunk-D2AKUKAQ.js.map → chunk-UIK3U36O.js.map} +0 -0
- /package/dist/{chunk-XLEP7SS3.js.map → chunk-UMSH4I3E.js.map} +0 -0
- /package/dist/{chunk-X7Y7WX73.js.map → chunk-UVYI6VIX.js.map} +0 -0
- /package/dist/{chunk-LHBOFROT.js.map → chunk-V2XMG7GM.js.map} +0 -0
- /package/dist/{chunk-NQOM6PHO.js.map → chunk-VCOCOBUS.js.map} +0 -0
- /package/dist/{chunk-4P4VLPGB.js.map → chunk-VECRSYQG.js.map} +0 -0
- /package/dist/{chunk-ID2PLENW.js.map → chunk-VHBGJAYC.js.map} +0 -0
- /package/dist/{chunk-RVYXSS3Q.js.map → chunk-VIS23B6O.js.map} +0 -0
- /package/dist/{chunk-NMKETOAX.js.map → chunk-VSHWPOUB.js.map} +0 -0
- /package/dist/{chunk-UBN5AOLT.js.map → chunk-W5EO4USJ.js.map} +0 -0
- /package/dist/{chunk-Y7427SL7.js.map → chunk-WCGJK4NQ.js.map} +0 -0
- /package/dist/{chunk-CCYXCT4B.js.map → chunk-WQYS47G4.js.map} +0 -0
- /package/dist/{chunk-JH2ALMPR.js.map → chunk-WR5POHQ3.js.map} +0 -0
- /package/dist/{chunk-DK5VMJL3.js.map → chunk-WT2VVYBM.js.map} +0 -0
- /package/dist/{chunk-EHIKY2IA.js.map → chunk-WU5WTCH5.js.map} +0 -0
- /package/dist/{chunk-QKXPOLQD.js.map → chunk-WUBGMV3G.js.map} +0 -0
- /package/dist/{chunk-O6YJI5VX.js.map → chunk-WUL6LDJ4.js.map} +0 -0
- /package/dist/{chunk-5EILJ6W2.js.map → chunk-WXPBDEMW.js.map} +0 -0
- /package/dist/{chunk-U7HJBODU.js.map → chunk-XKTHJHSQ.js.map} +0 -0
- /package/dist/{chunk-JFI3ATK4.js.map → chunk-XMYWYJM2.js.map} +0 -0
- /package/dist/{chunk-64S6X4IU.js.map → chunk-XTN54NML.js.map} +0 -0
- /package/dist/{chunk-VGFNTY3Y.js.map → chunk-YFXRLRKP.js.map} +0 -0
- /package/dist/{chunk-DAOMWGO2.js.map → chunk-ZX5N5D6M.js.map} +0 -0
- /package/dist/{chunk-PTRPOJ7U.js.map → chunk-ZXFKG7VZ.js.map} +0 -0
- /package/dist/{resolution-4ZEQPXUZ.js.map → extraction-window-drain.js.map} +0 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
chunkExtractionWindow,
|
|
5
|
+
classifyDrainResult,
|
|
6
|
+
extractionWindowBudgetChars,
|
|
7
|
+
mergeDrainResults,
|
|
8
|
+
} from "./extraction-window-drain.js";
|
|
9
|
+
import type { ExtractionRunResult } from "./orchestration/extraction-run.js";
|
|
10
|
+
import type { BufferTurn } from "./types.js";
|
|
11
|
+
|
|
12
|
+
function makeTurn(content: string, sessionKey = "s1"): BufferTurn {
|
|
13
|
+
return { role: "user", content, timestamp: "2026-10-29T00:00:00.000Z", sessionKey };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function skippedResult(reason: string): ExtractionRunResult {
|
|
17
|
+
return { status: "skipped", reason, persistedCount: 0, durableOutputCount: 0 };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test("chunkExtractionWindow returns a single chunk when the window fits the budget", () => {
|
|
21
|
+
const turns = [makeTurn("a".repeat(10)), makeTurn("b".repeat(10))];
|
|
22
|
+
const chunks = chunkExtractionWindow(turns, 100);
|
|
23
|
+
assert.equal(chunks.length, 1);
|
|
24
|
+
assert.deepEqual(chunks[0], turns);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("chunkExtractionWindow splits an oversized window into budget-fitting chunks in order", () => {
|
|
28
|
+
const turns = [
|
|
29
|
+
makeTurn("a".repeat(60)),
|
|
30
|
+
makeTurn("b".repeat(60)),
|
|
31
|
+
makeTurn("c".repeat(60)),
|
|
32
|
+
makeTurn("d".repeat(60)),
|
|
33
|
+
];
|
|
34
|
+
const chunks = chunkExtractionWindow(turns, 100);
|
|
35
|
+
assert.equal(chunks.length, 4);
|
|
36
|
+
for (const chunk of chunks) {
|
|
37
|
+
assert.ok(chunk.every((turn) => turn.content.length <= 100));
|
|
38
|
+
}
|
|
39
|
+
assert.deepEqual(
|
|
40
|
+
chunks.flat().map((turn) => turn.content[0]),
|
|
41
|
+
["a", "b", "c", "d"],
|
|
42
|
+
);
|
|
43
|
+
assert.deepEqual(chunks.flat(), turns);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("chunkExtractionWindow gives a single oversized turn its own chunk", () => {
|
|
47
|
+
const turns = [makeTurn("x".repeat(250)), makeTurn("y".repeat(10))];
|
|
48
|
+
const chunks = chunkExtractionWindow(turns, 100);
|
|
49
|
+
assert.equal(chunks.length, 2);
|
|
50
|
+
assert.equal(chunks[0]?.length, 1);
|
|
51
|
+
assert.equal(chunks[1]?.length, 1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("chunkExtractionWindow treats a non-positive budget as no chunking", () => {
|
|
55
|
+
const turns = [makeTurn("a".repeat(500)), makeTurn("b".repeat(500))];
|
|
56
|
+
assert.equal(chunkExtractionWindow(turns, 0).length, 1);
|
|
57
|
+
assert.equal(chunkExtractionWindow(turns, Number.POSITIVE_INFINITY).length, 1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("chunkExtractionWindow returns no chunks for an empty window", () => {
|
|
61
|
+
assert.deepEqual(chunkExtractionWindow([], 100), []);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("classifyDrainResult treats completed runs and benign skips as drained", () => {
|
|
65
|
+
assert.equal(classifyDrainResult({ status: "completed", persistedCount: 1, durableOutputCount: 2 }), "drained");
|
|
66
|
+
assert.equal(classifyDrainResult(skippedResult("below_threshold")), "drained");
|
|
67
|
+
assert.equal(classifyDrainResult(skippedResult("cron_session")), "drained");
|
|
68
|
+
assert.equal(classifyDrainResult(skippedResult("empty_normalized_turns")), "drained");
|
|
69
|
+
assert.equal(classifyDrainResult(skippedResult("processed_fingerprint")), "drained");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("classifyDrainResult reports breaker/backoff skips as deferred", () => {
|
|
73
|
+
assert.equal(classifyDrainResult(skippedResult("provider_circuit_open")), "deferred");
|
|
74
|
+
assert.equal(classifyDrainResult(skippedResult("extraction_backoff")), "deferred");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("classifyDrainResult reports empty extraction results as failed", () => {
|
|
78
|
+
assert.equal(
|
|
79
|
+
classifyDrainResult({ ...skippedResult("empty_extraction_result"), retainedBuffer: true }),
|
|
80
|
+
"failed",
|
|
81
|
+
"an empty result that kept the window for retry is a chunk failure",
|
|
82
|
+
);
|
|
83
|
+
assert.equal(
|
|
84
|
+
classifyDrainResult(skippedResult("empty_extraction_result")),
|
|
85
|
+
"drained",
|
|
86
|
+
"an empty result that cleared its turns extracted fine and found nothing durable",
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("mergeDrainResults sums counts, completes when any chunk completes, keeps the last skip reason", () => {
|
|
91
|
+
const first = skippedResult("empty_extraction_result");
|
|
92
|
+
const second: ExtractionRunResult = { status: "completed", persistedCount: 3, durableOutputCount: 4 };
|
|
93
|
+
// A completed chunk means the drain made durable progress.
|
|
94
|
+
const merged = mergeDrainResults(mergeDrainResults(null, first), second);
|
|
95
|
+
assert.equal(merged.status, "completed");
|
|
96
|
+
assert.equal(merged.persistedCount, 3);
|
|
97
|
+
assert.equal(merged.durableOutputCount, 4);
|
|
98
|
+
// All chunks skipped: the aggregate stays skipped with the last reason.
|
|
99
|
+
const mergedFailed = mergeDrainResults(
|
|
100
|
+
mergeDrainResults(null, skippedResult("provider_circuit_open")),
|
|
101
|
+
first,
|
|
102
|
+
);
|
|
103
|
+
assert.equal(mergedFailed.status, "skipped");
|
|
104
|
+
assert.equal(mergedFailed.reason, "empty_extraction_result");
|
|
105
|
+
assert.equal(mergedFailed.persistedCount, 0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("extractionWindowBudgetChars applies the learned local window only when the local model extracts", () => {
|
|
109
|
+
const registry = { calculateContextSizes: () => ({ maxInputChars: 12_000 }) };
|
|
110
|
+
const base = { registry, model: "local-model", maxContext: undefined };
|
|
111
|
+
|
|
112
|
+
assert.equal(extractionWindowBudgetChars({ ...base, localExtractionActive: true }), 12_000);
|
|
113
|
+
assert.equal(
|
|
114
|
+
extractionWindowBudgetChars({ ...base, localExtractionActive: false }),
|
|
115
|
+
Number.POSITIVE_INFINITY,
|
|
116
|
+
"gateway or disabled local LLM: the local window does not describe the extractor",
|
|
117
|
+
);
|
|
118
|
+
assert.equal(
|
|
119
|
+
extractionWindowBudgetChars({ ...base, registry: undefined, localExtractionActive: true }),
|
|
120
|
+
Number.POSITIVE_INFINITY,
|
|
121
|
+
"no registry (legacy host or test double): no budget",
|
|
122
|
+
);
|
|
123
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trigger-drain helpers (extraction trigger stall fix).
|
|
3
|
+
*
|
|
4
|
+
* A session whose buffered window outgrows the extraction model's context
|
|
5
|
+
* never drains: the trigger fires `extract_batch` on every observe, the
|
|
6
|
+
* oversized request fails, nothing is cleared, and the window only grows.
|
|
7
|
+
* These helpers split such a window into budget-fitting chunks and classify
|
|
8
|
+
* each extraction run so the caller can drain successes, defer on breaker/
|
|
9
|
+
* backoff retention, and drop repeatedly failing chunks.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ExtractionRunResult } from "./orchestration/extraction-run.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Split `turns` into chunks whose summed content length stays within
|
|
16
|
+
* `maxChars`. Chunks keep turn order and never split a turn; a single turn
|
|
17
|
+
* longer than the budget becomes its own chunk (the extraction prompt still
|
|
18
|
+
* clamps per-turn content). A non-positive or non-finite budget, or an empty
|
|
19
|
+
* window, yields at most one chunk (no chunking).
|
|
20
|
+
*/
|
|
21
|
+
export function chunkExtractionWindow<T extends { content: string }>(
|
|
22
|
+
turns: readonly T[],
|
|
23
|
+
maxChars: number,
|
|
24
|
+
): T[][] {
|
|
25
|
+
if (turns.length === 0) return [];
|
|
26
|
+
const budget =
|
|
27
|
+
Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : Number.POSITIVE_INFINITY;
|
|
28
|
+
const chunks: T[][] = [];
|
|
29
|
+
let current: T[] = [];
|
|
30
|
+
let currentChars = 0;
|
|
31
|
+
for (const turn of turns) {
|
|
32
|
+
const turnChars = turn.content.length;
|
|
33
|
+
if (current.length > 0 && currentChars + turnChars > budget) {
|
|
34
|
+
chunks.push(current);
|
|
35
|
+
current = [];
|
|
36
|
+
currentChars = 0;
|
|
37
|
+
}
|
|
38
|
+
current.push(turn);
|
|
39
|
+
currentChars += turnChars;
|
|
40
|
+
}
|
|
41
|
+
if (current.length > 0) chunks.push(current);
|
|
42
|
+
return chunks;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type DrainVerdict = "drained" | "deferred" | "failed";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Run results that deliberately RETAIN the buffer (provider breaker open,
|
|
49
|
+
* per-fingerprint backoff) are deferrals, not chunk failures — counting them
|
|
50
|
+
* would drop good content during a provider outage.
|
|
51
|
+
*/
|
|
52
|
+
const DRAIN_DEFERRAL_REASONS = {
|
|
53
|
+
provider_circuit_open: true,
|
|
54
|
+
extraction_backoff: true,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export function classifyDrainResult(result: ExtractionRunResult): DrainVerdict {
|
|
58
|
+
if (result.status !== "skipped") return "drained";
|
|
59
|
+
// An empty result is a failure only when the run kept the window for retry;
|
|
60
|
+
// otherwise it extracted fine, found nothing durable, and cleared its turns.
|
|
61
|
+
if (result.reason === "empty_extraction_result") {
|
|
62
|
+
return result.retainedBuffer === true ? "failed" : "drained";
|
|
63
|
+
}
|
|
64
|
+
if (result.reason !== undefined && result.reason in DRAIN_DEFERRAL_REASONS) return "deferred";
|
|
65
|
+
return "drained";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Combine chunk results into the single `ExtractionRunResult` the queue
|
|
70
|
+
* contract settles with: counts sum, any completed chunk completes the drain,
|
|
71
|
+
* and the last non-completed reason wins.
|
|
72
|
+
*/
|
|
73
|
+
export function mergeDrainResults(
|
|
74
|
+
previous: ExtractionRunResult | null,
|
|
75
|
+
next: ExtractionRunResult,
|
|
76
|
+
): ExtractionRunResult {
|
|
77
|
+
if (previous === null) return next;
|
|
78
|
+
const completed = previous.status === "completed" || next.status === "completed";
|
|
79
|
+
const merged: ExtractionRunResult = {
|
|
80
|
+
status: completed ? "completed" : "skipped",
|
|
81
|
+
persistedCount: previous.persistedCount + next.persistedCount,
|
|
82
|
+
durableOutputCount: previous.durableOutputCount + next.durableOutputCount,
|
|
83
|
+
};
|
|
84
|
+
if (!completed) merged.reason = next.reason ?? previous.reason;
|
|
85
|
+
if (previous.postPersistMetadataFailed === true || next.postPersistMetadataFailed === true) {
|
|
86
|
+
merged.postPersistMetadataFailed = true;
|
|
87
|
+
}
|
|
88
|
+
return merged;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The slice of ModelRegistry the drain budget needs. */
|
|
92
|
+
export interface WindowBudgetRegistry {
|
|
93
|
+
calculateContextSizes(modelId: string, maxContextOverride?: number): { maxInputChars: number };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Character budget for one extraction request. The learned local-model window
|
|
98
|
+
* only describes the extractor when the local LLM does the extracting; with a
|
|
99
|
+
* gateway extractor, a disabled local LLM, or no registry (legacy hosts, test
|
|
100
|
+
* doubles) there is no budget and the window runs in one request as before.
|
|
101
|
+
*/
|
|
102
|
+
export function extractionWindowBudgetChars(input: {
|
|
103
|
+
registry: WindowBudgetRegistry | undefined;
|
|
104
|
+
localExtractionActive: boolean;
|
|
105
|
+
model: string;
|
|
106
|
+
maxContext: number | undefined;
|
|
107
|
+
}): number {
|
|
108
|
+
if (!input.registry || !input.localExtractionActive) return Number.POSITIVE_INFINITY;
|
|
109
|
+
return input.registry.calculateContextSizes(input.model, input.maxContext).maxInputChars;
|
|
110
|
+
}
|
package/src/fallback-llm.test.ts
CHANGED
|
@@ -1453,6 +1453,57 @@ test("fallback llm uses the Responses API for openai-responses transports", { co
|
|
|
1453
1453
|
}
|
|
1454
1454
|
});
|
|
1455
1455
|
|
|
1456
|
+
test("fallback llm adds reasoningHeadroomTokens to max_output_tokens on the Responses API", { concurrency: false }, async () => {
|
|
1457
|
+
clearModelsJsonCache();
|
|
1458
|
+
clearSecretCache();
|
|
1459
|
+
|
|
1460
|
+
const llm = new FallbackLlmClient({
|
|
1461
|
+
agents: { defaults: { model: { primary: "responses-provider/demo-model" } } },
|
|
1462
|
+
models: {
|
|
1463
|
+
providers: {
|
|
1464
|
+
"responses-provider": {
|
|
1465
|
+
baseUrl: "https://responses.example/v1",
|
|
1466
|
+
api: "openai-responses",
|
|
1467
|
+
apiKey: "responses-key",
|
|
1468
|
+
models: [],
|
|
1469
|
+
},
|
|
1470
|
+
},
|
|
1471
|
+
},
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
const originalFetch = globalThis.fetch;
|
|
1475
|
+
const bodies: Array<{ max_output_tokens?: number }> = [];
|
|
1476
|
+
// SAFETY: the stub returns the Response shape the client reads and nothing else.
|
|
1477
|
+
globalThis.fetch = (async (_url, init) => {
|
|
1478
|
+
bodies.push(JSON.parse(String(init?.body ?? "{}")));
|
|
1479
|
+
return new Response(JSON.stringify({ output_text: "ok" }), {
|
|
1480
|
+
status: 200,
|
|
1481
|
+
headers: { "Content-Type": "application/json" },
|
|
1482
|
+
});
|
|
1483
|
+
}) as typeof fetch;
|
|
1484
|
+
|
|
1485
|
+
try {
|
|
1486
|
+
const messages = [{ role: "user" as const, content: "Say OK" }];
|
|
1487
|
+
// Non-finite headroom is ignored, like a negative one.
|
|
1488
|
+
await llm.chatCompletion(messages, { temperature: 0, maxTokens: 8, reasoningHeadroomTokens: Number.NaN });
|
|
1489
|
+
await llm.chatCompletion(messages, { temperature: 0, maxTokens: 64, reasoningHeadroomTokens: 1024 });
|
|
1490
|
+
await llm.chatCompletion(messages, { temperature: 0, maxTokens: 64 });
|
|
1491
|
+
await llm.chatCompletion(messages, { temperature: 0, maxTokens: 64, reasoningHeadroomTokens: -5 });
|
|
1492
|
+
await llm.chatCompletion(messages, { temperature: 0, reasoningHeadroomTokens: 1024 });
|
|
1493
|
+
await llm.chatCompletion(messages, { temperature: 0, maxTokens: 64, reasoningHeadroomTokens: Number.POSITIVE_INFINITY });
|
|
1494
|
+
assert.equal(bodies[5].max_output_tokens, 64, "an infinite headroom adds nothing (it would serialize as null)");
|
|
1495
|
+
assert.equal(bodies[0].max_output_tokens, 8, "a non-finite headroom adds nothing");
|
|
1496
|
+
assert.equal(bodies[1].max_output_tokens, 64 + 1024, "headroom is added on top of the compact budget");
|
|
1497
|
+
assert.equal(bodies[2].max_output_tokens, 64, "no headroom option: exactly maxTokens, as before");
|
|
1498
|
+
assert.equal(bodies[3].max_output_tokens, 64, "a negative headroom adds nothing");
|
|
1499
|
+
assert.equal(bodies[4].max_output_tokens, 4096, "no maxTokens: the default budget is not inflated by headroom");
|
|
1500
|
+
} finally {
|
|
1501
|
+
globalThis.fetch = originalFetch;
|
|
1502
|
+
clearModelsJsonCache();
|
|
1503
|
+
clearSecretCache();
|
|
1504
|
+
}
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1456
1507
|
test("fallback llm ignores echoed input_text blocks in responses output extraction", { concurrency: false }, async () => {
|
|
1457
1508
|
clearModelsJsonCache();
|
|
1458
1509
|
clearSecretCache();
|
|
@@ -2122,3 +2173,46 @@ test("timeout records TimeoutError on failureDiag (issue #2891)", { concurrency:
|
|
|
2122
2173
|
clearSecretCache();
|
|
2123
2174
|
}
|
|
2124
2175
|
});
|
|
2176
|
+
|
|
2177
|
+
test("fallback llm adds reasoningHeadroomTokens to the Chat Completions token limit too", { concurrency: false }, async () => {
|
|
2178
|
+
clearModelsJsonCache();
|
|
2179
|
+
clearSecretCache();
|
|
2180
|
+
|
|
2181
|
+
const llm = new FallbackLlmClient({
|
|
2182
|
+
agents: { defaults: { model: { primary: "chat-provider/demo-model" } } },
|
|
2183
|
+
models: {
|
|
2184
|
+
providers: {
|
|
2185
|
+
"chat-provider": {
|
|
2186
|
+
baseUrl: "https://chat.example/v1",
|
|
2187
|
+
api: "openai-completions",
|
|
2188
|
+
apiKey: "chat-key",
|
|
2189
|
+
models: [],
|
|
2190
|
+
},
|
|
2191
|
+
},
|
|
2192
|
+
},
|
|
2193
|
+
});
|
|
2194
|
+
|
|
2195
|
+
const originalFetch = globalThis.fetch;
|
|
2196
|
+
const bodies: Array<{ max_tokens?: number; max_completion_tokens?: number }> = [];
|
|
2197
|
+
// SAFETY: the stub returns the Chat Completions shape the client reads and nothing else.
|
|
2198
|
+
globalThis.fetch = (async (_url, init) => {
|
|
2199
|
+
bodies.push(JSON.parse(String(init?.body ?? "{}")));
|
|
2200
|
+
return new Response(JSON.stringify({ choices: [{ message: { content: "ok" } }] }), {
|
|
2201
|
+
status: 200,
|
|
2202
|
+
headers: { "Content-Type": "application/json" },
|
|
2203
|
+
});
|
|
2204
|
+
}) as typeof fetch;
|
|
2205
|
+
|
|
2206
|
+
try {
|
|
2207
|
+
const messages = [{ role: "user" as const, content: "Say OK" }];
|
|
2208
|
+
await llm.chatCompletion(messages, { temperature: 0, maxTokens: 64, reasoningHeadroomTokens: 1024 });
|
|
2209
|
+
await llm.chatCompletion(messages, { temperature: 0, maxTokens: 64 });
|
|
2210
|
+
const limit = (b: (typeof bodies)[number]) => b.max_completion_tokens ?? b.max_tokens;
|
|
2211
|
+
assert.equal(limit(bodies[0]), 64 + 1024, "headroom is added on top of the compact budget");
|
|
2212
|
+
assert.equal(limit(bodies[1]), 64, "no headroom option: exactly maxTokens, as before");
|
|
2213
|
+
} finally {
|
|
2214
|
+
globalThis.fetch = originalFetch;
|
|
2215
|
+
clearModelsJsonCache();
|
|
2216
|
+
clearSecretCache();
|
|
2217
|
+
}
|
|
2218
|
+
});
|
package/src/fallback-llm.ts
CHANGED
|
@@ -46,6 +46,7 @@ export {
|
|
|
46
46
|
export interface FallbackLlmOptions {
|
|
47
47
|
temperature?: number;
|
|
48
48
|
maxTokens?: number;
|
|
49
|
+
reasoningHeadroomTokens?: number; // Responses API only: extra max_output_tokens for reasoning (they count against it); unset adds none
|
|
49
50
|
timeoutMs?: number;
|
|
50
51
|
signal?: AbortSignal;
|
|
51
52
|
/** Set the Responses API storage policy. Other transports ignore this value. */
|
|
@@ -764,17 +765,16 @@ export class FallbackLlmClient {
|
|
|
764
765
|
return await this.callOllamaChat(effectiveConfig, model.modelId, messages, options);
|
|
765
766
|
}
|
|
766
767
|
|
|
768
|
+
const wanted = Math.floor(options.reasoningHeadroomTokens ?? 0);
|
|
769
|
+
const headroom = Number.isFinite(wanted) ? Math.max(0, wanted) : 0; // NaN and Infinity add nothing
|
|
770
|
+
const capped = options.maxTokens === undefined ? options : { ...options, maxTokens: options.maxTokens + headroom };
|
|
771
|
+
|
|
767
772
|
if (
|
|
768
773
|
model.providerConfig.api === "openai-responses" ||
|
|
769
774
|
model.providerConfig.api === "openai-codex-responses" ||
|
|
770
775
|
model.providerConfig.api === "azure-openai-responses"
|
|
771
776
|
) {
|
|
772
|
-
return await this.callOpenAIResponses(
|
|
773
|
-
effectiveConfig,
|
|
774
|
-
model.modelId,
|
|
775
|
-
messages,
|
|
776
|
-
options,
|
|
777
|
-
);
|
|
777
|
+
return await this.callOpenAIResponses(effectiveConfig, model.modelId, messages, capped);
|
|
778
778
|
}
|
|
779
779
|
|
|
780
780
|
// For OpenAI-compatible chat-completions APIs (openai-completions,
|
|
@@ -785,7 +785,7 @@ export class FallbackLlmClient {
|
|
|
785
785
|
effectiveConfig,
|
|
786
786
|
model.modelId,
|
|
787
787
|
messages,
|
|
788
|
-
|
|
788
|
+
capped,
|
|
789
789
|
shouldAssumeOpenAiChatCompletions(effectiveConfig.baseUrl),
|
|
790
790
|
);
|
|
791
791
|
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chunked trigger-drain for the extraction pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `turn-ingestion.ts`: that file is at the structural ratchet's
|
|
5
|
+
* line cap (issue #1995), and the drain is a self-contained unit — it only
|
|
6
|
+
* needs the host's `runExtraction`, the buffer drop, the storage side-file
|
|
7
|
+
* copy and the preemption hook.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { isAbortError, preemptedExtractionError } from "../abort-error.js";
|
|
12
|
+
import { describeError } from "../buffer-turn-helpers.js";
|
|
13
|
+
import { chunkExtractionWindow, classifyDrainResult, mergeDrainResults } from "../extraction-window-drain.js";
|
|
14
|
+
import { log } from "../logger.js";
|
|
15
|
+
import type { BufferTurn } from "../types.js";
|
|
16
|
+
import type { ExtractionRunCoordinator, ExtractionRunResult } from "./extraction-run.js";
|
|
17
|
+
|
|
18
|
+
type RunOptions = Parameters<ExtractionRunCoordinator["runExtraction"]>[1];
|
|
19
|
+
|
|
20
|
+
/** The `runExtraction` options the drain forwards, with the ones it reads made required. */
|
|
21
|
+
export type DrainRunOptions = RunOptions & {
|
|
22
|
+
bufferKey: string;
|
|
23
|
+
clearBufferAfterExtraction: boolean;
|
|
24
|
+
skipUserTurnThreshold: boolean;
|
|
25
|
+
failOnExtractionFailure: boolean;
|
|
26
|
+
forceExtractionAttempt: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** What the drain needs from its host (structurally satisfied by `TurnIngestionDeps`). */
|
|
30
|
+
export interface ExtractionDrainDeps {
|
|
31
|
+
config: {
|
|
32
|
+
extractionMinUserTurns: number;
|
|
33
|
+
extractionMinChars: number;
|
|
34
|
+
extractionMaxTurnChars: number;
|
|
35
|
+
extractionChunkDropAfterFailures: number;
|
|
36
|
+
};
|
|
37
|
+
extractionWindowBudgetChars?(): number;
|
|
38
|
+
runExtraction(turns: BufferTurn[], options: RunOptions): Promise<ExtractionRunResult>;
|
|
39
|
+
getStorage(): Promise<{
|
|
40
|
+
saveDroppedExtractionChunk(bufferKey: string, reason: string, turns: readonly BufferTurn[]): Promise<string>;
|
|
41
|
+
}>;
|
|
42
|
+
buffer: {
|
|
43
|
+
dropExtractionChunk(bufferKey: string, turns: readonly BufferTurn[], options?: { allowNonPrefix?: boolean }): Promise<number>;
|
|
44
|
+
};
|
|
45
|
+
extractionQueueCoordinator: { beginPreemptable(abort: () => void): void };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Identity of a chunk: its failure count must not follow a buffer key to different content. */
|
|
49
|
+
function chunkFingerprint(chunk: readonly BufferTurn[]): string {
|
|
50
|
+
const hash = createHash("sha256");
|
|
51
|
+
for (const turn of chunk) hash.update(`${turn.role}\u0000${turn.timestamp}\u0000${turn.content}\u0001`);
|
|
52
|
+
return hash.digest("hex");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class ExtractionDrain {
|
|
56
|
+
/**
|
|
57
|
+
* Consecutive failed attempts on the head chunk, per buffer key. In memory on
|
|
58
|
+
* purpose (a restart re-counts from zero). Bound to the chunk's content: a
|
|
59
|
+
* count recorded for one chunk never applies to a different one, so a force
|
|
60
|
+
* flush that clears a failing chunk cannot make unrelated content inherit its
|
|
61
|
+
* failures.
|
|
62
|
+
*/
|
|
63
|
+
private readonly failures = new Map<string, { fingerprint: string; count: number }>();
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Buffer key to the first turn of the chunk a qualified window stopped on.
|
|
67
|
+
* The rest of such a window stays eligible on the next attempt even when it
|
|
68
|
+
* is assistant-only or short: the window passed the thresholds as a whole,
|
|
69
|
+
* and re-judging its tail would clear it unextracted. Cleared when the
|
|
70
|
+
* window drains; one entry per key.
|
|
71
|
+
*/
|
|
72
|
+
private readonly qualified = new Map<string, string>();
|
|
73
|
+
|
|
74
|
+
constructor(private readonly deps: ExtractionDrainDeps) {}
|
|
75
|
+
|
|
76
|
+
/** Characters the coordinator would count for this window: user/assistant text, trimmed and capped per turn, context-only turns left out. */
|
|
77
|
+
private windowChars(turns: readonly BufferTurn[]): number {
|
|
78
|
+
let total = 0;
|
|
79
|
+
for (const turn of turns) {
|
|
80
|
+
if ((turn.role !== "user" && turn.role !== "assistant") || turn.extractionContextOnly === true) continue;
|
|
81
|
+
total += turn.content.trim().slice(0, this.deps.config.extractionMaxTurnChars).length;
|
|
82
|
+
}
|
|
83
|
+
return total;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private failureCount(bufferKey: string, chunk: readonly BufferTurn[]): number {
|
|
87
|
+
const entry = this.failures.get(bufferKey);
|
|
88
|
+
return entry && entry.fingerprint === chunkFingerprint(chunk) ? entry.count : 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private setFailureCount(bufferKey: string, chunk: readonly BufferTurn[], count: number): void {
|
|
92
|
+
if (count <= 0) this.failures.delete(bufferKey);
|
|
93
|
+
else this.failures.set(bufferKey, { fingerprint: chunkFingerprint(chunk), count });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Run `runExtraction` over the trigger-drain window. When the window outgrows
|
|
98
|
+
* the extraction model's input budget (the stall that keeps a session's buffer
|
|
99
|
+
* growing forever: every observe re-fires the trigger on a window that can
|
|
100
|
+
* never extract), split it into budget-fitting chunks and drain them in
|
|
101
|
+
* order; each successful run clears its chunk, so the buffer shrinks across
|
|
102
|
+
* observes. A chunk that fails `extractionChunkDropAfterFailures` times in a
|
|
103
|
+
* row is dropped (side-file copy first). Breaker/backoff retention results
|
|
104
|
+
* stop the drain without counting as chunk failures.
|
|
105
|
+
*
|
|
106
|
+
* Explicit drains (replay/import `clearBufferAfterExtraction: false`, force
|
|
107
|
+
* flush, fail-closed callers, accepted captures) and windows that fit the
|
|
108
|
+
* budget (or hosts without the budget accessor) take the single-run path.
|
|
109
|
+
*
|
|
110
|
+
* `preemptable`: the run was queued as preemptable automatic work. Each chunk's
|
|
111
|
+
* persist step ends the host's preemption registration, so the drain registers
|
|
112
|
+
* it again before every later chunk; a fail-closed flush can then abort the
|
|
113
|
+
* next chunk's model request instead of waiting behind the whole window.
|
|
114
|
+
*/
|
|
115
|
+
async run(turns: BufferTurn[], runOptions: DrainRunOptions, preemptable = false): Promise<ExtractionRunResult> {
|
|
116
|
+
const budgetChars = this.deps.extractionWindowBudgetChars?.() ?? Number.POSITIVE_INFINITY;
|
|
117
|
+
const chunkable =
|
|
118
|
+
runOptions.clearBufferAfterExtraction &&
|
|
119
|
+
!runOptions.forceExtractionAttempt &&
|
|
120
|
+
!runOptions.failOnExtractionFailure &&
|
|
121
|
+
!turns.some((turn) => turn.acceptedCapture === true) &&
|
|
122
|
+
Number.isFinite(budgetChars) &&
|
|
123
|
+
budgetChars > 0;
|
|
124
|
+
const chunks = chunkable ? chunkExtractionWindow(turns, budgetChars) : [];
|
|
125
|
+
const resumed =
|
|
126
|
+
chunks.length > 0 && this.qualified.get(runOptions.bufferKey) === chunkFingerprint(chunks[0].slice(0, 1));
|
|
127
|
+
// The tail of a window that already qualified goes through the chunk loop even when it now fits in one
|
|
128
|
+
// chunk, so its failures still count toward the drop threshold.
|
|
129
|
+
if (chunks.length <= 1 && !resumed) return await this.deps.runExtraction(turns, runOptions);
|
|
130
|
+
// Eligibility (minimum user turns and characters) belongs to the whole
|
|
131
|
+
// window: a window that never qualified keeps the single-run path, which
|
|
132
|
+
// judges it exactly as before.
|
|
133
|
+
if (!resumed) {
|
|
134
|
+
const windowUserTurns = turns.filter((turn) => turn.role === "user").length;
|
|
135
|
+
const belowChars = !runOptions.skipCharThreshold && this.windowChars(turns) < this.deps.config.extractionMinChars;
|
|
136
|
+
if ((!runOptions.skipUserTurnThreshold && windowUserTurns < this.deps.config.extractionMinUserTurns) || belowChars) {
|
|
137
|
+
return await this.deps.runExtraction(turns, runOptions);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// The window qualified as a whole; its chunks must not be re-judged one by
|
|
141
|
+
// one (an assistant-only or short chunk would be cleared unextracted).
|
|
142
|
+
const baseOptions = { ...runOptions, skipCharThreshold: true, skipUserTurnThreshold: true };
|
|
143
|
+
|
|
144
|
+
const bufferKey = runOptions.bufferKey;
|
|
145
|
+
// A native group clears its turns by identity (a pinned capture can sit in front); a drop must find them the same way.
|
|
146
|
+
const byIdentity = runOptions.clearMatchingTurns === true;
|
|
147
|
+
const dropAfter = this.deps.config.extractionChunkDropAfterFailures;
|
|
148
|
+
let aggregate: ExtractionRunResult | null = null;
|
|
149
|
+
for (const [position, chunk] of chunks.entries()) {
|
|
150
|
+
this.qualified.set(bufferKey, chunkFingerprint(chunk.slice(0, 1)));
|
|
151
|
+
// retentionWindow: the chunks processed so far. A chunk's deferred retention replaces theirs (retained turns do not pile
|
|
152
|
+
// up across chunks) and leaves the retained turns of chunks not yet processed in the buffer.
|
|
153
|
+
let chunkOptions: DrainRunOptions = { ...baseOptions, retentionWindow: chunks.slice(0, position + 1).flat() };
|
|
154
|
+
let preempt: AbortController | undefined;
|
|
155
|
+
if (preemptable && position > 0) {
|
|
156
|
+
const local = new AbortController();
|
|
157
|
+
preempt = local;
|
|
158
|
+
this.deps.extractionQueueCoordinator.beginPreemptable(() => local.abort(preemptedExtractionError()));
|
|
159
|
+
chunkOptions = {
|
|
160
|
+
...chunkOptions,
|
|
161
|
+
abortSignal: runOptions.abortSignal ? AbortSignal.any([runOptions.abortSignal, local.signal]) : local.signal,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
let failures = this.failureCount(bufferKey, chunk);
|
|
165
|
+
let result: ExtractionRunResult;
|
|
166
|
+
try {
|
|
167
|
+
result = await this.deps.runExtraction(chunk, chunkOptions);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
// The chunk-local abort leaves the outer signal untouched, so hand back the
|
|
170
|
+
// preemption reason the host looks for (as it does for the first chunk).
|
|
171
|
+
if (isAbortError(err)) throw preempt && preempt.signal.aborted && !runOptions.abortSignal?.aborted ? preempt.signal.reason : err;
|
|
172
|
+
failures += 1;
|
|
173
|
+
if (failures < dropAfter) {
|
|
174
|
+
this.setFailureCount(bufferKey, chunk, failures);
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
// A chunk that keeps throwing is dropped like one that keeps failing,
|
|
178
|
+
// so later chunks are not stuck behind it.
|
|
179
|
+
const thrown: ExtractionRunResult = {
|
|
180
|
+
status: "skipped",
|
|
181
|
+
reason: `thrown: ${describeError(err)}`,
|
|
182
|
+
persistedCount: 0,
|
|
183
|
+
durableOutputCount: 0,
|
|
184
|
+
};
|
|
185
|
+
if (!(await this.dropFailedChunk(bufferKey, chunk, dropAfter, thrown, byIdentity))) {
|
|
186
|
+
this.setFailureCount(bufferKey, chunk, failures);
|
|
187
|
+
throw err;
|
|
188
|
+
}
|
|
189
|
+
this.setFailureCount(bufferKey, chunk, 0);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const verdict = classifyDrainResult(result);
|
|
193
|
+
if (verdict === "drained") {
|
|
194
|
+
this.setFailureCount(bufferKey, chunk, 0);
|
|
195
|
+
aggregate = mergeDrainResults(aggregate, result);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
// Provider breaker/backoff retention: keep everything for retry.
|
|
199
|
+
if (verdict === "deferred") return mergeDrainResults(aggregate, result);
|
|
200
|
+
// An outage or a bad key is not the chunk's fault: keep the chunk and do not count toward dropping it.
|
|
201
|
+
if (result.failureClass === "provider_retryable" || result.failureClass === "auth_config") {
|
|
202
|
+
return mergeDrainResults(aggregate, result);
|
|
203
|
+
}
|
|
204
|
+
failures += 1;
|
|
205
|
+
if (failures >= dropAfter) {
|
|
206
|
+
if (!(await this.dropFailedChunk(bufferKey, chunk, dropAfter, result, byIdentity))) {
|
|
207
|
+
// No recovery copy: keep the only buffered copy and retry later.
|
|
208
|
+
this.setFailureCount(bufferKey, chunk, failures);
|
|
209
|
+
return mergeDrainResults(aggregate, result);
|
|
210
|
+
}
|
|
211
|
+
this.setFailureCount(bufferKey, chunk, 0);
|
|
212
|
+
aggregate = mergeDrainResults(aggregate, result);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
this.setFailureCount(bufferKey, chunk, failures);
|
|
216
|
+
// Retain the remaining window for the next trigger (existing behavior).
|
|
217
|
+
return mergeDrainResults(aggregate, result);
|
|
218
|
+
}
|
|
219
|
+
this.qualified.delete(bufferKey);
|
|
220
|
+
return aggregate ?? { status: "skipped", persistedCount: 0, durableOutputCount: 0 };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Side-file copy first (content survives a failed drop), then remove the failing chunk from the buffer. Never throws:
|
|
224
|
+
* the drop must not fail the extraction task that already settled its result. */
|
|
225
|
+
private async dropFailedChunk(
|
|
226
|
+
bufferKey: string,
|
|
227
|
+
chunk: readonly BufferTurn[],
|
|
228
|
+
dropAfter: number,
|
|
229
|
+
result: ExtractionRunResult,
|
|
230
|
+
byIdentity: boolean,
|
|
231
|
+
): Promise<boolean> {
|
|
232
|
+
const reason =
|
|
233
|
+
`dropped after ${dropAfter} repeated extraction failures ` +
|
|
234
|
+
`(last result: status=${result.status} reason=${result.reason ?? "n/a"})`;
|
|
235
|
+
try {
|
|
236
|
+
const storage = await this.deps.getStorage();
|
|
237
|
+
const path = await storage.saveDroppedExtractionChunk(bufferKey, reason, chunk);
|
|
238
|
+
log.warn(`buffer[${bufferKey}]: dropping ${chunk.length} turns — ${reason}; copy saved to ${path}`);
|
|
239
|
+
} catch (err) {
|
|
240
|
+
// Never destroy the only buffered copy without a recovery file.
|
|
241
|
+
log.warn(
|
|
242
|
+
`buffer[${bufferKey}]: could not save the dropped-chunk recovery copy; keeping the chunk in the buffer: ${describeError(err)}`,
|
|
243
|
+
);
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const dropped = await this.deps.buffer.dropExtractionChunk(bufferKey, chunk, { allowNonPrefix: byIdentity });
|
|
248
|
+
if (dropped > 0) {
|
|
249
|
+
log.warn(`buffer[${bufferKey}]: removed ${dropped} buffered turns — ${reason}`);
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
// Nothing removed (the live turns changed): the chunk is still buffered, so this is not a drop.
|
|
253
|
+
log.warn(`buffer[${bufferKey}]: dropped chunk no longer matches the buffer; keeping it and its failure count`);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
log.warn(
|
|
256
|
+
`buffer[${bufferKey}]: failed to remove dropped chunk from buffer (will retry next trigger): ${describeError(err)}`,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BufferTurn, ExtractionFailureClass, ExtractionResult, MetaState } from "../types.js";
|
|
2
2
|
import type { StorageManager } from "../index.js";
|
|
3
3
|
import { log } from "../logger.js";
|
|
4
|
+
import type { ExtractionRunResult } from "./extraction-run.js";
|
|
4
5
|
|
|
5
6
|
export function combineExtractionAbortSignals(
|
|
6
7
|
primary: AbortSignal | undefined,
|
|
@@ -11,6 +12,13 @@ export function combineExtractionAbortSignals(
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
/** Result of a run whose extraction found nothing durable; `retained` marks a recorded failure that left the window in the buffer. */
|
|
16
|
+
export function emptyExtractionResult(retained: boolean, failureClass?: ExtractionFailureClass): ExtractionRunResult {
|
|
17
|
+
const result: ExtractionRunResult = { status: "skipped", reason: "empty_extraction_result", persistedCount: 0, durableOutputCount: 0 };
|
|
18
|
+
if (retained) Object.assign(result, { retainedBuffer: true, failureClass: failureClass ?? "provider_retryable" }); // same default as the retry bookkeeping
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
15
23
|
return typeof value === "object" && value !== null;
|
|
16
24
|
}
|