@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.
@@ -746,6 +746,130 @@ var TierMigrationCoordinator = class {
746
746
  }
747
747
  };
748
748
 
749
+ // src/orchestration/extraction-queue-coordinator.ts
750
+ var ExtractionQueueCoordinator = class {
751
+ /**
752
+ * Background serial queue for extractions (agent_end optimization).
753
+ * Queue stores tasks that resolve when extraction should run.
754
+ */
755
+ queue = [];
756
+ /** Whether the serial drain loop is currently running. */
757
+ processing = false;
758
+ /** Current queue depth (idle-wait + test seam). */
759
+ get length() {
760
+ return this.queue.length;
761
+ }
762
+ /** Whether the serial drain loop is currently running (idle-wait + test seam). */
763
+ get isProcessing() {
764
+ return this.processing;
765
+ }
766
+ /**
767
+ * Enqueue a task and start the serial processor if it is idle. This is
768
+ * the production entry point — the orchestrator's `queueBufferedExtraction`
769
+ * builds each task closure and hands it here.
770
+ */
771
+ enqueue(task) {
772
+ this.queue.push(task);
773
+ if (!this.processing) {
774
+ this.processing = true;
775
+ this.processQueue().catch((err) => {
776
+ this.logExtractionQueueFailure(err, "processor");
777
+ this.processing = false;
778
+ });
779
+ }
780
+ }
781
+ /**
782
+ * Background serial queue processor.
783
+ * Processes extractions one at a time to avoid race conditions.
784
+ * Called automatically when items are queued via `enqueue`; also exposed
785
+ * for the characterization tests that push raw tasks and assert drain
786
+ * semantics directly.
787
+ */
788
+ async processQueue() {
789
+ while (this.queue.length > 0) {
790
+ const task = this.queue.shift();
791
+ if (task) {
792
+ try {
793
+ await task();
794
+ } catch (err) {
795
+ this.logExtractionQueueFailure(err, "task");
796
+ }
797
+ }
798
+ }
799
+ this.processing = false;
800
+ }
801
+ /**
802
+ * Classify + log a failure from either the per-task catch inside
803
+ * `processQueue()` or the outer `processQueue().catch(...)` in
804
+ * `enqueue()`. Issue #549: `throwIfRecallAborted`
805
+ * (used throughout `runExtraction`) raises an Error whose `name` is
806
+ * `"AbortError"`. That path fires when `before_reset` aborts a
807
+ * queued task to avoid duplicate extraction — it is intentional
808
+ * cancellation, not a failure. Downgrading the log to debug
809
+ * prevents spurious `error`-level lines that routinely appear
810
+ * right next to a successful `persisted: N facts, M entities` log
811
+ * and that confuse operators into thinking extraction is broken.
812
+ * Genuine extraction failures (network, parse, I/O) still log at
813
+ * `error`.
814
+ *
815
+ * Source differentiates the two call sites so the log message
816
+ * names the right layer (`task` vs `processor`).
817
+ */
818
+ logExtractionQueueFailure(err, source) {
819
+ const aborted = source === "task" ? "background extraction task aborted (session transition)" : "background extraction queue processor aborted (session transition)";
820
+ const failed = source === "task" ? "background extraction task failed" : "background extraction queue processor failed";
821
+ if (isAbortError(err)) {
822
+ log.debug(aborted);
823
+ } else {
824
+ log.error(failed, err);
825
+ }
826
+ }
827
+ /**
828
+ * Wait until the extraction queue is fully drained (no tasks queued, no
829
+ * drain in flight) or `timeoutMs` elapses. Used by bootstrap (after a
830
+ * dry-run import) and by tests that need to assert extraction settled.
831
+ * Returns false on timeout, true once idle.
832
+ */
833
+ async waitForIdle(timeoutMs = 6e4) {
834
+ const started = Date.now();
835
+ while (this.processing || this.queue.length > 0) {
836
+ if (Date.now() - started > timeoutMs) {
837
+ log.warn(`waitForExtractionIdle timed out after ${timeoutMs}ms`);
838
+ return false;
839
+ }
840
+ const { promise, resolve } = Promise.withResolvers();
841
+ setTimeout(resolve, 50);
842
+ await promise;
843
+ }
844
+ return true;
845
+ }
846
+ // ── Test seams ─────────────────────────────────────────────────────────
847
+ // Mirror the pre-extraction `orchestrator.extractionQueue` /
848
+ // `orchestrator.queueProcessing` direct-field access the characterization
849
+ // and flush deadline tests relied on. Each is a thin, well-named handle
850
+ // onto the coordinator's private state — no behavior change.
851
+ /**
852
+ * Push a raw task WITHOUT auto-starting the processor, matching the
853
+ * pre-extraction `this.extractionQueue.push(...)` used by the
854
+ * characterization tests that drive the drain manually via `processQueue`.
855
+ */
856
+ pushRaw(task) {
857
+ this.queue.push(task);
858
+ }
859
+ /** Remove + return the front task (test seam for the flush deadline tests). */
860
+ shift() {
861
+ return this.queue.shift();
862
+ }
863
+ /**
864
+ * Force the processor-busy flag so `enqueue` does not auto-start the
865
+ * drain — used by the flush deadline test to simulate a busy queue and
866
+ * assert a queued task's deadline expires while it waits.
867
+ */
868
+ setProcessingForTest(value) {
869
+ this.processing = value;
870
+ }
871
+ };
872
+
749
873
  // src/maintenance/pattern-reinforcement.ts
750
874
  function patternReinforcementKey(content) {
751
875
  return content.trim().toLowerCase().replace(/\s+/g, " ").slice(0, 200);
@@ -2998,10 +3122,14 @@ var Orchestrator = class _Orchestrator {
2998
3122
  passiveCorrectionDedup = /* @__PURE__ */ new Set();
2999
3123
  _passiveCorrectionService = null;
3000
3124
  passiveCorrectionTelemetry = { detected: 0, queued: 0, autoApplied: 0, suppressedReasonCounts: {} };
3001
- // Background serial queue for extractions (agent_end optimization)
3002
- // Queue stores promises that resolve when extraction should run
3003
- extractionQueue = [];
3004
- queueProcessing = false;
3125
+ /**
3126
+ * Background serial extraction queue coordinator (issue #1526 moved
3127
+ * from inline `extractionQueue`/`queueProcessing` fields). Owns queue
3128
+ * state + scheduling + the serial drain + failure classification; the
3129
+ * orchestrator builds each task closure in `queueBufferedExtraction` and
3130
+ * hands it to `enqueue`.
3131
+ */
3132
+ extractionQueueCoordinator;
3005
3133
  heartbeatObserverChains = /* @__PURE__ */ new Map();
3006
3134
  recentExtractionFingerprints = /* @__PURE__ */ new Map();
3007
3135
  consolidationObservers = /* @__PURE__ */ new Set();
@@ -3706,6 +3834,7 @@ var Orchestrator = class _Orchestrator {
3706
3834
  namespaceSearchRouter: this.namespaceSearchRouter,
3707
3835
  namespaceCatalog: this.namespaceCatalog
3708
3836
  });
3837
+ this.extractionQueueCoordinator = new ExtractionQueueCoordinator();
3709
3838
  const conversationIndexRuntime = createConversationIndexRuntime(config, {
3710
3839
  getQmd: () => this.conversationQmd,
3711
3840
  getFaiss: () => this.conversationFaiss
@@ -4928,15 +5057,7 @@ ${doc.content}` : doc.content,
4928
5057
  return result;
4929
5058
  }
4930
5059
  async waitForExtractionIdle(timeoutMs = 6e4) {
4931
- const started = Date.now();
4932
- while (this.queueProcessing || this.extractionQueue.length > 0) {
4933
- if (Date.now() - started > timeoutMs) {
4934
- log.warn(`waitForExtractionIdle timed out after ${timeoutMs}ms`);
4935
- return false;
4936
- }
4937
- await new Promise((resolve) => setTimeout(resolve, 50));
4938
- }
4939
- return true;
5060
+ return this.extractionQueueCoordinator.waitForIdle(timeoutMs);
4940
5061
  }
4941
5062
  async waitForConsolidationIdle(timeoutMs = 6e4) {
4942
5063
  const started = Date.now();
@@ -11161,7 +11282,7 @@ _Context: ${topQuestion.context}_`
11161
11282
  settleTask(new Error("replay extraction deadline exceeded (queue_wait)"));
11162
11283
  }, remainingMs);
11163
11284
  }
11164
- this.extractionQueue.push(async () => {
11285
+ this.extractionQueueCoordinator.enqueue(async () => {
11165
11286
  if (settled) return;
11166
11287
  if (typeof extractionDeadlineMs === "number" && extractionDeadlineMs <= Date.now()) {
11167
11288
  settleTask(new Error("replay extraction deadline exceeded (queue_wait)"));
@@ -11187,13 +11308,6 @@ _Context: ${topQuestion.context}_`
11187
11308
  }
11188
11309
  }
11189
11310
  });
11190
- if (!this.queueProcessing) {
11191
- this.queueProcessing = true;
11192
- this.processQueue().catch((err) => {
11193
- this.logExtractionQueueFailure(err, "processor");
11194
- this.queueProcessing = false;
11195
- });
11196
- }
11197
11311
  log.debug(`queued extraction from ${reason}`);
11198
11312
  }
11199
11313
  normalizeExtractionFingerprintTurns(turns) {
@@ -11237,50 +11351,6 @@ ${normalized}`).digest("hex");
11237
11351
  }
11238
11352
  return true;
11239
11353
  }
11240
- /**
11241
- * Background serial queue processor.
11242
- * Processes extractions one at a time to avoid race conditions.
11243
- * Called automatically when items are queued.
11244
- */
11245
- async processQueue() {
11246
- while (this.extractionQueue.length > 0) {
11247
- const task = this.extractionQueue.shift();
11248
- if (task) {
11249
- try {
11250
- await task();
11251
- } catch (err) {
11252
- this.logExtractionQueueFailure(err, "task");
11253
- }
11254
- }
11255
- }
11256
- this.queueProcessing = false;
11257
- }
11258
- /**
11259
- * Classify + log a failure from either the per-task catch inside
11260
- * `processQueue()` or the outer `processQueue().catch(...)` in
11261
- * `queueBufferedExtraction()`. Issue #549: `throwIfRecallAborted`
11262
- * (used throughout `runExtraction`) raises an Error whose `name` is
11263
- * `"AbortError"`. That path fires when `before_reset` aborts a
11264
- * queued task to avoid duplicate extraction — it is intentional
11265
- * cancellation, not a failure. Downgrading the log to debug
11266
- * prevents spurious `error`-level lines that routinely appear
11267
- * right next to a successful `persisted: N facts, M entities` log
11268
- * and that confuse operators into thinking extraction is broken.
11269
- * Genuine extraction failures (network, parse, I/O) still log at
11270
- * `error`.
11271
- *
11272
- * Source differentiates the two call sites so the log message
11273
- * names the right layer (`task` vs `processor`).
11274
- */
11275
- logExtractionQueueFailure(err, source) {
11276
- const aborted = source === "task" ? "background extraction task aborted (session transition)" : "background extraction queue processor aborted (session transition)";
11277
- const failed = source === "task" ? "background extraction task failed" : "background extraction queue processor failed";
11278
- if (isAbortError(err)) {
11279
- log.debug(aborted);
11280
- } else {
11281
- log.error(failed, err);
11282
- }
11283
- }
11284
11354
  /**
11285
11355
  * Passive correction capture (issue #1581) — detects corrections expressed
11286
11356
  * passively in conversation turns and routes them to the Correction Contract
@@ -16322,4 +16392,4 @@ export {
16322
16392
  resolvePersistedMemoryRelativePath,
16323
16393
  Orchestrator
16324
16394
  };
16325
- //# sourceMappingURL=chunk-QNEUZIAH.js.map
16395
+ //# sourceMappingURL=chunk-HEDZUSCS.js.map