@remnic/core 10.0.65 → 10.0.67
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 +16 -16
- package/dist/access-authorization-probe.js +17 -17
- package/dist/access-boundary.js +16 -16
- package/dist/access-cli.js +61 -61
- package/dist/access-extraction-force-flush.js +16 -16
- package/dist/access-http-briefing.js +17 -17
- package/dist/access-http-query.js +18 -18
- package/dist/access-http.js +33 -33
- package/dist/access-identity-continuity-surface.js +16 -16
- package/dist/access-lcm-surface.js +16 -16
- package/dist/access-mcp.js +26 -26
- package/dist/access-namespace-preflight.js +16 -16
- package/dist/access-observe-write-surface.js +16 -16
- package/dist/access-operations-batch.js +17 -17
- package/dist/access-operations.js +24 -24
- package/dist/access-recall-concurrency.js +16 -16
- package/dist/access-recall-diagnostics.js +2 -2
- package/dist/access-recall-response.js +16 -16
- package/dist/access-recall-surface.js +16 -16
- package/dist/access-service.js +16 -16
- package/dist/access-surface-catalog.js +18 -18
- package/dist/active-recall.js +3 -3
- package/dist/briefing.js +3 -3
- package/dist/buffer.js +2 -2
- package/dist/causal-consolidation.js +4 -4
- package/dist/{chunk-KISOMGY2.js → chunk-3RHU5MD7.js} +2 -2
- package/dist/{chunk-3LSTL6EY.js → chunk-4MPAKYZU.js} +2 -2
- package/dist/{chunk-OVL5AD3N.js → chunk-4NV7O76Q.js} +2 -2
- package/dist/{chunk-BH7NVSEC.js → chunk-4SOWMA55.js} +3 -3
- package/dist/{chunk-GROHOWR5.js → chunk-5MGKGEL7.js} +2 -2
- package/dist/{chunk-ZPLQ27MY.js → chunk-5UM4H64E.js} +2 -2
- package/dist/{chunk-2DNFRLLL.js → chunk-5Y6VA6X2.js} +2 -2
- package/dist/{chunk-GRAUQXQB.js → chunk-6WKWL53V.js} +13 -13
- package/dist/{chunk-HOR4RMUM.js → chunk-75DCOS6I.js} +24 -24
- package/dist/{chunk-25QK3LR2.js → chunk-7O2MC42R.js} +2 -2
- package/dist/{chunk-7ETWIBTO.js → chunk-ADYP7HXE.js} +3 -3
- package/dist/{chunk-FUDKGJDN.js → chunk-AVGDYYMA.js} +2 -2
- package/dist/{chunk-ECF627KC.js → chunk-AYJQ644F.js} +2 -2
- package/dist/{chunk-T22FBDG3.js → chunk-B3MWELZ3.js} +2 -2
- package/dist/{chunk-DRTSPRWL.js → chunk-B4DXREWC.js} +2 -2
- package/dist/{chunk-5UEGL5BF.js → chunk-CI3MAAIY.js} +2 -2
- package/dist/{chunk-7JYK7NNY.js → chunk-CTH2CQCB.js} +2 -2
- package/dist/{chunk-UVHXLVFJ.js → chunk-EURPSF4Y.js} +2 -2
- package/dist/{chunk-VPEA63FC.js → chunk-EWJZZDRG.js} +2 -2
- package/dist/{chunk-BOZQAUAJ.js → chunk-EXQJMFRF.js} +2 -2
- package/dist/{chunk-UH6ROXFJ.js → chunk-GGD2YDS6.js} +2 -2
- package/dist/{chunk-5JABRT5G.js → chunk-GHHQLSSY.js} +3 -3
- package/dist/{chunk-N4T5FW6U.js → chunk-GJ4HU6LK.js} +2 -2
- package/dist/{chunk-HRRAOTOQ.js → chunk-H5DIRCZP.js} +12 -12
- package/dist/{chunk-434E4BTO.js → chunk-HH73M65H.js} +3 -3
- package/dist/{chunk-LIGZO7XN.js → chunk-HPB5TF55.js} +2 -2
- package/dist/{chunk-E4QIX2OB.js → chunk-IAGVM7EY.js} +2 -2
- package/dist/{chunk-YJQJRXUI.js → chunk-IG3EZ3TF.js} +2 -2
- package/dist/{chunk-KNMSD2MQ.js → chunk-IVUVQPTQ.js} +2 -2
- package/dist/{chunk-XOFIJGB2.js → chunk-IZBIDPMY.js} +2 -2
- package/dist/{chunk-HFEGRMFG.js → chunk-JLGBJ4AX.js} +7 -7
- package/dist/{chunk-BBHKN2CN.js → chunk-JLMOH5JA.js} +2 -2
- package/dist/{chunk-TPCRDPUY.js → chunk-KZSGBWOJ.js} +2 -2
- package/dist/{chunk-SHHZWSSE.js → chunk-KZZ5EZ7O.js} +2 -2
- package/dist/{chunk-2ZSRACNK.js → chunk-LFYEQK45.js} +682 -549
- package/dist/chunk-LFYEQK45.js.map +1 -0
- package/dist/{chunk-FKGLJJVS.js → chunk-LN4X2NDA.js} +2 -2
- package/dist/{chunk-5ZHFPUBC.js → chunk-MOIJWWNA.js} +8 -8
- package/dist/{chunk-KO224R36.js → chunk-NCZGGGWI.js} +2 -2
- package/dist/{chunk-XVQVF2TK.js → chunk-NHWDTOBN.js} +269 -230
- package/dist/chunk-NHWDTOBN.js.map +1 -0
- package/dist/{chunk-7ZTGMEXF.js → chunk-O5ZINWW3.js} +2 -2
- package/dist/{chunk-WWH7GJ42.js → chunk-OCK4D5PC.js} +2 -2
- package/dist/{chunk-G6L3REL3.js → chunk-OPFVXM2D.js} +2 -2
- package/dist/{chunk-FR7PK7ED.js → chunk-P4TV3TQ2.js} +2 -2
- package/dist/{chunk-P2EM5LKH.js → chunk-PMDOGXA2.js} +2 -2
- package/dist/{chunk-NM2II57L.js → chunk-PRKNAIGZ.js} +2 -1
- package/dist/chunk-PRKNAIGZ.js.map +1 -0
- package/dist/{chunk-MRF5V5CD.js → chunk-QGFR73SN.js} +2 -2
- package/dist/{chunk-6D3IWYNY.js → chunk-R2BIVU2Z.js} +354 -292
- package/dist/chunk-R2BIVU2Z.js.map +1 -0
- package/dist/{chunk-HG3CG557.js → chunk-RLVI2M5Q.js} +2 -2
- package/dist/{chunk-HHL4MX3W.js → chunk-SXTVZ6JL.js} +2 -2
- package/dist/{chunk-CLSKGTAT.js → chunk-TECK5IVY.js} +2 -2
- package/dist/{chunk-SNS3DWG6.js → chunk-TVE7W67N.js} +2 -2
- package/dist/{chunk-KYSSUY66.js → chunk-U2CAGMUD.js} +2 -2
- package/dist/{chunk-XLCEFUAU.js → chunk-UVVFPCPX.js} +2 -2
- package/dist/{chunk-KQJUEQ2E.js → chunk-UW6LBFYN.js} +5 -5
- package/dist/{chunk-SEA56G6S.js → chunk-V4XE263I.js} +1 -1
- package/dist/chunk-V4XE263I.js.map +1 -0
- package/dist/{chunk-JL5HXL25.js → chunk-V6SBA5O7.js} +3 -3
- package/dist/{chunk-R7WYAXPO.js → chunk-VHSQU3RO.js} +81 -42
- package/dist/chunk-VHSQU3RO.js.map +1 -0
- package/dist/{chunk-B7VD36TI.js → chunk-WHROVQ5Q.js} +3 -3
- package/dist/{chunk-LDHR465K.js → chunk-Y3XPSGNF.js} +2 -2
- package/dist/{chunk-JPY3RMG2.js → chunk-ZORS5BG5.js} +4 -4
- package/dist/{chunk-KXY4HW2Y.js → chunk-ZXF3O7P4.js} +2 -2
- package/dist/cli.js +47 -47
- package/dist/coding/export-okf-codegraph.js +2 -2
- package/dist/compounding/engine.js +4 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -3
- package/dist/connectors/codex-materialize-runner.js +3 -3
- package/dist/connectors/index.js +3 -3
- package/dist/convergence-refresh.js +5 -5
- package/dist/corpus-watermark.js +5 -5
- package/dist/entity-retrieval.js +3 -3
- package/dist/external-wiki-access.js +17 -17
- package/dist/extraction.js +6 -6
- package/dist/harmonic-retrieval.js +3 -3
- package/dist/index.js +61 -61
- package/dist/location/index.js +5 -5
- package/dist/maintenance/memory-governance.js +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection-runner.js +5 -5
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/memory-browse-operations.js +17 -17
- package/dist/namespaces/migrate.js +4 -4
- package/dist/namespaces/storage.js +3 -3
- package/dist/operator-doctor-corpus.js +6 -6
- package/dist/operator-toolkit.js +12 -12
- package/dist/orchestration/buffer-drain-runner.js +61 -61
- package/dist/orchestration/consolidation-run.d.ts.map +1 -1
- package/dist/orchestration/extraction-drain.d.ts +4 -4
- package/dist/orchestration/extraction-drain.d.ts.map +1 -1
- package/dist/orchestration/extraction-persist-deps.d.ts +0 -2
- package/dist/orchestration/extraction-persist-deps.d.ts.map +1 -1
- package/dist/orchestration/extraction-persist.d.ts +10 -4
- package/dist/orchestration/extraction-persist.d.ts.map +1 -1
- package/dist/orchestration/extraction-queue-coordinator.d.ts +67 -33
- package/dist/orchestration/extraction-queue-coordinator.d.ts.map +1 -1
- package/dist/orchestration/extraction-run-helpers.d.ts +3 -2
- package/dist/orchestration/extraction-run-helpers.d.ts.map +1 -1
- package/dist/orchestration/extraction-run.d.ts +4 -14
- package/dist/orchestration/extraction-run.d.ts.map +1 -1
- package/dist/orchestration/fact-dedup-window.d.ts +49 -0
- package/dist/orchestration/fact-dedup-window.d.ts.map +1 -0
- package/dist/orchestration/maintenance.js +13 -13
- package/dist/orchestration/provider-breaker.d.ts +58 -0
- package/dist/orchestration/provider-breaker.d.ts.map +1 -0
- package/dist/orchestrator.d.ts +2 -17
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +61 -61
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-navigate-access.js +2 -2
- package/dist/recall-navigate-disclosure.js +2 -2
- package/dist/recall-navigate.js +2 -2
- package/dist/recall-navigation-operations.js +17 -17
- package/dist/recall-navigation.js +16 -16
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.js +2 -2
- package/dist/resume-bundles.js +4 -4
- package/dist/semantic-consolidation.js +4 -4
- package/dist/semantic-rule-promotion.js +3 -3
- package/dist/semantic-rule-verifier.js +3 -3
- package/dist/source-agent-qualifier.js +5 -5
- package/dist/standup/build.js +3 -3
- package/dist/storage/profile-mutation.d.ts +25 -0
- package/dist/storage/profile-mutation.d.ts.map +1 -0
- package/dist/storage.d.ts +4 -6
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +2 -2
- package/dist/support-passport/index.js +19 -19
- package/dist/threading.d.ts +21 -4
- package/dist/threading.d.ts.map +1 -1
- package/dist/threading.js +1 -1
- package/dist/transfer/export-okf.js +3 -3
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/verified-recall.js +3 -3
- package/dist/who-knows-cli.js +4 -4
- package/dist/who-knows.js +3 -3
- package/package.json +2 -2
- package/src/access-observe-replay-durability.test.ts +1 -1
- package/src/buffer-drain-handover.test.ts +3 -3
- package/src/buffer-drop-extraction-chunk.test.ts +0 -1
- package/src/config.test.ts +12 -0
- package/src/config.ts +2 -0
- package/src/connector-provenance-lifecycle.test.ts +5 -8
- package/src/fact-dedup-window.test.ts +116 -0
- package/src/faithfulness-graph-guard.test.ts +11 -6
- package/src/harmonic-construction.test.ts +16 -10
- package/src/orchestration/consolidation-profile-race.test.ts +178 -0
- package/src/orchestration/consolidation-run.ts +19 -7
- package/src/orchestration/extraction-drain.ts +10 -3
- package/src/orchestration/extraction-persist-deps.ts +0 -2
- package/src/orchestration/extraction-persist.ts +159 -148
- package/src/orchestration/extraction-queue-coordinator.ts +167 -56
- package/src/orchestration/extraction-run-helpers.ts +5 -4
- package/src/orchestration/extraction-run.test.ts +1 -2
- package/src/orchestration/extraction-run.ts +56 -99
- package/src/orchestration/fact-dedup-window.ts +122 -0
- package/src/orchestration/provider-breaker.test.ts +89 -0
- package/src/orchestration/provider-breaker.ts +158 -0
- package/src/orchestration/turn-ingestion.ts +5 -5
- package/src/orchestrator-extraction-queue.test.ts +179 -8
- package/src/orchestrator.ts +7 -23
- package/src/storage/profile-mutation.ts +119 -0
- package/src/storage-append-profile.test.ts +114 -0
- package/src/storage.ts +17 -48
- package/src/testing/orchestrator-lite.ts +1 -1
- package/src/threading.test.ts +153 -0
- package/src/threading.ts +95 -45
- package/src/types.ts +2 -0
- package/dist/chunk-2ZSRACNK.js.map +0 -1
- package/dist/chunk-6D3IWYNY.js.map +0 -1
- package/dist/chunk-NM2II57L.js.map +0 -1
- package/dist/chunk-R7WYAXPO.js.map +0 -1
- package/dist/chunk-SEA56G6S.js.map +0 -1
- package/dist/chunk-XVQVF2TK.js.map +0 -1
- /package/dist/{chunk-KISOMGY2.js.map → chunk-3RHU5MD7.js.map} +0 -0
- /package/dist/{chunk-3LSTL6EY.js.map → chunk-4MPAKYZU.js.map} +0 -0
- /package/dist/{chunk-OVL5AD3N.js.map → chunk-4NV7O76Q.js.map} +0 -0
- /package/dist/{chunk-BH7NVSEC.js.map → chunk-4SOWMA55.js.map} +0 -0
- /package/dist/{chunk-GROHOWR5.js.map → chunk-5MGKGEL7.js.map} +0 -0
- /package/dist/{chunk-ZPLQ27MY.js.map → chunk-5UM4H64E.js.map} +0 -0
- /package/dist/{chunk-2DNFRLLL.js.map → chunk-5Y6VA6X2.js.map} +0 -0
- /package/dist/{chunk-GRAUQXQB.js.map → chunk-6WKWL53V.js.map} +0 -0
- /package/dist/{chunk-HOR4RMUM.js.map → chunk-75DCOS6I.js.map} +0 -0
- /package/dist/{chunk-25QK3LR2.js.map → chunk-7O2MC42R.js.map} +0 -0
- /package/dist/{chunk-7ETWIBTO.js.map → chunk-ADYP7HXE.js.map} +0 -0
- /package/dist/{chunk-FUDKGJDN.js.map → chunk-AVGDYYMA.js.map} +0 -0
- /package/dist/{chunk-ECF627KC.js.map → chunk-AYJQ644F.js.map} +0 -0
- /package/dist/{chunk-T22FBDG3.js.map → chunk-B3MWELZ3.js.map} +0 -0
- /package/dist/{chunk-DRTSPRWL.js.map → chunk-B4DXREWC.js.map} +0 -0
- /package/dist/{chunk-5UEGL5BF.js.map → chunk-CI3MAAIY.js.map} +0 -0
- /package/dist/{chunk-7JYK7NNY.js.map → chunk-CTH2CQCB.js.map} +0 -0
- /package/dist/{chunk-UVHXLVFJ.js.map → chunk-EURPSF4Y.js.map} +0 -0
- /package/dist/{chunk-VPEA63FC.js.map → chunk-EWJZZDRG.js.map} +0 -0
- /package/dist/{chunk-BOZQAUAJ.js.map → chunk-EXQJMFRF.js.map} +0 -0
- /package/dist/{chunk-UH6ROXFJ.js.map → chunk-GGD2YDS6.js.map} +0 -0
- /package/dist/{chunk-5JABRT5G.js.map → chunk-GHHQLSSY.js.map} +0 -0
- /package/dist/{chunk-N4T5FW6U.js.map → chunk-GJ4HU6LK.js.map} +0 -0
- /package/dist/{chunk-HRRAOTOQ.js.map → chunk-H5DIRCZP.js.map} +0 -0
- /package/dist/{chunk-434E4BTO.js.map → chunk-HH73M65H.js.map} +0 -0
- /package/dist/{chunk-LIGZO7XN.js.map → chunk-HPB5TF55.js.map} +0 -0
- /package/dist/{chunk-E4QIX2OB.js.map → chunk-IAGVM7EY.js.map} +0 -0
- /package/dist/{chunk-YJQJRXUI.js.map → chunk-IG3EZ3TF.js.map} +0 -0
- /package/dist/{chunk-KNMSD2MQ.js.map → chunk-IVUVQPTQ.js.map} +0 -0
- /package/dist/{chunk-XOFIJGB2.js.map → chunk-IZBIDPMY.js.map} +0 -0
- /package/dist/{chunk-HFEGRMFG.js.map → chunk-JLGBJ4AX.js.map} +0 -0
- /package/dist/{chunk-BBHKN2CN.js.map → chunk-JLMOH5JA.js.map} +0 -0
- /package/dist/{chunk-TPCRDPUY.js.map → chunk-KZSGBWOJ.js.map} +0 -0
- /package/dist/{chunk-SHHZWSSE.js.map → chunk-KZZ5EZ7O.js.map} +0 -0
- /package/dist/{chunk-FKGLJJVS.js.map → chunk-LN4X2NDA.js.map} +0 -0
- /package/dist/{chunk-5ZHFPUBC.js.map → chunk-MOIJWWNA.js.map} +0 -0
- /package/dist/{chunk-KO224R36.js.map → chunk-NCZGGGWI.js.map} +0 -0
- /package/dist/{chunk-7ZTGMEXF.js.map → chunk-O5ZINWW3.js.map} +0 -0
- /package/dist/{chunk-WWH7GJ42.js.map → chunk-OCK4D5PC.js.map} +0 -0
- /package/dist/{chunk-G6L3REL3.js.map → chunk-OPFVXM2D.js.map} +0 -0
- /package/dist/{chunk-FR7PK7ED.js.map → chunk-P4TV3TQ2.js.map} +0 -0
- /package/dist/{chunk-P2EM5LKH.js.map → chunk-PMDOGXA2.js.map} +0 -0
- /package/dist/{chunk-MRF5V5CD.js.map → chunk-QGFR73SN.js.map} +0 -0
- /package/dist/{chunk-HG3CG557.js.map → chunk-RLVI2M5Q.js.map} +0 -0
- /package/dist/{chunk-HHL4MX3W.js.map → chunk-SXTVZ6JL.js.map} +0 -0
- /package/dist/{chunk-CLSKGTAT.js.map → chunk-TECK5IVY.js.map} +0 -0
- /package/dist/{chunk-SNS3DWG6.js.map → chunk-TVE7W67N.js.map} +0 -0
- /package/dist/{chunk-KYSSUY66.js.map → chunk-U2CAGMUD.js.map} +0 -0
- /package/dist/{chunk-XLCEFUAU.js.map → chunk-UVVFPCPX.js.map} +0 -0
- /package/dist/{chunk-KQJUEQ2E.js.map → chunk-UW6LBFYN.js.map} +0 -0
- /package/dist/{chunk-JL5HXL25.js.map → chunk-V6SBA5O7.js.map} +0 -0
- /package/dist/{chunk-B7VD36TI.js.map → chunk-WHROVQ5Q.js.map} +0 -0
- /package/dist/{chunk-LDHR465K.js.map → chunk-Y3XPSGNF.js.map} +0 -0
- /package/dist/{chunk-JPY3RMG2.js.map → chunk-ZORS5BG5.js.map} +0 -0
- /package/dist/{chunk-KXY4HW2Y.js.map → chunk-ZXF3O7P4.js.map} +0 -0
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Extraction queue coordinator — extracted from the orchestrator (issue #1526).
|
|
3
3
|
*
|
|
4
|
-
* Owns the background
|
|
5
|
-
*
|
|
4
|
+
* Owns the background bounded-parallel queue that drains extraction tasks
|
|
5
|
+
* while avoiding races between concurrent flush/heartbeat/bulk-import
|
|
6
6
|
* triggers:
|
|
7
|
-
* - queue state (`
|
|
7
|
+
* - queue state (`queue` + `processing` + per-task preempt slots)
|
|
8
8
|
* - the scheduling trigger (start the drain when the first task lands and
|
|
9
|
-
*
|
|
10
|
-
* - the
|
|
9
|
+
* capacity is free)
|
|
10
|
+
* - the drain loop itself (`processQueue`): up to `concurrency` tasks run
|
|
11
|
+
* at once; tasks sharing an entry `key` (a buffer/session) never run
|
|
12
|
+
* concurrently, so the same buffer is never extracted twice in parallel
|
|
11
13
|
* - failure classification (`logExtractionQueueFailure`) — issue #549:
|
|
12
14
|
* AbortError from session transitions logs at debug, real failures at
|
|
13
15
|
* error
|
|
@@ -19,51 +21,102 @@
|
|
|
19
21
|
* orchestrator builds each task closure and hands it to `enqueue`; the
|
|
20
22
|
* coordinator owns only the queue mechanics.
|
|
21
23
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
24
|
+
* Preemption is per task: each running task receives an
|
|
25
|
+
* `ExtractionPreemptHandle` at run time and registers its own abort, so two
|
|
26
|
+
* concurrent automatic extractions cannot clobber each other's registration
|
|
27
|
+
* (the previous single-slot `runningPreempt` could only hold one).
|
|
28
|
+
* `preemptRunning` aborts at most ONE armed task — the requested key's own
|
|
29
|
+
* running work when it is armed, else the most recently armed task — which
|
|
30
|
+
* is exactly the single-slot semantics generalized from one worker to N.
|
|
25
31
|
*/
|
|
26
32
|
|
|
27
33
|
import { isAbortError, preemptedExtractionError } from "../abort-error.js";
|
|
28
34
|
import { log } from "../logger.js";
|
|
29
35
|
|
|
36
|
+
/** Preemption registration for ONE running task. Armed work can be aborted
|
|
37
|
+
* by a fail-closed flush; disarmed work (past persist) cannot. */
|
|
38
|
+
export interface ExtractionPreemptHandle {
|
|
39
|
+
/** Register the task's abort (replacing any previous registration). */
|
|
40
|
+
arm(abort: () => void): void;
|
|
41
|
+
/** Remove the task's registration (idempotent). */
|
|
42
|
+
disarm(): void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A queued task; the coordinator passes the task's own preempt handle. */
|
|
46
|
+
export type ExtractionQueueTask = (preempt?: ExtractionPreemptHandle) => Promise<void>;
|
|
47
|
+
|
|
48
|
+
export interface ExtractionQueueEnqueueOptions {
|
|
49
|
+
/** Jump the queue and preempt the running automatic work (force flush). */
|
|
50
|
+
front?: boolean;
|
|
51
|
+
/** Buffer/session key. Tasks sharing a key never run concurrently. */
|
|
52
|
+
key?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface QueueEntry {
|
|
56
|
+
run: ExtractionQueueTask;
|
|
57
|
+
key?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface PreemptSlot {
|
|
61
|
+
key?: string;
|
|
62
|
+
armed?: { abort: () => void; seq: number };
|
|
63
|
+
}
|
|
64
|
+
|
|
30
65
|
/**
|
|
31
|
-
* Coordinates the background
|
|
32
|
-
*
|
|
66
|
+
* Coordinates the background bounded-parallel extraction queue. Owns the
|
|
67
|
+
* queue array, the per-key in-flight guard, and the per-task preempt slots.
|
|
33
68
|
*/
|
|
34
69
|
export class ExtractionQueueCoordinator {
|
|
70
|
+
/** Maximum tasks that run at the same time (config `extractionQueueConcurrency`). */
|
|
71
|
+
private readonly concurrency: number;
|
|
35
72
|
/**
|
|
36
|
-
* Background
|
|
37
|
-
*
|
|
73
|
+
* Background queue for extractions (agent_end optimization). Queue stores
|
|
74
|
+
* tasks that resolve when extraction should run.
|
|
38
75
|
*/
|
|
39
|
-
private readonly queue:
|
|
40
|
-
/** Whether the
|
|
76
|
+
private readonly queue: QueueEntry[] = [];
|
|
77
|
+
/** Whether the drain loop is currently running. */
|
|
41
78
|
private processing = false;
|
|
42
79
|
private accepting = true;
|
|
43
|
-
|
|
80
|
+
/** Buffer keys with a task currently running (per-key exclusion). */
|
|
81
|
+
private readonly activeKeys = new Set<string>();
|
|
82
|
+
/** Preempt slots of the currently running tasks, keyed by slot identity. */
|
|
83
|
+
private readonly preemptSlots = new Map<object, PreemptSlot>();
|
|
84
|
+
private preemptSeq = 0;
|
|
85
|
+
/** Resolves the processor's park while a new task arrives mid-drain. */
|
|
86
|
+
private wake?: () => void;
|
|
87
|
+
|
|
88
|
+
constructor(concurrency = 1) {
|
|
89
|
+
this.concurrency = Number.isInteger(concurrency) && concurrency >= 1 ? concurrency : 1;
|
|
90
|
+
}
|
|
44
91
|
|
|
45
92
|
/** Current queue depth (idle-wait + test seam). */
|
|
46
93
|
get length(): number {
|
|
47
94
|
return this.queue.length;
|
|
48
95
|
}
|
|
49
96
|
|
|
50
|
-
/** Whether the
|
|
97
|
+
/** Whether the drain loop is currently running (idle-wait + test seam). */
|
|
51
98
|
get isProcessing(): boolean {
|
|
52
99
|
return this.processing;
|
|
53
100
|
}
|
|
54
101
|
|
|
102
|
+
/** How many tasks may run at once (test seam + diagnostics). */
|
|
103
|
+
get maxConcurrency(): number {
|
|
104
|
+
return this.concurrency;
|
|
105
|
+
}
|
|
106
|
+
|
|
55
107
|
/**
|
|
56
|
-
* Enqueue a task and start the
|
|
108
|
+
* Enqueue a task and start the processor when capacity is free. This is
|
|
57
109
|
* the production entry point — the orchestrator's `queueBufferedExtraction`
|
|
58
|
-
* builds each task closure and hands it here.
|
|
110
|
+
* builds each task closure and hands it here with its buffer key.
|
|
59
111
|
*/
|
|
60
|
-
enqueue(task:
|
|
112
|
+
enqueue(task: ExtractionQueueTask, options?: ExtractionQueueEnqueueOptions): boolean {
|
|
61
113
|
if (!this.accepting) return false;
|
|
114
|
+
const entry: QueueEntry = { run: task, key: options?.key };
|
|
62
115
|
if (options?.front) {
|
|
63
|
-
this.preemptRunning();
|
|
64
|
-
this.queue.unshift(
|
|
116
|
+
this.preemptRunning(options.key);
|
|
117
|
+
this.queue.unshift(entry);
|
|
65
118
|
} else {
|
|
66
|
-
this.queue.push(
|
|
119
|
+
this.queue.push(entry);
|
|
67
120
|
}
|
|
68
121
|
if (!this.processing) {
|
|
69
122
|
this.processing = true;
|
|
@@ -71,38 +124,43 @@ export class ExtractionQueueCoordinator {
|
|
|
71
124
|
this.logExtractionQueueFailure(err, "processor");
|
|
72
125
|
this.processing = false;
|
|
73
126
|
});
|
|
127
|
+
} else {
|
|
128
|
+
// The processor may be parked waiting for an in-flight task while a
|
|
129
|
+
// slot is free; wake it so the new task starts instead of waiting
|
|
130
|
+
// behind work it could overlap with.
|
|
131
|
+
this.wake?.();
|
|
74
132
|
}
|
|
75
133
|
return true;
|
|
76
134
|
}
|
|
77
135
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
preemptRunning(): void {
|
|
88
|
-
const abort = this.runningPreempt;
|
|
89
|
-
this.runningPreempt = undefined;
|
|
136
|
+
preemptRunning(preferredKey?: string): void {
|
|
137
|
+
const armed = Array.from(this.preemptSlots.values()).filter((slot) => slot.armed !== undefined);
|
|
138
|
+
if (armed.length === 0) return;
|
|
139
|
+
const preferred = preferredKey !== undefined ? armed.find((slot) => slot.key === preferredKey) : undefined;
|
|
140
|
+
const chosen =
|
|
141
|
+
preferred ??
|
|
142
|
+
armed.reduce((a, b) => ((b.armed?.seq ?? -1) > (a.armed?.seq ?? -1) ? b : a));
|
|
143
|
+
const abort = chosen.armed?.abort;
|
|
144
|
+
chosen.armed = undefined;
|
|
90
145
|
abort?.();
|
|
91
146
|
}
|
|
147
|
+
|
|
92
148
|
runPreemptable<T>(
|
|
149
|
+
preempt: ExtractionPreemptHandle | undefined,
|
|
93
150
|
preemptable: boolean,
|
|
94
151
|
parent: AbortSignal | undefined,
|
|
95
152
|
run: (signal: AbortSignal) => Promise<T>,
|
|
96
153
|
): Promise<T> {
|
|
97
|
-
const
|
|
98
|
-
if (preemptable
|
|
99
|
-
|
|
154
|
+
const preemptController = new AbortController();
|
|
155
|
+
if (preemptable && preempt) {
|
|
156
|
+
preempt.arm(() => preemptController.abort(preemptedExtractionError()));
|
|
157
|
+
}
|
|
158
|
+
const signal = parent ? AbortSignal.any([parent, preemptController.signal]) : preemptController.signal;
|
|
100
159
|
return Promise.resolve(run(signal)).finally(() => {
|
|
101
|
-
if (preemptable)
|
|
160
|
+
if (preemptable && preempt) preempt.disarm();
|
|
102
161
|
});
|
|
103
162
|
}
|
|
104
163
|
|
|
105
|
-
|
|
106
164
|
/** Stop new producers from entering the queue. */
|
|
107
165
|
stopAccepting(): void {
|
|
108
166
|
this.accepting = false;
|
|
@@ -123,30 +181,82 @@ export class ExtractionQueueCoordinator {
|
|
|
123
181
|
}
|
|
124
182
|
|
|
125
183
|
/**
|
|
126
|
-
* Background
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* semantics directly.
|
|
184
|
+
* Background queue processor. Runs up to `concurrency` tasks at a time,
|
|
185
|
+
* never two tasks with the same entry key. Called automatically when items
|
|
186
|
+
* are queued via `enqueue`; also exposed for the characterization tests
|
|
187
|
+
* that push raw tasks and assert drain semantics directly.
|
|
131
188
|
*/
|
|
132
189
|
async processQueue(): Promise<void> {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
190
|
+
const inFlight = new Set<Promise<void>>();
|
|
191
|
+
try {
|
|
192
|
+
while (this.queue.length > 0 || inFlight.size > 0) {
|
|
193
|
+
while (inFlight.size < this.concurrency) {
|
|
194
|
+
const entry = this.takeRunnable();
|
|
195
|
+
if (!entry) break;
|
|
196
|
+
const running = this.runEntry(entry).finally(() => {
|
|
197
|
+
inFlight.delete(running);
|
|
198
|
+
});
|
|
199
|
+
inFlight.add(running);
|
|
200
|
+
}
|
|
201
|
+
// Park until a task settles OR a new task is enqueued. A queued task
|
|
202
|
+
// whose key is still held always implies an in-flight task holding
|
|
203
|
+
// it, so inFlight is non-empty here whenever the loop must wait.
|
|
204
|
+
if (inFlight.size === 0) break;
|
|
205
|
+
const { promise: wakePromise, resolve: wake } = Promise.withResolvers<void>();
|
|
206
|
+
this.wake = wake;
|
|
136
207
|
try {
|
|
137
|
-
await
|
|
138
|
-
}
|
|
139
|
-
this.
|
|
208
|
+
await Promise.race([wakePromise, ...inFlight]);
|
|
209
|
+
} finally {
|
|
210
|
+
this.wake = undefined;
|
|
140
211
|
}
|
|
141
212
|
}
|
|
213
|
+
} finally {
|
|
214
|
+
this.processing = false;
|
|
142
215
|
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Take the next queue entry whose key is not already running (FIFO within a key). */
|
|
219
|
+
private takeRunnable(): QueueEntry | undefined {
|
|
220
|
+
for (let index = 0; index < this.queue.length; index += 1) {
|
|
221
|
+
const entry = this.queue[index];
|
|
222
|
+
if (entry.key !== undefined && this.activeKeys.has(entry.key)) continue;
|
|
223
|
+
this.queue.splice(index, 1);
|
|
224
|
+
if (entry.key !== undefined) this.activeKeys.add(entry.key);
|
|
225
|
+
return entry;
|
|
226
|
+
}
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Run one entry with its own preempt slot; failures are classified + logged. */
|
|
231
|
+
private async runEntry(entry: QueueEntry): Promise<void> {
|
|
232
|
+
const slot: PreemptSlot = {};
|
|
233
|
+
this.preemptSlots.set(slot, { key: entry.key });
|
|
234
|
+
try {
|
|
235
|
+
await entry.run(this.createPreemptHandle(slot));
|
|
236
|
+
} catch (err) {
|
|
237
|
+
this.logExtractionQueueFailure(err, "task");
|
|
238
|
+
} finally {
|
|
239
|
+
this.preemptSlots.delete(slot);
|
|
240
|
+
if (entry.key !== undefined) this.activeKeys.delete(entry.key);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
143
243
|
|
|
144
|
-
|
|
244
|
+
private createPreemptHandle(slot: PreemptSlot): ExtractionPreemptHandle {
|
|
245
|
+
return {
|
|
246
|
+
arm: (abort: () => void) => {
|
|
247
|
+
const state = this.preemptSlots.get(slot);
|
|
248
|
+
if (state) state.armed = { abort, seq: this.preemptSeq++ };
|
|
249
|
+
},
|
|
250
|
+
disarm: () => {
|
|
251
|
+
const state = this.preemptSlots.get(slot);
|
|
252
|
+
if (state) state.armed = undefined;
|
|
253
|
+
},
|
|
254
|
+
};
|
|
145
255
|
}
|
|
146
256
|
|
|
147
257
|
/**
|
|
148
258
|
* Classify + log a failure from either the per-task catch inside
|
|
149
|
-
* `
|
|
259
|
+
* `runEntry` or the outer `processQueue().catch(...)` in
|
|
150
260
|
* `enqueue()`. Issue #549: `throwIfRecallAborted`
|
|
151
261
|
* (used throughout `runExtraction`) raises an Error whose `name` is
|
|
152
262
|
* `"AbortError"`. That path fires when `before_reset` aborts a
|
|
@@ -211,13 +321,14 @@ export class ExtractionQueueCoordinator {
|
|
|
211
321
|
* pre-extraction `this.extractionQueue.push(...)` used by the
|
|
212
322
|
* characterization tests that drive the drain manually via `processQueue`.
|
|
213
323
|
*/
|
|
214
|
-
pushRaw(task:
|
|
215
|
-
this.queue.push(task);
|
|
324
|
+
pushRaw(task: ExtractionQueueTask, options?: { key?: string }): void {
|
|
325
|
+
this.queue.push({ run: task, key: options?.key });
|
|
326
|
+
this.wake?.();
|
|
216
327
|
}
|
|
217
328
|
|
|
218
329
|
/** Remove + return the front task (test seam for the flush deadline tests). */
|
|
219
|
-
shift():
|
|
220
|
-
return this.queue.shift();
|
|
330
|
+
shift(): ExtractionQueueTask | undefined {
|
|
331
|
+
return this.queue.shift()?.run;
|
|
221
332
|
}
|
|
222
333
|
|
|
223
334
|
/**
|
|
@@ -177,7 +177,7 @@ export async function runExtractionPostPersistBestEffort(
|
|
|
177
177
|
export interface ExtractionDeferRetentionOptions {
|
|
178
178
|
clearBufferAfterExtraction: boolean;
|
|
179
179
|
extractionJudgeShadow: boolean;
|
|
180
|
-
|
|
180
|
+
deferredCount: number;
|
|
181
181
|
normalizedTurns: BufferTurn[];
|
|
182
182
|
bufferKey: string;
|
|
183
183
|
retainDeferredTurns: (bufferKey: string, turns: BufferTurn[], max: number) => Promise<void>;
|
|
@@ -193,7 +193,7 @@ export async function runExtractionDeferRetention(
|
|
|
193
193
|
): Promise<void> {
|
|
194
194
|
await runPostPersistBestEffort("during_defer_retention", async () => {
|
|
195
195
|
if (options.clearBufferAfterExtraction && !options.extractionJudgeShadow) {
|
|
196
|
-
const deferredCount = options.
|
|
196
|
+
const deferredCount = options.deferredCount;
|
|
197
197
|
if (deferredCount > 0 && options.normalizedTurns.length > 0) {
|
|
198
198
|
await options.retainDeferredTurns(options.bufferKey, options.normalizedTurns, 10);
|
|
199
199
|
} else {
|
|
@@ -229,7 +229,8 @@ export interface ExtractionPostPersistOptions {
|
|
|
229
229
|
options?: { ignoreAbort?: boolean; ignoreDeadline?: boolean; propagateErrors?: boolean },
|
|
230
230
|
) => Promise<void>;
|
|
231
231
|
extractionJudgeShadow: boolean;
|
|
232
|
-
|
|
232
|
+
/** Facts this run's persist deferred to the judge — drives retention (#1523). */
|
|
233
|
+
deferredCount: number;
|
|
233
234
|
normalizedTurns: BufferTurn[];
|
|
234
235
|
bufferKey: string;
|
|
235
236
|
retainDeferredTurns: (bufferKey: string, turns: BufferTurn[], max: number) => Promise<void>;
|
|
@@ -283,7 +284,7 @@ export async function runExtractionPostPersist(
|
|
|
283
284
|
await runExtractionDeferRetention(runBestEffort, {
|
|
284
285
|
clearBufferAfterExtraction: options.clearBufferAfterExtraction,
|
|
285
286
|
extractionJudgeShadow: options.extractionJudgeShadow,
|
|
286
|
-
|
|
287
|
+
deferredCount: options.deferredCount,
|
|
287
288
|
normalizedTurns: options.normalizedTurns,
|
|
288
289
|
bufferKey: options.bufferKey,
|
|
289
290
|
retainDeferredTurns: options.retainDeferredTurns,
|
|
@@ -175,7 +175,7 @@ async function makeHarness(overrides: Record<string, unknown> = {}): Promise<Har
|
|
|
175
175
|
persistExtraction: async () => {
|
|
176
176
|
persistCalls += 1;
|
|
177
177
|
await persistHandler();
|
|
178
|
-
return { persistedIds: ["memory-1"], memoryPathById: new Map() };
|
|
178
|
+
return { persistedIds: ["memory-1"], memoryPathById: new Map(), deferredCount: 0, pendingReviewIds: [] };
|
|
179
179
|
},
|
|
180
180
|
maybeCapturePassiveCorrections: async (_turns, options) => {
|
|
181
181
|
await passiveCaptureHandler();
|
|
@@ -193,7 +193,6 @@ async function makeHarness(overrides: Record<string, unknown> = {}): Promise<Har
|
|
|
193
193
|
maybeScheduleConsolidation: () => {},
|
|
194
194
|
requestQmdMaintenance: () => {},
|
|
195
195
|
runTierMigrationCycle: async () => migrationSummary,
|
|
196
|
-
getLastPersistExtractionDeferredCount: () => 0,
|
|
197
196
|
recordProcessedExtractionFingerprint: async (storage, fingerprint, meta) => {
|
|
198
197
|
recordedProcessedCount += 1;
|
|
199
198
|
await fingerprintRecorder(storage, fingerprint, meta);
|
|
@@ -71,7 +71,7 @@ export interface ExtractionRunCoordinatorDeps {
|
|
|
71
71
|
sourceText?: string,
|
|
72
72
|
graphCaps?: GraphConstructionCapabilitySet,
|
|
73
73
|
lifecycleCaps?: MemoryLifecycleCapabilitySet
|
|
74
|
-
) => Promise<
|
|
74
|
+
) => Promise<ExtractionPersistResult>;
|
|
75
75
|
|
|
76
76
|
maybeCapturePassiveCorrections: (
|
|
77
77
|
turns: readonly BufferTurn[],
|
|
@@ -89,7 +89,7 @@ export interface ExtractionRunCoordinatorDeps {
|
|
|
89
89
|
getCodingContextForSession: (sessionKey: string) => CodingContext | null;
|
|
90
90
|
applyCodingNamespaceOverlay: (sessionKey: string, namespace: string) => string;
|
|
91
91
|
boxBuilderFor: (storage: StorageManager) => Pick<BoxBuilder, "onExtraction">;
|
|
92
|
-
appendPersistedThreadEpisodes: (threadId: string, ids: string[]) => Promise<void>;
|
|
92
|
+
appendPersistedThreadEpisodes: (threadId: string, ids: string[], pendingReviewIds: string[]) => Promise<void>;
|
|
93
93
|
maybeScheduleConsolidation: (nonZero: boolean) => void;
|
|
94
94
|
requestQmdMaintenance: () => void;
|
|
95
95
|
runTierMigrationCycle: (
|
|
@@ -101,7 +101,6 @@ export interface ExtractionRunCoordinatorDeps {
|
|
|
101
101
|
force?: boolean;
|
|
102
102
|
}
|
|
103
103
|
) => Promise<TierMigrationCycleSummary>;
|
|
104
|
-
getLastPersistExtractionDeferredCount: () => number;
|
|
105
104
|
recordProcessedExtractionFingerprint: (
|
|
106
105
|
storage: StorageManager,
|
|
107
106
|
fingerprint: string,
|
|
@@ -125,6 +124,8 @@ import {
|
|
|
125
124
|
EXTRACTION_RETRY_STATE_MAX_ENTRIES,
|
|
126
125
|
} from "./extraction-run-helpers.js";
|
|
127
126
|
import type { ExtractionResilienceStatus, ExtractionRetryStateEntry } from "./extraction-run-helpers.js";
|
|
127
|
+
import type { ExtractionPersistResult } from "./extraction-persist.js";
|
|
128
|
+
import { ProviderCircuitBreaker, type ProbeOwnerToken } from "./provider-breaker.js";
|
|
128
129
|
|
|
129
130
|
export {
|
|
130
131
|
capExtractionRetryStateEntries,
|
|
@@ -150,12 +151,7 @@ export class ExtractionRunCoordinator {
|
|
|
150
151
|
// backoff still throttles, so a restart can't resurrect the hot loop.
|
|
151
152
|
private readonly extractionRetryState = new Map<string, Map<string, ExtractionRetryStateEntry>>();
|
|
152
153
|
private readonly hydratedRetryNamespaces = new Set<string>();
|
|
153
|
-
private providerBreaker
|
|
154
|
-
consecutiveFailures: number;
|
|
155
|
-
openUntilMs: number;
|
|
156
|
-
state: "closed" | "open" | "half_open";
|
|
157
|
-
lastReason: string;
|
|
158
|
-
} = { consecutiveFailures: 0, openUntilMs: 0, state: "closed", lastReason: "" };
|
|
154
|
+
private readonly providerBreaker = new ProviderCircuitBreaker();
|
|
159
155
|
|
|
160
156
|
constructor(private readonly deps: ExtractionRunCoordinatorDeps) {}
|
|
161
157
|
|
|
@@ -282,65 +278,6 @@ export class ExtractionRunCoordinator {
|
|
|
282
278
|
this.extractionRetryState.set(namespace, nsMap);
|
|
283
279
|
}
|
|
284
280
|
|
|
285
|
-
private logBreakerTransition(state: "closed" | "open" | "half_open"): void {
|
|
286
|
-
// Once per state change (not per attempt) — AGENTS.md observability rule.
|
|
287
|
-
log.warn("extraction: provider circuit breaker transition", {
|
|
288
|
-
state,
|
|
289
|
-
consecutiveFailures: this.providerBreaker.consecutiveFailures,
|
|
290
|
-
lastReason: this.providerBreaker.lastReason,
|
|
291
|
-
openUntilMs: this.providerBreaker.openUntilMs,
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/** True while the breaker suppresses non-forced attempts. Flips an expired
|
|
296
|
-
* `open` breaker to `half_open`, which allows exactly one probe (the next
|
|
297
|
-
* non-forced attempt) because the extraction queue is a serial drain. */
|
|
298
|
-
private isProviderBreakerOpen(now: number): boolean {
|
|
299
|
-
if (now < this.providerBreaker.openUntilMs) return true;
|
|
300
|
-
if (this.providerBreaker.state === "open") {
|
|
301
|
-
this.providerBreaker.state = "half_open";
|
|
302
|
-
this.logBreakerTransition("half_open");
|
|
303
|
-
}
|
|
304
|
-
return false;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
private onProviderFailure(cls: ExtractionFailureClass, now: number): void {
|
|
308
|
-
this.providerBreaker.consecutiveFailures += 1;
|
|
309
|
-
const trip =
|
|
310
|
-
cls === "auth_config" ||
|
|
311
|
-
// A failed half-open probe re-opens immediately (standard breaker
|
|
312
|
-
// semantics). Without this, an auth_config-opened breaker (which trips
|
|
313
|
-
// below the threshold) would get stuck half_open after a transient
|
|
314
|
-
// probe failure and stop suppressing (cursor review).
|
|
315
|
-
this.providerBreaker.state === "half_open" ||
|
|
316
|
-
this.providerBreaker.consecutiveFailures >= this.config.extractionBreakerFailureThreshold;
|
|
317
|
-
if (!trip) return;
|
|
318
|
-
const cooldown =
|
|
319
|
-
cls === "auth_config"
|
|
320
|
-
? this.config.extractionBreakerAuthCooldownMs
|
|
321
|
-
: this.config.extractionBreakerCooldownMs;
|
|
322
|
-
this.providerBreaker.openUntilMs = now + Math.max(0, cooldown);
|
|
323
|
-
this.providerBreaker.lastReason = cls;
|
|
324
|
-
if (this.providerBreaker.state !== "open") {
|
|
325
|
-
this.providerBreaker.state = "open";
|
|
326
|
-
this.logBreakerTransition("open");
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
private resetProviderBreakerOnSuccess(): void {
|
|
331
|
-
const wasDisturbed =
|
|
332
|
-
this.providerBreaker.state !== "closed" ||
|
|
333
|
-
this.providerBreaker.consecutiveFailures > 0 ||
|
|
334
|
-
this.providerBreaker.openUntilMs > 0;
|
|
335
|
-
if (!wasDisturbed) return;
|
|
336
|
-
const wasOpen = this.providerBreaker.state !== "closed";
|
|
337
|
-
this.providerBreaker.consecutiveFailures = 0;
|
|
338
|
-
this.providerBreaker.openUntilMs = 0;
|
|
339
|
-
this.providerBreaker.lastReason = "";
|
|
340
|
-
this.providerBreaker.state = "closed";
|
|
341
|
-
if (wasOpen) this.logBreakerTransition("closed");
|
|
342
|
-
}
|
|
343
|
-
|
|
344
281
|
/** Rebuild `meta.extractionRetryState` from the in-memory namespace mirror,
|
|
345
282
|
* bounded to the newest entries, and prune the mirror to match. */
|
|
346
283
|
private persistRetryStateToMeta(namespace: string, meta: MetaState): void {
|
|
@@ -370,10 +307,11 @@ export class ExtractionRunCoordinator {
|
|
|
370
307
|
cls: ExtractionFailureClass,
|
|
371
308
|
meta: MetaState,
|
|
372
309
|
now: number,
|
|
310
|
+
probeToken?: ProbeOwnerToken,
|
|
373
311
|
): Promise<void> {
|
|
374
312
|
try {
|
|
375
313
|
this.hydrateRetryStateFromMeta(namespace, meta);
|
|
376
|
-
this.applyExtractionFailureToMemory(namespace, fingerprint, cls, now);
|
|
314
|
+
this.applyExtractionFailureToMemory(namespace, fingerprint, cls, now, probeToken);
|
|
377
315
|
this.persistRetryStateToMeta(namespace, meta);
|
|
378
316
|
// The caller's storage.updateMeta persists this — saving here would write the same file twice.
|
|
379
317
|
} catch (err) {
|
|
@@ -389,6 +327,7 @@ export class ExtractionRunCoordinator {
|
|
|
389
327
|
fingerprint: string,
|
|
390
328
|
cls: ExtractionFailureClass,
|
|
391
329
|
now: number,
|
|
330
|
+
probeToken?: ProbeOwnerToken,
|
|
392
331
|
): void {
|
|
393
332
|
const nsMap = this.extractionRetryState.get(namespace) ?? new Map<string, ExtractionRetryStateEntry>();
|
|
394
333
|
const prev = nsMap.get(fingerprint);
|
|
@@ -409,7 +348,11 @@ export class ExtractionRunCoordinator {
|
|
|
409
348
|
}
|
|
410
349
|
nsMap.set(fingerprint, { attempts, nextEligibleAtMs, firstFailedAtMs, lastFailureClass: cls });
|
|
411
350
|
this.extractionRetryState.set(namespace, nsMap);
|
|
412
|
-
this.
|
|
351
|
+
this.providerBreaker.failure(cls, now, {
|
|
352
|
+
failureThreshold: this.config.extractionBreakerFailureThreshold,
|
|
353
|
+
cooldownMs: this.config.extractionBreakerCooldownMs,
|
|
354
|
+
authCooldownMs: this.config.extractionBreakerAuthCooldownMs,
|
|
355
|
+
}, probeToken);
|
|
413
356
|
}
|
|
414
357
|
|
|
415
358
|
/** Delete a fingerprint's retry entry from the mirror + meta. Returns whether
|
|
@@ -430,12 +373,7 @@ export class ExtractionRunCoordinator {
|
|
|
430
373
|
backoffFingerprintCount += nsMap.size;
|
|
431
374
|
}
|
|
432
375
|
return {
|
|
433
|
-
breaker:
|
|
434
|
-
state: this.providerBreaker.state,
|
|
435
|
-
openUntilMs: this.providerBreaker.openUntilMs,
|
|
436
|
-
consecutiveFailures: this.providerBreaker.consecutiveFailures,
|
|
437
|
-
lastReason: this.providerBreaker.lastReason,
|
|
438
|
-
},
|
|
376
|
+
breaker: this.providerBreaker.snapshot(),
|
|
439
377
|
backoffFingerprintCount,
|
|
440
378
|
};
|
|
441
379
|
}
|
|
@@ -834,13 +772,18 @@ export class ExtractionRunCoordinator {
|
|
|
834
772
|
// Resolve the gate through the shared lifecycle capability plan (issue
|
|
835
773
|
// #1523) rather than reading the raw config flag at each call site.
|
|
836
774
|
const extractionRetryEnabled = resolveMemoryLifecycleCapabilities(this.config).extractionRetry;
|
|
775
|
+
let extractionProbeOwned = false;
|
|
776
|
+
let extractionProbeToken: ProbeOwnerToken | undefined;
|
|
837
777
|
if (extractionFingerprint && !forcedExtractionAttempt && extractionRetryEnabled) {
|
|
838
778
|
try {
|
|
839
779
|
meta ??= await runDeadlineAware(() => storage.loadMeta(), "during_retry_gate");
|
|
840
780
|
this.hydrateRetryStateFromMeta(selfNamespace, meta);
|
|
841
781
|
const nowMs = Date.now();
|
|
782
|
+
const probeAttempt = this.providerBreaker.acquire(nowMs);
|
|
783
|
+
extractionProbeOwned = probeAttempt.probe;
|
|
784
|
+
extractionProbeToken = probeAttempt.token;
|
|
842
785
|
let suppressReason: "provider_circuit_open" | "extraction_backoff" | null = null;
|
|
843
|
-
if (
|
|
786
|
+
if (probeAttempt.suppressed) {
|
|
844
787
|
suppressReason = "provider_circuit_open";
|
|
845
788
|
} else {
|
|
846
789
|
const st = this.extractionRetryState.get(selfNamespace)?.get(extractionFingerprint);
|
|
@@ -865,6 +808,10 @@ export class ExtractionRunCoordinator {
|
|
|
865
808
|
log.warn("runExtraction: passive correction capture failed on suppressed attempt (non-fatal)", captureErr);
|
|
866
809
|
}
|
|
867
810
|
clearExtractionDeadlineTimer();
|
|
811
|
+
// Backoff suppression exits inside the gate, before the wrapper
|
|
812
|
+
// finally: release an acquired probe here (codex follow-up) so a
|
|
813
|
+
// cooldown-expired fingerprint cannot pin the breaker half_open.
|
|
814
|
+
if (extractionProbeOwned) this.providerBreaker.releaseProbe(extractionProbeToken);
|
|
868
815
|
return { status: "skipped", reason: suppressReason, persistedCount: 0, durableOutputCount: 0 };
|
|
869
816
|
}
|
|
870
817
|
} catch (err) {
|
|
@@ -873,26 +820,28 @@ export class ExtractionRunCoordinator {
|
|
|
873
820
|
log.warn("runExtraction: extraction retry gate check failed; proceeding (fail-open)", err);
|
|
874
821
|
}
|
|
875
822
|
}
|
|
876
|
-
|
|
877
823
|
// Pass existing entity names so the LLM can reuse them instead of inventing variants.
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
"during_list_entity_names",
|
|
881
|
-
);
|
|
882
|
-
let result: ExtractionResult;
|
|
824
|
+
// The probe owner must release on ANY pre-persistence exit too (codex P1):
|
|
825
|
+
// the big try below only covers from the extract call onward.
|
|
883
826
|
try {
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
"extraction aborted (during_extract)",
|
|
827
|
+
const existingEntities = await runDeadlineAware(
|
|
828
|
+
() => storage.listEntityNames(),
|
|
829
|
+
"during_list_entity_names",
|
|
888
830
|
);
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
831
|
+
let result: ExtractionResult;
|
|
832
|
+
try {
|
|
833
|
+
result = await raceRecallAbort(
|
|
834
|
+
this.deps.getExtraction().extract(normalizedTurns, existingEntities, extractionAbortSignal),
|
|
835
|
+
extractionAbortSignal,
|
|
836
|
+
"extraction aborted (during_extract)",
|
|
837
|
+
);
|
|
838
|
+
} catch (error) {
|
|
839
|
+
clearExtractionDeadlineTimer();
|
|
840
|
+
if (deadlineMs !== undefined && Date.now() >= deadlineMs) {
|
|
841
|
+
throw new ExtractionDeadlineError("during_extract");
|
|
842
|
+
}
|
|
843
|
+
throw error;
|
|
893
844
|
}
|
|
894
|
-
throw error;
|
|
895
|
-
}
|
|
896
845
|
try {
|
|
897
846
|
throwIfDeadlineExceeded("before_persist");
|
|
898
847
|
throwIfAborted("before_persist");
|
|
@@ -965,7 +914,7 @@ export class ExtractionRunCoordinator {
|
|
|
965
914
|
await runDeadlineAware(
|
|
966
915
|
() =>
|
|
967
916
|
storage.updateMeta((freshMeta) =>
|
|
968
|
-
this.recordExtractionFailure(selfNamespace, extractionFingerprint, failureClass, freshMeta, Date.now()),
|
|
917
|
+
this.recordExtractionFailure(selfNamespace, extractionFingerprint, failureClass, freshMeta, Date.now(), extractionProbeToken),
|
|
969
918
|
),
|
|
970
919
|
"during_retry_bookkeeping_save",
|
|
971
920
|
false,
|
|
@@ -976,14 +925,16 @@ export class ExtractionRunCoordinator {
|
|
|
976
925
|
// Fail-open, but retain the turns (a cleared buffer with no retry state drops them). Only a
|
|
977
926
|
// LOCK skip reaches here without the mutator having run — a later save must not double-count.
|
|
978
927
|
if (err instanceof Error && err.message.includes("meta.json.lock not acquired"))
|
|
979
|
-
this.applyExtractionFailureToMemory(selfNamespace, extractionFingerprint, failureClass, Date.now());
|
|
928
|
+
this.applyExtractionFailureToMemory(selfNamespace, extractionFingerprint, failureClass, Date.now(), extractionProbeToken);
|
|
980
929
|
recordedRetryFailure = true;
|
|
981
930
|
log.warn("runExtraction: retry state kept in memory; meta persist will catch up", err);
|
|
982
931
|
}
|
|
983
932
|
} else if (result.extractionSkippedReason === undefined) {
|
|
984
933
|
// Provider responded without failure → breaker heals; clear any
|
|
985
934
|
// parked backoff for this fingerprint so it proceeds normally.
|
|
986
|
-
|
|
935
|
+
// PRRT_kwDORJXyws6zxG6u: pass the run's probe token so the settlement
|
|
936
|
+
// only applies to a probe this run owns.
|
|
937
|
+
this.providerBreaker.success(extractionProbeToken);
|
|
987
938
|
try {
|
|
988
939
|
await runDeadlineAware(
|
|
989
940
|
() =>
|
|
@@ -1069,7 +1020,7 @@ export class ExtractionRunCoordinator {
|
|
|
1069
1020
|
throwIfDeadlineExceeded("before_persist");
|
|
1070
1021
|
throwIfAborted("before_persist");
|
|
1071
1022
|
|
|
1072
|
-
const { persistedIds } = await this.deps.persistExtraction(
|
|
1023
|
+
const { persistedIds, deferredCount, pendingReviewIds } = await this.deps.persistExtraction(
|
|
1073
1024
|
result,
|
|
1074
1025
|
storage,
|
|
1075
1026
|
threadIdForExtraction,
|
|
@@ -1106,7 +1057,7 @@ export class ExtractionRunCoordinator {
|
|
|
1106
1057
|
clearBufferAfterExtraction,
|
|
1107
1058
|
runPostPersistBestEffort,
|
|
1108
1059
|
extractionJudgeShadow: this.config.extractionJudgeShadow,
|
|
1109
|
-
|
|
1060
|
+
deferredCount,
|
|
1110
1061
|
normalizedTurns: normalizedTurns as BufferTurn[],
|
|
1111
1062
|
bufferKey,
|
|
1112
1063
|
retainDeferredTurns: (key, retainedTurns, max) =>
|
|
@@ -1155,7 +1106,7 @@ export class ExtractionRunCoordinator {
|
|
|
1155
1106
|
// always attached to the thread. The helper excludes pending_review ids (#1635).
|
|
1156
1107
|
if (resolvePresentationCapabilities(this.config).threading && threadIdForExtraction && persistedIds.length > 0) {
|
|
1157
1108
|
await runPostPersistBestEffort("during_thread_episode_append", () =>
|
|
1158
|
-
this.deps.appendPersistedThreadEpisodes(threadIdForExtraction, persistedIds),
|
|
1109
|
+
this.deps.appendPersistedThreadEpisodes(threadIdForExtraction, persistedIds, pendingReviewIds),
|
|
1159
1110
|
);
|
|
1160
1111
|
}
|
|
1161
1112
|
|
|
@@ -1196,5 +1147,11 @@ export class ExtractionRunCoordinator {
|
|
|
1196
1147
|
} finally {
|
|
1197
1148
|
clearExtractionDeadlineTimer();
|
|
1198
1149
|
}
|
|
1150
|
+
} finally {
|
|
1151
|
+
// The half-open probe is OWNED, not timed: the WRAPPER finally releases
|
|
1152
|
+
// it on every exit path, including pre-persistence ones (codex P1) that
|
|
1153
|
+
// never reach the inner try above.
|
|
1154
|
+
if (extractionProbeOwned) this.providerBreaker.releaseProbe(extractionProbeToken);
|
|
1155
|
+
}
|
|
1199
1156
|
}
|
|
1200
1157
|
}
|