@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
@@ -10,22 +10,28 @@
10
10
  // claim for the drain worker.
11
11
  // 4. default-key mirror: handing over "default" clears the legacy mirror.
12
12
  import assert from "node:assert/strict";
13
- import { existsSync, mkdtempSync, readdirSync, readFileSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
13
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
14
14
  import os from "node:os";
15
15
  import { hostname } from "node:os";
16
16
  import path from "node:path";
17
17
  import test from "node:test";
18
18
 
19
+ import type { BufferStorage } from "./buffer-persistence.js";
20
+ import type { SessionObserverState } from "./session-observer-state.js";
21
+ import type { TranscriptManager } from "./transcript.js";
19
22
  import { SmartBuffer } from "./buffer.js";
23
+ import { parseConfig } from "./config.js";
24
+ import { ClaimBufferStorage } from "./orchestration/buffer-drain-runner.js";
25
+ import { ExtractionDrain } from "./orchestration/extraction-drain.js";
20
26
  import type { ExtractionPreemptHandle } from "./orchestration/extraction-queue-coordinator.js";
21
- import { BufferSessionFiles } from "./storage/buffer-session-files.js";
27
+ import { ExtractionQueueCoordinator } from "./orchestration/extraction-queue-coordinator.js";
28
+ import type { ExtractionRunResult } from "./orchestration/extraction-run.js";
29
+ import { TurnIngestionCoordinator, type TurnIngestionDeps } from "./orchestration/turn-ingestion.js";
22
30
  import { BufferDrainClaims, type DrainLease } from "./storage/buffer-drain-claims.js";
23
- import type { BufferStorage } from "./buffer-persistence.js";
31
+ import { bufferSessionFileName } from "./storage/buffer-session-files.js";
32
+ import { BufferSessionFiles } from "./storage/buffer-session-files.js";
24
33
  import type { BufferState } from "./types.js";
25
- import { TurnIngestionCoordinator, type TurnIngestionDeps } from "./orchestration/turn-ingestion.js";
26
34
  import type { BufferEntryState, BufferTurn, PluginConfig } from "./types.js";
27
- import { parseConfig } from "./config.js";
28
- import type { ExtractionRunResult } from "./orchestration/extraction-run.js";
29
35
 
30
36
  function plainIo() {
31
37
  return {
@@ -85,18 +91,37 @@ function makeHarness(): Harness {
85
91
  buffer,
86
92
  claims,
87
93
  storage,
88
- lease: { ownerId: "worker-1", pid: process.pid, host: hostname(), acquiredAt: now, heartbeatAt: now, leaseMs: 60_000 },
94
+ lease: {
95
+ ownerId: "worker-1",
96
+ pid: process.pid,
97
+ host: hostname(),
98
+ acquiredAt: now,
99
+ heartbeatAt: now,
100
+ leaseMs: 60_000,
101
+ },
89
102
  cleanup: () => rmSync(stateDir, { recursive: true, force: true }),
90
103
  };
91
104
  }
92
105
 
106
+ /**
107
+ * SAFETY: the harness stubs two observers this scenario never reads; the cast
108
+ * re-types the stub to the contract the coordinator declares.
109
+ */
110
+ function asStub<T>(cause: unknown): T {
111
+ // SAFETY: the caller supplies the stub shape the scenario exercises.
112
+ return cause as T;
113
+ }
114
+
93
115
  test("a claimed session's save hands over and evicts instead of writing a session file", async () => {
94
116
  const h = makeHarness();
95
117
  try {
96
118
  await h.buffer.load();
97
119
  await h.buffer.addTurn("sess-1", turn("old-1"));
98
120
  await h.buffer.addTurn("sess-1", turn("old-2"));
99
- assert.ok(readdirSync(path.join(h.stateDir, "buffer")).some((name) => name.endsWith(".json")), "seeded turns persisted as a session file");
121
+ assert.ok(
122
+ readdirSync(path.join(h.stateDir, "buffer")).some((name) => name.endsWith(".json")),
123
+ "seeded turns persisted as a session file"
124
+ );
100
125
 
101
126
  const outcome = await h.claims.claimFromSessionFile("sess-1", h.lease);
102
127
  assert.equal(outcome, "claimed");
@@ -112,7 +137,7 @@ test("a claimed session's save hands over and evicts instead of writing a sessio
112
137
  assert.deepEqual(
113
138
  claim.entry.turns.map((t) => t.content),
114
139
  ["old-1", "old-2", "new-3"],
115
- "the owner's newest turn merges into the claim; nothing duplicates",
140
+ "the owner's newest turn merges into the claim; nothing duplicates"
116
141
  );
117
142
  const topLevel = readdirSync(path.join(h.stateDir, "buffer")).filter((name) => name.endsWith(".json"));
118
143
  assert.deepEqual(topLevel, [], "a claimed session must never be written back as a session file");
@@ -126,11 +151,11 @@ test("a claimed session's save hands over and evicts instead of writing a sessio
126
151
  assert.deepEqual(
127
152
  claimAfter.entry.turns.map((t) => t.content),
128
153
  ["old-1", "old-2", "new-3", "post-eviction"],
129
- "post-handover observes keep flowing into the claim",
154
+ "post-handover observes keep flowing into the claim"
130
155
  );
131
156
  assert.deepEqual(
132
157
  readdirSync(path.join(h.stateDir, "buffer")).filter((name) => name.endsWith(".json")),
133
- [],
158
+ []
134
159
  );
135
160
  } finally {
136
161
  h.cleanup();
@@ -148,11 +173,14 @@ test("handover of the default key clears the legacy top-level mirror", async ()
148
173
  await h.buffer.save();
149
174
  const claim = await h.claims.load("default");
150
175
  assert.ok(claim);
151
- assert.deepEqual(claim.entry.turns.map((t) => t.content), ["legacy", "legacy-2"]);
176
+ assert.deepEqual(
177
+ claim.entry.turns.map((t) => t.content),
178
+ ["legacy", "legacy-2"]
179
+ );
152
180
  // No duplicate writes: the snapshot must settle empty after handover.
153
181
  assert.deepEqual(
154
182
  readdirSync(path.join(h.stateDir, "buffer")).filter((name) => name.endsWith(".json")),
155
- [],
183
+ []
156
184
  );
157
185
  } finally {
158
186
  h.cleanup();
@@ -200,15 +228,20 @@ function gateFixture(isClaimed: boolean) {
200
228
  void fn().catch(() => {});
201
229
  return true;
202
230
  },
203
- runPreemptable: (_preempt: ExtractionPreemptHandle | undefined, _preemptable: boolean, parent: AbortSignal | undefined, fn: (signal: AbortSignal) => Promise<void>) => fn(parent ?? new AbortController().signal),
231
+ runPreemptable: (
232
+ _preempt: ExtractionPreemptHandle | undefined,
233
+ _preemptable: boolean,
234
+ parent: AbortSignal | undefined,
235
+ fn: (signal: AbortSignal) => Promise<void>
236
+ ) => fn(parent ?? new AbortController().signal),
204
237
  },
205
238
  extractionWindowBudgetChars: () => 500,
206
239
  getStorage: async () => ({ saveDroppedExtractionChunk: async () => "/dropped" }),
207
240
  shouldQueueExtraction: () => true,
208
- runExtraction: (async (turns: BufferTurn[]) => {
241
+ runExtraction: async (turns: BufferTurn[]) => {
209
242
  runs.push({ turns });
210
243
  return { status: "completed", persistedCount: 0, durableOutputCount: 0 };
211
- }),
244
+ },
212
245
  // SAFETY: the fixture provides exactly the deps the coordinator reads
213
246
  // for the drain path (buffer, queue coordinator, budget, storage, dedupe
214
247
  // gate and runExtraction); the cast only fills the unused members.
@@ -285,8 +318,12 @@ test("the owner keeps the claim while a drain is still running on it", async ()
285
318
  extractionCount: 0,
286
319
  });
287
320
  assert.equal(saveOutcome?.status, "handed_over", "a running drain keeps its claim");
288
- assert.equal(await h.claims.load("sess-1") !== null, true, "the claim survives the owner's mid-drain save");
289
- assert.equal(existsSync(h.claims.leasePathForTest("sess-1")), true, "the lease survives the owner's mid-drain save");
321
+ assert.equal((await h.claims.load("sess-1")) !== null, true, "the claim survives the owner's mid-drain save");
322
+ assert.equal(
323
+ existsSync(h.claims.leasePathForTest("sess-1")),
324
+ true,
325
+ "the lease survives the owner's mid-drain save"
326
+ );
290
327
 
291
328
  // The drain finishes: everything extracted, the drainer acks (drops its
292
329
  // lease, leaving the tombstone). Only now does the owner's save consume it.
@@ -305,3 +342,311 @@ test("the owner keeps the claim while a drain is still running on it", async ()
305
342
  h.cleanup();
306
343
  }
307
344
  });
345
+
346
+ // ── In-process claim drain: isolated per-task execution (no global swap) ───
347
+
348
+ test("claim-backed queue tasks drain their own claims without a global buffer swap", async () => {
349
+ const h = makeHarness();
350
+ try {
351
+ await h.buffer.load();
352
+ const t0 = "2026-11-01T00:00:00.000Z";
353
+ await h.buffer.addTurn("sess-a", turn("a1", "sess-a", t0));
354
+ await h.buffer.addTurn("sess-b", turn("b1", "sess-b", t0));
355
+ await h.buffer.addTurn("sess-live", turn("live-1", "sess-live", t0));
356
+ await h.buffer.save();
357
+ const now = new Date().toISOString();
358
+ const mkLease = (ownerId: string): DrainLease => ({
359
+ ownerId,
360
+ pid: process.pid,
361
+ host: hostname(),
362
+ acquiredAt: now,
363
+ heartbeatAt: now,
364
+ leaseMs: 60_000,
365
+ });
366
+ assert.equal(await h.claims.claimFromSessionFile("sess-a", mkLease("drain-a")), "claimed");
367
+ assert.equal(await h.claims.claimFromSessionFile("sess-b", mkLease("drain-b")), "claimed");
368
+
369
+ // Identity tracker: a global swap (withBufferStorage) would replace this
370
+ // instance mid-task; the live deps read it live, exactly like selfDeps.
371
+ const liveHolder = { current: h.buffer };
372
+
373
+ // Per-key claim buffers over the SAME claim store (what the runner wires).
374
+ const claimBufferFor = async (key: string, ownerId: string): Promise<SmartBuffer> => {
375
+ const claim = await h.claims.load(key);
376
+ if (!claim) throw new Error(`test setup: claim for ${key} missing`);
377
+ const claimStorage = new ClaimBufferStorage(h.claims, key, ownerId, claim.entry);
378
+ return new SmartBuffer(makeConfig(), claimStorage);
379
+ };
380
+ const claimBuffers = new Map<string, SmartBuffer>([
381
+ ["sess-a", await claimBufferFor("sess-a", "drain-a")],
382
+ ["sess-b", await claimBufferFor("sess-b", "drain-b")],
383
+ ]);
384
+ for (const claimBuffer of claimBuffers.values()) await claimBuffer.load();
385
+
386
+ const realQueue = new ExtractionQueueCoordinator(1);
387
+ const runs: { task: string; turns: string[]; liveAtCall: SmartBuffer }[] = [];
388
+ const liveRuns = new Set<string>();
389
+ const makeDrainStub =
390
+ (task: string, claimBuffer: SmartBuffer | undefined) =>
391
+ // Mimic extraction-run: read the turns handed over, clear them through
392
+ // the CURRENT live instance (a swap would clear into a claim buffer).
393
+ async (turns: BufferTurn[]): Promise<ExtractionRunResult> => {
394
+ runs.push({ task, turns: turns.map((t) => t.content), liveAtCall: liveHolder.current });
395
+ const target = claimBuffer ?? liveHolder.current;
396
+ await target.clearAfterExtraction(task, turns, { allowNonPrefix: true });
397
+ if (!claimBuffer) liveRuns.add(task);
398
+ return { status: "completed", persistedCount: turns.length, durableOutputCount: turns.length };
399
+ };
400
+
401
+ const liveStub = makeDrainStub("sess-live", undefined);
402
+ const executionFor = (key: string) => {
403
+ const claimBuffer = claimBuffers.get(key)!;
404
+ const drain = new ExtractionDrain(
405
+ {
406
+ config: {
407
+ extractionMinUserTurns: 1,
408
+ extractionMinChars: 0,
409
+ extractionMaxTurnChars: 100_000,
410
+ extractionChunkDropAfterFailures: 3,
411
+ },
412
+ extractionWindowBudgetChars: () => 500,
413
+ runExtraction: makeDrainStub(key, claimBuffer),
414
+ getStorage: async () => ({ saveDroppedExtractionChunk: async () => "/dropped" }),
415
+ buffer: claimBuffer,
416
+ },
417
+ { dropFailedChunks: false }
418
+ );
419
+ return { buffer: claimBuffer, drain };
420
+ };
421
+ // SAFETY: the harness supplies exactly the members this scenario's
422
+ // TurnIngestionCoordinator reads; the remaining contract members are
423
+ // unreachable on this path.
424
+ const noopStorage = async () => ({ saveDroppedExtractionChunk: async () => "/dropped" });
425
+ const deps: TurnIngestionDeps = {
426
+ get buffer() {
427
+ return liveHolder.current;
428
+ },
429
+ bulkImportWriteNamespace: () => "bulk-import",
430
+ config: makeConfig(),
431
+ extractionQueueCoordinator: realQueue,
432
+ getStorage: async () => {
433
+ throw new Error("no storage on this path");
434
+ },
435
+ heartbeatObserverChains: new Map(),
436
+ lcmEngine: null,
437
+ passiveCorrectionDedup: new Set(),
438
+ passiveCorrectionTelemetry: {
439
+ detected: 0,
440
+ queued: 0,
441
+ autoApplied: 0,
442
+ suppressedReasonCounts: {},
443
+ },
444
+ passiveCorrectionService: () => {
445
+ throw new Error("passive corrections unreachable in this scenario");
446
+ },
447
+ shouldQueueExtraction: () => true,
448
+ runExtraction: liveStub,
449
+ queueBufferedExtraction: async () => undefined,
450
+ resolveMemoryIdOrHandle: () => "memory-1",
451
+ sessionObserver: asStub<SessionObserverState>({ turns: [], lastTurnAt: null }),
452
+ transcript: asStub<TranscriptManager>({ append: async () => undefined }),
453
+ };
454
+ const coordinator = new TurnIngestionCoordinator(deps);
455
+
456
+ const settled: Promise<void>[] = [];
457
+ const enqueueClaimTask = (key: string, ownerId: string): void => {
458
+ const claimBuffer = claimBuffers.get(key);
459
+ settled.push(
460
+ new Promise<void>((resolve, reject) => {
461
+ void coordinator
462
+ .queueBufferedExtraction(
463
+ // The claim's turns, grouped upstream (grouping is the runner's
464
+ // next commit; the seam carries whatever list it is given).
465
+ (claimBuffer?.getTurns(key) ?? []).map((t) => ({ ...t })),
466
+ "trigger_mode",
467
+ {
468
+ bufferKey: key,
469
+ clearBufferAfterExtraction: true,
470
+ clearMatchingTurns: true,
471
+ skipDedupeCheck: true,
472
+ skipCharThreshold: true,
473
+ skipUserTurnThreshold: true,
474
+ execution: executionFor(key),
475
+ onTaskSettled: (error) => (error ? reject(error) : resolve()),
476
+ }
477
+ )
478
+ .catch(reject);
479
+ })
480
+ );
481
+ };
482
+ enqueueClaimTask("sess-a", "drain-a");
483
+ enqueueClaimTask("sess-b", "drain-b");
484
+ // The live foreground capture arrives while the claim tasks hold the queue.
485
+ settled.push(
486
+ new Promise<void>((resolve, reject) => {
487
+ void coordinator
488
+ .queueBufferedExtraction([turn("live-2", "sess-live", "2026-11-01T00:01:00.000Z")], "trigger_mode", {
489
+ bufferKey: "sess-live",
490
+ clearBufferAfterExtraction: true,
491
+ onTaskSettled: (error) => (error ? reject(error) : resolve()),
492
+ })
493
+ .catch(reject);
494
+ })
495
+ );
496
+ await Promise.all(settled);
497
+
498
+ // No swap ever happened: every stub saw the live instance, and the live
499
+ // task's clear went through the live buffer (its turns are gone from it).
500
+ for (const run of runs) {
501
+ assert.equal(run.liveAtCall, h.buffer, "the live buffer instance must never be swapped mid-task");
502
+ }
503
+ assert.deepEqual(
504
+ runs.find((r) => r.task === "sess-a")?.turns,
505
+ ["a1"],
506
+ "task a extracted only its own claim's turns"
507
+ );
508
+ assert.deepEqual(
509
+ runs.find((r) => r.task === "sess-b")?.turns,
510
+ ["b1"],
511
+ "task b extracted only its own claim's turns"
512
+ );
513
+ assert.equal(liveRuns.has("sess-live"), true, "the live task ran through the live path");
514
+ assert.deepEqual(
515
+ h.buffer.getTurns("sess-live").map((t) => t.content),
516
+ ["live-1"],
517
+ "the live task cleared only its queued snapshot through the live buffer"
518
+ );
519
+ // Claim progress landed in the RIGHT claim files (no cross-talk), and the
520
+ // claims' turns were extracted from the claims, not from any live copy.
521
+ assert.deepEqual((await h.claims.load("sess-a"))?.entry.turns ?? [], [], "claim a is drained in place");
522
+ assert.deepEqual((await h.claims.load("sess-b"))?.entry.turns ?? [], [], "claim b is drained in place");
523
+ assert.deepEqual(
524
+ h.buffer.getTurns("sess-a").map((t) => t.content),
525
+ ["a1"],
526
+ "the live buffer's in-memory copy is untouched by the claim drain"
527
+ );
528
+ } finally {
529
+ h.cleanup();
530
+ }
531
+ });
532
+
533
+ /** Claim keys in the order `listPage` scans them: sorted claim file names
534
+ * (`list()` itself is directory-enumeration order, which is not sorted). */
535
+ async function claimKeysInListPageOrder(claims: BufferDrainClaims): Promise<string[]> {
536
+ const keys = (await claims.list()).map((listed) => listed.key);
537
+ const nameOf = new Map(keys.map((key) => [key, bufferSessionFileName(key)] as const));
538
+ const names = [...nameOf.values()].sort();
539
+ return keys.sort((left, right) => names.indexOf(nameOf.get(left)!) - names.indexOf(nameOf.get(right)!));
540
+ }
541
+
542
+ test("listPage never spends its page limit on empty tombstone claims", async () => {
543
+ const h = makeHarness();
544
+ try {
545
+ await h.buffer.load();
546
+ const keys = ["tomb-a", "tomb-b", "tomb-c", "tomb-d", "tomb-e", "tomb-pending"];
547
+ for (const key of keys) await h.buffer.addTurn(key, turn(`t-${key}`, key));
548
+ await h.buffer.save();
549
+ for (const key of keys) assert.equal(await h.claims.claimFromSessionFile(key, h.lease), "claimed");
550
+ // Claim file names are hashed: the LAST claim in listing order is the one
551
+ // left pending, so a limit counted against tombstones cannot reach it.
552
+ const order = await claimKeysInListPageOrder(h.claims);
553
+ const pending = order[order.length - 1]!;
554
+ for (const key of order) {
555
+ if (key === pending) continue;
556
+ const loaded = await h.claims.load(key);
557
+ await h.claims.writeEntry(key, h.lease.ownerId, entry([]), loaded!.entry);
558
+ assert.equal(await h.claims.ack(key, h.lease.ownerId), "acked");
559
+ }
560
+ const page = await h.claims.listPage(undefined, 1);
561
+ assert.deepEqual(
562
+ page.entries.map((listed) => listed.key),
563
+ [pending],
564
+ "the one pending claim is returned on the first page, past the tombstones"
565
+ );
566
+ assert.ok(page.entries.every((listed) => listed.turnCount > 0), "a page never lists an empty tombstone");
567
+ } finally {
568
+ h.cleanup();
569
+ }
570
+ });
571
+
572
+ test("listPage bounds the claim bodies it reads per call and resumes past a long tombstone run", async () => {
573
+ const h = makeHarness();
574
+ try {
575
+ await h.buffer.load();
576
+ // limit 1 reads at most 8 bodies per call: eight tombstones fill the
577
+ // budget exactly, so the pending claim behind them needs a second page.
578
+ const keys = Array.from({ length: 9 }, (_, index) => `run-${index}`);
579
+ for (const key of keys) await h.buffer.addTurn(key, turn(`t-${key}`, key));
580
+ await h.buffer.save();
581
+ for (const key of keys) assert.equal(await h.claims.claimFromSessionFile(key, h.lease), "claimed");
582
+ const order = await claimKeysInListPageOrder(h.claims);
583
+ const pending = order[order.length - 1]!;
584
+ for (const key of order) {
585
+ if (key === pending) continue;
586
+ const loaded = await h.claims.load(key);
587
+ await h.claims.writeEntry(key, h.lease.ownerId, entry([]), loaded!.entry);
588
+ assert.equal(await h.claims.ack(key, h.lease.ownerId), "acked");
589
+ }
590
+ const first = await h.claims.listPage(undefined, 1);
591
+ assert.deepEqual(first.entries, [], "the scan budget is spent on tombstones: nothing listed this call");
592
+ assert.notEqual(first.nextCursor, undefined, "the cursor advanced past every scanned name");
593
+ const second = await h.claims.listPage(first.nextCursor, 1);
594
+ assert.deepEqual(
595
+ second.entries.map((listed) => listed.key),
596
+ [pending],
597
+ "the next page resumes after the scanned run and reaches the pending claim"
598
+ );
599
+ } finally {
600
+ h.cleanup();
601
+ }
602
+ });
603
+
604
+ test("listPage returns exactly its limit when more pending claims exist", async () => {
605
+ const h = makeHarness();
606
+ try {
607
+ await h.buffer.load();
608
+ const keys = ["limit-a", "limit-b", "limit-c"];
609
+ for (const key of keys) await h.buffer.addTurn(key, turn(`t-${key}`, key));
610
+ await h.buffer.save();
611
+ for (const key of keys) assert.equal(await h.claims.claimFromSessionFile(key, h.lease), "claimed");
612
+ const page = await h.claims.listPage(undefined, 2);
613
+ assert.equal(page.entries.length, 2, "a page never exceeds its limit");
614
+ assert.notEqual(page.nextCursor, undefined, "a full page leaves a cursor for the remainder");
615
+ } finally {
616
+ h.cleanup();
617
+ }
618
+ });
619
+
620
+ test("listPage's cursor is the LAST CONSUMED claim, so a page boundary never skips one", async () => {
621
+ const h = makeHarness();
622
+ try {
623
+ await h.buffer.load();
624
+ for (const key of ["page-sess-a", "page-sess-b", "page-sess-c"]) {
625
+ await h.buffer.addTurn(key, turn(`t-${key}`, key));
626
+ }
627
+ await h.buffer.save();
628
+ for (const key of ["page-sess-a", "page-sess-b", "page-sess-c"]) {
629
+ assert.equal(await h.claims.claimFromSessionFile(key, h.lease), "claimed");
630
+ }
631
+ // Claim file names are hashed, so pagination is asserted order-free.
632
+ const seen = new Set<string>();
633
+ let cursor: string | undefined;
634
+ let pages = 0;
635
+ for (;;) {
636
+ const page = await h.claims.listPage(cursor, 2);
637
+ pages += 1;
638
+ for (const entry of page.entries) seen.add(entry.key);
639
+ if (page.nextCursor === undefined) break;
640
+ cursor = page.nextCursor;
641
+ if (pages > 10) throw new Error("pagination never exhausted");
642
+ }
643
+ assert.deepEqual(
644
+ [...seen].sort(),
645
+ ["page-sess-a", "page-sess-b", "page-sess-c"],
646
+ "every page-boundary claim must be visited exactly once across rotating pages"
647
+ );
648
+ assert.equal(seen.size, pages === 2 ? 3 : 3, "no claim repeats across the rotation");
649
+ } finally {
650
+ h.cleanup();
651
+ }
652
+ });