@remnic/core 9.3.728 → 9.3.729
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-boundary.d.ts +2 -2
- package/dist/access-cli.js +1 -1
- package/dist/access-http.d.ts +2 -2
- package/dist/access-mcp.d.ts +2 -2
- package/dist/access-operations.d.ts +7 -7
- package/dist/access-schema.d.ts +42 -42
- package/dist/{access-service-Bgpjxo4f.d.ts → access-service-BXMJqkTA.d.ts} +1 -1
- package/dist/access-service.d.ts +2 -2
- package/dist/access-surface-catalog.d.ts +2 -2
- package/dist/bootstrap.d.ts +1 -1
- package/dist/{chunk-QNEUZIAH.js → chunk-HEDZUSCS.js} +136 -66
- package/dist/chunk-HEDZUSCS.js.map +1 -0
- package/dist/{cli-BLME420h.d.ts → cli-DmAiDd8A.d.ts} +2 -2
- package/dist/cli.d.ts +3 -3
- package/dist/explicit-capture.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/mcp-memory-inspector-app.d.ts +2 -2
- package/dist/{orchestrator-DqPAz8ot.d.ts → orchestrator-BR0sGsME.d.ts} +104 -26
- package/dist/orchestrator.d.ts +1 -1
- package/dist/orchestrator.js +1 -1
- package/dist/schemas.d.ts +52 -52
- package/dist/shared-context/manager.d.ts +2 -2
- package/dist/transfer/types.d.ts +24 -24
- package/package.json +2 -2
- package/src/orchestration/extraction-queue-coordinator.ts +175 -0
- package/src/orchestrator-extraction-queue.test.ts +26 -38
- package/src/orchestrator-flush.test.ts +10 -15
- package/src/orchestrator.ts +14 -78
- package/dist/chunk-QNEUZIAH.js.map +0 -1
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extraction queue coordinator — extracted from the orchestrator (issue #1526).
|
|
3
|
+
*
|
|
4
|
+
* Owns the background serial queue that drains extraction tasks one at a
|
|
5
|
+
* time to avoid races between concurrent flush/heartbeat/bulk-import
|
|
6
|
+
* triggers:
|
|
7
|
+
* - queue state (`extractionQueue` + `queueProcessing`)
|
|
8
|
+
* - the scheduling trigger (start the drain when the first task lands and
|
|
9
|
+
* the processor is idle)
|
|
10
|
+
* - the serial drain loop itself (`processQueue`)
|
|
11
|
+
* - failure classification (`logExtractionQueueFailure`) — issue #549:
|
|
12
|
+
* AbortError from session transitions logs at debug, real failures at
|
|
13
|
+
* error
|
|
14
|
+
* - the idle wait (`waitForIdle`) used by bootstrap and tests
|
|
15
|
+
*
|
|
16
|
+
* Does NOT own the WHAT — `runExtraction`, the dedupe fingerprint check,
|
|
17
|
+
* the deadline timer, and the buffer-clear policy remain on the
|
|
18
|
+
* orchestrator (they need the full extraction dependency surface). The
|
|
19
|
+
* orchestrator builds each task closure and hands it to `enqueue`; the
|
|
20
|
+
* coordinator owns only the queue mechanics.
|
|
21
|
+
*
|
|
22
|
+
* Behavior-preserving move from orchestrator.ts. No logic changes — the
|
|
23
|
+
* orchestrator keeps a thin delegating `waitForExtractionIdle` and the
|
|
24
|
+
* `queueBufferedExtraction` builder delegates push+scheduling here.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { isAbortError } from "../abort-error.js";
|
|
28
|
+
import { log } from "../logger.js";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Coordinates the background serial extraction queue. Owns the in-flight
|
|
32
|
+
* guard + queue array that previously lived as private orchestrator fields.
|
|
33
|
+
*/
|
|
34
|
+
export class ExtractionQueueCoordinator {
|
|
35
|
+
/**
|
|
36
|
+
* Background serial queue for extractions (agent_end optimization).
|
|
37
|
+
* Queue stores tasks that resolve when extraction should run.
|
|
38
|
+
*/
|
|
39
|
+
private readonly queue: Array<() => Promise<void>> = [];
|
|
40
|
+
/** Whether the serial drain loop is currently running. */
|
|
41
|
+
private processing = false;
|
|
42
|
+
|
|
43
|
+
/** Current queue depth (idle-wait + test seam). */
|
|
44
|
+
get length(): number {
|
|
45
|
+
return this.queue.length;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Whether the serial drain loop is currently running (idle-wait + test seam). */
|
|
49
|
+
get isProcessing(): boolean {
|
|
50
|
+
return this.processing;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Enqueue a task and start the serial processor if it is idle. This is
|
|
55
|
+
* the production entry point — the orchestrator's `queueBufferedExtraction`
|
|
56
|
+
* builds each task closure and hands it here.
|
|
57
|
+
*/
|
|
58
|
+
enqueue(task: () => Promise<void>): void {
|
|
59
|
+
this.queue.push(task);
|
|
60
|
+
if (!this.processing) {
|
|
61
|
+
this.processing = true;
|
|
62
|
+
this.processQueue().catch((err) => {
|
|
63
|
+
this.logExtractionQueueFailure(err, "processor");
|
|
64
|
+
this.processing = false;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Background serial queue processor.
|
|
71
|
+
* Processes extractions one at a time to avoid race conditions.
|
|
72
|
+
* Called automatically when items are queued via `enqueue`; also exposed
|
|
73
|
+
* for the characterization tests that push raw tasks and assert drain
|
|
74
|
+
* semantics directly.
|
|
75
|
+
*/
|
|
76
|
+
async processQueue(): Promise<void> {
|
|
77
|
+
while (this.queue.length > 0) {
|
|
78
|
+
const task = this.queue.shift();
|
|
79
|
+
if (task) {
|
|
80
|
+
try {
|
|
81
|
+
await task();
|
|
82
|
+
} catch (err) {
|
|
83
|
+
this.logExtractionQueueFailure(err, "task");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.processing = false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Classify + log a failure from either the per-task catch inside
|
|
93
|
+
* `processQueue()` or the outer `processQueue().catch(...)` in
|
|
94
|
+
* `enqueue()`. Issue #549: `throwIfRecallAborted`
|
|
95
|
+
* (used throughout `runExtraction`) raises an Error whose `name` is
|
|
96
|
+
* `"AbortError"`. That path fires when `before_reset` aborts a
|
|
97
|
+
* queued task to avoid duplicate extraction — it is intentional
|
|
98
|
+
* cancellation, not a failure. Downgrading the log to debug
|
|
99
|
+
* prevents spurious `error`-level lines that routinely appear
|
|
100
|
+
* right next to a successful `persisted: N facts, M entities` log
|
|
101
|
+
* and that confuse operators into thinking extraction is broken.
|
|
102
|
+
* Genuine extraction failures (network, parse, I/O) still log at
|
|
103
|
+
* `error`.
|
|
104
|
+
*
|
|
105
|
+
* Source differentiates the two call sites so the log message
|
|
106
|
+
* names the right layer (`task` vs `processor`).
|
|
107
|
+
*/
|
|
108
|
+
logExtractionQueueFailure(
|
|
109
|
+
err: unknown,
|
|
110
|
+
source: "task" | "processor",
|
|
111
|
+
): void {
|
|
112
|
+
const aborted =
|
|
113
|
+
source === "task"
|
|
114
|
+
? "background extraction task aborted (session transition)"
|
|
115
|
+
: "background extraction queue processor aborted (session transition)";
|
|
116
|
+
const failed =
|
|
117
|
+
source === "task"
|
|
118
|
+
? "background extraction task failed"
|
|
119
|
+
: "background extraction queue processor failed";
|
|
120
|
+
if (isAbortError(err)) {
|
|
121
|
+
log.debug(aborted);
|
|
122
|
+
} else {
|
|
123
|
+
log.error(failed, err);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Wait until the extraction queue is fully drained (no tasks queued, no
|
|
129
|
+
* drain in flight) or `timeoutMs` elapses. Used by bootstrap (after a
|
|
130
|
+
* dry-run import) and by tests that need to assert extraction settled.
|
|
131
|
+
* Returns false on timeout, true once idle.
|
|
132
|
+
*/
|
|
133
|
+
async waitForIdle(timeoutMs: number = 60_000): Promise<boolean> {
|
|
134
|
+
const started = Date.now();
|
|
135
|
+
while (this.processing || this.queue.length > 0) {
|
|
136
|
+
if (Date.now() - started > timeoutMs) {
|
|
137
|
+
log.warn(`waitForExtractionIdle timed out after ${timeoutMs}ms`);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
141
|
+
setTimeout(resolve, 50);
|
|
142
|
+
await promise;
|
|
143
|
+
}
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Test seams ─────────────────────────────────────────────────────────
|
|
148
|
+
// Mirror the pre-extraction `orchestrator.extractionQueue` /
|
|
149
|
+
// `orchestrator.queueProcessing` direct-field access the characterization
|
|
150
|
+
// and flush deadline tests relied on. Each is a thin, well-named handle
|
|
151
|
+
// onto the coordinator's private state — no behavior change.
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Push a raw task WITHOUT auto-starting the processor, matching the
|
|
155
|
+
* pre-extraction `this.extractionQueue.push(...)` used by the
|
|
156
|
+
* characterization tests that drive the drain manually via `processQueue`.
|
|
157
|
+
*/
|
|
158
|
+
pushRaw(task: () => Promise<void>): void {
|
|
159
|
+
this.queue.push(task);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Remove + return the front task (test seam for the flush deadline tests). */
|
|
163
|
+
shift(): (() => Promise<void>) | undefined {
|
|
164
|
+
return this.queue.shift();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Force the processor-busy flag so `enqueue` does not auto-start the
|
|
169
|
+
* drain — used by the flush deadline test to simulate a busy queue and
|
|
170
|
+
* assert a queued task's deadline expires while it waits.
|
|
171
|
+
*/
|
|
172
|
+
setProcessingForTest(value: boolean): void {
|
|
173
|
+
this.processing = value;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { ExtractionQueueCoordinator } from "./orchestration/extraction-queue-coordinator.js";
|
|
5
5
|
import { initLogger, type LoggerBackend } from "./logger.js";
|
|
6
6
|
import { abortError } from "./abort-error.js";
|
|
7
7
|
|
|
@@ -31,17 +31,8 @@ function installCapturingLogger(): { entries: LogEntry[] } {
|
|
|
31
31
|
return { entries };
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
queueProcessing: boolean;
|
|
37
|
-
processQueue: () => Promise<void>;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
function createQueueOrchestrator(): QueueOrchestrator {
|
|
41
|
-
const orch = Object.create(Orchestrator.prototype) as QueueOrchestrator;
|
|
42
|
-
orch.extractionQueue = [];
|
|
43
|
-
orch.queueProcessing = true;
|
|
44
|
-
return orch;
|
|
34
|
+
function createCoordinator(): ExtractionQueueCoordinator {
|
|
35
|
+
return new ExtractionQueueCoordinator();
|
|
45
36
|
}
|
|
46
37
|
|
|
47
38
|
// ── Issue #549 ─────────────────────────────────────────────────────────────
|
|
@@ -53,11 +44,11 @@ test("processQueue logs an AbortError task at debug, not error (#549)", async ()
|
|
|
53
44
|
// That is intentional deduplication, not a failure. The queue
|
|
54
45
|
// processor must log it at debug.
|
|
55
46
|
const { entries } = installCapturingLogger();
|
|
56
|
-
const
|
|
57
|
-
|
|
47
|
+
const coordinator = createCoordinator();
|
|
48
|
+
coordinator.pushRaw(async () => {
|
|
58
49
|
throw abortError("extraction aborted (before_extract)");
|
|
59
50
|
});
|
|
60
|
-
await
|
|
51
|
+
await coordinator.processQueue();
|
|
61
52
|
|
|
62
53
|
const errorEntries = entries.filter((e) => e.level === "error");
|
|
63
54
|
const debugEntries = entries.filter(
|
|
@@ -80,11 +71,11 @@ test("processQueue still logs real task failures at error", async () => {
|
|
|
80
71
|
// Guard the other half: non-abort errors (network, parse, I/O) must
|
|
81
72
|
// continue to log at error level.
|
|
82
73
|
const { entries } = installCapturingLogger();
|
|
83
|
-
const
|
|
84
|
-
|
|
74
|
+
const coordinator = createCoordinator();
|
|
75
|
+
coordinator.pushRaw(async () => {
|
|
85
76
|
throw new Error("upstream LLM 500");
|
|
86
77
|
});
|
|
87
|
-
await
|
|
78
|
+
await coordinator.processQueue();
|
|
88
79
|
|
|
89
80
|
const errorEntries = entries.filter((e) => e.level === "error");
|
|
90
81
|
assert.equal(errorEntries.length, 1);
|
|
@@ -96,14 +87,14 @@ test("processQueue still logs real task failures at error", async () => {
|
|
|
96
87
|
|
|
97
88
|
test("processQueue handles a mixed run — abort goes to debug, failure to error", async () => {
|
|
98
89
|
const { entries } = installCapturingLogger();
|
|
99
|
-
const
|
|
100
|
-
|
|
90
|
+
const coordinator = createCoordinator();
|
|
91
|
+
coordinator.pushRaw(async () => {
|
|
101
92
|
throw abortError("extraction aborted (before_clear_buffer)");
|
|
102
93
|
});
|
|
103
|
-
|
|
94
|
+
coordinator.pushRaw(async () => {
|
|
104
95
|
throw new Error("I/O failure");
|
|
105
96
|
});
|
|
106
|
-
await
|
|
97
|
+
await coordinator.processQueue();
|
|
107
98
|
|
|
108
99
|
const errorCount = entries.filter((e) => e.level === "error").length;
|
|
109
100
|
const abortDebugCount = entries.filter(
|
|
@@ -121,30 +112,27 @@ test("processQueue handles a mixed run — abort goes to debug, failure to error
|
|
|
121
112
|
|
|
122
113
|
test("processQueue clears queueProcessing on exit regardless of task outcomes", async () => {
|
|
123
114
|
installCapturingLogger();
|
|
124
|
-
const
|
|
125
|
-
|
|
115
|
+
const coordinator = createCoordinator();
|
|
116
|
+
coordinator.setProcessingForTest(true);
|
|
117
|
+
coordinator.pushRaw(async () => {
|
|
126
118
|
throw abortError("extraction aborted");
|
|
127
119
|
});
|
|
128
|
-
await
|
|
129
|
-
assert.equal(
|
|
120
|
+
await coordinator.processQueue();
|
|
121
|
+
assert.equal(coordinator.isProcessing, false);
|
|
130
122
|
});
|
|
131
123
|
|
|
132
124
|
// ── Outer processQueue().catch() branch (Codex follow-up on #549) ──────────
|
|
133
125
|
|
|
134
|
-
type QueueProcessorOrchestrator = QueueOrchestrator & {
|
|
135
|
-
logExtractionQueueFailure: (err: unknown, source: "task" | "processor") => void;
|
|
136
|
-
};
|
|
137
|
-
|
|
138
126
|
test("logExtractionQueueFailure(processor) classifies AbortError as debug (#549)", async () => {
|
|
139
127
|
// Covers the outer `processQueue().catch(...)` path in
|
|
140
|
-
// `
|
|
128
|
+
// `enqueue`. A processor-level AbortError can
|
|
141
129
|
// bubble from e.g. a processQueue rewrite that awaits an external
|
|
142
130
|
// signal, so the handler must fail open to debug — otherwise a
|
|
143
131
|
// session-transition-triggered processor abort would get logged
|
|
144
132
|
// at error next to the successful extraction it just produced.
|
|
145
133
|
const { entries } = installCapturingLogger();
|
|
146
|
-
const
|
|
147
|
-
|
|
134
|
+
const coordinator = createCoordinator();
|
|
135
|
+
coordinator.logExtractionQueueFailure(
|
|
148
136
|
abortError("queue processor aborted"),
|
|
149
137
|
"processor",
|
|
150
138
|
);
|
|
@@ -160,8 +148,8 @@ test("logExtractionQueueFailure(processor) classifies AbortError as debug (#549)
|
|
|
160
148
|
|
|
161
149
|
test("logExtractionQueueFailure(processor) preserves error-level for real failures", async () => {
|
|
162
150
|
const { entries } = installCapturingLogger();
|
|
163
|
-
const
|
|
164
|
-
|
|
151
|
+
const coordinator = createCoordinator();
|
|
152
|
+
coordinator.logExtractionQueueFailure(
|
|
165
153
|
new Error("unexpected processor crash"),
|
|
166
154
|
"processor",
|
|
167
155
|
);
|
|
@@ -180,9 +168,9 @@ test("logExtractionQueueFailure names the right layer (task vs processor)", asyn
|
|
|
180
168
|
// tells them whether a specific extraction aborted or the queue
|
|
181
169
|
// processor itself did.
|
|
182
170
|
const { entries } = installCapturingLogger();
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
171
|
+
const coordinator = createCoordinator();
|
|
172
|
+
coordinator.logExtractionQueueFailure(abortError("a"), "task");
|
|
173
|
+
coordinator.logExtractionQueueFailure(abortError("b"), "processor");
|
|
186
174
|
const debugMessages = entries
|
|
187
175
|
.filter((e) => e.level === "debug")
|
|
188
176
|
.map((e) => e.message);
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
BulkImportBatchPartialFailureError,
|
|
8
8
|
Orchestrator,
|
|
9
9
|
} from "./orchestrator.js";
|
|
10
|
+
import { ExtractionQueueCoordinator } from "./orchestration/extraction-queue-coordinator.js";
|
|
10
11
|
import { parseConfig } from "./config.js";
|
|
11
12
|
import { stableHash } from "./coding/git-context.js";
|
|
12
13
|
import type { BufferTurn } from "./types.js";
|
|
@@ -117,8 +118,7 @@ test("flushSession waits for queued extraction task completion", async () => {
|
|
|
117
118
|
return [makeTurn("thread-a", "remember alpha")];
|
|
118
119
|
},
|
|
119
120
|
};
|
|
120
|
-
orchestrator.
|
|
121
|
-
orchestrator.queueProcessing = false;
|
|
121
|
+
orchestrator.extractionQueueCoordinator = new ExtractionQueueCoordinator();
|
|
122
122
|
orchestrator.runExtraction = async () => {
|
|
123
123
|
extractionStarted = true;
|
|
124
124
|
await new Promise<void>((resolve) => {
|
|
@@ -147,8 +147,8 @@ test("flushSession waits for queued extraction task completion", async () => {
|
|
|
147
147
|
test("ingestBulkImportBatch rejects when the extraction deadline expires in the queue", async () => {
|
|
148
148
|
const orchestrator = Object.create(Orchestrator.prototype) as any;
|
|
149
149
|
orchestrator.config = parseConfig({});
|
|
150
|
-
orchestrator.
|
|
151
|
-
orchestrator.
|
|
150
|
+
orchestrator.extractionQueueCoordinator = new ExtractionQueueCoordinator();
|
|
151
|
+
orchestrator.extractionQueueCoordinator.setProcessingForTest(true);
|
|
152
152
|
let runExtractionCalls = 0;
|
|
153
153
|
orchestrator.runExtraction = async () => {
|
|
154
154
|
runExtractionCalls += 1;
|
|
@@ -185,7 +185,7 @@ test("ingestBulkImportBatch rejects when the extraction deadline expires in the
|
|
|
185
185
|
);
|
|
186
186
|
assert.equal(runExtractionCalls, 0);
|
|
187
187
|
|
|
188
|
-
const queuedTask = orchestrator.
|
|
188
|
+
const queuedTask = orchestrator.extractionQueueCoordinator.shift();
|
|
189
189
|
assert.ok(queuedTask);
|
|
190
190
|
await queuedTask();
|
|
191
191
|
assert.equal(
|
|
@@ -198,8 +198,7 @@ test("ingestBulkImportBatch rejects when the extraction deadline expires in the
|
|
|
198
198
|
test("ingestBulkImportBatch does not report queue wait timeout after extraction starts", async () => {
|
|
199
199
|
const orchestrator = Object.create(Orchestrator.prototype) as any;
|
|
200
200
|
orchestrator.config = parseConfig({});
|
|
201
|
-
orchestrator.
|
|
202
|
-
orchestrator.queueProcessing = false;
|
|
201
|
+
orchestrator.extractionQueueCoordinator = new ExtractionQueueCoordinator();
|
|
203
202
|
let runExtractionCalls = 0;
|
|
204
203
|
orchestrator.runExtraction = async () => {
|
|
205
204
|
runExtractionCalls += 1;
|
|
@@ -234,8 +233,7 @@ test("ingestBulkImportBatch does not report queue wait timeout after extraction
|
|
|
234
233
|
test("ingestBulkImportBatch reports post-persist metadata failures separately", async () => {
|
|
235
234
|
const orchestrator = Object.create(Orchestrator.prototype) as any;
|
|
236
235
|
orchestrator.config = parseConfig({});
|
|
237
|
-
orchestrator.
|
|
238
|
-
orchestrator.queueProcessing = false;
|
|
236
|
+
orchestrator.extractionQueueCoordinator = new ExtractionQueueCoordinator();
|
|
239
237
|
orchestrator.runExtraction = async () => ({
|
|
240
238
|
status: "completed",
|
|
241
239
|
persistedCount: 1,
|
|
@@ -267,8 +265,7 @@ test("ingestBulkImportBatch reports post-persist metadata failures separately",
|
|
|
267
265
|
test("ingestBulkImportBatch can disable source-valid-at replay context", async () => {
|
|
268
266
|
const orchestrator = Object.create(Orchestrator.prototype) as any;
|
|
269
267
|
orchestrator.config = parseConfig({});
|
|
270
|
-
orchestrator.
|
|
271
|
-
orchestrator.queueProcessing = false;
|
|
268
|
+
orchestrator.extractionQueueCoordinator = new ExtractionQueueCoordinator();
|
|
272
269
|
const capturedSlices: BufferTurn[][] = [];
|
|
273
270
|
orchestrator.runExtraction = async (turns: BufferTurn[]) => {
|
|
274
271
|
capturedSlices.push(turns);
|
|
@@ -321,8 +318,7 @@ test("ingestBulkImportBatch can disable source-valid-at replay context", async (
|
|
|
321
318
|
test("ingestBulkImportBatch preserves partial metadata failure before a later slice rejects", async () => {
|
|
322
319
|
const orchestrator = Object.create(Orchestrator.prototype) as any;
|
|
323
320
|
orchestrator.config = parseConfig({});
|
|
324
|
-
orchestrator.
|
|
325
|
-
orchestrator.queueProcessing = false;
|
|
321
|
+
orchestrator.extractionQueueCoordinator = new ExtractionQueueCoordinator();
|
|
326
322
|
let runExtractionCalls = 0;
|
|
327
323
|
orchestrator.runExtraction = async () => {
|
|
328
324
|
runExtractionCalls += 1;
|
|
@@ -384,8 +380,7 @@ test("ingestBulkImportBatch preserves partial metadata failure before a later sl
|
|
|
384
380
|
test("ingestBulkImportBatch stops after the first failed source-valid-at slice", async () => {
|
|
385
381
|
const orchestrator = Object.create(Orchestrator.prototype) as any;
|
|
386
382
|
orchestrator.config = parseConfig({});
|
|
387
|
-
orchestrator.
|
|
388
|
-
orchestrator.queueProcessing = false;
|
|
383
|
+
orchestrator.extractionQueueCoordinator = new ExtractionQueueCoordinator();
|
|
389
384
|
let runExtractionCalls = 0;
|
|
390
385
|
orchestrator.runExtraction = async () => {
|
|
391
386
|
runExtractionCalls += 1;
|
package/src/orchestrator.ts
CHANGED
|
@@ -99,6 +99,7 @@ import {
|
|
|
99
99
|
} from "./fallback-llm.js";
|
|
100
100
|
import { MaintenanceScheduler } from "./orchestration/maintenance.js";
|
|
101
101
|
import { TierMigrationCoordinator } from "./orchestration/tier-migration-coordinator.js";
|
|
102
|
+
import { ExtractionQueueCoordinator } from "./orchestration/extraction-queue-coordinator.js";
|
|
102
103
|
import {
|
|
103
104
|
runLiveConnectorsOnce,
|
|
104
105
|
type LiveConnectorsRunSummary,
|
|
@@ -171,7 +172,6 @@ import {
|
|
|
171
172
|
import { SessionObserverState } from "./session-observer-state.js";
|
|
172
173
|
import {
|
|
173
174
|
abortError as sharedAbortError,
|
|
174
|
-
isAbortError,
|
|
175
175
|
throwIfAborted as sharedThrowIfAborted,
|
|
176
176
|
} from "./abort-error.js";
|
|
177
177
|
import { CODEX_THREAD_KEY_PREFIX } from "./thread-key.js";
|
|
@@ -2021,10 +2021,14 @@ export class Orchestrator {
|
|
|
2021
2021
|
suppressedReasonCounts: Record<string, number>;
|
|
2022
2022
|
} = { detected: 0, queued: 0, autoApplied: 0, suppressedReasonCounts: {} };
|
|
2023
2023
|
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2024
|
+
/**
|
|
2025
|
+
* Background serial extraction queue coordinator (issue #1526 — moved
|
|
2026
|
+
* from inline `extractionQueue`/`queueProcessing` fields). Owns queue
|
|
2027
|
+
* state + scheduling + the serial drain + failure classification; the
|
|
2028
|
+
* orchestrator builds each task closure in `queueBufferedExtraction` and
|
|
2029
|
+
* hands it to `enqueue`.
|
|
2030
|
+
*/
|
|
2031
|
+
readonly extractionQueueCoordinator: ExtractionQueueCoordinator;
|
|
2028
2032
|
private heartbeatObserverChains = new Map<string, Promise<void>>();
|
|
2029
2033
|
private recentExtractionFingerprints = new Map<string, number>();
|
|
2030
2034
|
private readonly consolidationObservers = new Set<
|
|
@@ -2991,6 +2995,8 @@ export class Orchestrator {
|
|
|
2991
2995
|
namespaceSearchRouter: this.namespaceSearchRouter,
|
|
2992
2996
|
namespaceCatalog: this.namespaceCatalog,
|
|
2993
2997
|
});
|
|
2998
|
+
// Issue #1526: background extraction queue lives on its own coordinator.
|
|
2999
|
+
this.extractionQueueCoordinator = new ExtractionQueueCoordinator();
|
|
2994
3000
|
const conversationIndexRuntime = createConversationIndexRuntime(config, {
|
|
2995
3001
|
getQmd: () => this.conversationQmd,
|
|
2996
3002
|
getFaiss: () => this.conversationFaiss,
|
|
@@ -4720,15 +4726,8 @@ export class Orchestrator {
|
|
|
4720
4726
|
}
|
|
4721
4727
|
|
|
4722
4728
|
async waitForExtractionIdle(timeoutMs: number = 60_000): Promise<boolean> {
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
if (Date.now() - started > timeoutMs) {
|
|
4726
|
-
log.warn(`waitForExtractionIdle timed out after ${timeoutMs}ms`);
|
|
4727
|
-
return false;
|
|
4728
|
-
}
|
|
4729
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
4730
|
-
}
|
|
4731
|
-
return true;
|
|
4729
|
+
// Issue #1526: queue state + idle wait moved to ExtractionQueueCoordinator.
|
|
4730
|
+
return this.extractionQueueCoordinator.waitForIdle(timeoutMs);
|
|
4732
4731
|
}
|
|
4733
4732
|
|
|
4734
4733
|
async waitForConsolidationIdle(timeoutMs: number = 60_000): Promise<boolean> {
|
|
@@ -13165,7 +13164,7 @@ export class Orchestrator {
|
|
|
13165
13164
|
}, remainingMs);
|
|
13166
13165
|
}
|
|
13167
13166
|
|
|
13168
|
-
this.
|
|
13167
|
+
this.extractionQueueCoordinator.enqueue(async () => {
|
|
13169
13168
|
if (settled) return;
|
|
13170
13169
|
if (
|
|
13171
13170
|
typeof extractionDeadlineMs === "number" &&
|
|
@@ -13196,13 +13195,6 @@ export class Orchestrator {
|
|
|
13196
13195
|
}
|
|
13197
13196
|
});
|
|
13198
13197
|
|
|
13199
|
-
if (!this.queueProcessing) {
|
|
13200
|
-
this.queueProcessing = true;
|
|
13201
|
-
this.processQueue().catch((err) => {
|
|
13202
|
-
this.logExtractionQueueFailure(err, "processor");
|
|
13203
|
-
this.queueProcessing = false;
|
|
13204
|
-
});
|
|
13205
|
-
}
|
|
13206
13198
|
log.debug(`queued extraction from ${reason}`);
|
|
13207
13199
|
}
|
|
13208
13200
|
|
|
@@ -13270,62 +13262,6 @@ export class Orchestrator {
|
|
|
13270
13262
|
return true;
|
|
13271
13263
|
}
|
|
13272
13264
|
|
|
13273
|
-
/**
|
|
13274
|
-
* Background serial queue processor.
|
|
13275
|
-
* Processes extractions one at a time to avoid race conditions.
|
|
13276
|
-
* Called automatically when items are queued.
|
|
13277
|
-
*/
|
|
13278
|
-
private async processQueue(): Promise<void> {
|
|
13279
|
-
while (this.extractionQueue.length > 0) {
|
|
13280
|
-
const task = this.extractionQueue.shift();
|
|
13281
|
-
if (task) {
|
|
13282
|
-
try {
|
|
13283
|
-
await task();
|
|
13284
|
-
} catch (err) {
|
|
13285
|
-
this.logExtractionQueueFailure(err, "task");
|
|
13286
|
-
}
|
|
13287
|
-
}
|
|
13288
|
-
}
|
|
13289
|
-
|
|
13290
|
-
this.queueProcessing = false;
|
|
13291
|
-
}
|
|
13292
|
-
|
|
13293
|
-
/**
|
|
13294
|
-
* Classify + log a failure from either the per-task catch inside
|
|
13295
|
-
* `processQueue()` or the outer `processQueue().catch(...)` in
|
|
13296
|
-
* `queueBufferedExtraction()`. Issue #549: `throwIfRecallAborted`
|
|
13297
|
-
* (used throughout `runExtraction`) raises an Error whose `name` is
|
|
13298
|
-
* `"AbortError"`. That path fires when `before_reset` aborts a
|
|
13299
|
-
* queued task to avoid duplicate extraction — it is intentional
|
|
13300
|
-
* cancellation, not a failure. Downgrading the log to debug
|
|
13301
|
-
* prevents spurious `error`-level lines that routinely appear
|
|
13302
|
-
* right next to a successful `persisted: N facts, M entities` log
|
|
13303
|
-
* and that confuse operators into thinking extraction is broken.
|
|
13304
|
-
* Genuine extraction failures (network, parse, I/O) still log at
|
|
13305
|
-
* `error`.
|
|
13306
|
-
*
|
|
13307
|
-
* Source differentiates the two call sites so the log message
|
|
13308
|
-
* names the right layer (`task` vs `processor`).
|
|
13309
|
-
*/
|
|
13310
|
-
private logExtractionQueueFailure(
|
|
13311
|
-
err: unknown,
|
|
13312
|
-
source: "task" | "processor",
|
|
13313
|
-
): void {
|
|
13314
|
-
const aborted =
|
|
13315
|
-
source === "task"
|
|
13316
|
-
? "background extraction task aborted (session transition)"
|
|
13317
|
-
: "background extraction queue processor aborted (session transition)";
|
|
13318
|
-
const failed =
|
|
13319
|
-
source === "task"
|
|
13320
|
-
? "background extraction task failed"
|
|
13321
|
-
: "background extraction queue processor failed";
|
|
13322
|
-
if (isAbortError(err)) {
|
|
13323
|
-
log.debug(aborted);
|
|
13324
|
-
} else {
|
|
13325
|
-
log.error(failed, err);
|
|
13326
|
-
}
|
|
13327
|
-
}
|
|
13328
|
-
|
|
13329
13265
|
/**
|
|
13330
13266
|
* Passive correction capture (issue #1581) — detects corrections expressed
|
|
13331
13267
|
* passively in conversation turns and routes them to the Correction Contract
|