@remnic/core 10.0.73 → 10.0.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-admin-ops-surface.js +49 -48
- package/dist/access-authorization-probe.js +50 -49
- package/dist/access-boundary.js +49 -48
- package/dist/access-cli.js +105 -104
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.js +49 -48
- package/dist/access-http-briefing.js +50 -49
- package/dist/access-http-query.js +51 -50
- package/dist/access-http.js +68 -67
- package/dist/access-identity-continuity-surface.js +49 -48
- package/dist/access-lcm-surface.js +49 -48
- package/dist/access-mcp.js +58 -57
- package/dist/access-namespace-preflight.js +49 -48
- package/dist/access-observe-helpers.d.ts.map +1 -1
- package/dist/access-observe-helpers.js +1 -1
- package/dist/access-observe-write-category.js +6 -6
- package/dist/access-observe-write-surface.js +49 -48
- package/dist/access-offline-manifest.js +5 -5
- package/dist/access-operations-batch.js +50 -49
- package/dist/access-operations.js +57 -56
- package/dist/access-recall-concurrency.js +49 -48
- package/dist/access-recall-response.js +49 -48
- package/dist/access-recall-surface.js +49 -48
- package/dist/access-schema.js +5 -5
- package/dist/access-service.js +49 -48
- package/dist/access-surface-catalog.js +51 -50
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/active-recall.js +18 -18
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/{auto-sync-ANOJDQD4.js → auto-sync-XHSS3XPL.js} +5 -5
- package/dist/briefing.js +15 -15
- package/dist/buffer-turn-helpers.d.ts +18 -0
- package/dist/buffer-turn-helpers.d.ts.map +1 -1
- package/dist/buffer-turn-helpers.js +5 -1
- package/dist/buffer.js +4 -4
- package/dist/calibration.js +5 -4
- package/dist/calibration.js.map +1 -1
- package/dist/causal-consolidation.js +16 -16
- package/dist/{chunk-7WFY2L5J.js → chunk-233S3MXC.js} +3 -3
- package/dist/{chunk-N7NAGECH.js → chunk-36B5VPRU.js} +2 -2
- package/dist/{chunk-5ORJHH43.js → chunk-3GGCMRO7.js} +2 -2
- package/dist/{chunk-RZED7FAU.js → chunk-3M7YP42P.js} +2 -2
- package/dist/{chunk-ICBUTKLL.js → chunk-3MBSD53O.js} +2 -2
- package/dist/{chunk-J72ZY7NU.js → chunk-4DLZ2ITE.js} +2 -2
- package/dist/{chunk-OUBEJWVI.js → chunk-4KLQI3L2.js} +42 -1
- package/dist/chunk-4KLQI3L2.js.map +1 -0
- package/dist/{chunk-WN2DASTS.js → chunk-5BE3FLKL.js} +4 -4
- package/dist/{chunk-S73ZVU37.js → chunk-5IQL4N37.js} +10 -10
- package/dist/{chunk-CIL4GHMD.js → chunk-6Q6AQ36B.js} +2 -2
- package/dist/{chunk-W4V7P5HP.js → chunk-6RPPZL7W.js} +2 -2
- package/dist/{chunk-EQ6EMJRG.js → chunk-6Y5ANLDH.js} +2 -2
- package/dist/{chunk-YPYSC3TX.js → chunk-7FFGWIBN.js} +2 -2
- package/dist/{chunk-VNTQU6KB.js → chunk-7PMOCQOL.js} +8 -13
- package/dist/chunk-7PMOCQOL.js.map +1 -0
- package/dist/{chunk-5RW247F5.js → chunk-AY2UK5XK.js} +14 -9
- package/dist/chunk-AY2UK5XK.js.map +1 -0
- package/dist/{chunk-5QV6CE6K.js → chunk-B36ACJG7.js} +2 -2
- package/dist/{chunk-ZBET3W26.js → chunk-BZTA7DR4.js} +3 -3
- package/dist/{chunk-JY4LXMZR.js → chunk-C44DRDZM.js} +2 -2
- package/dist/{chunk-UIFWZMO7.js → chunk-CLVILSBR.js} +7 -7
- package/dist/{chunk-OGQGDOYK.js → chunk-COIQ5N4J.js} +2 -2
- package/dist/{chunk-UAOJVQXC.js → chunk-DANFOVQG.js} +2 -2
- package/dist/{chunk-2R44BBPK.js → chunk-DEGE32C3.js} +2 -2
- package/dist/{chunk-YIJECSIX.js → chunk-DJDJBMFX.js} +9 -2
- package/dist/chunk-DJDJBMFX.js.map +1 -0
- package/dist/{chunk-LHMCZ6VW.js → chunk-E22CQZ3H.js} +14 -14
- package/dist/{chunk-JGLZ46JS.js → chunk-ELUG6NKQ.js} +2 -2
- package/dist/{chunk-X4DHAXFO.js → chunk-F3FNXE7C.js} +3 -3
- package/dist/{chunk-YIXWY4OZ.js → chunk-FETPTCQZ.js} +2 -2
- package/dist/{chunk-N2Y6COEM.js → chunk-FGUWCE5I.js} +2 -2
- package/dist/{chunk-FDUCIUQ5.js → chunk-FJ2VJTGA.js} +2 -2
- package/dist/{chunk-ENPJX3BB.js → chunk-FMFCQRDM.js} +3 -3
- package/dist/{chunk-BNU5TQLR.js → chunk-FOQ7TW2T.js} +2 -2
- package/dist/{chunk-4PMFSYH7.js → chunk-FP2AT4WL.js} +2 -2
- package/dist/{chunk-DLZGSZVC.js → chunk-FYQUUIPI.js} +4 -4
- package/dist/{chunk-XTN54NML.js → chunk-G3T6YMBO.js} +4 -4
- package/dist/{chunk-MUH7RR6T.js → chunk-G5NH3SSX.js} +5 -5
- package/dist/{chunk-DC3DWF2M.js → chunk-GCJA5KKT.js} +2 -2
- package/dist/{chunk-U6IL4724.js → chunk-GWOFCHPN.js} +2 -2
- package/dist/{chunk-TJ5YGCPV.js → chunk-GXVK754R.js} +2 -2
- package/dist/{chunk-FGGLVCHI.js → chunk-HB5TNQMM.js} +2 -2
- package/dist/{chunk-GXAZ4Q2S.js → chunk-HCFNZWFB.js} +2 -2
- package/dist/{chunk-F4ZHKDBW.js → chunk-HLWXCDUQ.js} +2 -2
- package/dist/{chunk-IQXQ3KWR.js → chunk-HTDXNIO6.js} +2 -2
- package/dist/{chunk-4RASUAN3.js → chunk-HXFIILGF.js} +4 -4
- package/dist/{chunk-S37O4S6E.js → chunk-I4LJJ44E.js} +2 -2
- package/dist/{chunk-USX5YIKB.js → chunk-IBQ7LGUJ.js} +6 -6
- package/dist/{chunk-UKQFCOGT.js → chunk-IPHIIRDN.js} +3 -3
- package/dist/chunk-IPHIIRDN.js.map +1 -0
- package/dist/{chunk-VSJ3ARFS.js → chunk-IRFQKR4Y.js} +2 -2
- package/dist/{chunk-BCBY5GYZ.js → chunk-IVXOYE43.js} +2 -2
- package/dist/{chunk-WXPBDEMW.js → chunk-JDQEQE2R.js} +2 -2
- package/dist/{chunk-2WPWEVAH.js → chunk-KAED7IBB.js} +2 -2
- package/dist/{chunk-OWZXDBV4.js → chunk-KNZDGZSY.js} +2 -2
- package/dist/{chunk-6KBCUIZP.js → chunk-L4QYW2HO.js} +2 -2
- package/dist/{chunk-LYOCAUI6.js → chunk-LJUQPT5G.js} +3 -3
- package/dist/{chunk-XPZM7GP6.js → chunk-MBENRPMY.js} +10 -10
- package/dist/chunk-MJFL75J6.js +42 -0
- package/dist/chunk-MJFL75J6.js.map +1 -0
- package/dist/{chunk-VK5HKVQ7.js → chunk-N6GEGRCT.js} +2 -2
- package/dist/{chunk-U2A56LLF.js → chunk-OBQ2NZAR.js} +10 -10
- package/dist/{chunk-WGRAB3KB.js → chunk-ORVPS4ZW.js} +2 -2
- package/dist/{chunk-PFUXPTBT.js → chunk-P3AQ2YSX.js} +4 -4
- package/dist/{chunk-DE4U5PTV.js → chunk-P4TEQSIZ.js} +2 -2
- package/dist/{chunk-UE5JYPU6.js → chunk-P7HNMNN2.js} +7 -7
- package/dist/{chunk-ZG4GNPRE.js → chunk-PBE7NFB5.js} +5 -5
- package/dist/{chunk-LN7CWLAU.js → chunk-PLGBPFEO.js} +2 -2
- package/dist/{chunk-5MA7ZLPD.js → chunk-QNUNAXBE.js} +35 -1
- package/dist/chunk-QNUNAXBE.js.map +1 -0
- package/dist/{chunk-23BA3USG.js → chunk-R24RYSQX.js} +43 -43
- package/dist/{chunk-WYOHQJKC.js → chunk-RXGFITWS.js} +4 -4
- package/dist/{chunk-JEX2JL3U.js → chunk-SHVLNI4L.js} +2 -2
- package/dist/{chunk-QUPTDKSX.js → chunk-SY4AFWUZ.js} +31 -31
- package/dist/{chunk-H2X32F2K.js → chunk-SYDPJUFR.js} +2 -2
- package/dist/{chunk-QIOQI7QZ.js → chunk-SZBZOUHR.js} +2 -2
- package/dist/{chunk-EGKDONTC.js → chunk-T6C3G67T.js} +10 -10
- package/dist/chunk-T6C3G67T.js.map +1 -0
- package/dist/{chunk-E3NBMTTX.js → chunk-TLTBACYX.js} +2 -2
- package/dist/{chunk-AVVCPA7U.js → chunk-UCPH56YX.js} +2 -2
- package/dist/{chunk-ZODKV27J.js → chunk-URS56HGW.js} +6 -6
- package/dist/{chunk-PJAMFIDE.js → chunk-VEQVXNWR.js} +4 -4
- package/dist/{chunk-AGRMXXVA.js → chunk-VEWTHVX2.js} +2 -2
- package/dist/{chunk-WNQ3B3Q5.js → chunk-VPEMGBTI.js} +2 -2
- package/dist/{chunk-DSSTOV5M.js → chunk-VYOAYFDV.js} +66 -49
- package/dist/chunk-VYOAYFDV.js.map +1 -0
- package/dist/{chunk-5JBVPJCT.js → chunk-WIAXVBQH.js} +2 -2
- package/dist/{chunk-BTU77RB5.js → chunk-X53HJENJ.js} +2 -2
- package/dist/{chunk-KY3KJUNN.js → chunk-XC7OYOQC.js} +6 -6
- package/dist/{chunk-43FLJLT2.js → chunk-Y5HZFIQO.js} +1 -1
- package/dist/{chunk-DOJBV6WR.js → chunk-YK6SFDP2.js} +4 -4
- package/dist/{chunk-QVZZE6VH.js → chunk-Z3CKK5RR.js} +3 -3
- package/dist/{chunk-AZNE2H6D.js → chunk-ZG57BEUS.js} +2 -2
- package/dist/{chunk-7P5VBRBS.js → chunk-ZZSFRWKD.js} +2 -2
- package/dist/cli.js +81 -80
- package/dist/coding/export-okf-codegraph.js +14 -14
- package/dist/compounding/engine.js +16 -16
- package/dist/config.js +18 -18
- package/dist/connectors/codex-materialize-runner.js +15 -15
- package/dist/connectors/index.js +15 -15
- package/dist/convergence-refresh.js +17 -17
- package/dist/corpus-watermark.js +17 -17
- package/dist/deep-recall-http-glue.js +6 -6
- package/dist/deep-recall-policy-llm.js +3 -3
- package/dist/deep-recall.js +2 -2
- package/dist/entity-retrieval.js +15 -15
- package/dist/external-wiki-access.js +50 -49
- package/dist/extraction-faithfulness.js +3 -3
- package/dist/extraction-judge.js +3 -3
- package/dist/extraction.js +23 -22
- package/dist/fallback-llm-codex-subscription.d.ts.map +1 -1
- package/dist/fallback-llm-codex-subscription.js +1 -1
- package/dist/fallback-llm.js +2 -2
- package/dist/{first-start-migration-A6PHIBUQ.js → first-start-migration-QVD22WWS.js} +4 -4
- package/dist/harmonic-retrieval.js +15 -15
- package/dist/index.js +130 -129
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +5 -5
- package/dist/local-llm-native-schema.d.ts +50 -0
- package/dist/local-llm-native-schema.d.ts.map +1 -0
- package/dist/local-llm-native-schema.js +8 -0
- package/dist/local-llm-native-schema.js.map +1 -0
- package/dist/local-llm.d.ts +1 -0
- package/dist/local-llm.d.ts.map +1 -1
- package/dist/local-llm.js +4 -3
- package/dist/location/index.js +17 -17
- package/dist/maintenance/memory-governance.js +15 -15
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +15 -15
- package/dist/maintenance/rebuild-memory-projection-runner.js +17 -17
- package/dist/maintenance/rebuild-memory-projection.js +16 -16
- package/dist/memory-browse-operations.js +50 -49
- package/dist/memory-browse.js +3 -3
- package/dist/model-registry.d.ts +11 -0
- package/dist/model-registry.d.ts.map +1 -1
- package/dist/model-registry.js +1 -1
- package/dist/namespaces/migrate.js +16 -16
- package/dist/namespaces/search.js +7 -7
- package/dist/namespaces/storage.js +15 -15
- package/dist/offline-sync-changeset-generations.js +3 -3
- package/dist/offline-sync-embedding-generation.js +2 -2
- package/dist/offline-sync-snapshot-stream.js +5 -5
- package/dist/offline-sync.js +7 -7
- package/dist/operator-doctor-corpus.js +18 -18
- package/dist/operator-toolkit.js +27 -27
- package/dist/orchestration/backlog-scheduler.d.ts +8 -0
- package/dist/orchestration/backlog-scheduler.d.ts.map +1 -1
- package/dist/orchestration/backlog-scheduler.js +118 -106
- package/dist/orchestration/backlog-scheduler.js.map +1 -1
- package/dist/orchestration/buffer-drain-runner.js +106 -105
- package/dist/orchestration/extraction-queue-coordinator.d.ts +6 -0
- package/dist/orchestration/extraction-queue-coordinator.d.ts.map +1 -1
- package/dist/orchestration/maintenance.js +28 -27
- package/dist/orchestrator.js +105 -104
- package/dist/recall-navigation-operations.js +50 -49
- package/dist/recall-navigation.js +49 -48
- package/dist/recall-planner-llm.js +2 -2
- package/dist/recall-state-view-admission.js +3 -3
- package/dist/recall-why-service.js +3 -3
- package/dist/resume-bundles.js +18 -18
- package/dist/search/factory.js +6 -6
- package/dist/search/index.js +11 -11
- package/dist/search/lancedb-backend.js +2 -2
- package/dist/search/meilisearch-backend.js +2 -2
- package/dist/search/orama-backend.js +2 -2
- package/dist/semantic-consolidation.js +16 -16
- package/dist/semantic-rule-promotion.js +15 -15
- package/dist/semantic-rule-verifier.js +15 -15
- package/dist/source-agent-qualifier.js +18 -18
- package/dist/standup/build.js +15 -15
- package/dist/storage/buffer-drain-claims.d.ts.map +1 -1
- package/dist/storage/buffer-drain-claims.js +2 -2
- package/dist/storage/buffer-session-files.js +2 -2
- package/dist/storage.js +14 -14
- package/dist/summarizer.js +7 -6
- package/dist/support-passport/index.js +52 -51
- package/dist/transfer/export-okf.js +15 -15
- package/dist/verified-recall.js +15 -15
- package/dist/who-knows-cli.js +16 -16
- package/dist/who-knows.js +15 -15
- package/package.json +2 -2
- package/src/access-observe-acceptance-boundary.test.ts +24 -18
- package/src/access-observe-helpers.test.ts +72 -0
- package/src/access-observe-helpers.ts +5 -2
- package/src/access-service-observe-lcm-parity.test.ts +9 -2
- package/src/access-service-observe-scope.test.ts +8 -1
- package/src/backlog-scheduler.test.ts +104 -0
- package/src/buffer-record-snapshots.test.ts +195 -0
- package/src/buffer-turn-helpers.test.ts +205 -0
- package/src/buffer-turn-helpers.ts +62 -0
- package/src/buffer.ts +8 -8
- package/src/fallback-llm-codex-subscription.ts +10 -1
- package/src/local-llm-json-schema-fallback.test.ts +315 -0
- package/src/local-llm-native-schema.ts +78 -0
- package/src/local-llm.ts +8 -9
- package/src/model-registry.ts +40 -0
- package/src/orchestration/backlog-scheduler.ts +32 -0
- package/src/orchestration/extraction-queue-coordinator.ts +18 -1
- package/src/orchestrator-extraction-queue.test.ts +142 -0
- package/src/storage/buffer-drain-claims.ts +5 -11
- package/dist/chunk-5MA7ZLPD.js.map +0 -1
- package/dist/chunk-5RW247F5.js.map +0 -1
- package/dist/chunk-DSSTOV5M.js.map +0 -1
- package/dist/chunk-EGKDONTC.js.map +0 -1
- package/dist/chunk-OUBEJWVI.js.map +0 -1
- package/dist/chunk-UKQFCOGT.js.map +0 -1
- package/dist/chunk-VNTQU6KB.js.map +0 -1
- package/dist/chunk-YIJECSIX.js.map +0 -1
- /package/dist/{auto-sync-ANOJDQD4.js.map → auto-sync-XHSS3XPL.js.map} +0 -0
- /package/dist/{chunk-7WFY2L5J.js.map → chunk-233S3MXC.js.map} +0 -0
- /package/dist/{chunk-N7NAGECH.js.map → chunk-36B5VPRU.js.map} +0 -0
- /package/dist/{chunk-5ORJHH43.js.map → chunk-3GGCMRO7.js.map} +0 -0
- /package/dist/{chunk-RZED7FAU.js.map → chunk-3M7YP42P.js.map} +0 -0
- /package/dist/{chunk-ICBUTKLL.js.map → chunk-3MBSD53O.js.map} +0 -0
- /package/dist/{chunk-J72ZY7NU.js.map → chunk-4DLZ2ITE.js.map} +0 -0
- /package/dist/{chunk-WN2DASTS.js.map → chunk-5BE3FLKL.js.map} +0 -0
- /package/dist/{chunk-S73ZVU37.js.map → chunk-5IQL4N37.js.map} +0 -0
- /package/dist/{chunk-CIL4GHMD.js.map → chunk-6Q6AQ36B.js.map} +0 -0
- /package/dist/{chunk-W4V7P5HP.js.map → chunk-6RPPZL7W.js.map} +0 -0
- /package/dist/{chunk-EQ6EMJRG.js.map → chunk-6Y5ANLDH.js.map} +0 -0
- /package/dist/{chunk-YPYSC3TX.js.map → chunk-7FFGWIBN.js.map} +0 -0
- /package/dist/{chunk-5QV6CE6K.js.map → chunk-B36ACJG7.js.map} +0 -0
- /package/dist/{chunk-ZBET3W26.js.map → chunk-BZTA7DR4.js.map} +0 -0
- /package/dist/{chunk-JY4LXMZR.js.map → chunk-C44DRDZM.js.map} +0 -0
- /package/dist/{chunk-UIFWZMO7.js.map → chunk-CLVILSBR.js.map} +0 -0
- /package/dist/{chunk-OGQGDOYK.js.map → chunk-COIQ5N4J.js.map} +0 -0
- /package/dist/{chunk-UAOJVQXC.js.map → chunk-DANFOVQG.js.map} +0 -0
- /package/dist/{chunk-2R44BBPK.js.map → chunk-DEGE32C3.js.map} +0 -0
- /package/dist/{chunk-LHMCZ6VW.js.map → chunk-E22CQZ3H.js.map} +0 -0
- /package/dist/{chunk-JGLZ46JS.js.map → chunk-ELUG6NKQ.js.map} +0 -0
- /package/dist/{chunk-X4DHAXFO.js.map → chunk-F3FNXE7C.js.map} +0 -0
- /package/dist/{chunk-YIXWY4OZ.js.map → chunk-FETPTCQZ.js.map} +0 -0
- /package/dist/{chunk-N2Y6COEM.js.map → chunk-FGUWCE5I.js.map} +0 -0
- /package/dist/{chunk-FDUCIUQ5.js.map → chunk-FJ2VJTGA.js.map} +0 -0
- /package/dist/{chunk-ENPJX3BB.js.map → chunk-FMFCQRDM.js.map} +0 -0
- /package/dist/{chunk-BNU5TQLR.js.map → chunk-FOQ7TW2T.js.map} +0 -0
- /package/dist/{chunk-4PMFSYH7.js.map → chunk-FP2AT4WL.js.map} +0 -0
- /package/dist/{chunk-DLZGSZVC.js.map → chunk-FYQUUIPI.js.map} +0 -0
- /package/dist/{chunk-XTN54NML.js.map → chunk-G3T6YMBO.js.map} +0 -0
- /package/dist/{chunk-MUH7RR6T.js.map → chunk-G5NH3SSX.js.map} +0 -0
- /package/dist/{chunk-DC3DWF2M.js.map → chunk-GCJA5KKT.js.map} +0 -0
- /package/dist/{chunk-U6IL4724.js.map → chunk-GWOFCHPN.js.map} +0 -0
- /package/dist/{chunk-TJ5YGCPV.js.map → chunk-GXVK754R.js.map} +0 -0
- /package/dist/{chunk-FGGLVCHI.js.map → chunk-HB5TNQMM.js.map} +0 -0
- /package/dist/{chunk-GXAZ4Q2S.js.map → chunk-HCFNZWFB.js.map} +0 -0
- /package/dist/{chunk-F4ZHKDBW.js.map → chunk-HLWXCDUQ.js.map} +0 -0
- /package/dist/{chunk-IQXQ3KWR.js.map → chunk-HTDXNIO6.js.map} +0 -0
- /package/dist/{chunk-4RASUAN3.js.map → chunk-HXFIILGF.js.map} +0 -0
- /package/dist/{chunk-S37O4S6E.js.map → chunk-I4LJJ44E.js.map} +0 -0
- /package/dist/{chunk-USX5YIKB.js.map → chunk-IBQ7LGUJ.js.map} +0 -0
- /package/dist/{chunk-VSJ3ARFS.js.map → chunk-IRFQKR4Y.js.map} +0 -0
- /package/dist/{chunk-BCBY5GYZ.js.map → chunk-IVXOYE43.js.map} +0 -0
- /package/dist/{chunk-WXPBDEMW.js.map → chunk-JDQEQE2R.js.map} +0 -0
- /package/dist/{chunk-2WPWEVAH.js.map → chunk-KAED7IBB.js.map} +0 -0
- /package/dist/{chunk-OWZXDBV4.js.map → chunk-KNZDGZSY.js.map} +0 -0
- /package/dist/{chunk-6KBCUIZP.js.map → chunk-L4QYW2HO.js.map} +0 -0
- /package/dist/{chunk-LYOCAUI6.js.map → chunk-LJUQPT5G.js.map} +0 -0
- /package/dist/{chunk-XPZM7GP6.js.map → chunk-MBENRPMY.js.map} +0 -0
- /package/dist/{chunk-VK5HKVQ7.js.map → chunk-N6GEGRCT.js.map} +0 -0
- /package/dist/{chunk-U2A56LLF.js.map → chunk-OBQ2NZAR.js.map} +0 -0
- /package/dist/{chunk-WGRAB3KB.js.map → chunk-ORVPS4ZW.js.map} +0 -0
- /package/dist/{chunk-PFUXPTBT.js.map → chunk-P3AQ2YSX.js.map} +0 -0
- /package/dist/{chunk-DE4U5PTV.js.map → chunk-P4TEQSIZ.js.map} +0 -0
- /package/dist/{chunk-UE5JYPU6.js.map → chunk-P7HNMNN2.js.map} +0 -0
- /package/dist/{chunk-ZG4GNPRE.js.map → chunk-PBE7NFB5.js.map} +0 -0
- /package/dist/{chunk-LN7CWLAU.js.map → chunk-PLGBPFEO.js.map} +0 -0
- /package/dist/{chunk-23BA3USG.js.map → chunk-R24RYSQX.js.map} +0 -0
- /package/dist/{chunk-WYOHQJKC.js.map → chunk-RXGFITWS.js.map} +0 -0
- /package/dist/{chunk-JEX2JL3U.js.map → chunk-SHVLNI4L.js.map} +0 -0
- /package/dist/{chunk-QUPTDKSX.js.map → chunk-SY4AFWUZ.js.map} +0 -0
- /package/dist/{chunk-H2X32F2K.js.map → chunk-SYDPJUFR.js.map} +0 -0
- /package/dist/{chunk-QIOQI7QZ.js.map → chunk-SZBZOUHR.js.map} +0 -0
- /package/dist/{chunk-E3NBMTTX.js.map → chunk-TLTBACYX.js.map} +0 -0
- /package/dist/{chunk-AVVCPA7U.js.map → chunk-UCPH56YX.js.map} +0 -0
- /package/dist/{chunk-ZODKV27J.js.map → chunk-URS56HGW.js.map} +0 -0
- /package/dist/{chunk-PJAMFIDE.js.map → chunk-VEQVXNWR.js.map} +0 -0
- /package/dist/{chunk-AGRMXXVA.js.map → chunk-VEWTHVX2.js.map} +0 -0
- /package/dist/{chunk-WNQ3B3Q5.js.map → chunk-VPEMGBTI.js.map} +0 -0
- /package/dist/{chunk-5JBVPJCT.js.map → chunk-WIAXVBQH.js.map} +0 -0
- /package/dist/{chunk-BTU77RB5.js.map → chunk-X53HJENJ.js.map} +0 -0
- /package/dist/{chunk-KY3KJUNN.js.map → chunk-XC7OYOQC.js.map} +0 -0
- /package/dist/{chunk-43FLJLT2.js.map → chunk-Y5HZFIQO.js.map} +0 -0
- /package/dist/{chunk-DOJBV6WR.js.map → chunk-YK6SFDP2.js.map} +0 -0
- /package/dist/{chunk-QVZZE6VH.js.map → chunk-Z3CKK5RR.js.map} +0 -0
- /package/dist/{chunk-AZNE2H6D.js.map → chunk-ZG57BEUS.js.map} +0 -0
- /package/dist/{chunk-7P5VBRBS.js.map → chunk-ZZSFRWKD.js.map} +0 -0
- /package/dist/{first-start-migration-A6PHIBUQ.js.map → first-start-migration-QVD22WWS.js.map} +0 -0
|
@@ -148,12 +148,15 @@ export class PendingObserveExtractionTracker {
|
|
|
148
148
|
controllers: new Set(previous ? [...previous.controllers, controller] : [controller]),
|
|
149
149
|
};
|
|
150
150
|
this.entries.set(key, entry);
|
|
151
|
+
// Any settle (success OR rejection — preemption is just one rejection
|
|
152
|
+
// kind) ends the pending entry; leaving a settled failure tracked wedged
|
|
153
|
+
// the key and kept every controller ever registered for it alive.
|
|
151
154
|
void promise
|
|
152
155
|
.then(() => {
|
|
153
156
|
if (this.entries.get(key) === entry) this.entries.delete(key);
|
|
154
157
|
})
|
|
155
|
-
.catch((
|
|
156
|
-
if (
|
|
158
|
+
.catch(() => {
|
|
159
|
+
if (this.entries.get(key) === entry) this.entries.delete(key);
|
|
157
160
|
});
|
|
158
161
|
}
|
|
159
162
|
|
|
@@ -639,11 +639,18 @@ test("#2128: a failed observe extraction is consumed after force-flush reports i
|
|
|
639
639
|
const failure = new Error("provider failed");
|
|
640
640
|
|
|
641
641
|
await service.observe(observeRequest({ sessionKey, skipExtraction: false }));
|
|
642
|
-
|
|
643
|
-
|
|
642
|
+
// The flush parks on the pending extraction and reports its failure
|
|
643
|
+
// (identity-preserved) when it rejects; with the last-write-wins bug the
|
|
644
|
+
// failure was never tracked at all.
|
|
645
|
+
const reported = assert.rejects(
|
|
644
646
|
service.extractionForceFlush({ sessionKey }),
|
|
645
647
|
(error: unknown) => error === failure,
|
|
646
648
|
);
|
|
649
|
+
const parked = Promise.withResolvers<void>();
|
|
650
|
+
setImmediate(parked.resolve);
|
|
651
|
+
await parked.promise;
|
|
652
|
+
rejectExtraction(failure);
|
|
653
|
+
await reported;
|
|
647
654
|
|
|
648
655
|
await service.observe(observeRequest({ sessionKey, skipExtraction: false }));
|
|
649
656
|
await assert.doesNotReject(() => service.extractionForceFlush({ sessionKey }));
|
|
@@ -256,8 +256,15 @@ test("a preempted observe extraction does not block the next force-flush", async
|
|
|
256
256
|
const key = tracker.key("session-a", "alice", "default");
|
|
257
257
|
tracker.track(key, Promise.reject(preemptedExtractionError()), new AbortController());
|
|
258
258
|
await tracker.wait("session-a", "alice", "default");
|
|
259
|
+
// A settled failure is no longer pending: the entry is removed on ANY
|
|
260
|
+
// settle, so the next flush is neither blocked nor failed by work that
|
|
261
|
+
// already finished. (A flush overlapping the failing extraction still
|
|
262
|
+
// surfaces its error — covered in access-observe-helpers.test.ts.)
|
|
259
263
|
tracker.track(key, Promise.reject(new Error("extraction failed")), new AbortController());
|
|
260
|
-
|
|
264
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
265
|
+
setImmediate(resolve);
|
|
266
|
+
await promise;
|
|
267
|
+
await tracker.wait("session-a", "alice", "default");
|
|
261
268
|
});
|
|
262
269
|
|
|
263
270
|
test("cancelling a tracked extraction with a preemption reason does not block the next flush", async () => {
|
|
@@ -804,3 +804,107 @@ test("claims past the concurrency bound are re-listed on the next tick, in page
|
|
|
804
804
|
await scheduler.stop();
|
|
805
805
|
}
|
|
806
806
|
});
|
|
807
|
+
|
|
808
|
+
// ── Provider-breaker churn guard ────────────────────────────────────────────
|
|
809
|
+
//
|
|
810
|
+
// While the shared provider breaker is OPEN inside its cooldown, a full
|
|
811
|
+
// submission reads the claim, rebuilds the buffer/coordinator and re-renders
|
|
812
|
+
// chunks before the breaker fails the attempt fast — pure heap pressure per
|
|
813
|
+
// poll. The scheduler must skip submission entirely; when the window lapses,
|
|
814
|
+
// submissions resume and the breaker's own half-open probe keeps recovery
|
|
815
|
+
// automatic.
|
|
816
|
+
|
|
817
|
+
function breakerInstrumentedDeps(
|
|
818
|
+
state: StubState,
|
|
819
|
+
queue: ExtractionQueueCoordinator,
|
|
820
|
+
): BacklogSchedulerDeps & { claimReads(): number } {
|
|
821
|
+
const deps = makeDeps(state, queue);
|
|
822
|
+
const originalClaim = deps.claims.claimFromSessionFile;
|
|
823
|
+
let claimReads = 0;
|
|
824
|
+
deps.claims.claimFromSessionFile = async (key, lease) => {
|
|
825
|
+
claimReads += 1;
|
|
826
|
+
return originalClaim(key, lease);
|
|
827
|
+
};
|
|
828
|
+
return { ...deps, claimReads: () => claimReads };
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
test("an open provider breaker suppresses submission: no drain, no claim read", async () => {
|
|
832
|
+
const queue = new ExtractionQueueCoordinator(2);
|
|
833
|
+
const state: StubState = {
|
|
834
|
+
counts: new Map([["live-open", { turns: 4, retainedTurns: 0 }]]),
|
|
835
|
+
claimed: new Set(["pending-claim"]),
|
|
836
|
+
claimTurns: new Map([["pending-claim", 7]]),
|
|
837
|
+
claimedThisRun: new Set(),
|
|
838
|
+
outcomes: [],
|
|
839
|
+
drained: [],
|
|
840
|
+
};
|
|
841
|
+
const deps = breakerInstrumentedDeps(state, queue);
|
|
842
|
+
const originalListPage = deps.claims.listPage;
|
|
843
|
+
let listPageCalls = 0;
|
|
844
|
+
deps.claims.listPage = async (...args) => {
|
|
845
|
+
listPageCalls += 1;
|
|
846
|
+
return originalListPage(...args);
|
|
847
|
+
};
|
|
848
|
+
const scheduler = new BacklogScheduler({
|
|
849
|
+
...deps,
|
|
850
|
+
breakerSnapshot: () => ({
|
|
851
|
+
state: "open",
|
|
852
|
+
openUntilMs: Date.now() + 300_000,
|
|
853
|
+
consecutiveFailures: 5,
|
|
854
|
+
lastReason: "provider 400s",
|
|
855
|
+
}),
|
|
856
|
+
}, { pollMs: 3_600_000 });
|
|
857
|
+
scheduler.start();
|
|
858
|
+
try {
|
|
859
|
+
for (let tick = 1; tick <= 2; tick += 1) {
|
|
860
|
+
await scheduler.tickForTest();
|
|
861
|
+
await scheduler.settleForTest();
|
|
862
|
+
}
|
|
863
|
+
assert.equal(state.drained.length, 0, "no attempt is built while the breaker is open");
|
|
864
|
+
assert.equal(deps.claimReads(), 0, "no claim is read while the breaker is open");
|
|
865
|
+
assert.equal(listPageCalls, 0, "claim discovery I/O is skipped while the breaker is open");
|
|
866
|
+
assert.equal(scheduler.snapshot().counters.claimed, 0);
|
|
867
|
+
} finally {
|
|
868
|
+
await scheduler.stop();
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
test("a lapsed breaker window resumes submission and drains the backlog", async () => {
|
|
873
|
+
const queue = new ExtractionQueueCoordinator(2);
|
|
874
|
+
const state: StubState = {
|
|
875
|
+
counts: new Map([["live-open", { turns: 4, retainedTurns: 0 }]]),
|
|
876
|
+
claimed: new Set(["pending-claim"]),
|
|
877
|
+
claimTurns: new Map([["pending-claim", 7]]),
|
|
878
|
+
claimedThisRun: new Set(),
|
|
879
|
+
outcomes: [
|
|
880
|
+
emptyOutcome("pending-claim", { turnsAtStart: 7 }),
|
|
881
|
+
emptyOutcome("live-open", { turnsAtStart: 4 }),
|
|
882
|
+
],
|
|
883
|
+
drained: [],
|
|
884
|
+
};
|
|
885
|
+
const deps = breakerInstrumentedDeps(state, queue);
|
|
886
|
+
// Window already lapsed: the next submission IS the half-open probe path.
|
|
887
|
+
const scheduler = new BacklogScheduler({
|
|
888
|
+
...deps,
|
|
889
|
+
breakerSnapshot: () => ({
|
|
890
|
+
state: "open",
|
|
891
|
+
openUntilMs: Date.now() - 1,
|
|
892
|
+
consecutiveFailures: 5,
|
|
893
|
+
lastReason: "provider 400s",
|
|
894
|
+
}),
|
|
895
|
+
}, { pollMs: 3_600_000 });
|
|
896
|
+
scheduler.start();
|
|
897
|
+
try {
|
|
898
|
+
await scheduler.tickForTest();
|
|
899
|
+
await scheduler.settleForTest();
|
|
900
|
+
await scheduler.tickForTest();
|
|
901
|
+
await scheduler.settleForTest();
|
|
902
|
+
assert.deepEqual(
|
|
903
|
+
state.drained.map((entry) => entry.key).sort(),
|
|
904
|
+
["live-open", "pending-claim"],
|
|
905
|
+
"a lapsed window lets submissions (the probe path) through"
|
|
906
|
+
);
|
|
907
|
+
} finally {
|
|
908
|
+
await scheduler.stop();
|
|
909
|
+
}
|
|
910
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
/**
|
|
3
|
+
* recordTurnUnlocked snapshot retention — issue: the per-turn mutation built
|
|
4
|
+
* `priorTurns` + `activeTurnsSnapshot` + `retainedTurnsSnapshot` (two full
|
|
5
|
+
* `copyBufferTurn` maps) on EVERY admitted turn, even when the decision path
|
|
6
|
+
* consumes none of them: a plain `keep_buffering` turn with no surprise probe
|
|
7
|
+
* eligible branch discards all three. The mutation now builds the snapshots
|
|
8
|
+
* only when the decision path needs them (`keep_buffering` under an
|
|
9
|
+
* eligible surprise branch, or any extraction decision), so steady-state
|
|
10
|
+
* buffering performs no per-turn snapshot copies.
|
|
11
|
+
*
|
|
12
|
+
* Harness: an instance-level spy wraps the (private) recordTurnUnlocked and
|
|
13
|
+
* captures each AddTurnMutationResult, so tests assert on exactly what the
|
|
14
|
+
* mutation returned — no production instrumentation.
|
|
15
|
+
*/
|
|
16
|
+
import test from "node:test";
|
|
17
|
+
|
|
18
|
+
import { type BufferSurpriseProbe, SmartBuffer } from "./buffer.js";
|
|
19
|
+
import { parseConfig } from "./config.js";
|
|
20
|
+
import type { BufferState, BufferTurn } from "./types.js";
|
|
21
|
+
|
|
22
|
+
class FakeStorage {
|
|
23
|
+
public saved: BufferState | null = null;
|
|
24
|
+
public saveCount = 0;
|
|
25
|
+
|
|
26
|
+
constructor(private readonly initial: BufferState) {}
|
|
27
|
+
|
|
28
|
+
async loadBuffer(): Promise<BufferState> {
|
|
29
|
+
return structuredClone(this.initial);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async saveBuffer(state: BufferState): Promise<void> {
|
|
33
|
+
this.saveCount += 1;
|
|
34
|
+
this.saved = structuredClone(state);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeTurn(sessionKey: string, content: string): BufferTurn {
|
|
39
|
+
return {
|
|
40
|
+
role: "user",
|
|
41
|
+
content,
|
|
42
|
+
timestamp: "2026-04-20T12:00:00.000Z",
|
|
43
|
+
sessionKey,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function emptyBuffer(): BufferState {
|
|
48
|
+
return { turns: [], lastExtractionAt: null, extractionCount: 0 };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type MutationCapture = {
|
|
52
|
+
priorTurns: BufferTurn[];
|
|
53
|
+
activeTurnsSnapshot: BufferTurn[];
|
|
54
|
+
retainedTurnsSnapshot: BufferTurn[];
|
|
55
|
+
decision: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type RecordSpy = (bufferKey: string, turn: BufferTurn) => Promise<MutationCapture>;
|
|
59
|
+
|
|
60
|
+
function spyMutations(buffer: SmartBuffer): MutationCapture[] {
|
|
61
|
+
const captures: MutationCapture[] = [];
|
|
62
|
+
// SAFETY: recordTurnUnlocked is a TS-private prototype method with a stable
|
|
63
|
+
// runtime shape; the spy re-exposes the identical signature on the instance.
|
|
64
|
+
const original = Object.getPrototypeOf(buffer).recordTurnUnlocked as RecordSpy;
|
|
65
|
+
Object.defineProperty(buffer, "recordTurnUnlocked", {
|
|
66
|
+
value: async (bufferKey: string, turn: BufferTurn): Promise<MutationCapture> => {
|
|
67
|
+
const result = await original.call(buffer, bufferKey, turn);
|
|
68
|
+
captures.push(result);
|
|
69
|
+
return result;
|
|
70
|
+
},
|
|
71
|
+
configurable: true,
|
|
72
|
+
writable: true,
|
|
73
|
+
});
|
|
74
|
+
return captures;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function fixedScoreProbe(score: number | null): BufferSurpriseProbe & { calls: number } {
|
|
78
|
+
const probe = {
|
|
79
|
+
calls: 0,
|
|
80
|
+
async scoreTurn(_key: string, _turn: BufferTurn, _prior: readonly BufferTurn[]): Promise<number | null> {
|
|
81
|
+
probe.calls += 1;
|
|
82
|
+
return score;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
return probe;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
test("plain keep_buffering turn performs no snapshot copies", async () => {
|
|
89
|
+
const storage = new FakeStorage(emptyBuffer());
|
|
90
|
+
const config = parseConfig({
|
|
91
|
+
bufferMaxTurns: 5,
|
|
92
|
+
triggerMode: "smart",
|
|
93
|
+
bufferSaveDebounceMs: 0,
|
|
94
|
+
});
|
|
95
|
+
const buffer = new SmartBuffer(config, storage);
|
|
96
|
+
const captures = spyMutations(buffer);
|
|
97
|
+
|
|
98
|
+
const decision = await buffer.addTurn("sess-1", makeTurn("sess-1", "ordinary content"));
|
|
99
|
+
|
|
100
|
+
assert.equal(decision, "keep_buffering");
|
|
101
|
+
assert.equal(captures.length, 1);
|
|
102
|
+
assert.equal(captures[0].priorTurns.length, 0, "no priorTurns copy for a decision that never reads it");
|
|
103
|
+
assert.equal(captures[0].activeTurnsSnapshot.length, 0, "no active-turn snapshot copies while buffering");
|
|
104
|
+
assert.equal(captures[0].retainedTurnsSnapshot.length, 0, "no retained-turn snapshot copies while buffering");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("extraction decisions still carry full snapshots", async () => {
|
|
108
|
+
const storage = new FakeStorage(emptyBuffer());
|
|
109
|
+
const config = parseConfig({
|
|
110
|
+
bufferMaxTurns: 2,
|
|
111
|
+
triggerMode: "every_n",
|
|
112
|
+
bufferSaveDebounceMs: 0,
|
|
113
|
+
});
|
|
114
|
+
const buffer = new SmartBuffer(config, storage);
|
|
115
|
+
const captures = spyMutations(buffer);
|
|
116
|
+
|
|
117
|
+
await buffer.addTurn("sess-1", makeTurn("sess-1", "one"));
|
|
118
|
+
const outcome = await buffer.addTurnWithOutcome("sess-1", makeTurn("sess-1", "two"));
|
|
119
|
+
|
|
120
|
+
assert.equal(outcome.decision, "extract_batch");
|
|
121
|
+
assert.notEqual(outcome.extractionTurns, undefined);
|
|
122
|
+
assert.equal(outcome.extractionTurns?.length, 2);
|
|
123
|
+
const last = captures[captures.length - 1];
|
|
124
|
+
assert.equal(last.decision, "extract_batch");
|
|
125
|
+
assert.equal(last.priorTurns.length, 1, "prior turn kept for extraction decisions");
|
|
126
|
+
assert.equal(last.activeTurnsSnapshot.length, 2, "active snapshot kept for extraction decisions");
|
|
127
|
+
assert.deepEqual(
|
|
128
|
+
last.activeTurnsSnapshot.map((t) => t.content),
|
|
129
|
+
["one", "two"]
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("surprise-eligible keep_buffering still builds the snapshots the probe path reads", async () => {
|
|
134
|
+
const storage = new FakeStorage(emptyBuffer());
|
|
135
|
+
const probe = fixedScoreProbe(null);
|
|
136
|
+
const config = parseConfig({
|
|
137
|
+
bufferSurpriseTriggerEnabled: true,
|
|
138
|
+
bufferMaxTurns: 5,
|
|
139
|
+
triggerMode: "smart",
|
|
140
|
+
bufferSaveDebounceMs: 0,
|
|
141
|
+
});
|
|
142
|
+
const buffer = new SmartBuffer(config, storage, probe);
|
|
143
|
+
const captures = spyMutations(buffer);
|
|
144
|
+
|
|
145
|
+
const decision = await buffer.addTurn("sess-1", makeTurn("sess-1", "ordinary content"));
|
|
146
|
+
|
|
147
|
+
assert.equal(decision, "keep_buffering");
|
|
148
|
+
assert.equal(probe.calls, 1, "the surprise probe must still be consulted");
|
|
149
|
+
const capture = captures[0];
|
|
150
|
+
assert.equal(capture.priorTurns.length, 0, "first turn has no priors");
|
|
151
|
+
assert.equal(capture.activeTurnsSnapshot.length, 1, "snapshot kept while the surprise branch may promote");
|
|
152
|
+
assert.equal(capture.retainedTurnsSnapshot.length, 0, "nothing retained yet");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("surprise promotion still extracts from a consistent snapshot", async () => {
|
|
156
|
+
const storage = new FakeStorage(emptyBuffer());
|
|
157
|
+
const probe = fixedScoreProbe(0.99);
|
|
158
|
+
const config = parseConfig({
|
|
159
|
+
bufferSurpriseTriggerEnabled: true,
|
|
160
|
+
bufferMaxTurns: 5,
|
|
161
|
+
triggerMode: "smart",
|
|
162
|
+
bufferSurpriseThreshold: 0.5,
|
|
163
|
+
bufferSaveDebounceMs: 0,
|
|
164
|
+
});
|
|
165
|
+
const buffer = new SmartBuffer(config, storage, probe);
|
|
166
|
+
|
|
167
|
+
const outcome = await buffer.addTurnWithOutcome("sess-1", makeTurn("sess-1", "novel"));
|
|
168
|
+
|
|
169
|
+
assert.equal(outcome.decision, "extract_now");
|
|
170
|
+
assert.deepEqual(
|
|
171
|
+
outcome.extractionTurns?.map((t) => t.content),
|
|
172
|
+
["novel"]
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("keep_buffering under every_n never builds snapshots even with a probe present", async () => {
|
|
177
|
+
const storage = new FakeStorage(emptyBuffer());
|
|
178
|
+
const probe = fixedScoreProbe(0.99);
|
|
179
|
+
const config = parseConfig({
|
|
180
|
+
bufferSurpriseTriggerEnabled: true,
|
|
181
|
+
bufferMaxTurns: 5,
|
|
182
|
+
triggerMode: "every_n",
|
|
183
|
+
bufferSaveDebounceMs: 0,
|
|
184
|
+
});
|
|
185
|
+
const buffer = new SmartBuffer(config, storage, probe);
|
|
186
|
+
const captures = spyMutations(buffer);
|
|
187
|
+
|
|
188
|
+
const decision = await buffer.addTurn("sess-1", makeTurn("sess-1", "ordinary content"));
|
|
189
|
+
|
|
190
|
+
assert.equal(decision, "keep_buffering");
|
|
191
|
+
assert.equal(probe.calls, 0, "surprise branch is smart-mode only");
|
|
192
|
+
assert.equal(captures[0].activeTurnsSnapshot.length, 0);
|
|
193
|
+
assert.equal(captures[0].retainedTurnsSnapshot.length, 0);
|
|
194
|
+
assert.equal(captures[0].priorTurns.length, 0);
|
|
195
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
/**
|
|
3
|
+
* mergeTurns identity comparison — issue: the claim-merge hot path
|
|
4
|
+
* (buffer-drain-claims.ts `mergeIntoClaim`) deduped incoming turns with
|
|
5
|
+
* `merged.some(isEqual)`: O(existing × incoming) per observe while a claim
|
|
6
|
+
* only grows during a stalled drain. The identity comparison now goes
|
|
7
|
+
* through a Set of identity keys built from the exact `bufferTurnsEqual`
|
|
8
|
+
* fields, so the merge is O(n + m) with byte-identical merge semantics:
|
|
9
|
+
* dedupe identical turns (including duplicates within `incoming`), append
|
|
10
|
+
* genuinely-new turns in arrival order, existing entries always win.
|
|
11
|
+
*
|
|
12
|
+
* The identity key must be injective over `bufferTurnsEqual` equality
|
|
13
|
+
* classes — including the undefined-vs-null distinctions on optional fields
|
|
14
|
+
* (`providerThreadId`) and the plan-identity triple (#3251).
|
|
15
|
+
*/
|
|
16
|
+
import test from "node:test";
|
|
17
|
+
|
|
18
|
+
import { bufferTurnIdentityKey, bufferTurnsEqual, mergeBufferTurnsByIdentity } from "./buffer-turn-helpers.js";
|
|
19
|
+
import type { ScopeProfileLayerResolution } from "./namespaces/scope-profiles.js";
|
|
20
|
+
import type { BufferTurn } from "./types.js";
|
|
21
|
+
|
|
22
|
+
function turn(overrides: Partial<BufferTurn> & { content: string }): BufferTurn {
|
|
23
|
+
return { role: "user", timestamp: "2026-11-01T00:00:00.000Z", ...overrides };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test("bufferTurnIdentityKey matches bufferTurnsEqual exactly (same key ⇔ equal)", () => {
|
|
27
|
+
const base = turn({ content: "hello", sessionKey: "s1" });
|
|
28
|
+
const same = turn({ content: "hello", sessionKey: "s1" });
|
|
29
|
+
const different = [
|
|
30
|
+
turn({ content: "hello", sessionKey: "s2" }),
|
|
31
|
+
turn({ content: "world", sessionKey: "s1" }),
|
|
32
|
+
turn({ content: "hello", sessionKey: "s1", role: "assistant" as const }),
|
|
33
|
+
turn({ content: "hello", sessionKey: "s1", timestamp: "2026-11-02T00:00:00.000Z" }),
|
|
34
|
+
turn({ content: "hello", sessionKey: "s1", providerThreadId: null }),
|
|
35
|
+
turn({ content: "hello", sessionKey: "s1", providerThreadId: "t1" }),
|
|
36
|
+
turn({ content: "hello", sessionKey: "s1", turnFingerprint: "fp" }),
|
|
37
|
+
turn({ content: "hello", sessionKey: "s1", persistProcessedFingerprint: true }),
|
|
38
|
+
turn({ content: "hello", sessionKey: "s1", persistProcessedFingerprint: false }),
|
|
39
|
+
turn({ content: "hello", sessionKey: "s1", sourceConnector: "cli" }),
|
|
40
|
+
turn({ content: "hello", sessionKey: "s1", originRole: "user" as const }),
|
|
41
|
+
turn({ content: "hello", sessionKey: "s1", ambientCapture: true }),
|
|
42
|
+
turn({ content: "hello", sessionKey: "s1", ambientCapture: false }),
|
|
43
|
+
turn({ content: "hello", sessionKey: "s1", sourceValidAt: "2026-01-01T00:00:00.000Z" }),
|
|
44
|
+
turn({ content: "hello", sessionKey: "s1", acceptedCapture: true }),
|
|
45
|
+
turn({ content: "hello", sessionKey: "s1", acceptedCapture: false }),
|
|
46
|
+
turn({ content: "hello", sessionKey: "s1", writeNamespaceOverride: "ns" }),
|
|
47
|
+
turn({ content: "hello", sessionKey: "s1", principalOverride: "p1" }),
|
|
48
|
+
turn({
|
|
49
|
+
content: "hello",
|
|
50
|
+
sessionKey: "s1",
|
|
51
|
+
codingContext: { projectId: "proj", branch: "main", rootPath: "/r", defaultBranch: "main" },
|
|
52
|
+
}),
|
|
53
|
+
turn({
|
|
54
|
+
content: "hello",
|
|
55
|
+
sessionKey: "s1",
|
|
56
|
+
codingContext: { projectId: "proj", branch: "dev", rootPath: "/r", defaultBranch: "main" },
|
|
57
|
+
}),
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
assert.equal(bufferTurnIdentityKey(base), bufferTurnIdentityKey(same));
|
|
61
|
+
assert.equal(bufferTurnsEqual(base, same), true);
|
|
62
|
+
for (const other of different) {
|
|
63
|
+
assert.notEqual(
|
|
64
|
+
bufferTurnIdentityKey(base),
|
|
65
|
+
bufferTurnIdentityKey(other),
|
|
66
|
+
`key must differ for ${JSON.stringify(other)}`
|
|
67
|
+
);
|
|
68
|
+
assert.equal(bufferTurnsEqual(base, other), false);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("bufferTurnIdentityKey: undefined and null providerThreadId are distinct classes", () => {
|
|
73
|
+
const absent = turn({ content: "x" });
|
|
74
|
+
const explicitNull = turn({ content: "x", providerThreadId: null });
|
|
75
|
+
const present = turn({ content: "x", providerThreadId: "t" });
|
|
76
|
+
const keys = new Set([
|
|
77
|
+
bufferTurnIdentityKey(absent),
|
|
78
|
+
bufferTurnIdentityKey(explicitNull),
|
|
79
|
+
bufferTurnIdentityKey(present),
|
|
80
|
+
]);
|
|
81
|
+
assert.equal(keys.size, 3);
|
|
82
|
+
assert.equal(bufferTurnsEqual(absent, explicitNull), false);
|
|
83
|
+
assert.equal(bufferTurnsEqual(absent, present), false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("bufferTurnIdentityKey: null and undefined scope plan share the empty identity (#3251)", () => {
|
|
87
|
+
const noPlan = turn({ content: "x" });
|
|
88
|
+
const nullPlan = turn({ content: "x", scopeProfileWritePlan: null });
|
|
89
|
+
assert.equal(bufferTurnIdentityKey(noPlan), bufferTurnIdentityKey(nullPlan));
|
|
90
|
+
assert.equal(bufferTurnsEqual(noPlan, nullPlan), true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("mergeBufferTurnsByIdentity appends genuinely-new turns in arrival order", () => {
|
|
94
|
+
const existing = [turn({ content: "a" }), turn({ content: "b" })];
|
|
95
|
+
const incoming = [turn({ content: "c" }), turn({ content: "d" })];
|
|
96
|
+
const merged = mergeBufferTurnsByIdentity(existing, incoming);
|
|
97
|
+
assert.deepEqual(
|
|
98
|
+
merged.map((t) => t.content),
|
|
99
|
+
["a", "b", "c", "d"]
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("mergeBufferTurnsByIdentity dedupes turns identical to an existing entry", () => {
|
|
104
|
+
const existing = [turn({ content: "a" }), turn({ content: "b" })];
|
|
105
|
+
const incoming = [turn({ content: "b" }), turn({ content: "c" })];
|
|
106
|
+
const merged = mergeBufferTurnsByIdentity(existing, incoming);
|
|
107
|
+
assert.deepEqual(
|
|
108
|
+
merged.map((t) => t.content),
|
|
109
|
+
["a", "b", "c"]
|
|
110
|
+
);
|
|
111
|
+
// The kept entry is the EXISTING object, not the incoming copy.
|
|
112
|
+
assert.equal(merged[1], existing[1]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("mergeBufferTurnsByIdentity dedupes duplicates within incoming (first wins)", () => {
|
|
116
|
+
const existing: BufferTurn[] = [];
|
|
117
|
+
const first = turn({ content: "dup" });
|
|
118
|
+
const second = turn({ content: "dup" });
|
|
119
|
+
const merged = mergeBufferTurnsByIdentity(existing, [first, second]);
|
|
120
|
+
assert.equal(merged.length, 1);
|
|
121
|
+
assert.equal(merged[0], first);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("mergeBufferTurnsByIdentity keeps existing entries verbatim, duplicates included", () => {
|
|
125
|
+
const dupA = turn({ content: "a" });
|
|
126
|
+
const dupB = turn({ content: "a" });
|
|
127
|
+
const existing = [dupA, dupB];
|
|
128
|
+
const merged = mergeBufferTurnsByIdentity(existing, [turn({ content: "a" }), turn({ content: "b" })]);
|
|
129
|
+
assert.equal(merged.length, 3, "existing duplicates are preserved; incoming duplicate dropped");
|
|
130
|
+
assert.equal(merged[0], dupA);
|
|
131
|
+
assert.equal(merged[1], dupB);
|
|
132
|
+
assert.equal(merged[2].content, "b");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("mergeBufferTurnsByIdentity keeps existing order and does not mutate inputs", () => {
|
|
136
|
+
const existing = [turn({ content: "b" }), turn({ content: "a" })];
|
|
137
|
+
const incoming = [turn({ content: "a" }), turn({ content: "c" })];
|
|
138
|
+
const merged = mergeBufferTurnsByIdentity(existing, incoming);
|
|
139
|
+
assert.deepEqual(
|
|
140
|
+
merged.map((t) => t.content),
|
|
141
|
+
["b", "a", "c"]
|
|
142
|
+
);
|
|
143
|
+
assert.equal(existing.length, 2);
|
|
144
|
+
assert.deepEqual(
|
|
145
|
+
existing.map((t) => t.content),
|
|
146
|
+
["b", "a"]
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Identity fields only: scopeProfilePlanIdentity reads profileId, writeLayer
|
|
151
|
+
// and writeNamespace; the remaining members are inert for identity tests.
|
|
152
|
+
function pinnedPlan(namespace: string): NonNullable<BufferTurn["scopeProfileWritePlan"]> {
|
|
153
|
+
const layer: ScopeProfileLayerResolution = {
|
|
154
|
+
id: "userProject",
|
|
155
|
+
kind: "user-project",
|
|
156
|
+
namespace,
|
|
157
|
+
readable: true,
|
|
158
|
+
writable: true,
|
|
159
|
+
promotable: false,
|
|
160
|
+
reason: "identity-test layer",
|
|
161
|
+
};
|
|
162
|
+
return {
|
|
163
|
+
profileId: "p",
|
|
164
|
+
profile: {
|
|
165
|
+
readOrder: ["userProject"],
|
|
166
|
+
writeDefault: "userProject",
|
|
167
|
+
promotionTargets: [],
|
|
168
|
+
autoPromote: { enabled: false, targets: [], categories: [], minConfidenceTier: "speculative" },
|
|
169
|
+
},
|
|
170
|
+
baseNamespace: namespace,
|
|
171
|
+
writeLayer: "userProject",
|
|
172
|
+
writeNamespace: namespace,
|
|
173
|
+
readNamespaces: [namespace],
|
|
174
|
+
layers: [layer],
|
|
175
|
+
promotionTargets: [],
|
|
176
|
+
warnings: [],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
test("mergeBufferTurnsByIdentity stays consistent with bufferTurnsEqual on scope-pinned turns", () => {
|
|
181
|
+
const pinned = turn({
|
|
182
|
+
content: "cap",
|
|
183
|
+
acceptedCapture: true,
|
|
184
|
+
writeNamespaceOverride: "ns-a",
|
|
185
|
+
scopeProfileWritePlan: pinnedPlan("ns-a"),
|
|
186
|
+
});
|
|
187
|
+
const samePinned = turn({
|
|
188
|
+
content: "cap",
|
|
189
|
+
acceptedCapture: true,
|
|
190
|
+
writeNamespaceOverride: "ns-a",
|
|
191
|
+
scopeProfileWritePlan: pinnedPlan("ns-a"),
|
|
192
|
+
});
|
|
193
|
+
const otherPinned = turn({
|
|
194
|
+
content: "cap",
|
|
195
|
+
acceptedCapture: true,
|
|
196
|
+
writeNamespaceOverride: "ns-b",
|
|
197
|
+
scopeProfileWritePlan: pinnedPlan("ns-b"),
|
|
198
|
+
});
|
|
199
|
+
assert.equal(bufferTurnsEqual(pinned, samePinned), true);
|
|
200
|
+
assert.equal(bufferTurnsEqual(pinned, otherPinned), false);
|
|
201
|
+
const merged = mergeBufferTurnsByIdentity([pinned], [samePinned, otherPinned]);
|
|
202
|
+
assert.equal(merged.length, 2);
|
|
203
|
+
assert.equal(merged[0], pinned);
|
|
204
|
+
assert.equal(merged[1], otherPinned);
|
|
205
|
+
});
|
|
@@ -169,6 +169,68 @@ export function bufferTurnsEqual(left: BufferTurn | undefined, right: BufferTurn
|
|
|
169
169
|
);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Canonical identity key for a turn, injective over `bufferTurnsEqual`
|
|
174
|
+
* equality classes. Every compared field is a primitive (or the #3251 plan
|
|
175
|
+
* identity string), so set-membership replaces pairwise comparison and turn
|
|
176
|
+
* merges run O(n + m) instead of O(n × m). JSON.stringify of a string is
|
|
177
|
+
* injective (it escapes control characters), and the raw `\u0001` sentinels
|
|
178
|
+
* distinguish undefined from null — a distinction `===` makes and plain
|
|
179
|
+
* stringification would lose.
|
|
180
|
+
*/
|
|
181
|
+
export function bufferTurnIdentityKey(turn: BufferTurn): string {
|
|
182
|
+
const opt = (value: string | null | undefined): string =>
|
|
183
|
+
value === undefined ? "\u0001undefined" : value === null ? "\u0001null" : JSON.stringify(value);
|
|
184
|
+
const optFlag = (value: boolean | undefined): string =>
|
|
185
|
+
value === undefined ? "\u0001undefined" : String(value);
|
|
186
|
+
const ctx = turn.codingContext;
|
|
187
|
+
return JSON.stringify([
|
|
188
|
+
turn.role,
|
|
189
|
+
turn.content,
|
|
190
|
+
turn.timestamp,
|
|
191
|
+
opt(turn.sessionKey),
|
|
192
|
+
opt(turn.sessionOwnerPrincipal),
|
|
193
|
+
opt(turn.logicalSessionKey),
|
|
194
|
+
turn.providerThreadId === undefined ? "\u0001undefined" : turn.providerThreadId === null ? "\u0001null" : JSON.stringify(turn.providerThreadId),
|
|
195
|
+
opt(turn.turnFingerprint),
|
|
196
|
+
optFlag(turn.persistProcessedFingerprint),
|
|
197
|
+
opt(turn.sourceConnector),
|
|
198
|
+
opt(turn.originRole),
|
|
199
|
+
optFlag(turn.ambientCapture),
|
|
200
|
+
opt(turn.sourceValidAt),
|
|
201
|
+
optFlag(turn.acceptedCapture),
|
|
202
|
+
opt(ctx?.projectId),
|
|
203
|
+
opt(ctx?.rootPath),
|
|
204
|
+
opt(ctx?.branch),
|
|
205
|
+
opt(ctx?.defaultBranch),
|
|
206
|
+
opt(turn.writeNamespaceOverride),
|
|
207
|
+
opt(turn.principalOverride),
|
|
208
|
+
opt(turn.scopeProfileWritePlan ? scopeProfilePlanIdentity(turn.scopeProfileWritePlan) : undefined),
|
|
209
|
+
]);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Identity-dedupe `incoming` turns into `existing` (same contract as the
|
|
214
|
+
* claim-merge `mergeTurns`): identical turns are dropped — the EXISTING
|
|
215
|
+
* entry is kept — genuinely-new turns append in arrival order, and
|
|
216
|
+
* duplicates within `incoming` collapse to their first occurrence. O(n + m)
|
|
217
|
+
* via `bufferTurnIdentityKey` membership instead of pairwise equality.
|
|
218
|
+
*/
|
|
219
|
+
export function mergeBufferTurnsByIdentity(
|
|
220
|
+
existing: readonly BufferTurn[],
|
|
221
|
+
incoming: readonly BufferTurn[],
|
|
222
|
+
): BufferTurn[] {
|
|
223
|
+
const merged = [...existing];
|
|
224
|
+
const seen = new Set(merged.map(bufferTurnIdentityKey));
|
|
225
|
+
for (const turn of incoming) {
|
|
226
|
+
const key = bufferTurnIdentityKey(turn);
|
|
227
|
+
if (seen.has(key)) continue;
|
|
228
|
+
seen.add(key);
|
|
229
|
+
merged.push(turn);
|
|
230
|
+
}
|
|
231
|
+
return merged;
|
|
232
|
+
}
|
|
233
|
+
|
|
172
234
|
/** Replace only the processed snapshot; callers without one retain legacy whole-slot semantics. */
|
|
173
235
|
export function selectRetainedDeferredTurns(
|
|
174
236
|
existing: readonly BufferTurn[],
|
package/src/buffer.ts
CHANGED
|
@@ -646,16 +646,16 @@ export class SmartBuffer {
|
|
|
646
646
|
private async recordTurnUnlocked(bufferKey: string, turn: BufferTurn): Promise<AddTurnMutationResult> {
|
|
647
647
|
await this.loadUnlocked();
|
|
648
648
|
const entry = this.entryFor(bufferKey);
|
|
649
|
-
const priorTurns = entry.turns.slice();
|
|
650
|
-
entry.turns.push(turn);
|
|
651
|
-
const activeTurnsSnapshot = entry.turns.map(copyBufferTurn);
|
|
652
|
-
const retainedTurnsSnapshot = (entry.retainedTurns ?? []).map(copyBufferTurn);
|
|
653
|
-
if (bufferKey === "default") {
|
|
654
|
-
this.state.turns = entry.turns;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
649
|
const signal = scanSignals(turn.content, this.config.highSignalPatterns);
|
|
650
|
+
entry.turns.push(turn);
|
|
658
651
|
const decision = this.evaluate(entry, signal.level);
|
|
652
|
+
// Snapshots serve only extraction decisions and surprise-eligible keep_buffering turns (probe, smart mode, sub-high signal); steady-state buffering copies nothing.
|
|
653
|
+
const surpriseEligible = this.surpriseProbe !== null && this.config.triggerMode === "smart" && signal.level !== "high";
|
|
654
|
+
const snapshotsNeeded = decision !== "keep_buffering" || surpriseEligible;
|
|
655
|
+
const priorTurns = snapshotsNeeded ? entry.turns.slice(0, -1) : [];
|
|
656
|
+
const activeTurnsSnapshot = snapshotsNeeded ? entry.turns.map(copyBufferTurn) : [];
|
|
657
|
+
const retainedTurnsSnapshot = snapshotsNeeded ? (entry.retainedTurns ?? []).map(copyBufferTurn) : [];
|
|
658
|
+
if (bufferKey === "default") this.state.turns = entry.turns;
|
|
659
659
|
const turnCountInWindow = entry.turns.length;
|
|
660
660
|
|
|
661
661
|
this.persistence.markDirty(bufferKey);
|
|
@@ -201,7 +201,16 @@ export function isUnsupportedJsonSchemaError(error: unknown): boolean {
|
|
|
201
201
|
return (
|
|
202
202
|
/(?:response[_ ]?format|json[_ ]?schema|structured[_ ]?output)\b[^;,\n]{0,60}\b(?:unsupported|not supported|not implemented|not available)\b/i.test(message) ||
|
|
203
203
|
/\b(?:unsupported|unknown|unrecognized)(?:[\s:"'=]|parameter|argument|request|supplied|field|type|value){1,40}\b(?:response[_ ]?format|json[_ ]?schema|structured[_ ]?output|schema|format)\b/i.test(message) ||
|
|
204
|
-
/\b(?:does not|doesn't|cannot|can't) support(?:[\s:"'=]|the|native|type){1,20}\b(?:response[_ ]?format|json[_ ]?schema|structured[_ ]?output|schema|format)\b/i.test(message)
|
|
204
|
+
/\b(?:does not|doesn't|cannot|can't) support(?:[\s:"'=]|the|native|type){1,20}\b(?:response[_ ]?format|json[_ ]?schema|structured[_ ]?output|schema|format)\b/i.test(message) ||
|
|
205
|
+
// A strict provider parser rejecting the SCHEMA CONTENT itself (2026-11-05
|
|
206
|
+
// MiniMax-M3.1-Flash: "invalid params, Mismatch type string with value
|
|
207
|
+
// array ... \"episodeTitle\": {\"type\": [\"string\",\"null\"]}"). The
|
|
208
|
+
// provider never names response_format here; it dies parsing our payload.
|
|
209
|
+
// Evidence is the observed signature only — a quoted (JSON-escaped)
|
|
210
|
+
// type-array fragment. A generic "mismatch type" naming an unrelated
|
|
211
|
+
// parameter, or a bare echo of the format name, is not enough to degrade.
|
|
212
|
+
(/invalid\s+(?:params|parameters)/i.test(message) &&
|
|
213
|
+
/\\{0,4}"\s{0,8}type\s{0,8}\\{0,4}"\s{0,8}:\s{0,8}\[/.test(message))
|
|
205
214
|
);
|
|
206
215
|
}
|
|
207
216
|
|