@remnic/core 9.6.39 → 9.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-admin-ops-surface.d.ts +6 -6
- package/dist/access-admin-ops-surface.js +16 -16
- package/dist/access-boundary.d.ts +6 -6
- package/dist/access-boundary.js +17 -17
- package/dist/access-cli.js +56 -55
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +6 -6
- package/dist/access-http.js +22 -22
- package/dist/access-identity-continuity-surface.d.ts +5 -5
- package/dist/access-identity-continuity-surface.js +16 -16
- package/dist/access-lcm-surface.d.ts +6 -6
- package/dist/access-lcm-surface.js +16 -16
- package/dist/access-mcp.d.ts +6 -6
- package/dist/access-mcp.js +21 -21
- package/dist/access-observe-write-surface.d.ts +6 -6
- package/dist/access-observe-write-surface.js +16 -16
- package/dist/access-operations-batch.js +19 -19
- package/dist/access-operations.d.ts +6 -6
- package/dist/access-operations.js +20 -20
- package/dist/access-recall-surface.d.ts +6 -6
- package/dist/access-recall-surface.js +16 -16
- package/dist/{access-service-Fw9AYEqG.d.ts → access-service-CJ5hrLqR.d.ts} +4 -4
- package/dist/access-service.d.ts +6 -6
- package/dist/access-service.js +16 -16
- package/dist/access-surface-catalog.d.ts +6 -6
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-memory-bridge.js +3 -3
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +1 -1
- package/dist/{auto-sync-4A5YBWYO.js → auto-sync-676N6N6Y.js} +4 -4
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +5 -5
- package/dist/briefing.d.ts +2 -2
- package/dist/briefing.js +5 -5
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +2 -2
- package/dist/buffer.js +2 -2
- package/dist/bulk-import/index.d.ts +3 -3
- package/dist/calibration.d.ts +1 -1
- package/dist/calibration.js +1 -1
- package/dist/capabilities.d.ts +4 -2
- package/dist/capabilities.js +1 -1
- package/dist/{catalog-CnS_MP-8.d.ts → catalog-XCOyxXKw.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +7 -7
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/{chunk-SFHI7JBE.js → chunk-2FSHROYK.js} +3 -3
- package/dist/{chunk-WHZ7MK72.js → chunk-4YUPLX7U.js} +3 -3
- package/dist/{chunk-ZIKJD6NL.js → chunk-53Y3TETS.js} +2 -2
- package/dist/{chunk-UI4FVU6T.js → chunk-5CJVLG3H.js} +2 -2
- package/dist/{chunk-BAVVI5GP.js → chunk-7PND4PUC.js} +3 -3
- package/dist/{chunk-NREKOGZ2.js → chunk-ARD5NE4M.js} +3 -3
- package/dist/{chunk-GCTJG5MP.js → chunk-AWHOZTF6.js} +4 -4
- package/dist/{chunk-VSF7AAGE.js → chunk-BFPETA3S.js} +3 -2
- package/dist/chunk-BFPETA3S.js.map +1 -0
- package/dist/{chunk-ZGPDSEKG.js → chunk-BFVEEB7W.js} +2 -2
- package/dist/{chunk-AZK3NEHL.js → chunk-BLXNPBJI.js} +2 -2
- package/dist/chunk-C7SRUWBW.js +54 -0
- package/dist/chunk-C7SRUWBW.js.map +1 -0
- package/dist/{chunk-VTRF5QBX.js → chunk-CBOHQNTJ.js} +2 -2
- package/dist/{chunk-ZSDRGPMV.js → chunk-D6KNJRGX.js} +2 -2
- package/dist/{chunk-2V6L3L56.js → chunk-DDHT5J5M.js} +33 -8
- package/dist/chunk-DDHT5J5M.js.map +1 -0
- package/dist/{chunk-3WULMX34.js → chunk-DSWYYEIK.js} +10 -1
- package/dist/chunk-DSWYYEIK.js.map +1 -0
- package/dist/{chunk-U5JP7FCM.js → chunk-E47TABDR.js} +2 -2
- package/dist/{chunk-7TEWOG3R.js → chunk-FMECZFUT.js} +2 -2
- package/dist/{chunk-QWGNDWKB.js → chunk-FNMFJWBY.js} +2 -2
- package/dist/{chunk-CZGIIADK.js → chunk-FXGC5UUH.js} +56 -11
- package/dist/chunk-FXGC5UUH.js.map +1 -0
- package/dist/{chunk-JIKJPDQJ.js → chunk-GKCCHHZK.js} +15 -5
- package/dist/chunk-GKCCHHZK.js.map +1 -0
- package/dist/{chunk-EDD2G35Z.js → chunk-GWMTFDHF.js} +4 -4
- package/dist/{chunk-VCW74PNH.js → chunk-H67QFYUK.js} +2 -2
- package/dist/{chunk-5ELELET5.js → chunk-HT2DQ27U.js} +3 -3
- package/dist/{chunk-VU3SVYMA.js → chunk-HTIIXNJQ.js} +2 -2
- package/dist/{chunk-I73G4UH7.js → chunk-IALLX7EE.js} +2 -2
- package/dist/{chunk-PE6XR4GG.js → chunk-IKQTFQMI.js} +2 -2
- package/dist/{chunk-XZ5I7OZR.js → chunk-KIPLG7S7.js} +4 -4
- package/dist/{chunk-QLVLW7UD.js → chunk-KSBEEMQF.js} +17 -5
- package/dist/chunk-KSBEEMQF.js.map +1 -0
- package/dist/{chunk-RU73ZROD.js → chunk-KWJP22WP.js} +12 -12
- package/dist/{chunk-MNUHL34L.js → chunk-LKT755LP.js} +2 -2
- package/dist/{chunk-PI23H7FV.js → chunk-M7VGG3ID.js} +3 -3
- package/dist/{chunk-PWSZNML3.js → chunk-NGJT7IW5.js} +284 -38
- package/dist/chunk-NGJT7IW5.js.map +1 -0
- package/dist/{chunk-HVE3UVAB.js → chunk-NS4GEMLR.js} +3 -3
- package/dist/{chunk-ZMXXHUEZ.js → chunk-O24E5CFS.js} +3 -3
- package/dist/{chunk-3BZDBG7U.js → chunk-PML3GU4Q.js} +3 -3
- package/dist/{chunk-DQ2CJYUM.js → chunk-PSQ32V5P.js} +3 -3
- package/dist/{chunk-7XY6TJLM.js → chunk-PUPQ2PMC.js} +3 -3
- package/dist/{chunk-EBVUFGJ4.js → chunk-QFOLNA2C.js} +2 -2
- package/dist/{chunk-METJNEIB.js → chunk-QMUERLZK.js} +1 -1
- package/dist/chunk-QMUERLZK.js.map +1 -0
- package/dist/{chunk-PQG2AOL4.js → chunk-QRYNEVHP.js} +28 -4
- package/dist/{chunk-PQG2AOL4.js.map → chunk-QRYNEVHP.js.map} +1 -1
- package/dist/{chunk-CVEEJKRD.js → chunk-RQTCDICG.js} +2 -2
- package/dist/{chunk-F7AE7L6G.js → chunk-RZEJDF3S.js} +2 -2
- package/dist/{chunk-YFBTVOMB.js → chunk-SYNYI4ZM.js} +2 -2
- package/dist/{chunk-BRDJ2HFP.js → chunk-TRWPCR6P.js} +2 -2
- package/dist/{chunk-YAN6NTJN.js → chunk-UKA7GNI2.js} +3 -3
- package/dist/{chunk-TQNRI55H.js → chunk-UR6PBX2J.js} +4 -51
- package/dist/chunk-UR6PBX2J.js.map +1 -0
- package/dist/{chunk-XH2Y6UIW.js → chunk-VD56CJRR.js} +4 -4
- package/dist/{chunk-J3XHPFEU.js → chunk-VXMWDXIJ.js} +2 -2
- package/dist/{chunk-QT2RZNRT.js → chunk-VY6LKKVN.js} +2 -2
- package/dist/{chunk-PRTA6444.js → chunk-W4PGTAS2.js} +2 -2
- package/dist/{chunk-WX4XTAOI.js → chunk-WGE3OYC3.js} +7 -7
- package/dist/{chunk-RVLE2SNP.js → chunk-X6FSCBJZ.js} +22 -22
- package/dist/{chunk-6MWGGPJJ.js → chunk-Y4EMHIQ5.js} +7 -7
- package/dist/{chunk-IGCQHJSQ.js → chunk-Z3HJ5L3Z.js} +2 -2
- package/dist/{chunk-O2EL7EGG.js → chunk-ZQ7UBUZ6.js} +2 -2
- package/dist/{chunk-5SORASJK.js → chunk-ZXUG3AFP.js} +2 -2
- package/dist/{cli-PGF41MN6.d.ts → cli-CtECZr1q.d.ts} +5 -5
- package/dist/cli.d.ts +8 -8
- package/dist/cli.js +37 -36
- package/dist/compounding/engine.d.ts +2 -2
- package/dist/compounding/engine.js +7 -7
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +5 -5
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +7 -7
- package/dist/consolidation-provenance-check.d.ts +2 -2
- package/dist/consolidation-undo.d.ts +2 -2
- package/dist/contradiction/index.d.ts +2 -2
- package/dist/contradiction/index.js +2 -2
- package/dist/{contradiction-scan-SSKCAMLX.js → contradiction-scan-J57T5QV7.js} +3 -3
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/day-summary.d.ts +1 -1
- package/dist/day-summary.js +2 -2
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/embedding-fallback.js +2 -2
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +2 -2
- package/dist/entity-retrieval.js +6 -6
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +5 -5
- package/dist/explicit-capture.js +2 -2
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-faithfulness.js +2 -2
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction-judge.js +2 -2
- package/dist/extraction.d.ts +17 -2
- package/dist/extraction.js +12 -7
- package/dist/fallback-llm.d.ts +5 -2
- package/dist/fallback-llm.js +1 -1
- package/dist/{first-start-migration-4I3VDSYS.js → first-start-migration-TI67YMLG.js} +2 -2
- package/dist/graph-dashboard-diff.d.ts +1 -1
- package/dist/graph-dashboard-key.d.ts +1 -1
- package/dist/graph-dashboard-parser.d.ts +1 -1
- package/dist/graph-edge-reinforcement.d.ts +1 -1
- package/dist/graph-snapshot.d.ts +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/importers/index.d.ts +1 -1
- package/dist/index.d.ts +13 -13
- package/dist/index.js +56 -55
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/live-connectors-runner.js +3 -2
- package/dist/local-llm.d.ts +1 -1
- package/dist/local-llm.js +2 -2
- package/dist/local-model-endpoint.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +5 -5
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +5 -5
- package/dist/maintenance/rebuild-memory-projection.js +6 -6
- package/dist/mcp-memory-inspector-app.d.ts +6 -6
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +2 -2
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +3 -3
- package/dist/namespaces/migrate.js +10 -10
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/principal.js +2 -2
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +4 -4
- package/dist/namespaces/storage.d.ts +3 -3
- package/dist/namespaces/storage.js +6 -6
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +2 -2
- package/dist/operator-toolkit.js +16 -16
- package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
- package/dist/orchestration/compression-guideline-coordinator.js +2 -2
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +10 -10
- package/dist/{orchestrator-xePY-WPe.d.ts → orchestrator-CKctWsw9.d.ts} +51 -4
- package/dist/orchestrator.d.ts +5 -5
- package/dist/orchestrator.js +56 -55
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/{pipeline-Zgwz-Kne.d.ts → pipeline-BWwGIys5.d.ts} +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/{qmd-B6Xwfn5_.d.ts → qmd-DFuUrBWW.d.ts} +1 -1
- package/dist/qmd-preflight.d.ts +2 -2
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +2 -2
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-planner-llm.js +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-timings.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/replay/normalizers/chatgpt.d.ts +1 -1
- package/dist/replay/normalizers/claude.d.ts +1 -1
- package/dist/replay/normalizers/openclaw.d.ts +1 -1
- package/dist/replay/normalizers/shared.d.ts +1 -1
- package/dist/replay/runner.d.ts +1 -1
- package/dist/replay/types.d.ts +1 -1
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +2 -2
- package/dist/retrieval-agents.d.ts +2 -2
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/schemas.d.ts +22 -22
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/embed-helper.js +2 -2
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +3 -3
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +3 -3
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-ClfD_CWg.d.ts → semantic-consolidation-1Wlr7NWL.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +7 -7
- package/dist/semantic-rule-promotion.js +5 -5
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +5 -5
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +1 -1
- package/dist/shared-context/manager.js +2 -2
- package/dist/signal.d.ts +1 -1
- package/dist/{storage-ZrHZ0dSM.d.ts → storage-BQwP90sb.d.ts} +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.js +4 -4
- package/dist/summarizer.d.ts +1 -1
- package/dist/summarizer.js +4 -4
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +2 -2
- package/dist/temporal-timeline-recall.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +2 -2
- package/dist/tier-routing.d.ts +1 -1
- package/dist/{tier-stats-5XF4UBRQ.js → tier-stats-KV7IWCJR.js} +2 -2
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/types.d.ts +12 -12
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-BbtQZBq9.d.ts → types-DgIkyAsK.d.ts} +39 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +5 -5
- package/package.json +2 -2
- package/src/buffer-session.test.ts +40 -8
- package/src/buffer.ts +41 -17
- package/src/capabilities.test.ts +1 -0
- package/src/capabilities.ts +4 -0
- package/src/config.test.ts +19 -0
- package/src/config.ts +29 -0
- package/src/extraction.test.ts +39 -0
- package/src/extraction.ts +72 -2
- package/src/fallback-llm.ts +26 -5
- package/src/operator-toolkit.ts +53 -0
- package/src/orchestration/extraction-run.test.ts +639 -0
- package/src/orchestration/extraction-run.ts +366 -2
- package/src/orchestration/session-context.ts +2 -0
- package/src/orchestration/turn-ingestion.ts +5 -0
- package/src/orchestrator-flush.test.ts +36 -2
- package/src/orchestrator.ts +12 -1
- package/src/storage.ts +34 -0
- package/src/types.ts +40 -0
- package/dist/chunk-2V6L3L56.js.map +0 -1
- package/dist/chunk-3WULMX34.js.map +0 -1
- package/dist/chunk-CZGIIADK.js.map +0 -1
- package/dist/chunk-JIKJPDQJ.js.map +0 -1
- package/dist/chunk-METJNEIB.js.map +0 -1
- package/dist/chunk-PWSZNML3.js.map +0 -1
- package/dist/chunk-QLVLW7UD.js.map +0 -1
- package/dist/chunk-TQNRI55H.js.map +0 -1
- package/dist/chunk-VSF7AAGE.js.map +0 -1
- /package/dist/{auto-sync-4A5YBWYO.js.map → auto-sync-676N6N6Y.js.map} +0 -0
- /package/dist/{chunk-SFHI7JBE.js.map → chunk-2FSHROYK.js.map} +0 -0
- /package/dist/{chunk-WHZ7MK72.js.map → chunk-4YUPLX7U.js.map} +0 -0
- /package/dist/{chunk-ZIKJD6NL.js.map → chunk-53Y3TETS.js.map} +0 -0
- /package/dist/{chunk-UI4FVU6T.js.map → chunk-5CJVLG3H.js.map} +0 -0
- /package/dist/{chunk-BAVVI5GP.js.map → chunk-7PND4PUC.js.map} +0 -0
- /package/dist/{chunk-NREKOGZ2.js.map → chunk-ARD5NE4M.js.map} +0 -0
- /package/dist/{chunk-GCTJG5MP.js.map → chunk-AWHOZTF6.js.map} +0 -0
- /package/dist/{chunk-ZGPDSEKG.js.map → chunk-BFVEEB7W.js.map} +0 -0
- /package/dist/{chunk-AZK3NEHL.js.map → chunk-BLXNPBJI.js.map} +0 -0
- /package/dist/{chunk-VTRF5QBX.js.map → chunk-CBOHQNTJ.js.map} +0 -0
- /package/dist/{chunk-ZSDRGPMV.js.map → chunk-D6KNJRGX.js.map} +0 -0
- /package/dist/{chunk-U5JP7FCM.js.map → chunk-E47TABDR.js.map} +0 -0
- /package/dist/{chunk-7TEWOG3R.js.map → chunk-FMECZFUT.js.map} +0 -0
- /package/dist/{chunk-QWGNDWKB.js.map → chunk-FNMFJWBY.js.map} +0 -0
- /package/dist/{chunk-EDD2G35Z.js.map → chunk-GWMTFDHF.js.map} +0 -0
- /package/dist/{chunk-VCW74PNH.js.map → chunk-H67QFYUK.js.map} +0 -0
- /package/dist/{chunk-5ELELET5.js.map → chunk-HT2DQ27U.js.map} +0 -0
- /package/dist/{chunk-VU3SVYMA.js.map → chunk-HTIIXNJQ.js.map} +0 -0
- /package/dist/{chunk-I73G4UH7.js.map → chunk-IALLX7EE.js.map} +0 -0
- /package/dist/{chunk-PE6XR4GG.js.map → chunk-IKQTFQMI.js.map} +0 -0
- /package/dist/{chunk-XZ5I7OZR.js.map → chunk-KIPLG7S7.js.map} +0 -0
- /package/dist/{chunk-RU73ZROD.js.map → chunk-KWJP22WP.js.map} +0 -0
- /package/dist/{chunk-MNUHL34L.js.map → chunk-LKT755LP.js.map} +0 -0
- /package/dist/{chunk-PI23H7FV.js.map → chunk-M7VGG3ID.js.map} +0 -0
- /package/dist/{chunk-HVE3UVAB.js.map → chunk-NS4GEMLR.js.map} +0 -0
- /package/dist/{chunk-ZMXXHUEZ.js.map → chunk-O24E5CFS.js.map} +0 -0
- /package/dist/{chunk-3BZDBG7U.js.map → chunk-PML3GU4Q.js.map} +0 -0
- /package/dist/{chunk-DQ2CJYUM.js.map → chunk-PSQ32V5P.js.map} +0 -0
- /package/dist/{chunk-7XY6TJLM.js.map → chunk-PUPQ2PMC.js.map} +0 -0
- /package/dist/{chunk-EBVUFGJ4.js.map → chunk-QFOLNA2C.js.map} +0 -0
- /package/dist/{chunk-CVEEJKRD.js.map → chunk-RQTCDICG.js.map} +0 -0
- /package/dist/{chunk-F7AE7L6G.js.map → chunk-RZEJDF3S.js.map} +0 -0
- /package/dist/{chunk-YFBTVOMB.js.map → chunk-SYNYI4ZM.js.map} +0 -0
- /package/dist/{chunk-BRDJ2HFP.js.map → chunk-TRWPCR6P.js.map} +0 -0
- /package/dist/{chunk-YAN6NTJN.js.map → chunk-UKA7GNI2.js.map} +0 -0
- /package/dist/{chunk-XH2Y6UIW.js.map → chunk-VD56CJRR.js.map} +0 -0
- /package/dist/{chunk-J3XHPFEU.js.map → chunk-VXMWDXIJ.js.map} +0 -0
- /package/dist/{chunk-QT2RZNRT.js.map → chunk-VY6LKKVN.js.map} +0 -0
- /package/dist/{chunk-PRTA6444.js.map → chunk-W4PGTAS2.js.map} +0 -0
- /package/dist/{chunk-WX4XTAOI.js.map → chunk-WGE3OYC3.js.map} +0 -0
- /package/dist/{chunk-RVLE2SNP.js.map → chunk-X6FSCBJZ.js.map} +0 -0
- /package/dist/{chunk-6MWGGPJJ.js.map → chunk-Y4EMHIQ5.js.map} +0 -0
- /package/dist/{chunk-IGCQHJSQ.js.map → chunk-Z3HJ5L3Z.js.map} +0 -0
- /package/dist/{chunk-O2EL7EGG.js.map → chunk-ZQ7UBUZ6.js.map} +0 -0
- /package/dist/{chunk-5SORASJK.js.map → chunk-ZXUG3AFP.js.map} +0 -0
- /package/dist/{contradiction-scan-SSKCAMLX.js.map → contradiction-scan-J57T5QV7.js.map} +0 -0
- /package/dist/{first-start-migration-4I3VDSYS.js.map → first-start-migration-TI67YMLG.js.map} +0 -0
- /package/dist/{tier-stats-5XF4UBRQ.js.map → tier-stats-KV7IWCJR.js.map} +0 -0
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extraction retry/backoff + circuit-breaker tests (extraction hot-loop
|
|
3
|
+
* hardening). Drives a real ExtractionRunCoordinator over tmpdir StorageManager
|
|
4
|
+
* instances with a fake extraction engine so the retry gate, per-fingerprint
|
|
5
|
+
* backoff, provider circuit breaker, recovery, class-specific caps, the
|
|
6
|
+
* never-mark-processed invariant, force-flush bypass, config parity, backoff
|
|
7
|
+
* math, cache coherence, and namespace isolation are all exercised end to end.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import test from "node:test";
|
|
15
|
+
|
|
16
|
+
import { SmartBuffer } from "../buffer.js";
|
|
17
|
+
import {
|
|
18
|
+
ExtractionRunCoordinator,
|
|
19
|
+
type ExtractionRunCoordinatorDeps,
|
|
20
|
+
computeExtractionRetryNextEligibleMs,
|
|
21
|
+
capExtractionRetryStateEntries,
|
|
22
|
+
} from "./extraction-run.js";
|
|
23
|
+
import { StorageManager } from "../storage.js";
|
|
24
|
+
import { parseConfig } from "../config.js";
|
|
25
|
+
import type { ExtractionEngine } from "../extraction.js";
|
|
26
|
+
import type { ThreadingManager } from "../threading.js";
|
|
27
|
+
import type { BufferTurn, ExtractionResult, ExtractionFailureClass, PluginConfig } from "../types.js";
|
|
28
|
+
import type { TierMigrationCycleSummary } from "../recall-state.js";
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Result factories
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
function failureResult(cls: ExtractionFailureClass): ExtractionResult {
|
|
35
|
+
return {
|
|
36
|
+
facts: [],
|
|
37
|
+
profileUpdates: [],
|
|
38
|
+
entities: [],
|
|
39
|
+
questions: [],
|
|
40
|
+
extractionFailure: `synthetic_${cls}`,
|
|
41
|
+
extractionFailureClass: cls,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function successResult(): ExtractionResult {
|
|
46
|
+
return {
|
|
47
|
+
facts: [{ content: "a durable fact", category: "fact", confidence: 0.9, tags: [] }],
|
|
48
|
+
profileUpdates: [],
|
|
49
|
+
entities: [],
|
|
50
|
+
questions: [],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function makeTurns(content: string): BufferTurn[] {
|
|
55
|
+
return [
|
|
56
|
+
{
|
|
57
|
+
role: "user",
|
|
58
|
+
content: `u:${content}`,
|
|
59
|
+
timestamp: "2026-07-15T00:00:00Z",
|
|
60
|
+
persistProcessedFingerprint: true,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
role: "assistant",
|
|
64
|
+
content: `a:${content}`,
|
|
65
|
+
timestamp: "2026-07-15T00:00:01Z",
|
|
66
|
+
persistProcessedFingerprint: true,
|
|
67
|
+
},
|
|
68
|
+
] as BufferTurn[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const migrationSummary: TierMigrationCycleSummary = {
|
|
72
|
+
trigger: "extraction",
|
|
73
|
+
scanned: 0,
|
|
74
|
+
migrated: 0,
|
|
75
|
+
promoted: 0,
|
|
76
|
+
demoted: 0,
|
|
77
|
+
limit: 0,
|
|
78
|
+
dryRun: false,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Harness
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
interface Harness {
|
|
86
|
+
config: PluginConfig;
|
|
87
|
+
storageForNs: (ns: string) => Promise<StorageManager>;
|
|
88
|
+
newCoordinator: () => ExtractionRunCoordinator;
|
|
89
|
+
engineCalls: () => number;
|
|
90
|
+
setRespond: (fn: (turns: BufferTurn[]) => ExtractionResult) => void;
|
|
91
|
+
recordedProcessedCount: () => number;
|
|
92
|
+
run: (
|
|
93
|
+
coord: ExtractionRunCoordinator,
|
|
94
|
+
content: string,
|
|
95
|
+
opts?: { force?: boolean; failClosed?: boolean; writeNamespaceOverride?: string },
|
|
96
|
+
) => Promise<{ status: string; reason?: string }>;
|
|
97
|
+
cleanup: () => Promise<void>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function makeHarness(overrides: Record<string, unknown> = {}): Promise<Harness> {
|
|
101
|
+
StorageManager.clearAllStaticCaches();
|
|
102
|
+
const baseDir = await mkdtemp(path.join(os.tmpdir(), "extraction-retry-"));
|
|
103
|
+
const config = parseConfig({
|
|
104
|
+
memoryDir: baseDir,
|
|
105
|
+
qmdEnabled: false,
|
|
106
|
+
...overrides,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const storages = new Map<string, StorageManager>();
|
|
110
|
+
const storageForNs = async (ns: string): Promise<StorageManager> => {
|
|
111
|
+
let s = storages.get(ns);
|
|
112
|
+
if (!s) {
|
|
113
|
+
s = new StorageManager(path.join(baseDir, `ns-${ns.replace(/[^a-z0-9]+/gi, "_")}`));
|
|
114
|
+
await s.ensureDirectories();
|
|
115
|
+
storages.set(ns, s);
|
|
116
|
+
}
|
|
117
|
+
return s;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const defaultStorage = await storageForNs("default");
|
|
121
|
+
const buffer = new SmartBuffer(config, defaultStorage);
|
|
122
|
+
await buffer.load();
|
|
123
|
+
|
|
124
|
+
let engineCalls = 0;
|
|
125
|
+
let respond: (turns: BufferTurn[]) => ExtractionResult = () => successResult();
|
|
126
|
+
let recordedProcessedCount = 0;
|
|
127
|
+
|
|
128
|
+
const makeDeps = (): ExtractionRunCoordinatorDeps => ({
|
|
129
|
+
config,
|
|
130
|
+
getBuffer: () => buffer,
|
|
131
|
+
getExtraction: () => ({
|
|
132
|
+
extract: async (turns: Parameters<ExtractionEngine["extract"]>[0]): Promise<ExtractionResult> => {
|
|
133
|
+
engineCalls += 1;
|
|
134
|
+
return respond(turns as BufferTurn[]);
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
getStorageRouter: () => ({
|
|
138
|
+
storageFor: async (ns: string) => storageForNs(ns),
|
|
139
|
+
}),
|
|
140
|
+
getThreading: () => ({
|
|
141
|
+
processTurn: async (..._args: Parameters<ThreadingManager["processTurn"]>) => "thread-1",
|
|
142
|
+
updateThreadTitle: async (..._args: Parameters<ThreadingManager["updateThreadTitle"]>) => {},
|
|
143
|
+
}),
|
|
144
|
+
persistExtraction: async () => ["memory-1"],
|
|
145
|
+
maybeCapturePassiveCorrections: async () => {},
|
|
146
|
+
resolveSelfNamespace: () => "default",
|
|
147
|
+
getCodingContextForSession: () => null,
|
|
148
|
+
applyCodingNamespaceOverlay: () => "default",
|
|
149
|
+
boxBuilderFor: () => ({ onExtraction: async () => {} }),
|
|
150
|
+
appendPersistedThreadEpisodes: async () => {},
|
|
151
|
+
maybeScheduleConsolidation: () => {},
|
|
152
|
+
requestQmdMaintenance: () => {},
|
|
153
|
+
runTierMigrationCycle: async () => migrationSummary,
|
|
154
|
+
getLastPersistExtractionDeferredCount: () => 0,
|
|
155
|
+
recordProcessedExtractionFingerprint: async () => {
|
|
156
|
+
recordedProcessedCount += 1;
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
config,
|
|
162
|
+
storageForNs,
|
|
163
|
+
newCoordinator: () => new ExtractionRunCoordinator(makeDeps()),
|
|
164
|
+
engineCalls: () => engineCalls,
|
|
165
|
+
setRespond: (fn) => {
|
|
166
|
+
respond = fn;
|
|
167
|
+
},
|
|
168
|
+
recordedProcessedCount: () => recordedProcessedCount,
|
|
169
|
+
run: async (coord, content, opts = {}) => {
|
|
170
|
+
const result = await coord.runExtraction(makeTurns(content), {
|
|
171
|
+
skipCharThreshold: true,
|
|
172
|
+
skipUserTurnThreshold: true,
|
|
173
|
+
clearBufferAfterExtraction: false,
|
|
174
|
+
bufferKey: content,
|
|
175
|
+
failOnExtractionFailure: opts.failClosed === true,
|
|
176
|
+
forceExtractionAttempt: opts.force === true,
|
|
177
|
+
writeNamespaceOverride: opts.writeNamespaceOverride,
|
|
178
|
+
});
|
|
179
|
+
return { status: result.status, reason: result.reason };
|
|
180
|
+
},
|
|
181
|
+
cleanup: async () => {
|
|
182
|
+
StorageManager.clearAllStaticCaches();
|
|
183
|
+
await rm(baseDir, { recursive: true, force: true });
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Backoff math (pure)
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
test("computeExtractionRetryNextEligibleMs: progresses through schedule and caps", () => {
|
|
193
|
+
const sched = [1000, 5000, 30000];
|
|
194
|
+
const cap = 60000;
|
|
195
|
+
const noJitter = () => 0.5; // jitter factor = 1 exactly
|
|
196
|
+
assert.equal(computeExtractionRetryNextEligibleMs(1, sched, cap, 0.2, 0, noJitter), 1000);
|
|
197
|
+
assert.equal(computeExtractionRetryNextEligibleMs(2, sched, cap, 0.2, 0, noJitter), 5000);
|
|
198
|
+
assert.equal(computeExtractionRetryNextEligibleMs(3, sched, cap, 0.2, 0, noJitter), 30000);
|
|
199
|
+
// Past the schedule end → clamps to the last step, then to cap.
|
|
200
|
+
assert.equal(computeExtractionRetryNextEligibleMs(9, sched, cap, 0.2, 0, noJitter), 30000);
|
|
201
|
+
// A schedule value above the cap is clamped to the cap.
|
|
202
|
+
assert.equal(computeExtractionRetryNextEligibleMs(1, [999999], cap, 0.2, 0, noJitter), cap);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("computeExtractionRetryNextEligibleMs: jitter stays within +/- ratio", () => {
|
|
206
|
+
const base = 10000;
|
|
207
|
+
const ratio = 0.2;
|
|
208
|
+
for (const r of [0, 0.25, 0.5, 0.75, 1]) {
|
|
209
|
+
const value = computeExtractionRetryNextEligibleMs(1, [base], base * 100, ratio, 0, () => r);
|
|
210
|
+
assert.ok(value >= base * (1 - ratio) - 1, `>= lower bound for rng=${r} (${value})`);
|
|
211
|
+
assert.ok(value <= base * (1 + ratio) + 1, `<= upper bound for rng=${r} (${value})`);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("capExtractionRetryStateEntries: caps to newest and guards maxEntries<=0", () => {
|
|
216
|
+
const entries = Array.from({ length: 10 }, (_, i) => ({
|
|
217
|
+
fingerprint: `fp${i}`,
|
|
218
|
+
firstFailedAt: `2026-07-15T00:00:0${i}.000Z`,
|
|
219
|
+
}));
|
|
220
|
+
const capped = capExtractionRetryStateEntries(entries, 3);
|
|
221
|
+
assert.equal(capped.length, 3);
|
|
222
|
+
assert.deepEqual(
|
|
223
|
+
capped.map((e) => e.fingerprint),
|
|
224
|
+
["fp7", "fp8", "fp9"],
|
|
225
|
+
);
|
|
226
|
+
// maxEntries<=0 must return [] (not ALL entries via slice(-0)).
|
|
227
|
+
assert.deepEqual(capExtractionRetryStateEntries(entries, 0), []);
|
|
228
|
+
assert.deepEqual(capExtractionRetryStateEntries(entries, -5), []);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// Backoff eligibility gate
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
test("backoff: a provider_retryable failure parks the same fingerprint; extract() not called again", async () => {
|
|
236
|
+
const h = await makeHarness({
|
|
237
|
+
extractionRetryScheduleMs: [3_600_000],
|
|
238
|
+
extractionBreakerFailureThreshold: 100,
|
|
239
|
+
});
|
|
240
|
+
try {
|
|
241
|
+
const coord = h.newCoordinator();
|
|
242
|
+
h.setRespond(() => failureResult("provider_retryable"));
|
|
243
|
+
const r1 = await h.run(coord, "topic-a");
|
|
244
|
+
assert.equal(r1.status, "skipped");
|
|
245
|
+
assert.equal(h.engineCalls(), 1, "first attempt calls extract()");
|
|
246
|
+
|
|
247
|
+
const r2 = await h.run(coord, "topic-a");
|
|
248
|
+
assert.equal(r2.reason, "extraction_backoff");
|
|
249
|
+
assert.equal(h.engineCalls(), 1, "second attempt is suppressed by backoff");
|
|
250
|
+
|
|
251
|
+
// A distinct fingerprint is unaffected by another fingerprint's backoff.
|
|
252
|
+
const r3 = await h.run(coord, "topic-b");
|
|
253
|
+
assert.equal(h.engineCalls(), 2, "distinct fingerprint still runs");
|
|
254
|
+
assert.equal(r3.status, "skipped");
|
|
255
|
+
} finally {
|
|
256
|
+
await h.cleanup();
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("backoff: rewriting nextEligibleAt to the past re-enables the fingerprint (restart-safe hydration)", async () => {
|
|
261
|
+
const h = await makeHarness({
|
|
262
|
+
extractionRetryScheduleMs: [3_600_000],
|
|
263
|
+
extractionBreakerFailureThreshold: 100,
|
|
264
|
+
});
|
|
265
|
+
try {
|
|
266
|
+
h.setRespond(() => failureResult("provider_retryable"));
|
|
267
|
+
const coord = h.newCoordinator();
|
|
268
|
+
await h.run(coord, "topic-a");
|
|
269
|
+
assert.equal(h.engineCalls(), 1);
|
|
270
|
+
|
|
271
|
+
// Simulate the backoff window elapsing by rewinding the persisted state,
|
|
272
|
+
// then hydrate a FRESH coordinator (as a daemon restart would).
|
|
273
|
+
const storage = await h.storageForNs("default");
|
|
274
|
+
const meta = await storage.loadMeta();
|
|
275
|
+
assert.equal(meta.extractionRetryState?.length, 1, "failure persisted to meta.json");
|
|
276
|
+
meta.extractionRetryState = (meta.extractionRetryState ?? []).map((e) => ({
|
|
277
|
+
...e,
|
|
278
|
+
nextEligibleAt: "2000-01-01T00:00:00.000Z",
|
|
279
|
+
}));
|
|
280
|
+
await storage.saveMeta(meta);
|
|
281
|
+
|
|
282
|
+
h.setRespond(() => successResult());
|
|
283
|
+
const coord2 = h.newCoordinator();
|
|
284
|
+
const r = await h.run(coord2, "topic-a");
|
|
285
|
+
assert.equal(h.engineCalls(), 2, "eligible-again fingerprint runs after restart hydration");
|
|
286
|
+
assert.equal(r.status, "completed");
|
|
287
|
+
} finally {
|
|
288
|
+
await h.cleanup();
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// Circuit breaker
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
test("circuit breaker: opens after threshold failures and suppresses distinct fingerprints", async () => {
|
|
297
|
+
const h = await makeHarness({
|
|
298
|
+
extractionRetryScheduleMs: [3_600_000],
|
|
299
|
+
extractionBreakerFailureThreshold: 2,
|
|
300
|
+
extractionBreakerCooldownMs: 3_600_000,
|
|
301
|
+
});
|
|
302
|
+
try {
|
|
303
|
+
const coord = h.newCoordinator();
|
|
304
|
+
h.setRespond(() => failureResult("provider_retryable"));
|
|
305
|
+
await h.run(coord, "fp-1");
|
|
306
|
+
await h.run(coord, "fp-2");
|
|
307
|
+
assert.equal(h.engineCalls(), 2, "two distinct fingerprints failed, tripping the breaker");
|
|
308
|
+
assert.equal(coord.getExtractionResilienceStatus().breaker.state, "open");
|
|
309
|
+
|
|
310
|
+
// A THIRD distinct fingerprint is short-circuited without calling extract().
|
|
311
|
+
const r = await h.run(coord, "fp-3");
|
|
312
|
+
assert.equal(r.reason, "provider_circuit_open");
|
|
313
|
+
assert.equal(h.engineCalls(), 2, "breaker-open suppresses distinct fingerprints too");
|
|
314
|
+
} finally {
|
|
315
|
+
await h.cleanup();
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("circuit breaker: a forced flush still calls extract() while the breaker is open and records the failure", async () => {
|
|
320
|
+
const h = await makeHarness({
|
|
321
|
+
extractionRetryScheduleMs: [3_600_000],
|
|
322
|
+
extractionBreakerFailureThreshold: 1,
|
|
323
|
+
extractionBreakerCooldownMs: 3_600_000,
|
|
324
|
+
});
|
|
325
|
+
try {
|
|
326
|
+
const coord = h.newCoordinator();
|
|
327
|
+
h.setRespond(() => failureResult("provider_retryable"));
|
|
328
|
+
await h.run(coord, "fp-1");
|
|
329
|
+
assert.equal(coord.getExtractionResilienceStatus().breaker.state, "open");
|
|
330
|
+
assert.equal(h.engineCalls(), 1);
|
|
331
|
+
|
|
332
|
+
// Force-flush bypasses the gate (rule 18) but still records the failure.
|
|
333
|
+
const r = await h.run(coord, "fp-2", { force: true });
|
|
334
|
+
assert.equal(h.engineCalls(), 2, "forced attempt calls extract() despite open breaker");
|
|
335
|
+
assert.equal(r.status, "skipped");
|
|
336
|
+
const storage = await h.storageForNs("default");
|
|
337
|
+
const meta = await storage.loadMeta();
|
|
338
|
+
const fps = (meta.extractionRetryState ?? []).map((e) => e.fingerprint);
|
|
339
|
+
assert.equal(fps.length, 2, "forced failure was recorded into retry state");
|
|
340
|
+
} finally {
|
|
341
|
+
await h.cleanup();
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("circuit breaker: auth_config failure opens the breaker immediately (no hot loop)", async () => {
|
|
346
|
+
const h = await makeHarness({
|
|
347
|
+
extractionRetryScheduleMs: [3_600_000],
|
|
348
|
+
extractionBreakerFailureThreshold: 100, // far above one failure
|
|
349
|
+
extractionBreakerAuthCooldownMs: 3_600_000,
|
|
350
|
+
});
|
|
351
|
+
try {
|
|
352
|
+
const coord = h.newCoordinator();
|
|
353
|
+
h.setRespond(() => failureResult("auth_config"));
|
|
354
|
+
await h.run(coord, "fp-1");
|
|
355
|
+
assert.equal(coord.getExtractionResilienceStatus().breaker.state, "open");
|
|
356
|
+
|
|
357
|
+
const r = await h.run(coord, "fp-2");
|
|
358
|
+
assert.equal(r.reason, "provider_circuit_open");
|
|
359
|
+
assert.equal(h.engineCalls(), 1, "one auth_config failure suppresses subsequent extraction");
|
|
360
|
+
} finally {
|
|
361
|
+
await h.cleanup();
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("recovery: half-open probe succeeds → breaker closes", async () => {
|
|
366
|
+
const h = await makeHarness({
|
|
367
|
+
extractionRetryScheduleMs: [3_600_000],
|
|
368
|
+
extractionBreakerFailureThreshold: 1,
|
|
369
|
+
extractionBreakerCooldownMs: 0, // cooldown already elapsed on the next call
|
|
370
|
+
});
|
|
371
|
+
try {
|
|
372
|
+
const coord = h.newCoordinator();
|
|
373
|
+
h.setRespond(() => failureResult("provider_retryable"));
|
|
374
|
+
await h.run(coord, "fp-1");
|
|
375
|
+
assert.equal(coord.getExtractionResilienceStatus().breaker.state, "open");
|
|
376
|
+
|
|
377
|
+
// Cooldown 0 → next call flips open→half_open and allows one probe.
|
|
378
|
+
h.setRespond(() => successResult());
|
|
379
|
+
const r = await h.run(coord, "fp-2");
|
|
380
|
+
assert.equal(r.status, "completed", "half-open probe runs");
|
|
381
|
+
assert.equal(coord.getExtractionResilienceStatus().breaker.state, "closed");
|
|
382
|
+
assert.equal(coord.getExtractionResilienceStatus().breaker.consecutiveFailures, 0);
|
|
383
|
+
} finally {
|
|
384
|
+
await h.cleanup();
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("recovery: a FAILED half-open probe re-opens the breaker (cursor review)", async () => {
|
|
389
|
+
// An auth_config failure trips the breaker below the consecutive-failure
|
|
390
|
+
// threshold. After the auth cooldown elapses (half_open), a transient probe
|
|
391
|
+
// failure must re-open the breaker — not leave it stuck half_open where it
|
|
392
|
+
// no longer suppresses anything.
|
|
393
|
+
const h = await makeHarness({
|
|
394
|
+
extractionRetryScheduleMs: [3_600_000],
|
|
395
|
+
extractionBreakerFailureThreshold: 100, // auth trips below threshold
|
|
396
|
+
extractionBreakerAuthCooldownMs: 0, // half-open on the next call
|
|
397
|
+
extractionBreakerCooldownMs: 3_600_000, // a re-open parks for a long time
|
|
398
|
+
});
|
|
399
|
+
try {
|
|
400
|
+
const coord = h.newCoordinator();
|
|
401
|
+
h.setRespond(() => failureResult("auth_config"));
|
|
402
|
+
await h.run(coord, "fp-a"); // auth failure → breaker opens immediately
|
|
403
|
+
assert.equal(coord.getExtractionResilienceStatus().breaker.state, "open");
|
|
404
|
+
|
|
405
|
+
// Auth cooldown 0 → this call flips open→half_open and probes; the probe
|
|
406
|
+
// fails transiently → the breaker must re-open with the transient cooldown.
|
|
407
|
+
h.setRespond(() => failureResult("provider_retryable"));
|
|
408
|
+
await h.run(coord, "fp-b");
|
|
409
|
+
assert.equal(
|
|
410
|
+
coord.getExtractionResilienceStatus().breaker.state,
|
|
411
|
+
"open",
|
|
412
|
+
"failed half-open probe re-opens the breaker",
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
// While re-opened, a third fingerprint is suppressed without calling extract().
|
|
416
|
+
const calls = h.engineCalls();
|
|
417
|
+
const r3 = await h.run(coord, "fp-c");
|
|
418
|
+
assert.equal(r3.reason, "provider_circuit_open", "re-opened breaker suppresses");
|
|
419
|
+
assert.equal(h.engineCalls(), calls, "no extract() call while re-opened");
|
|
420
|
+
} finally {
|
|
421
|
+
await h.cleanup();
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("recovery: a successful extract clears the fingerprint's retry state so it proceeds again", async () => {
|
|
426
|
+
const h = await makeHarness({
|
|
427
|
+
extractionRetryScheduleMs: [3_600_000],
|
|
428
|
+
extractionBreakerFailureThreshold: 100,
|
|
429
|
+
});
|
|
430
|
+
try {
|
|
431
|
+
const coord = h.newCoordinator();
|
|
432
|
+
h.setRespond(() => failureResult("provider_retryable"));
|
|
433
|
+
await h.run(coord, "fp-1"); // fail → parked
|
|
434
|
+
await h.run(coord, "fp-1"); // suppressed
|
|
435
|
+
assert.equal(h.engineCalls(), 1);
|
|
436
|
+
|
|
437
|
+
h.setRespond(() => successResult());
|
|
438
|
+
await h.run(coord, "fp-1", { force: true }); // forced probe succeeds → clears state
|
|
439
|
+
assert.equal(h.engineCalls(), 2);
|
|
440
|
+
assert.equal(coord.getExtractionResilienceStatus().backoffFingerprintCount, 0, "retry state cleared on success");
|
|
441
|
+
|
|
442
|
+
// Same fingerprint, non-forced, now proceeds because the entry cleared.
|
|
443
|
+
await h.run(coord, "fp-1");
|
|
444
|
+
assert.equal(h.engineCalls(), 3, "cleared fingerprint runs without the gate");
|
|
445
|
+
} finally {
|
|
446
|
+
await h.cleanup();
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test("recovery: a successful FORCED flush on a fresh coordinator clears a persisted backoff entry (cursor/codex review)", async () => {
|
|
451
|
+
// A forced flush bypasses the retry gate and never hydrates the in-memory
|
|
452
|
+
// mirror, so the success-heal path must still clear the persisted meta entry
|
|
453
|
+
// — otherwise a stale backoff survives a successful forced flush and keeps
|
|
454
|
+
// blocking normal extraction until the timer expires.
|
|
455
|
+
const h = await makeHarness({
|
|
456
|
+
extractionRetryScheduleMs: [3_600_000],
|
|
457
|
+
extractionBreakerFailureThreshold: 100,
|
|
458
|
+
});
|
|
459
|
+
try {
|
|
460
|
+
const coordA = h.newCoordinator();
|
|
461
|
+
h.setRespond(() => failureResult("provider_retryable"));
|
|
462
|
+
await h.run(coordA, "fp-heal"); // fail → parked, persisted to meta.json
|
|
463
|
+
const storage = await h.storageForNs("default");
|
|
464
|
+
let meta = await storage.loadMeta();
|
|
465
|
+
assert.equal((meta.extractionRetryState ?? []).length, 1, "failure persisted");
|
|
466
|
+
|
|
467
|
+
// Fresh coordinator (mirror empty, namespace not hydrated) — forced success.
|
|
468
|
+
const coordB = h.newCoordinator();
|
|
469
|
+
h.setRespond(() => successResult());
|
|
470
|
+
await h.run(coordB, "fp-heal", { force: true });
|
|
471
|
+
|
|
472
|
+
meta = await storage.loadMeta();
|
|
473
|
+
assert.equal(
|
|
474
|
+
(meta.extractionRetryState ?? []).length,
|
|
475
|
+
0,
|
|
476
|
+
"successful forced flush must clear the persisted backoff entry",
|
|
477
|
+
);
|
|
478
|
+
} finally {
|
|
479
|
+
await h.cleanup();
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// ---------------------------------------------------------------------------
|
|
484
|
+
// Class-specific caps
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
|
|
487
|
+
test("parse_empty: long-parks a fingerprint after extractionParseEmptyMaxAttempts", async () => {
|
|
488
|
+
const h = await makeHarness({
|
|
489
|
+
extractionRetryScheduleMs: [1000],
|
|
490
|
+
extractionRetryMaxBackoffMs: 9_000_000,
|
|
491
|
+
extractionParseEmptyMaxAttempts: 2,
|
|
492
|
+
extractionBreakerFailureThreshold: 100,
|
|
493
|
+
extractionRetryJitterRatio: 0,
|
|
494
|
+
});
|
|
495
|
+
try {
|
|
496
|
+
const coord = h.newCoordinator();
|
|
497
|
+
h.setRespond(() => failureResult("parse_empty"));
|
|
498
|
+
// Force each attempt so the gate does not suppress; drive past the cap.
|
|
499
|
+
await h.run(coord, "fp-1", { force: true });
|
|
500
|
+
await h.run(coord, "fp-1", { force: true });
|
|
501
|
+
await h.run(coord, "fp-1", { force: true }); // attempt 3 > cap of 2 → long-park
|
|
502
|
+
|
|
503
|
+
const storage = await h.storageForNs("default");
|
|
504
|
+
const meta = await storage.loadMeta();
|
|
505
|
+
const entry = (meta.extractionRetryState ?? [])[0];
|
|
506
|
+
assert.ok(entry, "retry state entry exists");
|
|
507
|
+
assert.equal(entry.attempts, 3);
|
|
508
|
+
assert.equal(entry.lastFailureClass, "parse_empty");
|
|
509
|
+
const parkMs = Date.parse(entry.nextEligibleAt) - Date.now();
|
|
510
|
+
assert.ok(parkMs > 8_000_000, `long-parked to ~maxBackoff (got ${parkMs}ms)`);
|
|
511
|
+
assert.equal(h.recordedProcessedCount(), 0, "parse_empty fingerprint is never marked processed");
|
|
512
|
+
} finally {
|
|
513
|
+
await h.cleanup();
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// ---------------------------------------------------------------------------
|
|
518
|
+
// Invariant guard (regression): never mark processed on failure
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
|
|
521
|
+
test("invariant: a failing extraction never records a processed fingerprint", async () => {
|
|
522
|
+
const h = await makeHarness({ extractionBreakerFailureThreshold: 100 });
|
|
523
|
+
try {
|
|
524
|
+
const coord = h.newCoordinator();
|
|
525
|
+
for (const cls of ["provider_retryable", "parse_empty", "auth_config"] as ExtractionFailureClass[]) {
|
|
526
|
+
h.setRespond(() => failureResult(cls));
|
|
527
|
+
await h.run(coord, `fp-${cls}`, { force: true });
|
|
528
|
+
}
|
|
529
|
+
assert.equal(h.recordedProcessedCount(), 0, "no processed fingerprint recorded across all failure classes");
|
|
530
|
+
} finally {
|
|
531
|
+
await h.cleanup();
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// ---------------------------------------------------------------------------
|
|
536
|
+
// Config parity: extractionRetryEnabled=false restores pre-change behavior
|
|
537
|
+
// ---------------------------------------------------------------------------
|
|
538
|
+
|
|
539
|
+
test("extractionRetryEnabled=false: extractor is called on every observe with no gate", async () => {
|
|
540
|
+
const h = await makeHarness({
|
|
541
|
+
extractionRetryEnabled: false,
|
|
542
|
+
extractionBreakerFailureThreshold: 1,
|
|
543
|
+
});
|
|
544
|
+
try {
|
|
545
|
+
const coord = h.newCoordinator();
|
|
546
|
+
h.setRespond(() => failureResult("provider_retryable"));
|
|
547
|
+
await h.run(coord, "fp-1");
|
|
548
|
+
await h.run(coord, "fp-1");
|
|
549
|
+
await h.run(coord, "fp-1");
|
|
550
|
+
assert.equal(h.engineCalls(), 3, "no backoff/breaker gate when the feature is disabled");
|
|
551
|
+
assert.equal(coord.getExtractionResilienceStatus().breaker.state, "closed");
|
|
552
|
+
const storage = await h.storageForNs("default");
|
|
553
|
+
const meta = await storage.loadMeta();
|
|
554
|
+
assert.equal((meta.extractionRetryState ?? []).length, 0, "no retry state persisted when disabled");
|
|
555
|
+
} finally {
|
|
556
|
+
await h.cleanup();
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// ---------------------------------------------------------------------------
|
|
561
|
+
// Cache coherence across instances
|
|
562
|
+
// ---------------------------------------------------------------------------
|
|
563
|
+
|
|
564
|
+
test("cache coherence: a failure recorded by one coordinator gates a second over the same meta", async () => {
|
|
565
|
+
const h = await makeHarness({
|
|
566
|
+
extractionRetryScheduleMs: [3_600_000],
|
|
567
|
+
extractionBreakerFailureThreshold: 100,
|
|
568
|
+
});
|
|
569
|
+
try {
|
|
570
|
+
const coordA = h.newCoordinator();
|
|
571
|
+
h.setRespond(() => failureResult("provider_retryable"));
|
|
572
|
+
await h.run(coordA, "shared-fp");
|
|
573
|
+
assert.equal(h.engineCalls(), 1);
|
|
574
|
+
|
|
575
|
+
// A second coordinator over the same memory dir hydrates from meta.json and
|
|
576
|
+
// honors the persisted backoff without calling extract().
|
|
577
|
+
const coordB = h.newCoordinator();
|
|
578
|
+
const r = await h.run(coordB, "shared-fp");
|
|
579
|
+
assert.equal(r.reason, "extraction_backoff");
|
|
580
|
+
assert.equal(h.engineCalls(), 1, "coordinator B honors coordinator A's persisted failure");
|
|
581
|
+
} finally {
|
|
582
|
+
await h.cleanup();
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// ---------------------------------------------------------------------------
|
|
587
|
+
// Namespace isolation vs. provider-global breaker
|
|
588
|
+
// ---------------------------------------------------------------------------
|
|
589
|
+
|
|
590
|
+
test("namespace isolation: per-fingerprint backoff is namespace-scoped; breaker is provider-global", async () => {
|
|
591
|
+
const h = await makeHarness({
|
|
592
|
+
extractionRetryScheduleMs: [3_600_000],
|
|
593
|
+
extractionBreakerFailureThreshold: 100, // keep the breaker closed for the per-fp assertion
|
|
594
|
+
});
|
|
595
|
+
try {
|
|
596
|
+
const coord = h.newCoordinator();
|
|
597
|
+
h.setRespond(() => failureResult("provider_retryable"));
|
|
598
|
+
// Same fingerprint content, routed to namespace A → parked in A only.
|
|
599
|
+
await h.run(coord, "same-content", { writeNamespaceOverride: "nsA" });
|
|
600
|
+
assert.equal(h.engineCalls(), 1);
|
|
601
|
+
const rA = await h.run(coord, "same-content", { writeNamespaceOverride: "nsA" });
|
|
602
|
+
assert.equal(rA.reason, "extraction_backoff", "namespace A fingerprint is parked");
|
|
603
|
+
assert.equal(h.engineCalls(), 1);
|
|
604
|
+
|
|
605
|
+
// The identical content routed to namespace B is NOT gated by A's backoff.
|
|
606
|
+
const rB = await h.run(coord, "same-content", { writeNamespaceOverride: "nsB" });
|
|
607
|
+
assert.equal(h.engineCalls(), 2, "namespace B is not gated by namespace A's per-fingerprint backoff");
|
|
608
|
+
assert.equal(rB.status, "skipped");
|
|
609
|
+
|
|
610
|
+
// Meta isolation: each namespace persists only its own retry state.
|
|
611
|
+
const metaA = await (await h.storageForNs("nsA")).loadMeta();
|
|
612
|
+
const metaB = await (await h.storageForNs("nsB")).loadMeta();
|
|
613
|
+
assert.equal((metaA.extractionRetryState ?? []).length, 1);
|
|
614
|
+
assert.equal((metaB.extractionRetryState ?? []).length, 1);
|
|
615
|
+
} finally {
|
|
616
|
+
await h.cleanup();
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
test("namespace isolation: the provider breaker suppresses extraction across namespaces", async () => {
|
|
621
|
+
const h = await makeHarness({
|
|
622
|
+
extractionRetryScheduleMs: [3_600_000],
|
|
623
|
+
extractionBreakerFailureThreshold: 1,
|
|
624
|
+
extractionBreakerCooldownMs: 3_600_000,
|
|
625
|
+
});
|
|
626
|
+
try {
|
|
627
|
+
const coord = h.newCoordinator();
|
|
628
|
+
h.setRespond(() => failureResult("provider_retryable"));
|
|
629
|
+
await h.run(coord, "fp-a", { writeNamespaceOverride: "nsA" });
|
|
630
|
+
assert.equal(coord.getExtractionResilienceStatus().breaker.state, "open");
|
|
631
|
+
|
|
632
|
+
// A different namespace is still suppressed — the breaker is provider-global.
|
|
633
|
+
const r = await h.run(coord, "fp-b", { writeNamespaceOverride: "nsB" });
|
|
634
|
+
assert.equal(r.reason, "provider_circuit_open");
|
|
635
|
+
assert.equal(h.engineCalls(), 1, "provider-global breaker suppresses namespace B too");
|
|
636
|
+
} finally {
|
|
637
|
+
await h.cleanup();
|
|
638
|
+
}
|
|
639
|
+
});
|