@remnic/core 10.0.66 → 10.0.68

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