@remnic/core 10.0.70 → 10.0.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/dist/access-admin-ops-surface.js +14 -14
  2. package/dist/access-authorization-probe.js +15 -15
  3. package/dist/access-boundary.js +14 -14
  4. package/dist/access-cli.js +58 -58
  5. package/dist/access-extraction-force-flush.js +14 -14
  6. package/dist/access-http-briefing.js +15 -15
  7. package/dist/access-http-query.js +16 -16
  8. package/dist/access-http.js +31 -31
  9. package/dist/access-identity-continuity-surface.js +14 -14
  10. package/dist/access-lcm-surface.js +14 -14
  11. package/dist/access-mcp.js +24 -24
  12. package/dist/access-namespace-preflight.js +14 -14
  13. package/dist/access-observe-write-surface.js +14 -14
  14. package/dist/access-operations-batch.js +16 -16
  15. package/dist/access-operations.js +23 -23
  16. package/dist/access-recall-concurrency.js +14 -14
  17. package/dist/access-recall-response.js +14 -14
  18. package/dist/access-recall-surface.js +14 -14
  19. package/dist/access-service.js +14 -14
  20. package/dist/access-surface-catalog.js +16 -16
  21. package/dist/active-recall.js +3 -3
  22. package/dist/briefing.js +4 -4
  23. package/dist/buffer-turn-helpers.d.ts +10 -1
  24. package/dist/buffer-turn-helpers.d.ts.map +1 -1
  25. package/dist/buffer-turn-helpers.js +3 -1
  26. package/dist/buffer.d.ts +4 -0
  27. package/dist/buffer.d.ts.map +1 -1
  28. package/dist/buffer.js +3 -3
  29. package/dist/causal-consolidation.js +5 -5
  30. package/dist/{chunk-B7YAIHAV.js → chunk-23BA3USG.js} +10 -10
  31. package/dist/{chunk-3F7N4I5I.js → chunk-2R44BBPK.js} +2 -2
  32. package/dist/{chunk-BW33YTF3.js → chunk-2WPWEVAH.js} +2 -2
  33. package/dist/{chunk-EF3HNVL4.js → chunk-4PMFSYH7.js} +2 -2
  34. package/dist/{chunk-J5TVHCDG.js → chunk-5ORJHH43.js} +2 -2
  35. package/dist/{chunk-MK7HXGM6.js → chunk-6KBCUIZP.js} +2 -2
  36. package/dist/{chunk-OX6GY22Q.js → chunk-7P5VBRBS.js} +2 -2
  37. package/dist/{chunk-3AASGFR4.js → chunk-7WFY2L5J.js} +3 -3
  38. package/dist/{chunk-YLMTFRZR.js → chunk-AGRMXXVA.js} +2 -2
  39. package/dist/{chunk-WIW4LOZL.js → chunk-AZNE2H6D.js} +2 -2
  40. package/dist/{chunk-GT4G2S2Y.js → chunk-BCBY5GYZ.js} +2 -2
  41. package/dist/{chunk-EIZ4MK33.js → chunk-BTU77RB5.js} +2 -2
  42. package/dist/{chunk-ZS5LREUZ.js → chunk-DC3DWF2M.js} +2 -2
  43. package/dist/{chunk-LTST4R7U.js → chunk-DE4U5PTV.js} +49 -45
  44. package/dist/chunk-DE4U5PTV.js.map +1 -0
  45. package/dist/{chunk-OAQO5AZJ.js → chunk-DSSTOV5M.js} +440 -267
  46. package/dist/chunk-DSSTOV5M.js.map +1 -0
  47. package/dist/{chunk-VDYRXI3E.js → chunk-EGKDONTC.js} +9 -3
  48. package/dist/chunk-EGKDONTC.js.map +1 -0
  49. package/dist/{chunk-FM6UKZAE.js → chunk-EQ6EMJRG.js} +2 -2
  50. package/dist/{chunk-TSZP7GGJ.js → chunk-FGGLVCHI.js} +2 -2
  51. package/dist/{chunk-K7LLGCPH.js → chunk-GXAZ4Q2S.js} +2 -2
  52. package/dist/{chunk-2GV24KFD.js → chunk-H2X32F2K.js} +2 -2
  53. package/dist/{chunk-2YPIFJHL.js → chunk-ICBUTKLL.js} +2 -2
  54. package/dist/{chunk-3W37NITP.js → chunk-IQXQ3KWR.js} +2 -2
  55. package/dist/{chunk-HNF7IMYQ.js → chunk-J72ZY7NU.js} +2 -2
  56. package/dist/{chunk-6QS5NQJH.js → chunk-JGLZ46JS.js} +2 -2
  57. package/dist/{chunk-MRN2MKB7.js → chunk-JY4LXMZR.js} +2 -2
  58. package/dist/{chunk-UJ2FP2RV.js → chunk-KY3KJUNN.js} +2 -2
  59. package/dist/{chunk-64U42JL5.js → chunk-LHMCZ6VW.js} +10 -10
  60. package/dist/{chunk-YRMSHA7H.js → chunk-LN7CWLAU.js} +2 -2
  61. package/dist/{chunk-XR4QR3JT.js → chunk-LYOCAUI6.js} +3 -3
  62. package/dist/{chunk-VA3GNAWQ.js → chunk-LZIOY7ZU.js} +4 -5
  63. package/dist/chunk-LZIOY7ZU.js.map +1 -0
  64. package/dist/{chunk-GMSRLDVR.js → chunk-N2Y6COEM.js} +2 -2
  65. package/dist/{chunk-XDUP6SWG.js → chunk-OGQGDOYK.js} +2 -2
  66. package/dist/{chunk-U7MIMS2Y.js → chunk-OUBEJWVI.js} +17 -1
  67. package/dist/chunk-OUBEJWVI.js.map +1 -0
  68. package/dist/{chunk-5FMGPV34.js → chunk-OWZXDBV4.js} +2 -2
  69. package/dist/{chunk-SQO5SEKA.js → chunk-PJAMFIDE.js} +3 -3
  70. package/dist/{chunk-I6HRHJRA.js → chunk-QIOQI7QZ.js} +2 -2
  71. package/dist/{chunk-OAKOAOEG.js → chunk-QUPTDKSX.js} +22 -22
  72. package/dist/{chunk-MCFI6G7U.js → chunk-QVZZE6VH.js} +3 -3
  73. package/dist/{chunk-LYTKNK5O.js → chunk-S37O4S6E.js} +2 -2
  74. package/dist/{chunk-ILD7N3XC.js → chunk-S73ZVU37.js} +2 -2
  75. package/dist/{chunk-5VQSKBNF.js → chunk-U6IL4724.js} +2 -2
  76. package/dist/{chunk-D3AGBN6G.js → chunk-UAOJVQXC.js} +2 -2
  77. package/dist/{chunk-MKMIY3E7.js → chunk-UE5JYPU6.js} +7 -7
  78. package/dist/{chunk-K47G2CBP.js → chunk-USX5YIKB.js} +5 -5
  79. package/dist/{chunk-YIG4HPY4.js → chunk-VK5HKVQ7.js} +2 -2
  80. package/dist/{chunk-ZOBYTRBU.js → chunk-VNTQU6KB.js} +47 -2
  81. package/dist/chunk-VNTQU6KB.js.map +1 -0
  82. package/dist/{chunk-THFSCUFC.js → chunk-VSJ3ARFS.js} +2 -2
  83. package/dist/{chunk-4BOBW6H4.js → chunk-W4V7P5HP.js} +2 -2
  84. package/dist/{chunk-DM3UZUCN.js → chunk-WNQ3B3Q5.js} +2 -2
  85. package/dist/{chunk-MFKBYXBY.js → chunk-WYOHQJKC.js} +6 -6
  86. package/dist/{chunk-66PEACZP.js → chunk-X4DHAXFO.js} +3 -3
  87. package/dist/{chunk-ZYEWTVNQ.js → chunk-XPZM7GP6.js} +7 -7
  88. package/dist/{chunk-2BBMCMJS.js → chunk-YPYSC3TX.js} +2 -2
  89. package/dist/{chunk-JGQEF33K.js → chunk-ZBET3W26.js} +2 -2
  90. package/dist/chunk-ZODKV27J.js +443 -0
  91. package/dist/chunk-ZODKV27J.js.map +1 -0
  92. package/dist/cli.js +43 -43
  93. package/dist/coding/export-okf-codegraph.js +3 -3
  94. package/dist/compounding/engine.js +5 -5
  95. package/dist/config.js +3 -3
  96. package/dist/connectors/codex-materialize-runner.js +4 -4
  97. package/dist/connectors/index.js +4 -4
  98. package/dist/convergence-refresh.js +6 -6
  99. package/dist/corpus-watermark.js +6 -6
  100. package/dist/entity-retrieval.js +4 -4
  101. package/dist/external-wiki-access.js +15 -15
  102. package/dist/extraction-window-drain.d.ts.map +1 -1
  103. package/dist/extraction-window-drain.js +1 -1
  104. package/dist/extraction.js +7 -7
  105. package/dist/harmonic-retrieval.js +4 -4
  106. package/dist/index.js +58 -58
  107. package/dist/location/index.js +6 -6
  108. package/dist/maintenance/memory-governance.js +4 -4
  109. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -4
  110. package/dist/maintenance/rebuild-memory-projection-runner.js +6 -6
  111. package/dist/maintenance/rebuild-memory-projection.js +5 -5
  112. package/dist/memory-browse-operations.js +15 -15
  113. package/dist/namespaces/migrate.js +5 -5
  114. package/dist/namespaces/storage.js +4 -4
  115. package/dist/operator-doctor-corpus.js +7 -7
  116. package/dist/operator-toolkit.js +12 -12
  117. package/dist/orchestration/backlog-scheduler.d.ts +151 -0
  118. package/dist/orchestration/backlog-scheduler.d.ts.map +1 -0
  119. package/dist/orchestration/backlog-scheduler.js +891 -0
  120. package/dist/orchestration/backlog-scheduler.js.map +1 -0
  121. package/dist/orchestration/buffer-drain-runner.d.ts +31 -27
  122. package/dist/orchestration/buffer-drain-runner.d.ts.map +1 -1
  123. package/dist/orchestration/buffer-drain-runner.js +64 -269
  124. package/dist/orchestration/buffer-drain-runner.js.map +1 -1
  125. package/dist/orchestration/extraction-drain.d.ts +50 -17
  126. package/dist/orchestration/extraction-drain.d.ts.map +1 -1
  127. package/dist/orchestration/extraction-queue-coordinator.d.ts +19 -1
  128. package/dist/orchestration/extraction-queue-coordinator.d.ts.map +1 -1
  129. package/dist/orchestration/extraction-run.d.ts +8 -0
  130. package/dist/orchestration/extraction-run.d.ts.map +1 -1
  131. package/dist/orchestration/extraction-task-queueing.d.ts +44 -0
  132. package/dist/orchestration/extraction-task-queueing.d.ts.map +1 -0
  133. package/dist/orchestration/maintenance.js +14 -14
  134. package/dist/orchestration/turn-ingestion.d.ts +11 -5
  135. package/dist/orchestration/turn-ingestion.d.ts.map +1 -1
  136. package/dist/orchestrator.d.ts +13 -0
  137. package/dist/orchestrator.d.ts.map +1 -1
  138. package/dist/orchestrator.js +59 -59
  139. package/dist/recall-navigation-operations.js +15 -15
  140. package/dist/recall-navigation.js +14 -14
  141. package/dist/resume-bundles.js +3 -3
  142. package/dist/semantic-consolidation.js +5 -5
  143. package/dist/semantic-rule-promotion.js +4 -4
  144. package/dist/semantic-rule-verifier.js +4 -4
  145. package/dist/source-agent-qualifier.js +6 -6
  146. package/dist/standup/build.js +4 -4
  147. package/dist/storage/buffer-drain-claims.d.ts +15 -0
  148. package/dist/storage/buffer-drain-claims.d.ts.map +1 -1
  149. package/dist/storage/buffer-drain-claims.js +2 -2
  150. package/dist/storage/buffer-session-files.js +2 -2
  151. package/dist/storage.js +3 -3
  152. package/dist/support-passport/index.js +17 -17
  153. package/dist/transfer/export-okf.js +4 -4
  154. package/dist/verified-recall.js +4 -4
  155. package/dist/who-knows-cli.js +5 -5
  156. package/dist/who-knows.js +4 -4
  157. package/package.json +12 -2
  158. package/src/backlog-scheduler.test.ts +806 -0
  159. package/src/buffer-drain-handover.test.ts +363 -18
  160. package/src/buffer-drain-inprocess.test.ts +355 -0
  161. package/src/buffer-drain-model-boundary.test.ts +1062 -0
  162. package/src/buffer-drop-extraction-chunk.test.ts +237 -22
  163. package/src/buffer-turn-helpers.ts +26 -1
  164. package/src/buffer.ts +8 -0
  165. package/src/extraction-window-drain.ts +7 -7
  166. package/src/orchestration/backlog-scheduler.ts +481 -0
  167. package/src/orchestration/buffer-drain-runner.ts +545 -180
  168. package/src/orchestration/extraction-drain.ts +287 -108
  169. package/src/orchestration/extraction-queue-coordinator.ts +51 -3
  170. package/src/orchestration/extraction-run.test.ts +41 -0
  171. package/src/orchestration/extraction-run.ts +24 -5
  172. package/src/orchestration/extraction-task-queueing.ts +87 -0
  173. package/src/orchestration/turn-ingestion.ts +46 -59
  174. package/src/orchestrator-extraction-queue.test.ts +46 -2
  175. package/src/orchestrator-flush.test.ts +4 -1
  176. package/src/orchestrator.ts +47 -21
  177. package/src/storage/buffer-drain-claims.ts +55 -0
  178. package/dist/chunk-LTST4R7U.js.map +0 -1
  179. package/dist/chunk-OAQO5AZJ.js.map +0 -1
  180. package/dist/chunk-U7MIMS2Y.js.map +0 -1
  181. package/dist/chunk-VA3GNAWQ.js.map +0 -1
  182. package/dist/chunk-VDYRXI3E.js.map +0 -1
  183. package/dist/chunk-ZOBYTRBU.js.map +0 -1
  184. /package/dist/{chunk-B7YAIHAV.js.map → chunk-23BA3USG.js.map} +0 -0
  185. /package/dist/{chunk-3F7N4I5I.js.map → chunk-2R44BBPK.js.map} +0 -0
  186. /package/dist/{chunk-BW33YTF3.js.map → chunk-2WPWEVAH.js.map} +0 -0
  187. /package/dist/{chunk-EF3HNVL4.js.map → chunk-4PMFSYH7.js.map} +0 -0
  188. /package/dist/{chunk-J5TVHCDG.js.map → chunk-5ORJHH43.js.map} +0 -0
  189. /package/dist/{chunk-MK7HXGM6.js.map → chunk-6KBCUIZP.js.map} +0 -0
  190. /package/dist/{chunk-OX6GY22Q.js.map → chunk-7P5VBRBS.js.map} +0 -0
  191. /package/dist/{chunk-3AASGFR4.js.map → chunk-7WFY2L5J.js.map} +0 -0
  192. /package/dist/{chunk-YLMTFRZR.js.map → chunk-AGRMXXVA.js.map} +0 -0
  193. /package/dist/{chunk-WIW4LOZL.js.map → chunk-AZNE2H6D.js.map} +0 -0
  194. /package/dist/{chunk-GT4G2S2Y.js.map → chunk-BCBY5GYZ.js.map} +0 -0
  195. /package/dist/{chunk-EIZ4MK33.js.map → chunk-BTU77RB5.js.map} +0 -0
  196. /package/dist/{chunk-ZS5LREUZ.js.map → chunk-DC3DWF2M.js.map} +0 -0
  197. /package/dist/{chunk-FM6UKZAE.js.map → chunk-EQ6EMJRG.js.map} +0 -0
  198. /package/dist/{chunk-TSZP7GGJ.js.map → chunk-FGGLVCHI.js.map} +0 -0
  199. /package/dist/{chunk-K7LLGCPH.js.map → chunk-GXAZ4Q2S.js.map} +0 -0
  200. /package/dist/{chunk-2GV24KFD.js.map → chunk-H2X32F2K.js.map} +0 -0
  201. /package/dist/{chunk-2YPIFJHL.js.map → chunk-ICBUTKLL.js.map} +0 -0
  202. /package/dist/{chunk-3W37NITP.js.map → chunk-IQXQ3KWR.js.map} +0 -0
  203. /package/dist/{chunk-HNF7IMYQ.js.map → chunk-J72ZY7NU.js.map} +0 -0
  204. /package/dist/{chunk-6QS5NQJH.js.map → chunk-JGLZ46JS.js.map} +0 -0
  205. /package/dist/{chunk-MRN2MKB7.js.map → chunk-JY4LXMZR.js.map} +0 -0
  206. /package/dist/{chunk-UJ2FP2RV.js.map → chunk-KY3KJUNN.js.map} +0 -0
  207. /package/dist/{chunk-64U42JL5.js.map → chunk-LHMCZ6VW.js.map} +0 -0
  208. /package/dist/{chunk-YRMSHA7H.js.map → chunk-LN7CWLAU.js.map} +0 -0
  209. /package/dist/{chunk-XR4QR3JT.js.map → chunk-LYOCAUI6.js.map} +0 -0
  210. /package/dist/{chunk-GMSRLDVR.js.map → chunk-N2Y6COEM.js.map} +0 -0
  211. /package/dist/{chunk-XDUP6SWG.js.map → chunk-OGQGDOYK.js.map} +0 -0
  212. /package/dist/{chunk-5FMGPV34.js.map → chunk-OWZXDBV4.js.map} +0 -0
  213. /package/dist/{chunk-SQO5SEKA.js.map → chunk-PJAMFIDE.js.map} +0 -0
  214. /package/dist/{chunk-I6HRHJRA.js.map → chunk-QIOQI7QZ.js.map} +0 -0
  215. /package/dist/{chunk-OAKOAOEG.js.map → chunk-QUPTDKSX.js.map} +0 -0
  216. /package/dist/{chunk-MCFI6G7U.js.map → chunk-QVZZE6VH.js.map} +0 -0
  217. /package/dist/{chunk-LYTKNK5O.js.map → chunk-S37O4S6E.js.map} +0 -0
  218. /package/dist/{chunk-ILD7N3XC.js.map → chunk-S73ZVU37.js.map} +0 -0
  219. /package/dist/{chunk-5VQSKBNF.js.map → chunk-U6IL4724.js.map} +0 -0
  220. /package/dist/{chunk-D3AGBN6G.js.map → chunk-UAOJVQXC.js.map} +0 -0
  221. /package/dist/{chunk-MKMIY3E7.js.map → chunk-UE5JYPU6.js.map} +0 -0
  222. /package/dist/{chunk-K47G2CBP.js.map → chunk-USX5YIKB.js.map} +0 -0
  223. /package/dist/{chunk-YIG4HPY4.js.map → chunk-VK5HKVQ7.js.map} +0 -0
  224. /package/dist/{chunk-THFSCUFC.js.map → chunk-VSJ3ARFS.js.map} +0 -0
  225. /package/dist/{chunk-4BOBW6H4.js.map → chunk-W4V7P5HP.js.map} +0 -0
  226. /package/dist/{chunk-DM3UZUCN.js.map → chunk-WNQ3B3Q5.js.map} +0 -0
  227. /package/dist/{chunk-MFKBYXBY.js.map → chunk-WYOHQJKC.js.map} +0 -0
  228. /package/dist/{chunk-66PEACZP.js.map → chunk-X4DHAXFO.js.map} +0 -0
  229. /package/dist/{chunk-ZYEWTVNQ.js.map → chunk-XPZM7GP6.js.map} +0 -0
  230. /package/dist/{chunk-2BBMCMJS.js.map → chunk-YPYSC3TX.js.map} +0 -0
  231. /package/dist/{chunk-JGQEF33K.js.map → chunk-ZBET3W26.js.map} +0 -0
@@ -1,14 +1,14 @@
1
- import test from "node:test";
2
1
  import assert from "node:assert/strict";
3
2
  import { mkdtemp, rm } from "node:fs/promises";
4
3
  import os from "node:os";
5
4
  import path from "node:path";
5
+ import test from "node:test";
6
6
  import { SmartBuffer } from "./buffer.js";
7
7
  import { parseConfig } from "./config.js";
8
- import { StorageManager } from "./storage.js";
9
- import type { BufferTurn } from "./types.js";
10
8
  import { ExtractionDrain } from "./orchestration/extraction-drain.js";
11
9
  import type { ExtractionRunResult } from "./orchestration/extraction-run.js";
10
+ import { StorageManager } from "./storage.js";
11
+ import type { BufferTurn } from "./types.js";
12
12
 
13
13
  function makeTurn(sessionKey: string, content: string): BufferTurn {
14
14
  return {
@@ -20,7 +20,9 @@ function makeTurn(sessionKey: string, content: string): BufferTurn {
20
20
  }
21
21
 
22
22
  /** A real buffer on real storage; `restart()` is what a process restart would load from disk. */
23
- async function withBuffer(run: (buffer: SmartBuffer, restart: () => Promise<SmartBuffer>) => Promise<void>): Promise<void> {
23
+ async function withBuffer(
24
+ run: (buffer: SmartBuffer, restart: () => Promise<SmartBuffer>) => Promise<void>
25
+ ): Promise<void> {
24
26
  const root = await mkdtemp(path.join(os.tmpdir(), "remnic-drop-chunk-"));
25
27
  try {
26
28
  const buffer = new SmartBuffer(parseConfig({}), new StorageManager(root));
@@ -43,8 +45,15 @@ test("dropExtractionChunk removes the matched prefix and the drop survives a res
43
45
  const dropped = await buffer.dropExtractionChunk("s1", turns.slice(0, 2));
44
46
 
45
47
  assert.equal(dropped, 2);
46
- assert.deepEqual(buffer.getTurns("s1").map((turn) => turn.content), ["turn three"]);
47
- assert.deepEqual((await restart()).getTurns("s1").map((turn) => turn.content), ["turn three"], "what a restart loads");
48
+ assert.deepEqual(
49
+ buffer.getTurns("s1").map((turn) => turn.content),
50
+ ["turn three"]
51
+ );
52
+ assert.deepEqual(
53
+ (await restart()).getTurns("s1").map((turn) => turn.content),
54
+ ["turn three"],
55
+ "what a restart loads"
56
+ );
48
57
  // A drop is not an extraction: the extraction bookkeeping stays untouched.
49
58
  assert.equal(buffer.getExtractionCount("s1"), 0);
50
59
  });
@@ -77,25 +86,54 @@ test("dropExtractionChunk drops the whole matched window and a restart finds the
77
86
  });
78
87
  });
79
88
 
80
- const FAILED: ExtractionRunResult = { status: "skipped", reason: "empty_extraction_result", retainedBuffer: true, failureClass: "parse_empty", persistedCount: 0, durableOutputCount: 0 };
89
+ const FAILED: ExtractionRunResult = {
90
+ status: "skipped",
91
+ reason: "empty_extraction_result",
92
+ retainedBuffer: true,
93
+ failureClass: "parse_empty",
94
+ persistedCount: 0,
95
+ durableOutputCount: 0,
96
+ };
81
97
  const DONE: ExtractionRunResult = { status: "completed", persistedCount: 1, durableOutputCount: 1 };
82
98
 
83
99
  /** w7K's probe on #3266: a pinned capture sits before a native group drained with clearMatchingTurns. */
84
- async function drainBehindPinnedCapture(buffer: SmartBuffer, results: ExtractionRunResult[], dropAfter: number): Promise<ExtractionRunResult> {
100
+ async function drainBehindPinnedCapture(
101
+ buffer: SmartBuffer,
102
+ results: ExtractionRunResult[],
103
+ dropAfter: number,
104
+ overrides: {
105
+ getStorage?: () => Promise<{
106
+ saveDroppedExtractionChunk: (bufferKey: string, reason: string, turns: readonly BufferTurn[]) => Promise<string>;
107
+ }>;
108
+ runExtractionOverride?: () => Promise<ExtractionRunResult>;
109
+ } = {}
110
+ ): Promise<ExtractionRunResult> {
85
111
  const pinned: BufferTurn = { ...makeTurn("s1", "pinned capture"), acceptedCapture: true };
86
112
  const native = [makeTurn("s1", "a".repeat(60)), makeTurn("s1", "b".repeat(60))];
87
113
  for (const turn of [pinned, ...native]) await buffer.addTurn("s1", turn);
88
114
  await buffer.flushPendingSave();
89
115
  const drain = new ExtractionDrain({
90
- config: { extractionMinUserTurns: 1, extractionMinChars: 0, extractionMaxTurnChars: 100_000, extractionChunkDropAfterFailures: dropAfter },
116
+ config: {
117
+ extractionMinUserTurns: 1,
118
+ extractionMinChars: 0,
119
+ extractionMaxTurnChars: 100_000,
120
+ extractionChunkDropAfterFailures: dropAfter,
121
+ },
91
122
  extractionWindowBudgetChars: () => 100,
92
123
  runExtraction: async (turns, options) => {
124
+ if (overrides.runExtractionOverride) return await overrides.runExtractionOverride();
93
125
  const result = results.shift() ?? DONE;
94
126
  // A successful run clears its chunk the way the coordinator does for a native group: by identity.
95
- if (result.status === "completed") await buffer.clearAfterExtraction("s1", turns, options?.clearMatchingTurns === true ? { allowNonPrefix: true } : undefined);
127
+ if (result.status === "completed")
128
+ await buffer.clearAfterExtraction(
129
+ "s1",
130
+ turns,
131
+ options?.clearMatchingTurns === true ? { allowNonPrefix: true } : undefined
132
+ );
96
133
  return result;
97
134
  },
98
- getStorage: async () => ({ saveDroppedExtractionChunk: async () => "/recovery/copy.json" }),
135
+ getStorage:
136
+ overrides.getStorage ?? (async () => ({ saveDroppedExtractionChunk: async () => "/recovery/copy.json" })),
99
137
  buffer,
100
138
  });
101
139
  return await drain.run(native, {
@@ -113,8 +151,16 @@ test("a dropped chunk behind a pinned capture is removed by identity, like a suc
113
151
  const result = await drainBehindPinnedCapture(buffer, [FAILED, DONE], 1);
114
152
 
115
153
  assert.equal(result.status, "completed");
116
- assert.deepEqual(buffer.getTurns("s1").map((turn) => turn.content), ["pinned capture"], "the failed turn is gone, the pinned capture stays");
117
- assert.deepEqual((await restart()).getTurns("s1").map((turn) => turn.content), ["pinned capture"], "and a restart agrees");
154
+ assert.deepEqual(
155
+ buffer.getTurns("s1").map((turn) => turn.content),
156
+ ["pinned capture"],
157
+ "the failed turn is gone, the pinned capture stays"
158
+ );
159
+ assert.deepEqual(
160
+ (await restart()).getTurns("s1").map((turn) => turn.content),
161
+ ["pinned capture"],
162
+ "and a restart agrees"
163
+ );
118
164
  });
119
165
  });
120
166
 
@@ -131,10 +177,16 @@ test("a failed chunk made only of deferred-retained turns can be dropped, and a
131
177
 
132
178
  assert.equal(dropped, 2);
133
179
  assert.deepEqual(buffer.getRetainedDeferredTurns("s1"), []);
134
- assert.deepEqual(buffer.getTurns("s1").map((turn) => turn.content), ["live turn"]);
180
+ assert.deepEqual(
181
+ buffer.getTurns("s1").map((turn) => turn.content),
182
+ ["live turn"]
183
+ );
135
184
  const reloaded = await restart();
136
185
  assert.deepEqual(reloaded.getRetainedDeferredTurns("s1"), [], "retained turns stay dropped after a restart");
137
- assert.deepEqual(reloaded.getTurns("s1").map((turn) => turn.content), ["live turn"]);
186
+ assert.deepEqual(
187
+ reloaded.getTurns("s1").map((turn) => turn.content),
188
+ ["live turn"]
189
+ );
138
190
  });
139
191
  });
140
192
 
@@ -149,7 +201,10 @@ test("a chunk that spans retained and live turns drops both", async () => {
149
201
  const dropped = await buffer.dropExtractionChunk("s1", [...retained, live[0]!]);
150
202
 
151
203
  assert.equal(dropped, 2);
152
- assert.deepEqual((await restart()).getTurns("s1").map((turn) => turn.content), ["live two"]);
204
+ assert.deepEqual(
205
+ (await restart()).getTurns("s1").map((turn) => turn.content),
206
+ ["live two"]
207
+ );
153
208
  });
154
209
  });
155
210
 
@@ -161,7 +216,11 @@ test("deferred retention across chunks of one window stays within one retention
161
216
  for (let chunk = 0; chunk < 3; chunk++) {
162
217
  await buffer.retainDeferredTurns("s1", window.slice(chunk * 10, chunk * 10 + 10), 10, window);
163
218
  }
164
- assert.equal(buffer.getRetainedDeferredTurns("s1").length, 10, "the latest chunk's retention replaces the earlier ones");
219
+ assert.equal(
220
+ buffer.getRetainedDeferredTurns("s1").length,
221
+ 10,
222
+ "the latest chunk's retention replaces the earlier ones"
223
+ );
165
224
  assert.equal((await restart()).getRetainedDeferredTurns("s1").length, 10);
166
225
  });
167
226
  });
@@ -176,10 +235,19 @@ test("a chunk holding only some retained turns drops those and keeps the rest of
176
235
  const dropped = await buffer.dropExtractionChunk("s1", retained.slice(0, 2));
177
236
 
178
237
  assert.equal(dropped, 2);
179
- assert.deepEqual(buffer.getRetainedDeferredTurns("s1").map((turn) => turn.content), ["retained three"]);
238
+ assert.deepEqual(
239
+ buffer.getRetainedDeferredTurns("s1").map((turn) => turn.content),
240
+ ["retained three"]
241
+ );
180
242
  const reloaded = await restart();
181
- assert.deepEqual(reloaded.getRetainedDeferredTurns("s1").map((turn) => turn.content), ["retained three"]);
182
- assert.deepEqual(reloaded.getTurns("s1").map((turn) => turn.content), ["retained three", "live turn"]);
243
+ assert.deepEqual(
244
+ reloaded.getRetainedDeferredTurns("s1").map((turn) => turn.content),
245
+ ["retained three"]
246
+ );
247
+ assert.deepEqual(
248
+ reloaded.getTurns("s1").map((turn) => turn.content),
249
+ ["retained three", "live turn"]
250
+ );
183
251
  });
184
252
  });
185
253
 
@@ -189,7 +257,154 @@ test("retention for an early chunk leaves the retained turns of a later, unproce
189
257
  await buffer.retainDeferredTurns("s1", retained);
190
258
  // Chunk 0 holds the first retained turn; nothing was deferred in it, so it clears the slot for the turns it processed.
191
259
  await buffer.retainDeferredTurns("s1", [], 0, retained.slice(0, 1));
192
- assert.deepEqual(buffer.getRetainedDeferredTurns("s1").map((turn) => turn.content), ["late retained"]);
193
- assert.deepEqual((await restart()).getRetainedDeferredTurns("s1").map((turn) => turn.content), ["late retained"]);
260
+ assert.deepEqual(
261
+ buffer.getRetainedDeferredTurns("s1").map((turn) => turn.content),
262
+ ["late retained"]
263
+ );
264
+ assert.deepEqual(
265
+ (await restart()).getRetainedDeferredTurns("s1").map((turn) => turn.content),
266
+ ["late retained"]
267
+ );
268
+ });
269
+ });
270
+
271
+ test("a no-drop drain retains a repeatedly failing chunk as durable retryable backlog", async () => {
272
+ await withBuffer(async (buffer, restart) => {
273
+ const window = [makeTurn("s1", "a".repeat(60)), makeTurn("s1", "b".repeat(60))];
274
+ for (const turn of window) await buffer.addTurn("s1", turn);
275
+ await buffer.flushPendingSave();
276
+
277
+ let sideFiles = 0;
278
+ const drain = new ExtractionDrain(
279
+ {
280
+ config: {
281
+ extractionMinUserTurns: 1,
282
+ extractionMinChars: 0,
283
+ extractionMaxTurnChars: 100_000,
284
+ extractionChunkDropAfterFailures: 2,
285
+ },
286
+ extractionWindowBudgetChars: () => 100,
287
+ runExtraction: async () => {
288
+ throw new Error("provider down");
289
+ },
290
+ getStorage: async () => ({
291
+ saveDroppedExtractionChunk: async () => {
292
+ sideFiles += 1;
293
+ return "/recovery/copy.json";
294
+ },
295
+ }),
296
+ buffer,
297
+ },
298
+ { dropFailedChunks: false }
299
+ );
300
+
301
+ const runOptions = {
302
+ bufferKey: "s1",
303
+ clearBufferAfterExtraction: true,
304
+ clearMatchingTurns: true,
305
+ skipUserTurnThreshold: false,
306
+ failOnExtractionFailure: false,
307
+ forceExtractionAttempt: false,
308
+ } as const;
309
+
310
+ // First window: the head chunk fails once (below the threshold) — it throws and stays.
311
+ await assert.rejects(() => drain.run(window, runOptions), /provider down/);
312
+ assert.equal(sideFiles, 0, "no recovery side-file below the threshold");
313
+ // Second window: the same head chunk reaches the drop threshold. The no-drop
314
+ // policy must RETAIN it — no side-file copy, no removal, no completion.
315
+ await assert.rejects(() => drain.run(window, runOptions), /provider down/);
316
+ assert.equal(sideFiles, 0, "a no-drop drain must never side-file a failed chunk");
317
+ assert.deepEqual(
318
+ (await restart()).getTurns("s1").map((turn) => turn.content),
319
+ ["a".repeat(60), "b".repeat(60)],
320
+ "both chunks stay in the buffer as durable retryable backlog"
321
+ );
322
+ });
323
+ });
324
+
325
+ test("a failed chunk with NO recovery copy is kept as retryable backlog, not dropped", async () => {
326
+ await withBuffer(async (buffer, restart) => {
327
+ const result = await drainBehindPinnedCapture(buffer, [FAILED, FAILED, DONE], 1, {
328
+ getStorage: async () => ({
329
+ saveDroppedExtractionChunk: async () => {
330
+ throw new Error("disk full");
331
+ },
332
+ }),
333
+ });
334
+
335
+ // dropFailedChunk returns false: the only buffered copy stays and the
336
+ // drain stops on that chunk (later chunks are not acked past it).
337
+ assert.notEqual(result.status, "completed", "no silent drop without a recovery copy");
338
+ assert.deepEqual(
339
+ (await restart()).getTurns("s1").some((turn) => turn.content === "a".repeat(60)),
340
+ true,
341
+ "the failed chunk's content stays buffered for retry"
342
+ );
343
+ });
344
+ });
345
+
346
+ test("a failing buffer removal keeps the chunk: the drop is retried next trigger", async () => {
347
+ await withBuffer(async (buffer, restart) => {
348
+ buffer.dropExtractionChunk = async () => {
349
+ throw new Error("buffer busy");
350
+ };
351
+ const result = await drainBehindPinnedCapture(buffer, [FAILED, FAILED, DONE], 2);
352
+
353
+ assert.notEqual(result.status, "completed");
354
+ assert.deepEqual(
355
+ (await restart()).getTurns("s1").some((turn) => turn.content === "a".repeat(60)),
356
+ true,
357
+ "the chunk stays buffered when the buffer removal itself fails"
358
+ );
359
+ });
360
+ });
361
+
362
+ test("a chunk the buffer no longer matches is kept: nothing removed is not a drop", async () => {
363
+ await withBuffer(async (buffer, restart) => {
364
+ buffer.dropExtractionChunk = async () => 0;
365
+ const result = await drainBehindPinnedCapture(buffer, [FAILED, FAILED, DONE], 1);
366
+
367
+ assert.notEqual(result.status, "completed");
368
+ assert.deepEqual(
369
+ (await restart()).getTurns("s1").some((turn) => turn.content === "a".repeat(60)),
370
+ true,
371
+ "nothing removed means the chunk is still buffered"
372
+ );
373
+ });
374
+ });
375
+
376
+ test("one failure below the drop threshold keeps the chunk (no drop, no side file)", async () => {
377
+ await withBuffer(async (buffer, restart) => {
378
+ const result = await drainBehindPinnedCapture(buffer, [FAILED, DONE], 3);
379
+
380
+ assert.notEqual(result.status, "completed", "a single failure below the threshold retains the window");
381
+ assert.deepEqual(
382
+ (await restart()).getTurns("s1").some((turn) => turn.content === "a".repeat(60)),
383
+ true,
384
+ "the failing chunk stays buffered for the next trigger"
385
+ );
386
+ });
387
+ });
388
+
389
+ test("a thrown chunk dropped at the threshold must not end the drain: later chunks still run", async () => {
390
+ await withBuffer(async (buffer, restart) => {
391
+ // chunk1 THROWS (model crash); at dropAfter 1 it is side-file-copied and
392
+ // dropped — the live drop policy then CONTINUES to the next chunk.
393
+ let calls = 0;
394
+ const result = await drainBehindPinnedCapture(buffer, [DONE], 1, {
395
+ getStorage: async () => ({ saveDroppedExtractionChunk: async () => "/recovery/copy.json" }),
396
+ runExtractionOverride: async () => {
397
+ calls += 1;
398
+ if (calls === 1) throw new Error("model connection reset");
399
+ return DONE;
400
+ },
401
+ });
402
+
403
+ assert.equal(result.status, "completed", "the drain finishes with the later chunk");
404
+ assert.deepEqual(
405
+ (await restart()).getTurns("s1").filter((turn) => turn.content === "a".repeat(60)),
406
+ [],
407
+ "the thrown chunk was dropped by identity"
408
+ );
194
409
  });
195
410
  });
@@ -1,4 +1,4 @@
1
- import type { BufferEntryState, BufferTurn } from "./types.js";
1
+ import type { BufferEntryState, BufferState, BufferTurn } from "./types.js";
2
2
 
3
3
  export interface BufferTurnOwner {
4
4
  /**
@@ -120,6 +120,25 @@ export function subtractBufferTurns(turns: readonly BufferTurn[], drained: reado
120
120
  return kept;
121
121
  }
122
122
 
123
+ /** Per-key pending counts over the IN-MEMORY buffer state (counts only — no
124
+ * turn payloads). The claim store's answer stays authoritative for claimed
125
+ * keys; callers must filter through `BufferDrainClaims.isClaimed` before
126
+ * treating a count here as work. */
127
+ export interface BufferEntryCounts {
128
+ turns: number;
129
+ retainedTurns: number;
130
+ }
131
+
132
+ export function bufferEntryCounts(state: BufferState): Map<string, BufferEntryCounts> {
133
+ const out = new Map<string, BufferEntryCounts>();
134
+ const entries = state.entries;
135
+ if (!entries) return out;
136
+ for (const [key, entry] of Object.entries(entries)) {
137
+ out.set(key, { turns: entry.turns.length, retainedTurns: entry.retainedTurns?.length ?? 0 });
138
+ }
139
+ return out;
140
+ }
141
+
123
142
  export function bufferTurnsEqual(left: BufferTurn | undefined, right: BufferTurn): boolean {
124
143
  if (!left) return false;
125
144
  return (
@@ -200,6 +219,12 @@ export function groupTurnsByExtractionScope(turns: BufferTurn[]): ExtractionScop
200
219
  turn.writeNamespaceOverride ?? "",
201
220
  turn.principalOverride ?? "",
202
221
  scopeProfilePlanIdentity(turn.scopeProfileWritePlan),
222
+ // Accepted captures are deliberately accepted content: they must never
223
+ // share a scope group with ordinary turns, or a group-level capture
224
+ // policy (the explicit-mode gate, the planless-accepted null pin) is
225
+ // applied to ordinary conversation turns because `.some(acceptedCapture)`
226
+ // only looked at the merged group.
227
+ turn.acceptedCapture === true ? "accepted" : "",
203
228
  turn.codingContext?.projectId ?? "",
204
229
  turn.codingContext?.rootPath ?? "",
205
230
  turn.codingContext?.branch ?? "",
package/src/buffer.ts CHANGED
@@ -17,6 +17,7 @@ import { scopeProfilePlanIdentity, subtractBufferTurns } from "./buffer-turn-hel
17
17
  import { ExtractionDeadlineError } from "./orchestration/extraction-run.js";
18
18
  import type { ExtractionBufferSnapshot } from "./extraction-liveness.js";
19
19
  import {
20
+ bufferEntryCounts,
20
21
  bufferTurnArrayIsSuffixOfSnapshot,
21
22
  bufferTurnArraysEqual,
22
23
  bufferTurnsEqual,
@@ -27,6 +28,7 @@ import {
27
28
  matchingQueuedExtractionPrefixLength,
28
29
  probeWithTimeout,
29
30
  selectRetainedDeferredTurns,
31
+ type BufferEntryCounts,
30
32
  } from "./buffer-turn-helpers.js";
31
33
 
32
34
  export type TriggerDecision = "extract_now" | "extract_batch" | "keep_buffering";
@@ -332,6 +334,12 @@ export class SmartBuffer {
332
334
  return this.storage.isBufferSessionClaimed?.(key) === true;
333
335
  }
334
336
 
337
+ /** Per-key pending counts over the IN-MEMORY state (counts only — no turn
338
+ * payloads), for the daemon backlog scheduler's bounded candidate scan. */
339
+ entryCounts(): Map<string, BufferEntryCounts> {
340
+ return bufferEntryCounts(this.state);
341
+ }
342
+
335
343
  async save(): Promise<void> {
336
344
  await this.enqueueMutation(async () => this.saveUnlocked());
337
345
  }
@@ -46,22 +46,22 @@ export type DrainVerdict = "drained" | "deferred" | "failed";
46
46
 
47
47
  /**
48
48
  * Run results that deliberately RETAIN the buffer (provider breaker open,
49
- * per-fingerprint backoff) are deferrals, not chunk failures — counting them
50
- * would drop good content during a provider outage.
49
+ * per-fingerprint backoff, a scope policy refusal) are deferrals, not chunk
50
+ * failures — counting them would eventually drop good content that never got
51
+ * its attempt.
51
52
  */
52
53
  const DRAIN_DEFERRAL_REASONS = {
53
54
  provider_circuit_open: true,
54
55
  extraction_backoff: true,
56
+ scope_profile_no_writable_layer: true,
55
57
  };
56
58
 
57
59
  export function classifyDrainResult(result: ExtractionRunResult): DrainVerdict {
58
60
  if (result.status !== "skipped") return "drained";
59
- // An empty result is a failure only when the run kept the window for retry;
60
- // otherwise it extracted fine, found nothing durable, and cleared its turns.
61
- if (result.reason === "empty_extraction_result") {
62
- return result.retainedBuffer === true ? "failed" : "drained";
63
- }
64
61
  if (result.reason !== undefined && result.reason in DRAIN_DEFERRAL_REASONS) return "deferred";
62
+ // A run that kept its window for retry did NOT consume the content: it is a
63
+ // chunk failure, never a drain — even when the window was retained.
64
+ if (result.retainedBuffer === true) return "failed";
65
65
  return "drained";
66
66
  }
67
67