@remnic/core 10.0.70 → 10.0.72
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 +14 -14
- package/dist/access-authorization-probe.js +15 -15
- package/dist/access-boundary.js +14 -14
- package/dist/access-cli.js +58 -58
- package/dist/access-extraction-force-flush.js +14 -14
- package/dist/access-http-briefing.js +15 -15
- package/dist/access-http-query.js +16 -16
- package/dist/access-http.js +31 -31
- package/dist/access-identity-continuity-surface.js +14 -14
- package/dist/access-lcm-surface.js +14 -14
- package/dist/access-mcp.js +24 -24
- package/dist/access-namespace-preflight.js +14 -14
- package/dist/access-observe-write-surface.js +14 -14
- package/dist/access-operations-batch.js +16 -16
- package/dist/access-operations.js +23 -23
- package/dist/access-recall-concurrency.js +14 -14
- package/dist/access-recall-response.js +14 -14
- package/dist/access-recall-surface.js +14 -14
- package/dist/access-service.js +14 -14
- package/dist/access-surface-catalog.js +16 -16
- package/dist/active-recall.js +3 -3
- package/dist/briefing.js +4 -4
- package/dist/buffer-turn-helpers.d.ts +10 -1
- package/dist/buffer-turn-helpers.d.ts.map +1 -1
- package/dist/buffer-turn-helpers.js +3 -1
- package/dist/buffer.d.ts +4 -0
- package/dist/buffer.d.ts.map +1 -1
- package/dist/buffer.js +3 -3
- package/dist/causal-consolidation.js +5 -5
- package/dist/{chunk-B7YAIHAV.js → chunk-23BA3USG.js} +10 -10
- package/dist/{chunk-3F7N4I5I.js → chunk-2R44BBPK.js} +2 -2
- package/dist/{chunk-BW33YTF3.js → chunk-2WPWEVAH.js} +2 -2
- package/dist/{chunk-EF3HNVL4.js → chunk-4PMFSYH7.js} +2 -2
- package/dist/{chunk-J5TVHCDG.js → chunk-5ORJHH43.js} +2 -2
- package/dist/{chunk-MK7HXGM6.js → chunk-6KBCUIZP.js} +2 -2
- package/dist/{chunk-OX6GY22Q.js → chunk-7P5VBRBS.js} +2 -2
- package/dist/{chunk-3AASGFR4.js → chunk-7WFY2L5J.js} +3 -3
- package/dist/{chunk-YLMTFRZR.js → chunk-AGRMXXVA.js} +2 -2
- package/dist/{chunk-WIW4LOZL.js → chunk-AZNE2H6D.js} +2 -2
- package/dist/{chunk-GT4G2S2Y.js → chunk-BCBY5GYZ.js} +2 -2
- package/dist/{chunk-EIZ4MK33.js → chunk-BTU77RB5.js} +2 -2
- package/dist/{chunk-ZS5LREUZ.js → chunk-DC3DWF2M.js} +2 -2
- package/dist/{chunk-LTST4R7U.js → chunk-DE4U5PTV.js} +49 -45
- package/dist/chunk-DE4U5PTV.js.map +1 -0
- package/dist/{chunk-OAQO5AZJ.js → chunk-DSSTOV5M.js} +440 -267
- package/dist/chunk-DSSTOV5M.js.map +1 -0
- package/dist/{chunk-VDYRXI3E.js → chunk-EGKDONTC.js} +9 -3
- package/dist/chunk-EGKDONTC.js.map +1 -0
- package/dist/{chunk-FM6UKZAE.js → chunk-EQ6EMJRG.js} +2 -2
- package/dist/{chunk-TSZP7GGJ.js → chunk-FGGLVCHI.js} +2 -2
- package/dist/{chunk-K7LLGCPH.js → chunk-GXAZ4Q2S.js} +2 -2
- package/dist/{chunk-2GV24KFD.js → chunk-H2X32F2K.js} +2 -2
- package/dist/{chunk-2YPIFJHL.js → chunk-ICBUTKLL.js} +2 -2
- package/dist/{chunk-3W37NITP.js → chunk-IQXQ3KWR.js} +2 -2
- package/dist/{chunk-HNF7IMYQ.js → chunk-J72ZY7NU.js} +2 -2
- package/dist/{chunk-6QS5NQJH.js → chunk-JGLZ46JS.js} +2 -2
- package/dist/{chunk-MRN2MKB7.js → chunk-JY4LXMZR.js} +2 -2
- package/dist/{chunk-UJ2FP2RV.js → chunk-KY3KJUNN.js} +2 -2
- package/dist/{chunk-64U42JL5.js → chunk-LHMCZ6VW.js} +10 -10
- package/dist/{chunk-YRMSHA7H.js → chunk-LN7CWLAU.js} +2 -2
- package/dist/{chunk-XR4QR3JT.js → chunk-LYOCAUI6.js} +3 -3
- package/dist/{chunk-VA3GNAWQ.js → chunk-LZIOY7ZU.js} +4 -5
- package/dist/chunk-LZIOY7ZU.js.map +1 -0
- package/dist/{chunk-GMSRLDVR.js → chunk-N2Y6COEM.js} +2 -2
- package/dist/{chunk-XDUP6SWG.js → chunk-OGQGDOYK.js} +2 -2
- package/dist/{chunk-U7MIMS2Y.js → chunk-OUBEJWVI.js} +17 -1
- package/dist/chunk-OUBEJWVI.js.map +1 -0
- package/dist/{chunk-5FMGPV34.js → chunk-OWZXDBV4.js} +2 -2
- package/dist/{chunk-SQO5SEKA.js → chunk-PJAMFIDE.js} +3 -3
- package/dist/{chunk-I6HRHJRA.js → chunk-QIOQI7QZ.js} +2 -2
- package/dist/{chunk-OAKOAOEG.js → chunk-QUPTDKSX.js} +22 -22
- package/dist/{chunk-MCFI6G7U.js → chunk-QVZZE6VH.js} +3 -3
- package/dist/{chunk-LYTKNK5O.js → chunk-S37O4S6E.js} +2 -2
- package/dist/{chunk-ILD7N3XC.js → chunk-S73ZVU37.js} +2 -2
- package/dist/{chunk-5VQSKBNF.js → chunk-U6IL4724.js} +2 -2
- package/dist/{chunk-D3AGBN6G.js → chunk-UAOJVQXC.js} +2 -2
- package/dist/{chunk-MKMIY3E7.js → chunk-UE5JYPU6.js} +7 -7
- package/dist/{chunk-K47G2CBP.js → chunk-USX5YIKB.js} +5 -5
- package/dist/{chunk-YIG4HPY4.js → chunk-VK5HKVQ7.js} +2 -2
- package/dist/{chunk-ZOBYTRBU.js → chunk-VNTQU6KB.js} +47 -2
- package/dist/chunk-VNTQU6KB.js.map +1 -0
- package/dist/{chunk-THFSCUFC.js → chunk-VSJ3ARFS.js} +2 -2
- package/dist/{chunk-4BOBW6H4.js → chunk-W4V7P5HP.js} +2 -2
- package/dist/{chunk-DM3UZUCN.js → chunk-WNQ3B3Q5.js} +2 -2
- package/dist/{chunk-MFKBYXBY.js → chunk-WYOHQJKC.js} +6 -6
- package/dist/{chunk-66PEACZP.js → chunk-X4DHAXFO.js} +3 -3
- package/dist/{chunk-ZYEWTVNQ.js → chunk-XPZM7GP6.js} +7 -7
- package/dist/{chunk-2BBMCMJS.js → chunk-YPYSC3TX.js} +2 -2
- package/dist/{chunk-JGQEF33K.js → chunk-ZBET3W26.js} +2 -2
- package/dist/chunk-ZODKV27J.js +443 -0
- package/dist/chunk-ZODKV27J.js.map +1 -0
- package/dist/cli.js +43 -43
- package/dist/coding/export-okf-codegraph.js +3 -3
- package/dist/compounding/engine.js +5 -5
- package/dist/config.js +3 -3
- package/dist/connectors/codex-materialize-runner.js +4 -4
- package/dist/connectors/index.js +4 -4
- package/dist/convergence-refresh.js +6 -6
- package/dist/corpus-watermark.js +6 -6
- package/dist/entity-retrieval.js +4 -4
- package/dist/external-wiki-access.js +15 -15
- package/dist/extraction-window-drain.d.ts.map +1 -1
- package/dist/extraction-window-drain.js +1 -1
- package/dist/extraction.js +7 -7
- package/dist/harmonic-retrieval.js +4 -4
- package/dist/index.js +58 -58
- package/dist/location/index.js +6 -6
- package/dist/maintenance/memory-governance.js +4 -4
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -4
- package/dist/maintenance/rebuild-memory-projection-runner.js +6 -6
- package/dist/maintenance/rebuild-memory-projection.js +5 -5
- package/dist/memory-browse-operations.js +15 -15
- package/dist/namespaces/migrate.js +5 -5
- package/dist/namespaces/storage.js +4 -4
- package/dist/operator-doctor-corpus.js +7 -7
- package/dist/operator-toolkit.js +12 -12
- package/dist/orchestration/backlog-scheduler.d.ts +151 -0
- package/dist/orchestration/backlog-scheduler.d.ts.map +1 -0
- package/dist/orchestration/backlog-scheduler.js +891 -0
- package/dist/orchestration/backlog-scheduler.js.map +1 -0
- package/dist/orchestration/buffer-drain-runner.d.ts +31 -27
- package/dist/orchestration/buffer-drain-runner.d.ts.map +1 -1
- package/dist/orchestration/buffer-drain-runner.js +64 -269
- package/dist/orchestration/buffer-drain-runner.js.map +1 -1
- package/dist/orchestration/extraction-drain.d.ts +50 -17
- package/dist/orchestration/extraction-drain.d.ts.map +1 -1
- package/dist/orchestration/extraction-queue-coordinator.d.ts +19 -1
- package/dist/orchestration/extraction-queue-coordinator.d.ts.map +1 -1
- package/dist/orchestration/extraction-run.d.ts +8 -0
- package/dist/orchestration/extraction-run.d.ts.map +1 -1
- package/dist/orchestration/extraction-task-queueing.d.ts +44 -0
- package/dist/orchestration/extraction-task-queueing.d.ts.map +1 -0
- package/dist/orchestration/maintenance.js +14 -14
- package/dist/orchestration/turn-ingestion.d.ts +11 -5
- package/dist/orchestration/turn-ingestion.d.ts.map +1 -1
- package/dist/orchestrator.d.ts +13 -0
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +59 -59
- package/dist/recall-navigation-operations.js +15 -15
- package/dist/recall-navigation.js +14 -14
- package/dist/resume-bundles.js +3 -3
- package/dist/semantic-consolidation.js +5 -5
- package/dist/semantic-rule-promotion.js +4 -4
- package/dist/semantic-rule-verifier.js +4 -4
- package/dist/source-agent-qualifier.js +6 -6
- package/dist/standup/build.js +4 -4
- package/dist/storage/buffer-drain-claims.d.ts +15 -0
- package/dist/storage/buffer-drain-claims.d.ts.map +1 -1
- package/dist/storage/buffer-drain-claims.js +2 -2
- package/dist/storage/buffer-session-files.js +2 -2
- package/dist/storage.js +3 -3
- package/dist/support-passport/index.js +17 -17
- package/dist/transfer/export-okf.js +4 -4
- package/dist/verified-recall.js +4 -4
- package/dist/who-knows-cli.js +5 -5
- package/dist/who-knows.js +4 -4
- package/package.json +12 -2
- package/src/backlog-scheduler.test.ts +806 -0
- package/src/buffer-drain-handover.test.ts +363 -18
- package/src/buffer-drain-inprocess.test.ts +355 -0
- package/src/buffer-drain-model-boundary.test.ts +1062 -0
- package/src/buffer-drop-extraction-chunk.test.ts +237 -22
- package/src/buffer-turn-helpers.ts +26 -1
- package/src/buffer.ts +8 -0
- package/src/extraction-window-drain.ts +7 -7
- package/src/orchestration/backlog-scheduler.ts +481 -0
- package/src/orchestration/buffer-drain-runner.ts +545 -180
- package/src/orchestration/extraction-drain.ts +287 -108
- package/src/orchestration/extraction-queue-coordinator.ts +51 -3
- package/src/orchestration/extraction-run.test.ts +41 -0
- package/src/orchestration/extraction-run.ts +24 -5
- package/src/orchestration/extraction-task-queueing.ts +87 -0
- package/src/orchestration/turn-ingestion.ts +46 -59
- package/src/orchestrator-extraction-queue.test.ts +46 -2
- package/src/orchestrator-flush.test.ts +4 -1
- package/src/orchestrator.ts +47 -21
- package/src/storage/buffer-drain-claims.ts +55 -0
- package/dist/chunk-LTST4R7U.js.map +0 -1
- package/dist/chunk-OAQO5AZJ.js.map +0 -1
- package/dist/chunk-U7MIMS2Y.js.map +0 -1
- package/dist/chunk-VA3GNAWQ.js.map +0 -1
- package/dist/chunk-VDYRXI3E.js.map +0 -1
- package/dist/chunk-ZOBYTRBU.js.map +0 -1
- /package/dist/{chunk-B7YAIHAV.js.map → chunk-23BA3USG.js.map} +0 -0
- /package/dist/{chunk-3F7N4I5I.js.map → chunk-2R44BBPK.js.map} +0 -0
- /package/dist/{chunk-BW33YTF3.js.map → chunk-2WPWEVAH.js.map} +0 -0
- /package/dist/{chunk-EF3HNVL4.js.map → chunk-4PMFSYH7.js.map} +0 -0
- /package/dist/{chunk-J5TVHCDG.js.map → chunk-5ORJHH43.js.map} +0 -0
- /package/dist/{chunk-MK7HXGM6.js.map → chunk-6KBCUIZP.js.map} +0 -0
- /package/dist/{chunk-OX6GY22Q.js.map → chunk-7P5VBRBS.js.map} +0 -0
- /package/dist/{chunk-3AASGFR4.js.map → chunk-7WFY2L5J.js.map} +0 -0
- /package/dist/{chunk-YLMTFRZR.js.map → chunk-AGRMXXVA.js.map} +0 -0
- /package/dist/{chunk-WIW4LOZL.js.map → chunk-AZNE2H6D.js.map} +0 -0
- /package/dist/{chunk-GT4G2S2Y.js.map → chunk-BCBY5GYZ.js.map} +0 -0
- /package/dist/{chunk-EIZ4MK33.js.map → chunk-BTU77RB5.js.map} +0 -0
- /package/dist/{chunk-ZS5LREUZ.js.map → chunk-DC3DWF2M.js.map} +0 -0
- /package/dist/{chunk-FM6UKZAE.js.map → chunk-EQ6EMJRG.js.map} +0 -0
- /package/dist/{chunk-TSZP7GGJ.js.map → chunk-FGGLVCHI.js.map} +0 -0
- /package/dist/{chunk-K7LLGCPH.js.map → chunk-GXAZ4Q2S.js.map} +0 -0
- /package/dist/{chunk-2GV24KFD.js.map → chunk-H2X32F2K.js.map} +0 -0
- /package/dist/{chunk-2YPIFJHL.js.map → chunk-ICBUTKLL.js.map} +0 -0
- /package/dist/{chunk-3W37NITP.js.map → chunk-IQXQ3KWR.js.map} +0 -0
- /package/dist/{chunk-HNF7IMYQ.js.map → chunk-J72ZY7NU.js.map} +0 -0
- /package/dist/{chunk-6QS5NQJH.js.map → chunk-JGLZ46JS.js.map} +0 -0
- /package/dist/{chunk-MRN2MKB7.js.map → chunk-JY4LXMZR.js.map} +0 -0
- /package/dist/{chunk-UJ2FP2RV.js.map → chunk-KY3KJUNN.js.map} +0 -0
- /package/dist/{chunk-64U42JL5.js.map → chunk-LHMCZ6VW.js.map} +0 -0
- /package/dist/{chunk-YRMSHA7H.js.map → chunk-LN7CWLAU.js.map} +0 -0
- /package/dist/{chunk-XR4QR3JT.js.map → chunk-LYOCAUI6.js.map} +0 -0
- /package/dist/{chunk-GMSRLDVR.js.map → chunk-N2Y6COEM.js.map} +0 -0
- /package/dist/{chunk-XDUP6SWG.js.map → chunk-OGQGDOYK.js.map} +0 -0
- /package/dist/{chunk-5FMGPV34.js.map → chunk-OWZXDBV4.js.map} +0 -0
- /package/dist/{chunk-SQO5SEKA.js.map → chunk-PJAMFIDE.js.map} +0 -0
- /package/dist/{chunk-I6HRHJRA.js.map → chunk-QIOQI7QZ.js.map} +0 -0
- /package/dist/{chunk-OAKOAOEG.js.map → chunk-QUPTDKSX.js.map} +0 -0
- /package/dist/{chunk-MCFI6G7U.js.map → chunk-QVZZE6VH.js.map} +0 -0
- /package/dist/{chunk-LYTKNK5O.js.map → chunk-S37O4S6E.js.map} +0 -0
- /package/dist/{chunk-ILD7N3XC.js.map → chunk-S73ZVU37.js.map} +0 -0
- /package/dist/{chunk-5VQSKBNF.js.map → chunk-U6IL4724.js.map} +0 -0
- /package/dist/{chunk-D3AGBN6G.js.map → chunk-UAOJVQXC.js.map} +0 -0
- /package/dist/{chunk-MKMIY3E7.js.map → chunk-UE5JYPU6.js.map} +0 -0
- /package/dist/{chunk-K47G2CBP.js.map → chunk-USX5YIKB.js.map} +0 -0
- /package/dist/{chunk-YIG4HPY4.js.map → chunk-VK5HKVQ7.js.map} +0 -0
- /package/dist/{chunk-THFSCUFC.js.map → chunk-VSJ3ARFS.js.map} +0 -0
- /package/dist/{chunk-4BOBW6H4.js.map → chunk-W4V7P5HP.js.map} +0 -0
- /package/dist/{chunk-DM3UZUCN.js.map → chunk-WNQ3B3Q5.js.map} +0 -0
- /package/dist/{chunk-MFKBYXBY.js.map → chunk-WYOHQJKC.js.map} +0 -0
- /package/dist/{chunk-66PEACZP.js.map → chunk-X4DHAXFO.js.map} +0 -0
- /package/dist/{chunk-ZYEWTVNQ.js.map → chunk-XPZM7GP6.js.map} +0 -0
- /package/dist/{chunk-2BBMCMJS.js.map → chunk-YPYSC3TX.js.map} +0 -0
- /package/dist/{chunk-JGQEF33K.js.map → chunk-ZBET3W26.js.map} +0 -0
|
@@ -106,6 +106,13 @@ export interface ExtractionRunCoordinatorDeps {
|
|
|
106
106
|
fingerprint: string,
|
|
107
107
|
preloadedMeta?: Awaited<ReturnType<StorageManager["loadMeta"]>>
|
|
108
108
|
) => Promise<void>;
|
|
109
|
+
/**
|
|
110
|
+
* Orchestrator-wide provider resilience state. Absent for a standalone
|
|
111
|
+
* coordinator (own breaker); the orchestrator passes ITS breaker so every
|
|
112
|
+
* isolated claim-backed drain accumulates failures toward the same
|
|
113
|
+
* threshold and honors a foreground-opened breaker (review round).
|
|
114
|
+
*/
|
|
115
|
+
providerBreaker?: ProviderCircuitBreaker;
|
|
109
116
|
}
|
|
110
117
|
|
|
111
118
|
// Recall/extraction abort helpers live in orchestrator-helpers.ts since
|
|
@@ -151,9 +158,11 @@ export class ExtractionRunCoordinator {
|
|
|
151
158
|
// backoff still throttles, so a restart can't resurrect the hot loop.
|
|
152
159
|
private readonly extractionRetryState = new Map<string, Map<string, ExtractionRetryStateEntry>>();
|
|
153
160
|
private readonly hydratedRetryNamespaces = new Set<string>();
|
|
154
|
-
private readonly providerBreaker
|
|
161
|
+
private readonly providerBreaker: ProviderCircuitBreaker;
|
|
155
162
|
|
|
156
|
-
constructor(private readonly deps: ExtractionRunCoordinatorDeps) {
|
|
163
|
+
constructor(private readonly deps: ExtractionRunCoordinatorDeps) {
|
|
164
|
+
this.providerBreaker = deps.providerBreaker ?? new ProviderCircuitBreaker();
|
|
165
|
+
}
|
|
157
166
|
|
|
158
167
|
private get config(): PluginConfig {
|
|
159
168
|
return this.deps.config;
|
|
@@ -661,10 +670,14 @@ export class ExtractionRunCoordinator {
|
|
|
661
670
|
log.warn(
|
|
662
671
|
`runExtraction: skipping scope profile ${scopeProfileWritePlan.profileId} because write layer ${scopeProfileWritePlan.writeLayer} is not writable inside the profile read stack`
|
|
663
672
|
);
|
|
664
|
-
|
|
673
|
+
// Policy refusal (same family as namespace_write_revoked): the window
|
|
674
|
+
// stays in the buffer/claim pending a later attempt. Clearing here
|
|
675
|
+
// would destroy content policy never wrote — the claim drain would
|
|
676
|
+
// erase denied backlog and ack it as drained.
|
|
665
677
|
return {
|
|
666
678
|
status: "skipped",
|
|
667
679
|
reason: "scope_profile_no_writable_layer",
|
|
680
|
+
retainedBuffer: true,
|
|
668
681
|
persistedCount: 0,
|
|
669
682
|
durableOutputCount: 0,
|
|
670
683
|
};
|
|
@@ -854,10 +867,13 @@ export class ExtractionRunCoordinator {
|
|
|
854
867
|
if (options.failOnExtractionFailure) {
|
|
855
868
|
throw new Error("extraction failed: invalid_extraction_result");
|
|
856
869
|
}
|
|
857
|
-
|
|
870
|
+
// Actual extraction failure: the window is retained for a retry (same
|
|
871
|
+
// no-drop contract as a recorded provider failure), never destroyed.
|
|
858
872
|
return {
|
|
859
873
|
status: "skipped",
|
|
860
874
|
reason: "invalid_extraction_result",
|
|
875
|
+
retainedBuffer: true,
|
|
876
|
+
failureClass: "parse_empty",
|
|
861
877
|
persistedCount: 0,
|
|
862
878
|
durableOutputCount: 0,
|
|
863
879
|
};
|
|
@@ -878,10 +894,13 @@ export class ExtractionRunCoordinator {
|
|
|
878
894
|
if (options.failOnExtractionFailure) {
|
|
879
895
|
throw new Error("extraction failed: invalid_extraction_result");
|
|
880
896
|
}
|
|
881
|
-
|
|
897
|
+
// Actual extraction failure: the window is retained for a retry (same
|
|
898
|
+
// no-drop contract as a recorded provider failure), never destroyed.
|
|
882
899
|
return {
|
|
883
900
|
status: "skipped",
|
|
884
901
|
reason: "invalid_extraction_result",
|
|
902
|
+
retainedBuffer: true,
|
|
903
|
+
failureClass: "parse_empty",
|
|
885
904
|
persistedCount: 0,
|
|
886
905
|
durableOutputCount: 0,
|
|
887
906
|
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extraction task queueing — the scope-group and execution-context pieces of
|
|
3
|
+
* the turn-ingestion coordinator, split out to keep that module inside the
|
|
4
|
+
* house source-size cap (issue #1995).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { SmartBuffer } from "../buffer.js";
|
|
8
|
+
import { groupTurnsByExtractionScope } from "../buffer-turn-helpers.js";
|
|
9
|
+
import { log } from "../logger.js";
|
|
10
|
+
import type { ResolvedScopeProfilePlan } from "../namespaces/scope-profiles.js";
|
|
11
|
+
import type { BufferTurn, CodingContext } from "../types.js";
|
|
12
|
+
import type { ExtractionDrain } from "./extraction-drain.js";
|
|
13
|
+
import type { TurnIngestionDeps } from "./turn-ingestion.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Isolated per-task execution context (claim-backed backlog drain).
|
|
17
|
+
* Bound at enqueue time and read ONLY inside the enqueued closure, so the
|
|
18
|
+
* task runs against its own buffer/extraction/drain no matter which async
|
|
19
|
+
* context the coordinator's processor executes it from. Absent for live
|
|
20
|
+
* work, which keeps the shared orchestrator state.
|
|
21
|
+
*/
|
|
22
|
+
export interface ExtractionTaskExecutionContext {
|
|
23
|
+
/** The task's own buffer (a claim-backed SmartBuffer). */
|
|
24
|
+
buffer: SmartBuffer;
|
|
25
|
+
/**
|
|
26
|
+
* The task's own chunked drain; its deps carry the isolated runExtraction +
|
|
27
|
+
* buffer (claim tasks build one with `dropFailedChunks: false` — the
|
|
28
|
+
* no-drop backlog policy).
|
|
29
|
+
*/
|
|
30
|
+
drain: ExtractionDrain;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Queue options the capture-scope grouping forwards to the coordinator. */
|
|
34
|
+
export interface CaptureScopeGroupQueueOptions {
|
|
35
|
+
deadlineMs?: number;
|
|
36
|
+
abortSignal?: AbortSignal;
|
|
37
|
+
writeNamespaceOverride?: string;
|
|
38
|
+
principalOverride?: string;
|
|
39
|
+
scopeProfileWritePlan?: ResolvedScopeProfilePlan | null;
|
|
40
|
+
codingContext?: CodingContext;
|
|
41
|
+
onExtractionQueued?: (extraction: Promise<void>) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Buffer-approved captures skip threshold/dedupe rechecks, not provider
|
|
46
|
+
* retry cooldowns. Scope and staleness are enforced at the shared
|
|
47
|
+
* queueBufferedExtraction boundary.
|
|
48
|
+
*/
|
|
49
|
+
export function queueBufferedCaptureScopeGroups(
|
|
50
|
+
queueBufferedExtraction: TurnIngestionDeps["queueBufferedExtraction"],
|
|
51
|
+
extractionTurns: BufferTurn[],
|
|
52
|
+
bufferKey: string,
|
|
53
|
+
options: CaptureScopeGroupQueueOptions,
|
|
54
|
+
): void {
|
|
55
|
+
for (const group of groupTurnsByExtractionScope(extractionTurns)) {
|
|
56
|
+
const groupIsAccepted = group.turns.some((turn) => turn.acceptedCapture === true);
|
|
57
|
+
if (!groupIsAccepted) {
|
|
58
|
+
// Native groups keep their own routing; skipped accepted prefixes require identity clearing.
|
|
59
|
+
void queueBufferedExtraction(group.turns, "trigger_mode", {
|
|
60
|
+
bufferKey,
|
|
61
|
+
clearMatchingTurns: true,
|
|
62
|
+
preemptable: true,
|
|
63
|
+
}).catch((err) => log.error(`native trigger extraction failed: ${err}`));
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
// Accepted groups use ONLY their own pins — no fallback to the
|
|
67
|
+
// triggering observe's scope.
|
|
68
|
+
const { promise: extraction, resolve, reject } = Promise.withResolvers<void>();
|
|
69
|
+
void queueBufferedExtraction(group.turns, "trigger_mode", {
|
|
70
|
+
bufferKey,
|
|
71
|
+
clearBufferAfterExtraction: true,
|
|
72
|
+
clearMatchingTurns: true,
|
|
73
|
+
skipDedupeCheck: true,
|
|
74
|
+
skipCharThreshold: true,
|
|
75
|
+
skipUserTurnThreshold: true,
|
|
76
|
+
abortSignal: options.abortSignal,
|
|
77
|
+
extractionDeadlineMs: options.deadlineMs,
|
|
78
|
+
writeNamespaceOverride: group.writeNamespaceOverride,
|
|
79
|
+
principalOverride: group.principalOverride,
|
|
80
|
+
scopeProfileWritePlan: group.scopeProfileWritePlan ?? null,
|
|
81
|
+
preemptable: true,
|
|
82
|
+
onTaskSettled: (err) => (err ? reject(err) : resolve()),
|
|
83
|
+
}).catch(reject);
|
|
84
|
+
if (options.onExtractionQueued) options.onExtractionQueued(extraction);
|
|
85
|
+
else extraction.catch((err) => log.error(`buffered capture extraction failed: ${err}`));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -44,6 +44,11 @@ import type { BufferTurn, PluginConfig, SourceConnectorProvenance } from "../typ
|
|
|
44
44
|
import { groupTurnsByExtractionScope, bufferTurnsEqual } from "../buffer-turn-helpers.js";
|
|
45
45
|
import { ExtractionDrain } from "./extraction-drain.js";
|
|
46
46
|
import { skipClaimedSession } from "./buffer-drain-gate.js";
|
|
47
|
+
import {
|
|
48
|
+
queueBufferedCaptureScopeGroups,
|
|
49
|
+
type CaptureScopeGroupQueueOptions,
|
|
50
|
+
type ExtractionTaskExecutionContext,
|
|
51
|
+
} from "./extraction-task-queueing.js";
|
|
47
52
|
import { resolveScopeProfilePlan, type ResolvedScopeProfilePlan, hasExplicitNamespacePolicy } from "../namespaces/scope-profiles.js";
|
|
48
53
|
import { resolveCodingNamespaceOverlay, combineNamespaces } from "../coding/coding-namespace.js";
|
|
49
54
|
import { canWriteNamespace, defaultNamespaceForPrincipal } from "../namespaces/principal.js";
|
|
@@ -224,7 +229,7 @@ export class TurnIngestionCoordinator {
|
|
|
224
229
|
// #3251: native triggers must not run mixed accepted scopes under one
|
|
225
230
|
// policy — each scope group drains through the shared execution
|
|
226
231
|
// boundary with its own pins.
|
|
227
|
-
this.
|
|
232
|
+
queueBufferedCaptureScopeGroups(this.deps.queueBufferedExtraction, outcome.extractionTurns, bufferKey, {});
|
|
228
233
|
return;
|
|
229
234
|
}
|
|
230
235
|
await this.deps.queueBufferedExtraction(
|
|
@@ -463,7 +468,12 @@ export class TurnIngestionCoordinator {
|
|
|
463
468
|
);
|
|
464
469
|
}
|
|
465
470
|
if (outcome.decision !== "keep_buffering" && outcome.extractionTurns) {
|
|
466
|
-
|
|
471
|
+
queueBufferedCaptureScopeGroups(
|
|
472
|
+
this.deps.queueBufferedExtraction,
|
|
473
|
+
outcome.extractionTurns,
|
|
474
|
+
bufferKey,
|
|
475
|
+
options,
|
|
476
|
+
);
|
|
467
477
|
}
|
|
468
478
|
}
|
|
469
479
|
// Acceptance durability barrier (#1909 seam): surfaces storage failures
|
|
@@ -472,58 +482,6 @@ export class TurnIngestionCoordinator {
|
|
|
472
482
|
}
|
|
473
483
|
}
|
|
474
484
|
|
|
475
|
-
/**
|
|
476
|
-
* Buffer-approved captures skip threshold/dedupe rechecks, not provider retry cooldowns.
|
|
477
|
-
* Scope and staleness are enforced at the shared queueBufferedExtraction boundary.
|
|
478
|
-
*/
|
|
479
|
-
private queueBufferedCaptureScopeGroups(
|
|
480
|
-
extractionTurns: BufferTurn[],
|
|
481
|
-
bufferKey: string,
|
|
482
|
-
options: {
|
|
483
|
-
deadlineMs?: number;
|
|
484
|
-
abortSignal?: AbortSignal;
|
|
485
|
-
writeNamespaceOverride?: string;
|
|
486
|
-
principalOverride?: string;
|
|
487
|
-
scopeProfileWritePlan?: ResolvedScopeProfilePlan | null;
|
|
488
|
-
codingContext?: import("../types.js").CodingContext;
|
|
489
|
-
onExtractionQueued?: (extraction: Promise<void>) => void;
|
|
490
|
-
},
|
|
491
|
-
): void {
|
|
492
|
-
for (const group of groupTurnsByExtractionScope(extractionTurns)) {
|
|
493
|
-
const groupIsAccepted = group.turns.some((turn) => turn.acceptedCapture === true);
|
|
494
|
-
if (!groupIsAccepted) {
|
|
495
|
-
// Native groups keep their own routing; skipped accepted prefixes require identity clearing.
|
|
496
|
-
void this.deps.queueBufferedExtraction(group.turns, "trigger_mode", {
|
|
497
|
-
bufferKey,
|
|
498
|
-
clearMatchingTurns: true,
|
|
499
|
-
preemptable: true,
|
|
500
|
-
}).catch((err) => log.error(`native trigger extraction failed: ${err}`));
|
|
501
|
-
continue;
|
|
502
|
-
}
|
|
503
|
-
// Accepted groups use ONLY their own pins — no fallback to the
|
|
504
|
-
// triggering observe's scope.
|
|
505
|
-
const extraction = new Promise<void>((resolve, reject) => {
|
|
506
|
-
void this.deps.queueBufferedExtraction(group.turns, "trigger_mode", {
|
|
507
|
-
bufferKey,
|
|
508
|
-
clearBufferAfterExtraction: true,
|
|
509
|
-
clearMatchingTurns: true,
|
|
510
|
-
skipDedupeCheck: true,
|
|
511
|
-
skipCharThreshold: true,
|
|
512
|
-
skipUserTurnThreshold: true,
|
|
513
|
-
abortSignal: options.abortSignal,
|
|
514
|
-
extractionDeadlineMs: options.deadlineMs,
|
|
515
|
-
writeNamespaceOverride: group.writeNamespaceOverride,
|
|
516
|
-
principalOverride: group.principalOverride,
|
|
517
|
-
scopeProfileWritePlan: group.scopeProfileWritePlan ?? null,
|
|
518
|
-
preemptable: true,
|
|
519
|
-
onTaskSettled: (err) => (err ? reject(err) : resolve()),
|
|
520
|
-
}).catch(reject);
|
|
521
|
-
});
|
|
522
|
-
if (options.onExtractionQueued) options.onExtractionQueued(extraction);
|
|
523
|
-
else extraction.catch((err) => log.error(`buffered capture extraction failed: ${err}`));
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
485
|
/**
|
|
528
486
|
* Ingest a batch of bulk-import turns (#460). Like ingestReplayBatch, this
|
|
529
487
|
* normalizes user/assistant turns into the extraction buffer and awaits
|
|
@@ -802,7 +760,7 @@ export class TurnIngestionCoordinator {
|
|
|
802
760
|
if (turns.some((turn) => turn.acceptedCapture === true)) {
|
|
803
761
|
// #3251: heartbeat triggers must not run mixed accepted scopes
|
|
804
762
|
// under one policy — per-scope groups via the shared boundary.
|
|
805
|
-
this.
|
|
763
|
+
queueBufferedCaptureScopeGroups(this.deps.queueBufferedExtraction, turns, bufferKey, {});
|
|
806
764
|
return;
|
|
807
765
|
}
|
|
808
766
|
await this.deps.queueBufferedExtraction(turns, "heartbeat_observer", {
|
|
@@ -842,6 +800,14 @@ export class TurnIngestionCoordinator {
|
|
|
842
800
|
abortSignal?: AbortSignal;
|
|
843
801
|
/** Automatic buffered work only. Replay and import must not set this. */
|
|
844
802
|
preemptable?: boolean;
|
|
803
|
+
/**
|
|
804
|
+
* Invoked when the queued closure has FULLY exited (after settlement).
|
|
805
|
+
* Settlement can fire before the closure exits — e.g. an abort listener
|
|
806
|
+
* settles while persistence is still in flight — so callers that
|
|
807
|
+
* release resources on settlement (claim leases) can use this to wait
|
|
808
|
+
* for the actual task exit first.
|
|
809
|
+
*/
|
|
810
|
+
onTaskExit?: () => void;
|
|
845
811
|
/** Fail-closed force-flush only. Not implied by failOnExtractionFailure. */
|
|
846
812
|
preemptRunning?: boolean;
|
|
847
813
|
writeNamespaceOverride?: string;
|
|
@@ -852,10 +818,14 @@ export class TurnIngestionCoordinator {
|
|
|
852
818
|
*/
|
|
853
819
|
principalOverride?: string;
|
|
854
820
|
scopeProfileWritePlan?: ResolvedScopeProfilePlan | null;
|
|
821
|
+
/** Isolated per-task execution context (claim-backed backlog drain). */
|
|
822
|
+
execution?: ExtractionTaskExecutionContext;
|
|
855
823
|
} = {},
|
|
856
824
|
): Promise<void> {
|
|
857
825
|
const bufferKey = options.bufferKey ?? turnsToExtract[0]?.sessionKey ?? "default";
|
|
858
|
-
|
|
826
|
+
// The execution context owns the gate's buffer view: a claim-backed task
|
|
827
|
+
// reads its own claim (never "claimed"), not the live buffer's gate.
|
|
828
|
+
if (skipClaimedSession(options.execution?.buffer ?? this.deps.buffer, bufferKey, (result) => options.onTaskSettled?.(undefined, result))) return;
|
|
859
829
|
if (
|
|
860
830
|
!options.skipDedupeCheck &&
|
|
861
831
|
!this.deps.shouldQueueExtraction(turnsToExtract, { bufferKey })
|
|
@@ -933,7 +903,8 @@ export class TurnIngestionCoordinator {
|
|
|
933
903
|
if (settled) return;
|
|
934
904
|
const preemptable = options.preemptable === true && options.preemptRunning !== true;
|
|
935
905
|
const accepted = this.deps.extractionQueueCoordinator.enqueue(async (preemptHandle) => {
|
|
936
|
-
|
|
906
|
+
try {
|
|
907
|
+
await this.deps.extractionQueueCoordinator.runPreemptable(preemptHandle, preemptable, options.abortSignal, async (abortSignal) => {
|
|
937
908
|
if (settled) return;
|
|
938
909
|
if (
|
|
939
910
|
typeof extractionDeadlineMs === "number" &&
|
|
@@ -943,6 +914,17 @@ export class TurnIngestionCoordinator {
|
|
|
943
914
|
return;
|
|
944
915
|
}
|
|
945
916
|
clearQueueWaitTimer();
|
|
917
|
+
// Execution-time claim recheck (review round): the scheduler's
|
|
918
|
+
// async claim decision may complete while this LIVE task sits queued;
|
|
919
|
+
// its turns then belong to the claim runner, and extracting the stale
|
|
920
|
+
// snapshot here would persist duplicate outputs. The enqueue-time gate
|
|
921
|
+
// plus the key reservation only park the task until the decision
|
|
922
|
+
// settles. Isolated claim tasks (options.execution) read their own
|
|
923
|
+
// claim-backed buffer and bypass this gate by construction.
|
|
924
|
+
if (
|
|
925
|
+
options.execution === undefined &&
|
|
926
|
+
skipClaimedSession(this.deps.buffer, bufferKey, (result) => settleTask(undefined, result))
|
|
927
|
+
) return;
|
|
946
928
|
try {
|
|
947
929
|
// #3251 shared execution boundary for accepted captures (trigger and
|
|
948
930
|
// flush drains both converge here): re-read the live buffer so a
|
|
@@ -951,8 +933,9 @@ export class TurnIngestionCoordinator {
|
|
|
951
933
|
// WITHOUT clearing, and the plan is never retargeted.
|
|
952
934
|
let effectiveTurns = turnsToExtract;
|
|
953
935
|
let effectiveScopePlan = options.scopeProfileWritePlan;
|
|
936
|
+
const executionBuffer = options.execution?.buffer;
|
|
954
937
|
if (effectiveTurns.some((turn) => turn.acceptedCapture === true)) {
|
|
955
|
-
const liveTurns = this.deps.buffer
|
|
938
|
+
const liveTurns = (executionBuffer ?? this.deps.buffer)
|
|
956
939
|
.getTurns(bufferKey)
|
|
957
940
|
.filter((live) => effectiveTurns.some((snapshotted) => bufferTurnsEqual(live, snapshotted)));
|
|
958
941
|
if (liveTurns.length === 0) {
|
|
@@ -1073,7 +1056,7 @@ export class TurnIngestionCoordinator {
|
|
|
1073
1056
|
effectiveScopePlan = fresh;
|
|
1074
1057
|
}
|
|
1075
1058
|
}
|
|
1076
|
-
const result = await this.drain.run(effectiveTurns, {
|
|
1059
|
+
const result = await (options.execution?.drain ?? this.drain).run(effectiveTurns, {
|
|
1077
1060
|
clearBufferAfterExtraction:
|
|
1078
1061
|
options.clearBufferAfterExtraction ?? true,
|
|
1079
1062
|
clearMatchingTurns: options.clearMatchingTurns === true,
|
|
@@ -1099,9 +1082,13 @@ export class TurnIngestionCoordinator {
|
|
|
1099
1082
|
}
|
|
1100
1083
|
}
|
|
1101
1084
|
});
|
|
1085
|
+
} finally {
|
|
1086
|
+
options.onTaskExit?.();
|
|
1087
|
+
}
|
|
1102
1088
|
}, { front: options.preemptRunning === true, key: bufferKey });
|
|
1103
1089
|
if (!accepted) {
|
|
1104
1090
|
settleTask(new Error("extraction queue is stopped"));
|
|
1091
|
+
options.onTaskExit?.();
|
|
1105
1092
|
return;
|
|
1106
1093
|
}
|
|
1107
1094
|
|
|
@@ -184,7 +184,6 @@ test("logExtractionQueueFailure names the right layer (task vs processor)", asyn
|
|
|
184
184
|
);
|
|
185
185
|
});
|
|
186
186
|
|
|
187
|
-
|
|
188
187
|
test("pauseAndDrain waits for accepted work after its observation timeout", async () => {
|
|
189
188
|
const coordinator = createCoordinator();
|
|
190
189
|
const started = Promise.withResolvers<void>();
|
|
@@ -270,7 +269,6 @@ test("a front enqueue does not abort a task that did not register as preemptable
|
|
|
270
269
|
assert.deepEqual(order, ["protected", "flush"]);
|
|
271
270
|
});
|
|
272
271
|
|
|
273
|
-
|
|
274
272
|
// ── Bounded-parallel queue (extractionQueueConcurrency) ─────────────────────
|
|
275
273
|
|
|
276
274
|
function createParallelCoordinator(limit: number): ExtractionQueueCoordinator {
|
|
@@ -441,3 +439,49 @@ test("pauseAndDrain waits for concurrent accepted work and rejects new producers
|
|
|
441
439
|
assert.equal(second.outcome, "finished");
|
|
442
440
|
assert.equal(coordinator.isProcessing, false);
|
|
443
441
|
});
|
|
442
|
+
|
|
443
|
+
// ── Key reservations (daemon backlog scheduler) ────────────────────────────
|
|
444
|
+
|
|
445
|
+
function delay(ms: number): Promise<void> {
|
|
446
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
test("tryReserveKey defers a queued task's start until the reservation releases", async () => {
|
|
450
|
+
const queue = createCoordinator();
|
|
451
|
+
assert.equal(queue.tryReserveKey("s1"), true, "a free key reserves synchronously");
|
|
452
|
+
|
|
453
|
+
let ran = false;
|
|
454
|
+
queue.pushRaw(async () => {
|
|
455
|
+
ran = true;
|
|
456
|
+
}, { key: "s1" });
|
|
457
|
+
const processing = queue.processQueue();
|
|
458
|
+
await delay(20);
|
|
459
|
+
assert.equal(ran, false, "a reserved key's queued task must not start");
|
|
460
|
+
assert.equal(queue.tryReserveKey("s1"), false, "the pending reservation is exclusive");
|
|
461
|
+
|
|
462
|
+
queue.releaseKeyReservation("s1");
|
|
463
|
+
await processing;
|
|
464
|
+
assert.equal(ran, true, "the task runs once the claim decision settles");
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test("tryReserveKey refuses active and queued keys, and releases idempotently", async () => {
|
|
468
|
+
const queue = createCoordinator();
|
|
469
|
+
let release: () => void = () => {};
|
|
470
|
+
const gate = new Promise<void>((resolve) => {
|
|
471
|
+
release = resolve;
|
|
472
|
+
});
|
|
473
|
+
queue.pushRaw(async () => {
|
|
474
|
+
await gate;
|
|
475
|
+
}, { key: "s1" });
|
|
476
|
+
const processing = queue.processQueue();
|
|
477
|
+
await delay(20);
|
|
478
|
+
assert.equal(queue.tryReserveKey("s1"), false, "an active key cannot be reserved");
|
|
479
|
+
|
|
480
|
+
queue.releaseKeyReservation("s1");
|
|
481
|
+
queue.releaseKeyReservation("s1"); // idempotent
|
|
482
|
+
release();
|
|
483
|
+
await processing;
|
|
484
|
+
|
|
485
|
+
assert.equal(queue.tryReserveKey("s1"), true, "a settled key reserves again");
|
|
486
|
+
queue.releaseKeyReservation("s1");
|
|
487
|
+
});
|
|
@@ -1492,7 +1492,10 @@ test("runExtraction skips active scope profile writes when no layer is writable"
|
|
|
1492
1492
|
|
|
1493
1493
|
assert.equal(result.status, "skipped");
|
|
1494
1494
|
assert.equal(result.reason, "scope_profile_no_writable_layer");
|
|
1495
|
-
|
|
1495
|
+
// Policy refusal (review round): the window is RETAINED, never
|
|
1496
|
+
// cleared — clearing here would destroy content policy never wrote.
|
|
1497
|
+
assert.equal(clearCalls, 0);
|
|
1498
|
+
assert.equal(result.retainedBuffer, true);
|
|
1496
1499
|
});
|
|
1497
1500
|
|
|
1498
1501
|
test("runExtraction writes buffered turns to active scope profile write layer", async () => {
|
package/src/orchestrator.ts
CHANGED
|
@@ -98,6 +98,7 @@ import {
|
|
|
98
98
|
gatewayTaskChainOptions,
|
|
99
99
|
} from "./fallback-llm.js";
|
|
100
100
|
import { MaintenanceScheduler } from "./orchestration/maintenance.js";
|
|
101
|
+
import { ProviderCircuitBreaker } from "./orchestration/provider-breaker.js";
|
|
101
102
|
import { TierMigrationCoordinator } from "./orchestration/tier-migration-coordinator.js";
|
|
102
103
|
import { ExtractionQueueCoordinator } from "./orchestration/extraction-queue-coordinator.js";
|
|
103
104
|
import { CompressionGuidelineCoordinator } from "./orchestration/compression-guideline-coordinator.js";
|
|
@@ -119,6 +120,7 @@ import {
|
|
|
119
120
|
} from "./orchestration/contradiction-linking-coordinator.js";
|
|
120
121
|
import {
|
|
121
122
|
ExtractionRunCoordinator,
|
|
123
|
+
type ExtractionRunCoordinatorDeps,
|
|
122
124
|
type ExtractionRunResult,
|
|
123
125
|
type ExtractionResilienceStatus,
|
|
124
126
|
} from "./orchestration/extraction-run.js";
|
|
@@ -756,33 +758,57 @@ export class Orchestrator {
|
|
|
756
758
|
|
|
757
759
|
readonly extractionQueueCoordinator: ExtractionQueueCoordinator;
|
|
758
760
|
private _extractionRunCoordinator: ExtractionRunCoordinator | undefined;
|
|
761
|
+
private _providerBreaker: ProviderCircuitBreaker | undefined;
|
|
762
|
+
|
|
763
|
+
/** One breaker per orchestrator: foreground and every isolated claim-backed
|
|
764
|
+
* drain accumulate toward the same threshold and honor each other's open
|
|
765
|
+
* state (review round — isolated coordinators must not reset
|
|
766
|
+
* provider resilience). */
|
|
767
|
+
private get providerBreaker(): ProviderCircuitBreaker {
|
|
768
|
+
if (!this._providerBreaker) this._providerBreaker = new ProviderCircuitBreaker();
|
|
769
|
+
return this._providerBreaker;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/** The run-coordinator deps bundle, parameterized by its buffer accessor so
|
|
773
|
+
* an isolated claim-backed drain can bind its own SmartBuffer (the daemon
|
|
774
|
+
* backlog scheduler) while live extraction keeps `this.buffer`. */
|
|
775
|
+
private extractionRunDeps(getBuffer: () => SmartBuffer): ExtractionRunCoordinatorDeps {
|
|
776
|
+
return {
|
|
777
|
+
config: this.config,
|
|
778
|
+
getBuffer,
|
|
779
|
+
providerBreaker: this.providerBreaker,
|
|
780
|
+
getExtraction: () => this.extraction,
|
|
781
|
+
getStorageRouter: () => this.storageRouter,
|
|
782
|
+
getThreading: () => this.threading,
|
|
783
|
+
persistExtraction: (result, storage, threadId, sourceContext, baseNamespace, scopeProfileWritePlan, sourceText, graphCaps, lifecycleCaps) =>
|
|
784
|
+
this.persistExtraction(result, storage, threadId, sourceContext, baseNamespace, scopeProfileWritePlan, sourceText, graphCaps, lifecycleCaps),
|
|
785
|
+
maybeCapturePassiveCorrections: (turns, opts) => this.maybeCapturePassiveCorrections(turns, opts),
|
|
786
|
+
resolveSelfNamespace: (sessionKey) => this.resolveSelfNamespace(sessionKey),
|
|
787
|
+
getCodingContextForSession: (sessionKey) => this.getCodingContextForSession(sessionKey),
|
|
788
|
+
applyCodingNamespaceOverlay: (sessionKey, namespace) => this.applyCodingNamespaceOverlay(sessionKey, namespace),
|
|
789
|
+
boxBuilderFor: (storage) => this.boxBuilderFor(storage),
|
|
790
|
+
appendPersistedThreadEpisodes: (threadId, ids, pendingReviewIds) =>
|
|
791
|
+
this.appendPersistedThreadEpisodes(threadId, ids, pendingReviewIds),
|
|
792
|
+
maybeScheduleConsolidation: (nonZero) => this.maybeScheduleConsolidation(nonZero),
|
|
793
|
+
requestQmdMaintenance: () => this.requestQmdMaintenance(),
|
|
794
|
+
runTierMigrationCycle: (storage, trigger, options) => this.runTierMigrationCycle(storage, trigger, options),
|
|
795
|
+
recordProcessedExtractionFingerprint: (storage, fingerprint, preloadedMeta) =>
|
|
796
|
+
this.recordProcessedExtractionFingerprint(storage, fingerprint, preloadedMeta),
|
|
797
|
+
};
|
|
798
|
+
}
|
|
759
799
|
|
|
760
800
|
private get extractionRunCoordinator(): ExtractionRunCoordinator {
|
|
761
801
|
if (!this._extractionRunCoordinator) {
|
|
762
|
-
this._extractionRunCoordinator = new ExtractionRunCoordinator(
|
|
763
|
-
config: this.config,
|
|
764
|
-
getBuffer: () => this.buffer,
|
|
765
|
-
getExtraction: () => this.extraction,
|
|
766
|
-
getStorageRouter: () => this.storageRouter,
|
|
767
|
-
getThreading: () => this.threading,
|
|
768
|
-
persistExtraction: (result, storage, threadId, sourceContext, baseNamespace, scopeProfileWritePlan, sourceText, graphCaps, lifecycleCaps) =>
|
|
769
|
-
this.persistExtraction(result, storage, threadId, sourceContext, baseNamespace, scopeProfileWritePlan, sourceText, graphCaps, lifecycleCaps),
|
|
770
|
-
maybeCapturePassiveCorrections: (turns, opts) => this.maybeCapturePassiveCorrections(turns, opts),
|
|
771
|
-
resolveSelfNamespace: (sessionKey) => this.resolveSelfNamespace(sessionKey),
|
|
772
|
-
getCodingContextForSession: (sessionKey) => this.getCodingContextForSession(sessionKey),
|
|
773
|
-
applyCodingNamespaceOverlay: (sessionKey, namespace) => this.applyCodingNamespaceOverlay(sessionKey, namespace),
|
|
774
|
-
boxBuilderFor: (storage) => this.boxBuilderFor(storage),
|
|
775
|
-
appendPersistedThreadEpisodes: (threadId, ids, pendingReviewIds) =>
|
|
776
|
-
this.appendPersistedThreadEpisodes(threadId, ids, pendingReviewIds),
|
|
777
|
-
maybeScheduleConsolidation: (nonZero) => this.maybeScheduleConsolidation(nonZero),
|
|
778
|
-
requestQmdMaintenance: () => this.requestQmdMaintenance(),
|
|
779
|
-
runTierMigrationCycle: (storage, trigger, options) => this.runTierMigrationCycle(storage, trigger, options),
|
|
780
|
-
recordProcessedExtractionFingerprint: (storage, fingerprint, preloadedMeta) =>
|
|
781
|
-
this.recordProcessedExtractionFingerprint(storage, fingerprint, preloadedMeta),
|
|
782
|
-
});
|
|
802
|
+
this._extractionRunCoordinator = new ExtractionRunCoordinator(this.extractionRunDeps(() => this.buffer));
|
|
783
803
|
}
|
|
784
804
|
return this._extractionRunCoordinator;
|
|
785
805
|
}
|
|
806
|
+
|
|
807
|
+
/** An extraction run coordinator bound to `buffer` — per drain task, so a
|
|
808
|
+
* claim-backed drain never shares the live buffer's mutable state. */
|
|
809
|
+
extractionRunCoordinatorForBuffer(buffer: SmartBuffer): ExtractionRunCoordinator {
|
|
810
|
+
return new ExtractionRunCoordinator(this.extractionRunDeps(() => buffer));
|
|
811
|
+
}
|
|
786
812
|
private _consolidationRunCoordinator: ConsolidationRunCoordinator | undefined;
|
|
787
813
|
private get consolidationRunCoordinator(): ConsolidationRunCoordinator {
|
|
788
814
|
if (!this._consolidationRunCoordinator) {
|
|
@@ -239,6 +239,10 @@ function parseLease(raw: string): DrainLease | null {
|
|
|
239
239
|
};
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
/** Claim bodies `listPage` may read per requested entry: bounds one scan's
|
|
243
|
+
* I/O even when the directory is mostly empty tombstones. */
|
|
244
|
+
const LIST_PAGE_SCAN_FACTOR = 8;
|
|
245
|
+
|
|
242
246
|
export class BufferDrainClaims {
|
|
243
247
|
constructor(
|
|
244
248
|
private readonly stateDir: string,
|
|
@@ -582,6 +586,57 @@ export class BufferDrainClaims {
|
|
|
582
586
|
return out;
|
|
583
587
|
}
|
|
584
588
|
|
|
589
|
+
/**
|
|
590
|
+
* Paged listing for bounded periodic scans: at most `limit` claims with
|
|
591
|
+
* pending turns, starting AFTER `afterName` (a raw entry name returned as
|
|
592
|
+
* `nextCursor` by a previous page — file-name order is stable, so a
|
|
593
|
+
* rotating cursor visits every claim fairly without ever materializing
|
|
594
|
+
* the whole directory's bodies). Empty tombstones never count toward the
|
|
595
|
+
* limit; the number of bodies read per call is bounded separately
|
|
596
|
+
* (`LIST_PAGE_SCAN_FACTOR` per requested entry), and the cursor advances
|
|
597
|
+
* past every scanned name so a long tombstone run is crossed over several
|
|
598
|
+
* pages instead of starving the pending claims behind it.
|
|
599
|
+
*/
|
|
600
|
+
async listPage(afterName: string | undefined, limit: number): Promise<{ entries: DrainClaimListing[]; nextCursor?: string }> {
|
|
601
|
+
let names: string[];
|
|
602
|
+
try {
|
|
603
|
+
names = (await readdir(this.dir)).filter((name) => name.endsWith(".json") && !name.endsWith(".lease.json")).sort();
|
|
604
|
+
} catch (err) {
|
|
605
|
+
// SAFETY: readdir rejects with NodeJS system errors; only the ENOENT
|
|
606
|
+
// (no claims directory yet) case maps to an empty listing.
|
|
607
|
+
const errno = (err as NodeJS.ErrnoException | undefined)?.code;
|
|
608
|
+
if (errno === "ENOENT") return { entries: [] };
|
|
609
|
+
throw err;
|
|
610
|
+
}
|
|
611
|
+
const start = afterName === undefined ? 0 : names.findIndex((name) => name > afterName);
|
|
612
|
+
const out: DrainClaimListing[] = [];
|
|
613
|
+
// The cursor is the LAST CONSUMED entry name: the next page starts
|
|
614
|
+
// strictly after it, so the first unconsumed name is never skipped.
|
|
615
|
+
// Returning the first unconsumed name instead would make every page
|
|
616
|
+
// boundary permanently skip one claim.
|
|
617
|
+
let cursor: string | undefined;
|
|
618
|
+
const scanBudget = Math.max(limit, 1) * LIST_PAGE_SCAN_FACTOR;
|
|
619
|
+
let scanned = 0;
|
|
620
|
+
for (let index = start < 0 ? names.length : start; index < names.length; index += 1) {
|
|
621
|
+
if (out.length >= limit || scanned >= scanBudget) break;
|
|
622
|
+
const name = names[index]!;
|
|
623
|
+
scanned += 1;
|
|
624
|
+
cursor = name;
|
|
625
|
+
const claim = await this.readClaimBodyByKey(name);
|
|
626
|
+
if (!claim) continue;
|
|
627
|
+
const turnCount = claim.entry.turns.length + (claim.entry.retainedTurns?.length ?? 0);
|
|
628
|
+
if (turnCount <= 0) continue;
|
|
629
|
+
const lease = await this.readLease(claim.key);
|
|
630
|
+
out.push({
|
|
631
|
+
key: claim.key,
|
|
632
|
+
turnCount,
|
|
633
|
+
leased: lease !== null,
|
|
634
|
+
leaseExpired: lease !== null ? isLeaseExpired(lease) : true,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
return { entries: out, nextCursor: cursor };
|
|
638
|
+
}
|
|
639
|
+
|
|
585
640
|
private async readClaimBodyByKey(name: string): Promise<DrainClaimFile | null> {
|
|
586
641
|
try {
|
|
587
642
|
return parseBody(await this.io.read(path.join(this.dir, name)));
|