@remnic/core 9.3.727 → 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.
Files changed (95) hide show
  1. package/dist/access-boundary.d.ts +2 -2
  2. package/dist/access-boundary.js +6 -6
  3. package/dist/access-cli.js +11 -11
  4. package/dist/access-http.d.ts +2 -2
  5. package/dist/access-http.js +10 -10
  6. package/dist/access-mcp.d.ts +2 -2
  7. package/dist/access-mcp.js +9 -9
  8. package/dist/access-operations-batch.js +7 -7
  9. package/dist/access-operations.d.ts +8 -8
  10. package/dist/access-operations.js +8 -8
  11. package/dist/access-schema.d.ts +46 -46
  12. package/dist/{access-service-Bgpjxo4f.d.ts → access-service-BXMJqkTA.d.ts} +1 -1
  13. package/dist/access-service.d.ts +2 -2
  14. package/dist/access-service.js +5 -5
  15. package/dist/access-surface-catalog.d.ts +2 -2
  16. package/dist/bootstrap.d.ts +1 -1
  17. package/dist/capabilities.d.ts +43 -1
  18. package/dist/capabilities.js +5 -3
  19. package/dist/{chunk-DVRHLN2O.js → chunk-3RCBIOOJ.js} +18 -2
  20. package/dist/{chunk-DVRHLN2O.js.map → chunk-3RCBIOOJ.js.map} +1 -1
  21. package/dist/{chunk-R7IYU5DZ.js → chunk-4ESAR77N.js} +2 -2
  22. package/dist/{chunk-SNKPUJHR.js → chunk-76NHR6ZL.js} +3 -3
  23. package/dist/{chunk-6PY5YJAV.js → chunk-BQCHEV7O.js} +8 -3
  24. package/dist/{chunk-6PY5YJAV.js.map → chunk-BQCHEV7O.js.map} +1 -1
  25. package/dist/{chunk-ZA2S2VLL.js → chunk-C2PG7IEH.js} +5 -2
  26. package/dist/chunk-C2PG7IEH.js.map +1 -0
  27. package/dist/{chunk-MJIHJ7OG.js → chunk-H5BWGCRZ.js} +11 -10
  28. package/dist/chunk-H5BWGCRZ.js.map +1 -0
  29. package/dist/{chunk-XPHVAOBT.js → chunk-HEDZUSCS.js} +180 -101
  30. package/dist/chunk-HEDZUSCS.js.map +1 -0
  31. package/dist/{chunk-N3VNUM4Z.js → chunk-HGNOCPZX.js} +4 -4
  32. package/dist/{chunk-CSRXQ6NB.js → chunk-I5REUWLH.js} +10 -4
  33. package/dist/chunk-I5REUWLH.js.map +1 -0
  34. package/dist/{chunk-NSA5ZRUL.js → chunk-M4AYVEWS.js} +3 -3
  35. package/dist/{chunk-OEEW4KJA.js → chunk-O6YH5F47.js} +3 -3
  36. package/dist/{chunk-27QQC7ZX.js → chunk-UM5AWJNQ.js} +6 -5
  37. package/dist/chunk-UM5AWJNQ.js.map +1 -0
  38. package/dist/{chunk-2KAYTPPT.js → chunk-UTHQPSRC.js} +2 -2
  39. package/dist/{chunk-CTOQEZSN.js → chunk-V4GNSDFH.js} +2 -2
  40. package/dist/{chunk-EHISUJFN.js → chunk-WAS3NV46.js} +6 -3
  41. package/dist/chunk-WAS3NV46.js.map +1 -0
  42. package/dist/{chunk-DKJCRGWM.js → chunk-YGIQORJ3.js} +2 -2
  43. package/dist/{cli-BLME420h.d.ts → cli-DmAiDd8A.d.ts} +2 -2
  44. package/dist/cli.d.ts +3 -3
  45. package/dist/cli.js +13 -13
  46. package/dist/embedding-fallback.js +2 -1
  47. package/dist/explicit-capture.d.ts +1 -1
  48. package/dist/extraction.js +2 -1
  49. package/dist/{first-start-migration-PG5HBC3K.js → first-start-migration-22LPFHD5.js} +6 -2
  50. package/dist/first-start-migration-22LPFHD5.js.map +1 -0
  51. package/dist/index.d.ts +4 -4
  52. package/dist/index.js +16 -16
  53. package/dist/mcp-memory-inspector-app.d.ts +2 -2
  54. package/dist/namespaces/migrate.js +5 -4
  55. package/dist/namespaces/search.js +4 -3
  56. package/dist/operator-toolkit.js +6 -6
  57. package/dist/{orchestrator-DqPAz8ot.d.ts → orchestrator-BR0sGsME.d.ts} +104 -26
  58. package/dist/orchestrator.d.ts +1 -1
  59. package/dist/orchestrator.js +7 -7
  60. package/dist/schemas.d.ts +74 -74
  61. package/dist/search/embed-helper.js +2 -1
  62. package/dist/search/factory.js +3 -2
  63. package/dist/search/index.js +3 -2
  64. package/dist/shared-context/manager.d.ts +2 -2
  65. package/dist/transfer/types.d.ts +12 -12
  66. package/package.json +2 -2
  67. package/src/access-service.ts +2 -1
  68. package/src/capabilities.test.ts +155 -0
  69. package/src/capabilities.ts +91 -0
  70. package/src/chat/chat-engine.test.ts +39 -1
  71. package/src/chat/chat-session.ts +25 -2
  72. package/src/cli.ts +3 -2
  73. package/src/embedding-fallback.ts +2 -1
  74. package/src/extraction.ts +7 -3
  75. package/src/maintenance/first-start-migration.ts +3 -1
  76. package/src/orchestration/extraction-queue-coordinator.ts +175 -0
  77. package/src/orchestrator-extraction-queue.test.ts +26 -38
  78. package/src/orchestrator-flush.test.ts +10 -15
  79. package/src/orchestrator.ts +52 -104
  80. package/src/search/embed-helper.ts +3 -2
  81. package/dist/chunk-27QQC7ZX.js.map +0 -1
  82. package/dist/chunk-CSRXQ6NB.js.map +0 -1
  83. package/dist/chunk-EHISUJFN.js.map +0 -1
  84. package/dist/chunk-MJIHJ7OG.js.map +0 -1
  85. package/dist/chunk-XPHVAOBT.js.map +0 -1
  86. package/dist/chunk-ZA2S2VLL.js.map +0 -1
  87. package/dist/first-start-migration-PG5HBC3K.js.map +0 -1
  88. /package/dist/{chunk-R7IYU5DZ.js.map → chunk-4ESAR77N.js.map} +0 -0
  89. /package/dist/{chunk-SNKPUJHR.js.map → chunk-76NHR6ZL.js.map} +0 -0
  90. /package/dist/{chunk-N3VNUM4Z.js.map → chunk-HGNOCPZX.js.map} +0 -0
  91. /package/dist/{chunk-NSA5ZRUL.js.map → chunk-M4AYVEWS.js.map} +0 -0
  92. /package/dist/{chunk-OEEW4KJA.js.map → chunk-O6YH5F47.js.map} +0 -0
  93. /package/dist/{chunk-2KAYTPPT.js.map → chunk-UTHQPSRC.js.map} +0 -0
  94. /package/dist/{chunk-CTOQEZSN.js.map → chunk-V4GNSDFH.js.map} +0 -0
  95. /package/dist/{chunk-DKJCRGWM.js.map → chunk-YGIQORJ3.js.map} +0 -0
package/src/extraction.ts CHANGED
@@ -38,6 +38,7 @@ import { normalizeProcedureSteps } from "./procedural/procedure-types.js";
38
38
  import { normalizeReasoningTrace } from "./reasoning-trace-types.js";
39
39
  import { looksLikeMechanicalTelemetryTranscript } from "./telemetry-transcript.js";
40
40
  import { buildFactProvenance, type ProvenanceTurnInput } from "./provenance.js";
41
+ import { resolveMemoryLifecycleCapabilities } from "./capabilities.js";
41
42
 
42
43
  type ExtractionQuestion = ExtractionResult["questions"][number];
43
44
  type ExtractedFactResult = ExtractionResult["facts"][number];
@@ -1055,6 +1056,7 @@ export class ExtractionEngine {
1055
1056
  }
1056
1057
 
1057
1058
  async extract(turns: BufferTurn[], existingEntities?: string[]): Promise<ExtractionResult> {
1059
+ const lifecycleCaps = resolveMemoryLifecycleCapabilities(this.config);
1058
1060
 
1059
1061
  // Guard: skip if buffer is empty or all turns are whitespace-only
1060
1062
  const substantiveTurns = turns.filter((t) => t.content.trim().length > 0);
@@ -1083,7 +1085,7 @@ export class ExtractionEngine {
1083
1085
  return { facts: [], profileUpdates: [], entities: [], questions: [] };
1084
1086
  }
1085
1087
  if (
1086
- this.config.extractionTelemetryPrefilterEnabled &&
1088
+ lifecycleCaps.extractionTelemetryPrefilter &&
1087
1089
  looksLikeMechanicalTelemetryTranscript(conversation)
1088
1090
  ) {
1089
1091
  log.debug("extraction skipped — mechanical action/state telemetry without durable-memory cues");
@@ -1331,6 +1333,7 @@ export class ExtractionEngine {
1331
1333
  * Uses a minimal prompt to fit within local model context limits (typically 4k-8k).
1332
1334
  */
1333
1335
  private async extractWithLocalLlm(conversation: string, existingEntities?: string[]): Promise<ExtractionResult | null> {
1336
+ const lifecycleCaps = resolveMemoryLifecycleCapabilities(this.config);
1334
1337
  log.debug(
1335
1338
  `extractWithLocalLlm: starting extraction, localLlmEnabled=${this.shouldUseLocalLlm}, model=${this.config.localLlmModel}`,
1336
1339
  );
@@ -1406,7 +1409,7 @@ These are durable insights - capture them:
1406
1409
  - CRITICAL: Use canonical hyphenated entity names (e.g., "jane-doe" not "janedoe")
1407
1410
  - CRITICAL: NEVER extract the same fact twice - check for duplicates before adding to facts array
1408
1411
  - CRITICAL: NEVER extract cron job schedules, automation configurations, or system monitoring details (these are operational noise)
1409
- - If uncertain about relevance, prefer NOT extracting${this.config.extractionScopeClassificationEnabled ? `
1412
+ - If uncertain about relevance, prefer NOT extracting${lifecycleCaps.extractionScopeClassification ? `
1410
1413
  - For each fact, set "scope" to "global" (cross-project knowledge: framework bugs, library behavior, user preferences, tool configs, general patterns) or "project" (codebase-specific: file paths, env configs, deployment details, project workarounds). When in doubt, prefer "project".` : ""}
1411
1414
 
1412
1415
  === Structured Attributes ===
@@ -1645,6 +1648,7 @@ Omit "eventTime" when the fact has no explicit temporal anchor — do NOT guess
1645
1648
  * Build extraction instructions shared between local and cloud LLM.
1646
1649
  */
1647
1650
  private buildExtractionInstructions(existingEntities?: string[]): string {
1651
+ const lifecycleCaps = resolveMemoryLifecycleCapabilities(this.config);
1648
1652
  return `You are a memory extraction system. Analyze the following conversation and extract durable, reusable memories.
1649
1653
 
1650
1654
  Memory categories:
@@ -1681,7 +1685,7 @@ Rules:
1681
1685
  * Inferred (0.40-0.69): Pattern recognition — reasonable guess from limited evidence
1682
1686
  * Speculative (0.00-0.39): Tentative hypothesis — weak signal, needs future confirmation. Speculative memories auto-expire after 30 days if not confirmed.${this.config.provenance?.enabled ? `
1683
1687
  - Source quotes: For each fact, include a "quote" field containing the EXACT verbatim words from the conversation that support the fact. Copy a contiguous span from a single speaker turn (not a paraphrase, not a summary). Cap at ~300 characters. This grounds every memory in the literal utterance that created it.` : ""}
1684
- - For commitments: include any deadline or timeframe mentioned${this.config.extractionScopeClassificationEnabled ? `
1688
+ - For commitments: include any deadline or timeframe mentioned${lifecycleCaps.extractionScopeClassification ? `
1685
1689
 
1686
1690
  Scope classification:
1687
1691
  For each fact, set "scope" to one of:
@@ -18,6 +18,7 @@
18
18
 
19
19
  import path from "node:path";
20
20
  import { access, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
21
+ import { resolveMemoryLifecycleCapabilities } from "../capabilities.js";
21
22
  import type { StorageManager } from "../storage.js";
22
23
  import type { PluginConfig } from "../types.js";
23
24
  import {
@@ -179,6 +180,7 @@ export async function runFirstStartMigration(
179
180
  signal,
180
181
  } = options;
181
182
  const now = (options.now ?? (() => new Date()))();
183
+ const lifecycleCaps = resolveMemoryLifecycleCapabilities(config);
182
184
  const abortedResult = (candidateCount = 0, demotedCount = 0, failureCount = 0): FirstStartMigrationResult => ({
183
185
  skipped: candidateCount === 0 && demotedCount === 0 && failureCount === 0,
184
186
  skipReason: candidateCount === 0 && demotedCount === 0 && failureCount === 0 ? "aborted" : undefined,
@@ -193,7 +195,7 @@ export async function runFirstStartMigration(
193
195
  return abortedResult();
194
196
  }
195
197
 
196
- if (!config.lifecyclePolicyEnabled) {
198
+ if (!lifecycleCaps.lifecyclePolicy) {
197
199
  return {
198
200
  skipped: true,
199
201
  skipReason: "lifecyclePolicyEnabled is false",
@@ -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 { Orchestrator } from "./orchestrator.js";
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
- type QueueOrchestrator = {
35
- extractionQueue: Array<() => Promise<void>>;
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 orch = createQueueOrchestrator();
57
- orch.extractionQueue.push(async () => {
47
+ const coordinator = createCoordinator();
48
+ coordinator.pushRaw(async () => {
58
49
  throw abortError("extraction aborted (before_extract)");
59
50
  });
60
- await orch.processQueue();
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 orch = createQueueOrchestrator();
84
- orch.extractionQueue.push(async () => {
74
+ const coordinator = createCoordinator();
75
+ coordinator.pushRaw(async () => {
85
76
  throw new Error("upstream LLM 500");
86
77
  });
87
- await orch.processQueue();
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 orch = createQueueOrchestrator();
100
- orch.extractionQueue.push(async () => {
90
+ const coordinator = createCoordinator();
91
+ coordinator.pushRaw(async () => {
101
92
  throw abortError("extraction aborted (before_clear_buffer)");
102
93
  });
103
- orch.extractionQueue.push(async () => {
94
+ coordinator.pushRaw(async () => {
104
95
  throw new Error("I/O failure");
105
96
  });
106
- await orch.processQueue();
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 orch = createQueueOrchestrator();
125
- orch.extractionQueue.push(async () => {
115
+ const coordinator = createCoordinator();
116
+ coordinator.setProcessingForTest(true);
117
+ coordinator.pushRaw(async () => {
126
118
  throw abortError("extraction aborted");
127
119
  });
128
- await orch.processQueue();
129
- assert.equal(orch.queueProcessing, false);
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
- // `queueBufferedExtraction`. A processor-level AbortError can
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 orch = createQueueOrchestrator() as QueueProcessorOrchestrator;
147
- orch.logExtractionQueueFailure(
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 orch = createQueueOrchestrator() as QueueProcessorOrchestrator;
164
- orch.logExtractionQueueFailure(
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 orch = createQueueOrchestrator() as QueueProcessorOrchestrator;
184
- orch.logExtractionQueueFailure(abortError("a"), "task");
185
- orch.logExtractionQueueFailure(abortError("b"), "processor");
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.extractionQueue = [];
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.extractionQueue = [];
151
- orchestrator.queueProcessing = true;
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.extractionQueue.shift();
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.extractionQueue = [];
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.extractionQueue = [];
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.extractionQueue = [];
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.extractionQueue = [];
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.extractionQueue = [];
388
- orchestrator.queueProcessing = false;
383
+ orchestrator.extractionQueueCoordinator = new ExtractionQueueCoordinator();
389
384
  let runExtractionCalls = 0;
390
385
  orchestrator.runExtraction = async () => {
391
386
  runExtractionCalls += 1;