@remnic/core 10.0.70 → 10.0.71

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
@@ -0,0 +1,355 @@
1
+ // In-process claim drain (daemon backlog scheduler task body): the runner
2
+ // must drain a claim through the shared extraction queue WITHOUT the global
3
+ // buffer swap, group the claim's turns by captured extraction scope (exact
4
+ // pins + CURRENT policy re-applied from the original binding), keep failing
5
+ // chunks as durable retryable backlog (no drop, no side file), and leave the
6
+ // claim retryable across cancel / dead-lease adoption.
7
+ import assert from "node:assert/strict";
8
+ import { existsSync } from "node:fs";
9
+ import { hostname } from "node:os";
10
+ import path from "node:path";
11
+ import test from "node:test";
12
+
13
+ import { resolveNamespaceStorageRoot } from "./namespaces/storage.js";
14
+ import { BufferDrainRunner } from "./orchestration/buffer-drain-runner.js";
15
+ import { Orchestrator } from "./orchestrator.js";
16
+ import type { DrainLease } from "./storage/buffer-drain-claims.js";
17
+ import {
18
+ cleanupDir,
19
+ makeHarmonicLifecycleConfig,
20
+ memoryFilesContaining,
21
+ mkTempMemoryDir,
22
+ singleFactResult,
23
+ stubExtraction,
24
+ } from "./testing/orchestrator-lite.js";
25
+ import type { BufferTurn, PluginConfig } from "./types.js";
26
+
27
+ function turn(content: string, sessionKey: string, timestamp = "2026-11-01T00:00:00.000Z"): BufferTurn {
28
+ return { role: "user", content, timestamp, sessionKey };
29
+ }
30
+
31
+ interface Drainer {
32
+ orchestrator: Orchestrator;
33
+ runner: BufferDrainRunner;
34
+ calls: BufferTurn[][];
35
+ memoryDir: string;
36
+ config: PluginConfig;
37
+ cleanup(): Promise<void>;
38
+ }
39
+
40
+ interface DrainerOverrides {
41
+ namespacesEnabled?: boolean;
42
+ defaultNamespace?: string;
43
+ }
44
+
45
+ async function makeDrainer(overrides: DrainerOverrides = {}): Promise<Drainer> {
46
+ const memoryDir = await mkTempMemoryDir("drain-inprocess");
47
+ const config = makeHarmonicLifecycleConfig(memoryDir, { ...overrides });
48
+ const orchestrator = new Orchestrator(config);
49
+ const calls = stubExtraction(orchestrator, (turns) => singleFactResult(`fact for ${turns[0]?.content ?? "empty"}`));
50
+ return {
51
+ orchestrator,
52
+ runner: new BufferDrainRunner(orchestrator),
53
+ calls,
54
+ memoryDir,
55
+ config,
56
+ cleanup: async () => {
57
+ await orchestrator.destroy().catch(() => undefined);
58
+ await cleanupDir(memoryDir);
59
+ },
60
+ };
61
+ }
62
+
63
+ function gateOnContent(calls: BufferTurn[][], content: string) {
64
+ let release!: () => void;
65
+ const enteredPromise = new Promise<void>((resolveEntered) => {
66
+ const original = calls.push.bind(calls);
67
+ Object.defineProperty(calls, "push", {
68
+ value: (turns: BufferTurn[]) => {
69
+ const out = original(turns);
70
+ if (turns.some((t) => t.content === content)) {
71
+ resolveEntered();
72
+ return new Promise<void>((resolveRelease) => {
73
+ release = resolveRelease;
74
+ }).then(() => out);
75
+ }
76
+ return out;
77
+ },
78
+ });
79
+ });
80
+ return { release: () => release(), entered: () => enteredPromise };
81
+ }
82
+
83
+ function leaseFor(ownerId: string): DrainLease {
84
+ const now = new Date().toISOString();
85
+ return { ownerId, pid: process.pid, host: hostname(), acquiredAt: now, heartbeatAt: now, leaseMs: 60_000 };
86
+ }
87
+
88
+ test("an in-process claim drain never swaps the live buffer: concurrent live saves persist normally", async () => {
89
+ const d = await makeDrainer();
90
+ try {
91
+ await d.orchestrator.buffer.addTurn("sess-a", turn("a1", "sess-a"));
92
+ await d.orchestrator.buffer.addTurn("sess-b", turn("b1", "sess-b"));
93
+ await d.orchestrator.buffer.save();
94
+ assert.equal(await d.runner.claim("sess-a", "drain-1"), true);
95
+
96
+ const gate = gateOnContent(d.calls, "a1");
97
+ const draining = d.runner.drainSession("sess-a", { ownerId: "drain-1" });
98
+ await gate.entered();
99
+
100
+ // While the claim drain holds the extraction open, the foreground owner
101
+ // keeps capturing: a live save must land in LIVE session storage, never
102
+ // inside the claim's storage view.
103
+ await d.orchestrator.buffer.addTurn("sess-b", turn("b2", "sess-b", "2026-11-01T00:01:00.000Z"));
104
+ await d.orchestrator.buffer.save();
105
+ const persisted = await d.orchestrator.storage.loadBuffer();
106
+ assert.ok(
107
+ persisted.entries?.["sess-b"]?.turns.some((t) => t.content === "b2"),
108
+ "the concurrent live save persists through live storage"
109
+ );
110
+ const claim = await d.runner.claims.load("sess-a");
111
+ assert.deepEqual(
112
+ claim?.entry.turns.map((t) => t.content),
113
+ ["a1"],
114
+ "the claim holds only its own session's turns"
115
+ );
116
+
117
+ gate.release();
118
+ const outcome = await draining;
119
+ assert.equal(outcome.status, "drained");
120
+ } finally {
121
+ await d.cleanup();
122
+ }
123
+ });
124
+
125
+ const NAMESPACED = {
126
+ namespacesEnabled: true,
127
+ defaultNamespace: "default",
128
+ principalFromSessionKeyMode: "prefix",
129
+ namespacePolicies: [
130
+ { name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
131
+ { name: "default", readPrincipals: ["default"], writePrincipals: ["default"] },
132
+ ],
133
+ };
134
+
135
+ test("a mixed-scope claim drains each group under its exact captured scope", async () => {
136
+ const d = await makeDrainer(NAMESPACED);
137
+ try {
138
+ const pinned: BufferTurn = {
139
+ ...turn("pinned capture", "sess-mixed"),
140
+ acceptedCapture: true,
141
+ writeNamespaceOverride: "alice",
142
+ principalOverride: "alice",
143
+ };
144
+ await d.orchestrator.buffer.addTurn("sess-mixed", pinned);
145
+ await d.orchestrator.buffer.addTurn("sess-mixed", turn("native one", "sess-mixed", "2026-11-01T00:01:00.000Z"));
146
+ await d.orchestrator.buffer.save();
147
+ assert.equal(await d.runner.claim("sess-mixed", "drain-1"), true);
148
+
149
+ const outcome = await d.runner.drainSession("sess-mixed", { ownerId: "drain-1" });
150
+ assert.equal(outcome.status, "drained", "the whole mixed claim drains");
151
+ assert.equal(outcome.turnsRemaining, 0);
152
+ assert.ok(d.calls.length >= 2, "scope groups extract separately");
153
+ assert.deepEqual(
154
+ d.calls.find((turns) => turns.some((t) => t.content === "pinned capture"))?.map((t) => t.content),
155
+ ["pinned capture"],
156
+ "the pinned group extracts alone under its own scope"
157
+ );
158
+ assert.deepEqual(
159
+ d.calls.find((turns) => turns.some((t) => t.content === "native one"))?.map((t) => t.content),
160
+ ["native one"],
161
+ "the native group extracts alone"
162
+ );
163
+ // Durable outputs land in the EXACT captured namespaces.
164
+ const aliceRoot = await resolveNamespaceStorageRoot(d.config, "alice");
165
+ const defaultRoot = await resolveNamespaceStorageRoot(d.config, "default");
166
+ assert.ok(
167
+ (await memoryFilesContaining(aliceRoot, "fact for pinned capture")).length > 0,
168
+ "the pinned fact persists in alice"
169
+ );
170
+ assert.ok(
171
+ (await memoryFilesContaining(defaultRoot, "fact for native one")).length > 0,
172
+ "the native fact persists in the default namespace"
173
+ );
174
+ } finally {
175
+ await d.cleanup();
176
+ }
177
+ });
178
+
179
+ test("a policy-denied group stays pending, is reported, and never blocks other groups", async () => {
180
+ // Namespaces DISABLED: the pinned non-default namespace can no longer be
181
+ // honored, so the pinned group is denied while native work still drains.
182
+ const d = await makeDrainer({ namespacesEnabled: false, defaultNamespace: "default" });
183
+ try {
184
+ const pinned: BufferTurn = {
185
+ ...turn("pinned capture", "sess-denied"),
186
+ acceptedCapture: true,
187
+ writeNamespaceOverride: "alice",
188
+ principalOverride: "alice",
189
+ };
190
+ await d.orchestrator.buffer.addTurn("sess-denied", pinned);
191
+ await d.orchestrator.buffer.addTurn("sess-denied", turn("native one", "sess-denied", "2026-11-01T00:01:00.000Z"));
192
+ await d.orchestrator.buffer.save();
193
+ assert.equal(await d.runner.claim("sess-denied", "drain-1"), true);
194
+
195
+ const outcome = await d.runner.drainSession("sess-denied", { ownerId: "drain-1" });
196
+ assert.equal(outcome.status, "deferred", "denied backlog keeps the drain open");
197
+ assert.deepEqual(outcome.denied, [{ reason: "pinned_namespace_unavailable", turns: 1 }]);
198
+ assert.equal(outcome.reason, "pinned_namespace_unavailable");
199
+ assert.equal(outcome.turnsRemaining, 1, "the denied turn stays pending in the claim");
200
+ const claim = await d.runner.claims.load("sess-denied");
201
+ assert.deepEqual(
202
+ claim?.entry.turns.map((t) => t.content),
203
+ ["pinned capture"]
204
+ );
205
+ const defaultRoot = await resolveNamespaceStorageRoot(d.config, "default");
206
+ assert.ok(
207
+ (await memoryFilesContaining(defaultRoot, "fact for native one")).length > 0,
208
+ "the native group drained despite the denial"
209
+ );
210
+ } finally {
211
+ await d.cleanup();
212
+ }
213
+ });
214
+
215
+ test("a repeatedly failing group never drops: the claim keeps every turn and the lease is released", async () => {
216
+ const d = await makeDrainer();
217
+ try {
218
+ // Fail ONLY the failing session's extraction, once; others succeed.
219
+ const original = d.calls.push.bind(d.calls);
220
+ let failuresLeft = 1;
221
+ Object.defineProperty(d.calls, "push", {
222
+ value: (turns: BufferTurn[]) => {
223
+ if (turns.some((t) => t.sessionKey === "sess-fail") && failuresLeft-- > 0) throw new Error("provider down");
224
+ return original(turns);
225
+ },
226
+ });
227
+ await d.orchestrator.buffer.addTurn("sess-fail", turn("f1", "sess-fail"));
228
+ await d.orchestrator.buffer.addTurn("sess-fail", turn("f2", "sess-fail", "2026-11-01T00:01:00.000Z"));
229
+ await d.orchestrator.buffer.save();
230
+ assert.equal(await d.runner.claim("sess-fail", "drain-1"), true);
231
+
232
+ const outcome = await d.runner.drainSession("sess-fail", { ownerId: "drain-1" });
233
+ assert.equal(outcome.status, "failed");
234
+ assert.equal(outcome.turnsRemaining, 2, "both turns stay as durable retryable backlog");
235
+ const claim = await d.runner.claims.load("sess-fail");
236
+ assert.deepEqual(
237
+ claim?.entry.turns.map((t) => t.content),
238
+ ["f1", "f2"]
239
+ );
240
+ assert.equal(existsSync(d.runner.claims.leasePathForTest("sess-fail")), false, "the lease is released for retry");
241
+ assert.equal(existsSync(path.join(d.memoryDir, "dropped")), false, "no side-file recovery copy was written");
242
+ // And the retry genuinely drains it.
243
+ const retry = await d.runner.drainSession("sess-fail", { ownerId: "drain-1" });
244
+ assert.equal(retry.status, "drained");
245
+ } finally {
246
+ await d.cleanup();
247
+ }
248
+ });
249
+
250
+ test("cancelling mid-drain keeps the claim retryable; a rerun finishes it", async () => {
251
+ const d = await makeDrainer();
252
+ try {
253
+ await d.orchestrator.buffer.addTurn("sess-cancel", turn("c1", "sess-cancel"));
254
+ await d.orchestrator.buffer.save();
255
+ assert.equal(await d.runner.claim("sess-cancel", "drain-1"), true);
256
+
257
+ const controller = new AbortController();
258
+ const gate = gateOnContent(d.calls, "c1");
259
+ const draining = d.runner.drainSession("sess-cancel", { ownerId: "drain-1", signal: controller.signal });
260
+ await gate.entered();
261
+ controller.abort();
262
+ gate.release();
263
+ const outcome = await draining;
264
+ assert.notEqual(outcome.status, "drained", "an aborted run does not report drained");
265
+ assert.equal(outcome.turnsRemaining, 1, "the turn stays claimed");
266
+ assert.equal(existsSync(d.runner.claims.leasePathForTest("sess-cancel")), false, "the lease is released");
267
+
268
+ const rerun = await d.runner.drainSession("sess-cancel", { ownerId: "drain-1" });
269
+ assert.equal(rerun.status, "drained", "the rerun drains the retained claim");
270
+ assert.ok(
271
+ (await d.runner.claims.load("sess-cancel")) === null ||
272
+ (await d.runner.claims.load("sess-cancel"))?.entry.turns.length === 0
273
+ );
274
+ } finally {
275
+ await d.cleanup();
276
+ }
277
+ });
278
+
279
+ test("a large pinned accepted group chunks to the model budget before any clear", async () => {
280
+ const d = await makeDrainer(NAMESPACED);
281
+ try {
282
+ // Shadow the private budget seam (house field-seam pattern): 200 chars.
283
+ Object.defineProperty(d.orchestrator, "extractionWindowBudgetChars", {
284
+ value: () => 200,
285
+ configurable: true,
286
+ });
287
+ const contents = Array.from({ length: 8 }, (_, i) => `pinned fact ${i} ${"x".repeat(24)}`);
288
+ for (const [index, content] of contents.entries()) {
289
+ const pinned: BufferTurn = {
290
+ ...turn(content, "sess-big", `2026-11-01T00:0${index}:00.000Z`),
291
+ acceptedCapture: true,
292
+ writeNamespaceOverride: "alice",
293
+ principalOverride: "alice",
294
+ };
295
+ await d.orchestrator.buffer.addTurn("sess-big", pinned);
296
+ }
297
+ await d.orchestrator.buffer.save();
298
+ assert.equal(await d.runner.claim("sess-big", "drain-1"), true);
299
+
300
+ const outcome = await d.runner.drainSession("sess-big", { ownerId: "drain-1" });
301
+ assert.equal(outcome.status, "drained");
302
+ assert.ok(d.calls.length >= 2, `every turn must be seen by budget-fit calls, got ${d.calls.length}`);
303
+ for (const [index, callTurns] of d.calls.entries()) {
304
+ const total = callTurns.reduce((sum, t) => sum + t.content.trim().length, 0);
305
+ assert.ok(total <= 200, `model call ${index} exceeded the window budget: ${total}`);
306
+ }
307
+ const seen = new Set(d.calls.flat().map((t) => t.content));
308
+ for (const content of contents) {
309
+ assert.ok(seen.has(content), `turn never handed to extraction: ${content}`);
310
+ }
311
+ const claim = await d.runner.claims.load("sess-big");
312
+ assert.equal(claim?.entry.turns.length ?? 0, 0, "the claim empties only after every chunk extracted");
313
+ } finally {
314
+ await d.cleanup();
315
+ }
316
+ });
317
+
318
+ test("an ack that lands non-empty never reports drained: honest pending, own lease released", async () => {
319
+ const d = await makeDrainer();
320
+ try {
321
+ await d.orchestrator.buffer.addTurn("sess-race", turn("r1", "sess-race"));
322
+ await d.orchestrator.buffer.save();
323
+ assert.equal(await d.runner.claim("sess-race", "drain-1"), true);
324
+
325
+ const claims = d.runner.claims;
326
+ const originalAck = claims.ack.bind(claims);
327
+ claims.ack = async () => "not_empty";
328
+ const outcome = await d.runner.drainSession("sess-race", { ownerId: "drain-1" });
329
+ claims.ack = originalAck;
330
+
331
+ assert.equal(outcome.status, "deferred", "a non-empty ack is never reported drained");
332
+ assert.equal(outcome.reason, "ack_not_empty");
333
+ assert.equal(existsSync(d.runner.claims.leasePathForTest("sess-race")), false, "our lease is released");
334
+ const retry = await d.runner.drainSession("sess-race", { ownerId: "drain-1" });
335
+ assert.equal(retry.status, "drained");
336
+ } finally {
337
+ await d.cleanup();
338
+ }
339
+ });
340
+
341
+ test("an abandoned claim with a dead owner's lease is adopted and drained", async () => {
342
+ const d = await makeDrainer();
343
+ try {
344
+ await d.orchestrator.buffer.addTurn("sess-dead", turn("d1", "sess-dead"));
345
+ await d.orchestrator.buffer.save();
346
+ const deadLease: DrainLease = { ...leaseFor("dead-worker"), pid: 999_999_999 };
347
+ assert.equal(await d.runner.claims.claimFromSessionFile("sess-dead", deadLease), "claimed");
348
+
349
+ const outcome = await d.runner.drainSession("sess-dead", { ownerId: "daemon-drain-1" });
350
+ assert.equal(outcome.status, "drained", "the dead owner's claim is taken over and drained");
351
+ assert.equal(d.calls.length, 1);
352
+ } finally {
353
+ await d.cleanup();
354
+ }
355
+ });