@code-yeongyu/senpi 2026.10.25-3 → 2026.10.26-1

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 (218) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/bundle/chunks/{anthropic-messages-WBASBTIW.js → anthropic-messages-M235FJQV.js} +3 -3
  3. package/dist/bundle/chunks/{args-B444OQBK.js → args-FBNJOJSR.js} +1 -1
  4. package/dist/bundle/chunks/azure-openai-responses-YQRTQ7H6.js +2 -0
  5. package/dist/bundle/chunks/bedrock-converse-stream.js +3 -3
  6. package/dist/bundle/chunks/{chunk-FFQOEC76.js → chunk-3A3XCLNH.js} +3 -3
  7. package/dist/bundle/chunks/{chunk-EHQXLHRB.js → chunk-3LPWILUZ.js} +2 -2
  8. package/dist/bundle/chunks/{chunk-3BTN337S.js → chunk-3WP5RFQR.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-WLHNEUGD.js → chunk-5FQMNHXV.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-HVJI5T75.js → chunk-5KZH6EYQ.js} +2 -2
  11. package/dist/bundle/chunks/{chunk-4P7WARQZ.js → chunk-5LWUUXMH.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-ODTGC6YU.js → chunk-6U3TBFBA.js} +1 -1
  13. package/dist/bundle/chunks/chunk-6VWIMHMH.js +2 -0
  14. package/dist/bundle/chunks/{chunk-BWA4FVFW.js → chunk-7KOUQSBX.js} +2 -2
  15. package/dist/bundle/chunks/{chunk-7CNOCJCN.js → chunk-ADQAPDPV.js} +1 -1
  16. package/dist/bundle/chunks/{chunk-ITGYN22E.js → chunk-CDQPA4FP.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-WDPQ6CZJ.js → chunk-DMFAVE5N.js} +1 -1
  18. package/dist/bundle/chunks/{chunk-XF3LJC3O.js → chunk-DYWIFF2Q.js} +1 -1
  19. package/dist/bundle/chunks/{chunk-YRF4I43T.js → chunk-EZP6AYW2.js} +37 -31
  20. package/dist/bundle/chunks/chunk-GQJYNFMO.js +2 -0
  21. package/dist/bundle/chunks/{chunk-O3Q2ET6W.js → chunk-GWGNVYPB.js} +1 -1
  22. package/dist/bundle/chunks/{chunk-Z3UYNSVR.js → chunk-H4WJ4LY3.js} +1 -1
  23. package/dist/bundle/chunks/{chunk-UQNRW3VU.js → chunk-HWTD4QF3.js} +1 -1
  24. package/dist/bundle/chunks/{chunk-WD6WBYC5.js → chunk-I7QWTWVZ.js} +1 -1
  25. package/dist/bundle/chunks/{chunk-SWJXRC2I.js → chunk-J2YAYTB2.js} +1 -1
  26. package/dist/bundle/chunks/{chunk-TTU2RNVX.js → chunk-J5FCH7TI.js} +1 -1
  27. package/dist/bundle/chunks/chunk-JFQUWEN7.js +13 -0
  28. package/dist/bundle/chunks/{chunk-JA2R2ZSE.js → chunk-L2O6K3SJ.js} +1 -1
  29. package/dist/bundle/chunks/{chunk-4AOMNOGY.js → chunk-LO5MYLGF.js} +1 -1
  30. package/dist/bundle/chunks/{chunk-P3M2PF6K.js → chunk-MSHRIZGT.js} +8 -6
  31. package/dist/bundle/chunks/{chunk-T36LJ6MW.js → chunk-MY4ZD3EW.js} +1 -1
  32. package/dist/bundle/chunks/{chunk-UFJ2K7Q7.js → chunk-OQ3WDMB3.js} +1 -1
  33. package/dist/bundle/chunks/{chunk-7GZUFOM3.js → chunk-PH3JDRB2.js} +1 -1
  34. package/dist/bundle/chunks/{chunk-E6WYH3ZL.js → chunk-QREKAXKX.js} +7 -5
  35. package/dist/bundle/chunks/{chunk-MJ5NNWX4.js → chunk-QYFOHFPS.js} +1 -1
  36. package/dist/bundle/chunks/chunk-SOXQ42VS.js +2 -0
  37. package/dist/bundle/chunks/{chunk-FHXR4ZAC.js → chunk-UDM7VO6O.js} +1 -1
  38. package/dist/bundle/chunks/{chunk-OLCDIITS.js → chunk-UW6M3EXY.js} +1 -1
  39. package/dist/bundle/chunks/chunk-VICDRG3J.js +410 -0
  40. package/dist/bundle/chunks/chunk-VL7O5PO4.js +5 -0
  41. package/dist/bundle/chunks/chunk-XMJKZK5H.js +2 -0
  42. package/dist/bundle/chunks/{chunk-C5UFJ7B3.js → chunk-XMNPK472.js} +1 -1
  43. package/dist/bundle/chunks/{chunk-GSSD3R5X.js → chunk-XQ45JMGN.js} +1 -1
  44. package/dist/bundle/chunks/{client-WTYEICLH.js → chunk-Y3TMJV3J.js} +3 -3
  45. package/dist/bundle/chunks/{chunk-KXB5WIS5.js → chunk-Z6SAQ5O3.js} +1 -1
  46. package/dist/bundle/chunks/{chunk-CNL6QSUB.js → chunk-ZAUVY6ZC.js} +3 -3
  47. package/dist/bundle/chunks/{chunk-E4LBKOZG.js → chunk-ZLIVZQDY.js} +4 -4
  48. package/dist/bundle/chunks/{chunk-6AOVUTUR.js → chunk-ZXHLH4MW.js} +2 -2
  49. package/dist/bundle/chunks/cli-main-55EQA5PS.js +3 -0
  50. package/dist/bundle/chunks/client-LOS6GSEW.js +2 -0
  51. package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-K5H6BJP3.js → cloudflare-workers-ai-system-one-ZJQGIIRC.js} +1 -1
  52. package/dist/bundle/chunks/cursor-agent.js +3 -3
  53. package/dist/bundle/chunks/devin-agent.js +3 -3
  54. package/dist/bundle/chunks/dispatch-C2IBP5VV.js +9 -0
  55. package/dist/bundle/chunks/google-generative-ai-5G4RIQA6.js +2 -0
  56. package/dist/bundle/chunks/google-vertex-ALD4P27O.js +2 -0
  57. package/dist/bundle/chunks/{help-fast-path-3Z26DQSV.js → help-fast-path-PEIKGCKZ.js} +1 -1
  58. package/dist/bundle/chunks/host-command-657VCQ34.js +2 -0
  59. package/dist/bundle/chunks/{host-lifecycle-ONQIMWMX.js → host-lifecycle-HX3EGRRW.js} +1 -1
  60. package/dist/bundle/chunks/host-lifecycle.js +2 -2
  61. package/dist/bundle/chunks/{host-session-command-J65ENSV5.js → host-session-command-OV5XDOJK.js} +1 -1
  62. package/dist/bundle/chunks/{host-socket-request-3CWE4265.js → host-socket-request-K4LCUOUD.js} +1 -1
  63. package/dist/bundle/chunks/interactive-mode-MLXSYK2H.js +2 -0
  64. package/dist/bundle/chunks/{mcp-presenter-bootstrap-XUSCQVRC.js → mcp-presenter-bootstrap-FOMKF4TU.js} +1 -1
  65. package/dist/bundle/chunks/{mistral-conversations-DH44NOYV.js → mistral-conversations-RAE4K2XY.js} +1 -1
  66. package/dist/bundle/chunks/{multi-session-host-YI2WZOET.js → multi-session-host-774MACJU.js} +5 -6
  67. package/dist/bundle/chunks/{openai-codex-responses-SERAFKAS.js → openai-codex-responses-3CW4R7TG.js} +1 -1
  68. package/dist/bundle/chunks/{openai-completions-BZB5O45A.js → openai-completions-5UIJXQUF.js} +1 -1
  69. package/dist/bundle/chunks/openai-responses-2BO44MRW.js +3 -0
  70. package/dist/bundle/chunks/{package-manager-cli-GHZPJMRF.js → package-manager-cli-MZHVCX2S.js} +1 -1
  71. package/dist/bundle/chunks/{pi-messages-VXATX45B.js → pi-messages-RF6GER4R.js} +2 -2
  72. package/dist/bundle/chunks/{role-JVSBNKGE.js → role-ETSFSDTS.js} +1 -1
  73. package/dist/bundle/chunks/rotation-stream-ZMQB4GPO.js +2 -0
  74. package/dist/bundle/chunks/rpc-mode-UJXOIZCW.js +2 -0
  75. package/dist/bundle/chunks/{rpc-monitor-source-FFEQS744.js → rpc-monitor-source-DRZXJHTL.js} +1 -1
  76. package/dist/bundle/chunks/{schedule-command-YWG6D6ZF.js → schedule-command-KA7LFFNK.js} +1 -1
  77. package/dist/bundle/chunks/session-control-endpoint-MFWXUR7Z.js +3 -0
  78. package/dist/bundle/chunks/session-picker-BP33NRSL.js +2 -0
  79. package/dist/bundle/chunks/session-worker.js +212 -203
  80. package/dist/bundle/chunks/{typesafe-system-one-P75WJ767.js → typesafe-system-one-GNNZ5O5U.js} +1 -1
  81. package/dist/bundle/cli.js +2 -2
  82. package/dist/bundle/host-supervisor.js +2 -2
  83. package/dist/bundle/index.js +1 -1
  84. package/dist/bundle/rpc-entry.js +1 -1
  85. package/dist/bundle/runtime-manifest.json +1 -1
  86. package/dist/cli/host-command-args.d.ts +1 -0
  87. package/dist/cli/host-command-args.js +5 -0
  88. package/dist/cli/host-command.js +7 -1
  89. package/dist/cli.js +21 -3
  90. package/dist/core/extensions/builtin/mcp/catalog-cache.d.ts +2 -1
  91. package/dist/core/extensions/builtin/mcp/catalog-cache.js +19 -6
  92. package/dist/core/extensions/builtin/mcp/catalog.d.ts +3 -2
  93. package/dist/core/extensions/builtin/mcp/catalog.js +20 -3
  94. package/dist/core/extensions/builtin/mcp/elicitation.js +2 -4
  95. package/dist/core/extensions/builtin/mcp/expose/call.d.ts +2 -1
  96. package/dist/core/extensions/builtin/mcp/expose/call.js +2 -1
  97. package/dist/core/extensions/builtin/mcp/expose/register.js +1 -1
  98. package/dist/core/extensions/builtin/mcp/expose/schema-compat.d.ts +2 -2
  99. package/dist/core/extensions/builtin/mcp/expose/schema-compat.js +11 -1
  100. package/dist/core/extensions/builtin/mcp/invocation.js +16 -2
  101. package/dist/core/extensions/builtin/mcp/modern/request.d.ts +1 -1
  102. package/dist/core/extensions/builtin/mcp/modern/request.js +1 -1
  103. package/dist/core/extensions/builtin/mcp/modern/sdk-bridge.js +18 -3
  104. package/dist/core/extensions/builtin/mcp/modern/stdio-probe.js +23 -0
  105. package/dist/core/extensions/builtin/mcp/modern/subscriptions.d.ts +14 -0
  106. package/dist/core/extensions/builtin/mcp/modern/subscriptions.js +226 -0
  107. package/dist/core/extensions/builtin/mcp/modern/tool-schema.d.ts +13 -0
  108. package/dist/core/extensions/builtin/mcp/modern/tool-schema.js +75 -0
  109. package/dist/core/extensions/builtin/mcp/modern/tools.d.ts +107 -0
  110. package/dist/core/extensions/builtin/mcp/modern/tools.js +53 -0
  111. package/dist/core/extensions/builtin/mcp/resources.js +4 -0
  112. package/dist/core/extensions/builtin/mcp/tool-client.d.ts +13 -0
  113. package/dist/core/extensions/builtin/mcp/tool-client.js +17 -0
  114. package/dist/core/extensions/builtin/mcp/tool-contracts.d.ts +19 -0
  115. package/dist/core/extensions/builtin/mcp/tool-contracts.js +2 -0
  116. package/dist/core/extensions/builtin/mcp/wrap.js +2 -2
  117. package/dist/core/extensions/builtin/permission-system/bash-command-segments.d.ts +37 -0
  118. package/dist/core/extensions/builtin/permission-system/bash-command-segments.js +899 -0
  119. package/dist/core/extensions/builtin/permission-system/parsers.d.ts +2 -0
  120. package/dist/core/extensions/builtin/permission-system/parsers.js +44 -10
  121. package/dist/core/session-control-actions.js +0 -2
  122. package/dist/modes/interactive/session-control-attach.d.ts +20 -0
  123. package/dist/modes/interactive/session-control-attach.js +13 -0
  124. package/dist/modes/interactive/session-control-commands.js +37 -10
  125. package/dist/modes/interactive/session-control-registry.d.ts +2 -1
  126. package/dist/modes/interactive/session-control-registry.js +3 -2
  127. package/dist/modes/interactive/session-control-rpc-commands.d.ts +4 -2
  128. package/dist/modes/interactive/session-control-rpc-commands.js +133 -4
  129. package/dist/modes/interactive/session-control-server.js +9 -3
  130. package/dist/modes/rpc/compact-content-store.d.ts +96 -0
  131. package/dist/modes/rpc/compact-content-store.js +269 -0
  132. package/dist/modes/rpc/compact-session-state.d.ts +111 -0
  133. package/dist/modes/rpc/compact-session-state.js +206 -0
  134. package/dist/modes/rpc/compact-spool-dir.d.ts +5 -0
  135. package/dist/modes/rpc/compact-spool-dir.js +61 -0
  136. package/dist/modes/rpc/compact-storage-io.d.ts +27 -0
  137. package/dist/modes/rpc/compact-storage-io.js +19 -0
  138. package/dist/modes/rpc/connection-handler.d.ts +2 -0
  139. package/dist/modes/rpc/connection-handler.js +45 -47
  140. package/dist/modes/rpc/connection-question-bridge.d.ts +2 -1
  141. package/dist/modes/rpc/connection-question-bridge.js +6 -2
  142. package/dist/modes/rpc/custom-capability.d.ts +6 -0
  143. package/dist/modes/rpc/custom-capability.js +6 -0
  144. package/dist/modes/rpc/extension-ui-response.d.ts +7 -2
  145. package/dist/modes/rpc/extension-ui-response.js +27 -7
  146. package/dist/modes/rpc/get-entries-command.d.ts +13 -0
  147. package/dist/modes/rpc/get-entries-command.js +61 -0
  148. package/dist/modes/rpc/get-entries-page.d.ts +22 -0
  149. package/dist/modes/rpc/get-entries-page.js +41 -2
  150. package/dist/modes/rpc/get-snapshot.d.ts +18 -0
  151. package/dist/modes/rpc/get-snapshot.js +43 -0
  152. package/dist/modes/rpc/host-capabilities.js +7 -1
  153. package/dist/modes/rpc/host-ensure-start.js +1 -0
  154. package/dist/modes/rpc/host-ensure-types.d.ts +2 -0
  155. package/dist/modes/rpc/host-ensure.js +1 -0
  156. package/dist/modes/rpc/host-handoff.d.ts +2 -0
  157. package/dist/modes/rpc/host-handover-request.d.ts +1 -1
  158. package/dist/modes/rpc/host-handover-request.js +4 -3
  159. package/dist/modes/rpc/host-handover-wire.js +6 -0
  160. package/dist/modes/rpc/host-idle-handover.d.ts +2 -0
  161. package/dist/modes/rpc/host-idle-handover.js +3 -1
  162. package/dist/modes/rpc/host-observe-request.d.ts +2 -1
  163. package/dist/modes/rpc/host-observe-request.js +5 -1
  164. package/dist/modes/rpc/host-runner.d.ts +1 -0
  165. package/dist/modes/rpc/host-runner.js +9 -7
  166. package/dist/modes/rpc/host-successor.js +4 -0
  167. package/dist/modes/rpc/json-normal-value.d.ts +6 -0
  168. package/dist/modes/rpc/json-normal-value.js +100 -0
  169. package/dist/modes/rpc/mcp-presenter-bootstrap.js +5 -0
  170. package/dist/modes/rpc/media-placeholders.d.ts +9 -3
  171. package/dist/modes/rpc/media-placeholders.js +12 -4
  172. package/dist/modes/rpc/rpc-host-command-types.d.ts +2 -0
  173. package/dist/modes/rpc/rpc-question-types.d.ts +1 -0
  174. package/dist/modes/rpc/rpc-read-response-types.d.ts +9 -0
  175. package/dist/modes/rpc/rpc-session-command-types.d.ts +14 -1
  176. package/dist/modes/rpc/rpc-session-state-types.d.ts +3 -0
  177. package/dist/modes/rpc/rpc-session-state.js +2 -0
  178. package/dist/modes/rpc/rpc-types.d.ts +1 -0
  179. package/dist/modes/rpc/session-attachment-gate.js +1 -0
  180. package/dist/modes/rpc/session-binding.js +21 -18
  181. package/dist/modes/rpc/session-command-activity.js +1 -0
  182. package/dist/modes/rpc/session-event-fanout.d.ts +1 -1
  183. package/dist/modes/rpc/session-event-fanout.js +17 -9
  184. package/dist/modes/rpc/session-event-writer.js +22 -5
  185. package/dist/modes/rpc/session-extension-ui-requests.d.ts +2 -1
  186. package/dist/modes/rpc/session-extension-ui-requests.js +15 -10
  187. package/dist/modes/rpc/session-held.js +1 -0
  188. package/dist/modes/rpc/session-status-stream.d.ts +2 -1
  189. package/dist/modes/rpc/session-status-stream.js +8 -2
  190. package/dist/modes/rpc/session-status-subscriptions.d.ts +2 -1
  191. package/dist/modes/rpc/session-status-subscriptions.js +4 -0
  192. package/dist/modes/rpc/session-worker.js +5 -2
  193. package/dist/modes/rpc/snapshot-types.d.ts +29 -0
  194. package/dist/modes/rpc/snapshot-types.js +2 -0
  195. package/dist/modes/rpc/ui-answer-ledger.d.ts +13 -0
  196. package/dist/modes/rpc/ui-answer-ledger.js +34 -0
  197. package/dist/senpi +21 -3
  198. package/docs/mcp.md +20 -0
  199. package/docs/rpc.md +87 -11
  200. package/package.json +7 -6
  201. package/dist/bundle/chunks/azure-openai-responses-7I45DI72.js +0 -2
  202. package/dist/bundle/chunks/chunk-3LNSLJ7N.js +0 -2
  203. package/dist/bundle/chunks/chunk-6O4SEW2W.js +0 -5
  204. package/dist/bundle/chunks/chunk-7W7OJDIY.js +0 -410
  205. package/dist/bundle/chunks/chunk-DMI55M2N.js +0 -2
  206. package/dist/bundle/chunks/chunk-H3HNMY2W.js +0 -13
  207. package/dist/bundle/chunks/chunk-MSQCTH6V.js +0 -2
  208. package/dist/bundle/chunks/cli-main-HEKFFB5S.js +0 -3
  209. package/dist/bundle/chunks/dispatch-NYAULNVW.js +0 -9
  210. package/dist/bundle/chunks/google-generative-ai-UM5Y7T6B.js +0 -2
  211. package/dist/bundle/chunks/google-vertex-OKU2VQHL.js +0 -2
  212. package/dist/bundle/chunks/host-command-Z3WPHJOW.js +0 -2
  213. package/dist/bundle/chunks/interactive-mode-DEPQC6TX.js +0 -2
  214. package/dist/bundle/chunks/openai-responses-TNHQ6SJS.js +0 -3
  215. package/dist/bundle/chunks/rotation-stream-ZPVCFOVD.js +0 -2
  216. package/dist/bundle/chunks/rpc-mode-ORHBGBBV.js +0 -2
  217. package/dist/bundle/chunks/session-control-endpoint-36HAQSQO.js +0 -3
  218. package/dist/bundle/chunks/session-picker-25H5KNYP.js +0 -2
@@ -0,0 +1,269 @@
1
+ import { join } from "node:path";
2
+ import { createSpoolDir, removeSpoolDir } from "./compact-spool-dir.js";
3
+ import { CompactStorageReadError, describeCause, nodeCompactStorageIo, } from "./compact-storage-io.js";
4
+ /**
5
+ * Owned bounded leaves for compact current state (senpi#4272, R3.C6.4 / R3.C2.4).
6
+ *
7
+ * Acceptance never waits here: a leaf is created synchronously in RAM, and a spill only relocates
8
+ * that immutable value. Its RAM copy is released only after the whole file was written and closed;
9
+ * a failed write keeps the bytes resident, halts further spilling and is reported through
10
+ * `status()`, never dropped. All storage work runs as one serial asynchronous job chain.
11
+ */
12
+ /** 32,768 UTF-16 code units (64 KiB as UTF-16LE), so a split surrogate survives a leaf boundary exactly. */
13
+ export const COMPACT_LEAF_CODE_UNITS = 32_768;
14
+ const RECLAIM_WORK_ALLOWANCE_BYTES = 1024 * 1024;
15
+ const RECLAIM_RETRY_DELAYS_MS = [100, 400, 1_600, 6_400, 25_600];
16
+ const FAILURE_HISTORY = 32;
17
+ export class CompactContentStore {
18
+ constructor(options) {
19
+ this.leaves = new Map();
20
+ /** Resident leaves in creation order, so each spill takes the oldest without rescanning committed ones. */
21
+ this.ramLeaves = new Set();
22
+ this.failures = [];
23
+ this.nextLeafId = 1;
24
+ this.nextFile = 1;
25
+ this.chain = Promise.resolve();
26
+ this.queued = 0;
27
+ this.spillQueued = false;
28
+ this.halted = false;
29
+ this.retired = false;
30
+ this.residentPayloadBytes = 0;
31
+ this.pendingIoCopyBytes = 0;
32
+ this.pinnedOnlyBytes = 0;
33
+ this.spilledLiveBytes = 0;
34
+ this.spillBytesWritten = 0;
35
+ this.reclaimDebtBytes = 0;
36
+ this.dir = options.dir;
37
+ this.io = options.io ?? nodeCompactStorageIo;
38
+ this.spillAboveBytes = options.spillAboveBytes ?? 4 * 1024 * 1024;
39
+ this.spillTargetBytes = options.spillTargetBytes ?? 2 * 1024 * 1024;
40
+ }
41
+ /** Takes ownership of an immutable value with one live reference. */
42
+ createLeaf(text) {
43
+ const id = this.nextLeafId++;
44
+ this.leaves.set(id, { text, units: text.length, live: 1, pins: 0, state: "ram" });
45
+ this.ramLeaves.add(id);
46
+ this.residentPayloadBytes += text.length * 2;
47
+ this.scheduleSpill();
48
+ return id;
49
+ }
50
+ /** Moves live and cut-pin references; the last reference drops RAM or reclaims the file. */
51
+ adjust(id, live, pins) {
52
+ const leaf = this.leaves.get(id);
53
+ if (!leaf)
54
+ throw new Error(`compact leaf ${id} is not owned`);
55
+ const pinnedOnly = leaf.live === 0 && leaf.pins > 0;
56
+ leaf.live += live;
57
+ leaf.pins += pins;
58
+ if (pinnedOnly !== (leaf.live === 0 && leaf.pins > 0))
59
+ this.pinnedOnlyBytes += (pinnedOnly ? -2 : 2) * leaf.units;
60
+ if (leaf.live > 0 || leaf.pins > 0)
61
+ return;
62
+ this.leaves.delete(id);
63
+ this.ramLeaves.delete(id);
64
+ if (leaf.state === "committed" && leaf.path) {
65
+ this.spilledLiveBytes -= leaf.units * 2;
66
+ this.discard(leaf.path, leaf.units * 2);
67
+ return;
68
+ }
69
+ this.residentPayloadBytes -= leaf.units * 2;
70
+ }
71
+ /** Reads a leaf the caller still references, from RAM or its committed file. */
72
+ async readLeaf(id) {
73
+ const leaf = this.leaves.get(id);
74
+ if (!leaf)
75
+ throw new Error(`compact leaf ${id} is not owned`);
76
+ if (leaf.text !== undefined)
77
+ return leaf.text;
78
+ return (await this.readFile(leaf.path ?? "", leaf.units * 2, "leaf_read")).toString("utf16le");
79
+ }
80
+ /** Resolves once every queued storage job, including the ones they queued, has completed. */
81
+ async idle() {
82
+ while (this.queued > 0)
83
+ await this.chain;
84
+ }
85
+ /**
86
+ * Invalidates spilling, joins owned I/O, then removes the epoch directory only when this store
87
+ * created it and it still passes the ownership checks; otherwise rejects with `CompactSpoolRefusedError`.
88
+ */
89
+ async retire() {
90
+ this.retired = true;
91
+ await this.idle();
92
+ const token = this.spoolToken;
93
+ this.spoolToken = undefined;
94
+ if (token !== undefined)
95
+ await removeSpoolDir(this.dir, token);
96
+ }
97
+ status() {
98
+ return {
99
+ residentPayloadBytes: this.residentPayloadBytes,
100
+ pendingIoCopyBytes: this.pendingIoCopyBytes,
101
+ pinnedOnlyBytes: this.pinnedOnlyBytes,
102
+ spilledLiveBytes: this.spilledLiveBytes,
103
+ spillBytesWritten: this.spillBytesWritten,
104
+ reclaimDebtBytes: this.reclaimDebtBytes,
105
+ spillFailureRetainedBytes: this.spillStopped
106
+ ? Math.max(0, this.residentPayloadBytes - this.spillTargetBytes)
107
+ : 0,
108
+ spillHalted: this.spillStopped,
109
+ failures: [...this.failures],
110
+ };
111
+ }
112
+ /** Reads a committed file and rejects a missing, unreadable or short one. */
113
+ async readFile(path, bytes, operation) {
114
+ let data;
115
+ try {
116
+ data = await this.io.readFile(path);
117
+ }
118
+ catch (cause) {
119
+ throw this.readFailure(operation, path, cause);
120
+ }
121
+ if (data.byteLength !== bytes) {
122
+ throw this.readFailure(operation, path, new Error(`expected ${bytes} bytes, found ${data.byteLength}`));
123
+ }
124
+ return data;
125
+ }
126
+ /** Writes one new spool file; undefined means the write failed, was reported, and spilling halted. */
127
+ async writeFile(data, operation, retainedInMemoryBytes) {
128
+ const path = await this.ensureDir(retainedInMemoryBytes);
129
+ if (path === undefined)
130
+ return undefined;
131
+ const file = join(path, `leaf-${this.nextFile++}.bin`);
132
+ this.pendingIoCopyBytes += data.byteLength;
133
+ try {
134
+ await this.io.writeFile(file, data);
135
+ this.spillBytesWritten += data.byteLength;
136
+ return file;
137
+ }
138
+ catch (cause) {
139
+ this.fail(operation, file, cause, retainedInMemoryBytes);
140
+ this.discard(file, data.byteLength);
141
+ return undefined;
142
+ }
143
+ finally {
144
+ this.pendingIoCopyBytes -= data.byteLength;
145
+ }
146
+ }
147
+ /**
148
+ * An unreferenced (or partially written) spool file: reclamation debt until its unlink completes or
149
+ * finds it gone. Files dropped after retirement started belong to the directory retirement removes.
150
+ */
151
+ discard(path, bytes) {
152
+ if (this.retired)
153
+ return;
154
+ this.reclaimDebtBytes += bytes;
155
+ this.enqueue("reclaim", () => this.reclaim(path, bytes, 0));
156
+ }
157
+ /** A failed unlink keeps its debt and is retried with bounded backoff outside the job chain. */
158
+ async reclaim(path, bytes, attempt) {
159
+ try {
160
+ await this.io.unlink(path);
161
+ }
162
+ catch (cause) {
163
+ if (cause.code !== "ENOENT") {
164
+ this.fail("reclaim", path, cause, 0);
165
+ const delay = RECLAIM_RETRY_DELAYS_MS[attempt];
166
+ if (delay !== undefined) {
167
+ setTimeout(() => {
168
+ if (this.retired)
169
+ this.reclaimDebtBytes -= bytes;
170
+ else
171
+ this.enqueue("reclaim", () => this.reclaim(path, bytes, attempt + 1));
172
+ }, delay).unref?.();
173
+ }
174
+ return;
175
+ }
176
+ }
177
+ this.reclaimDebtBytes -= bytes;
178
+ this.scheduleSpill();
179
+ }
180
+ get spillAllowed() {
181
+ return !this.halted && !this.retired;
182
+ }
183
+ /** True while a storage failure or unreclaimed debt above the work allowance holds spilling back. */
184
+ get spillStopped() {
185
+ return this.halted || this.reclaimDebtBytes > RECLAIM_WORK_ALLOWANCE_BYTES;
186
+ }
187
+ enqueue(operation, job) {
188
+ this.queued++;
189
+ this.chain = this.chain
190
+ .then(job)
191
+ .catch((cause) => {
192
+ if (operation === "spill_write")
193
+ this.spillQueued = false;
194
+ this.fail(operation, this.dir, cause, 0);
195
+ })
196
+ .finally(() => {
197
+ this.queued--;
198
+ });
199
+ }
200
+ scheduleSpill() {
201
+ if (this.spillQueued || !this.spillAllowed || this.residentPayloadBytes <= this.spillAboveBytes)
202
+ return;
203
+ if (this.reclaimDebtBytes > RECLAIM_WORK_ALLOWANCE_BYTES)
204
+ return;
205
+ this.spillQueued = true;
206
+ this.enqueue("spill_write", () => this.spillNext());
207
+ }
208
+ /** Spills one oldest resident leaf per job, so other storage work interleaves fairly. */
209
+ async spillNext() {
210
+ const [id] = this.ramLeaves;
211
+ const leaf = id === undefined ? undefined : this.leaves.get(id);
212
+ if (id === undefined || !leaf || leaf.text === undefined || !this.spillAllowed) {
213
+ this.spillQueued = false;
214
+ return;
215
+ }
216
+ const bytes = leaf.units * 2;
217
+ leaf.state = "pending";
218
+ this.ramLeaves.delete(id);
219
+ const path = await this.writeFile(Buffer.from(leaf.text, "utf16le"), "spill_write", bytes);
220
+ const owned = this.leaves.get(id) === leaf;
221
+ if (path === undefined) {
222
+ leaf.state = "ram";
223
+ if (owned)
224
+ this.ramLeaves.add(id);
225
+ }
226
+ else if (!owned || this.retired) {
227
+ this.discard(path, bytes);
228
+ }
229
+ else {
230
+ leaf.state = "committed";
231
+ leaf.path = path;
232
+ leaf.text = undefined;
233
+ this.residentPayloadBytes -= bytes;
234
+ this.spilledLiveBytes += bytes;
235
+ }
236
+ this.spillQueued = false;
237
+ if (this.residentPayloadBytes > this.spillTargetBytes) {
238
+ this.spillQueued = this.spillAllowed;
239
+ if (this.spillQueued)
240
+ this.enqueue("spill_write", () => this.spillNext());
241
+ }
242
+ }
243
+ /** Creates the private epoch directory once; a refusal or failure halts spilling and is reported. */
244
+ ensureDir(retained) {
245
+ this.dirReady ??= createSpoolDir(this.dir).then((token) => {
246
+ this.spoolToken = token;
247
+ return this.dir;
248
+ }, (cause) => {
249
+ this.fail("spool_create", this.dir, cause, retained);
250
+ return undefined;
251
+ });
252
+ return this.dirReady;
253
+ }
254
+ readFailure(operation, path, cause) {
255
+ this.fail(operation, path, cause, 0);
256
+ return new CompactStorageReadError(operation, path, cause);
257
+ }
258
+ fail(operation, path, cause, retained) {
259
+ if (operation === "spill_write" || operation === "spool_create")
260
+ this.halted = true;
261
+ this.record({ operation, path, cause: describeCause(cause), retainedInMemoryBytes: retained });
262
+ }
263
+ record(failure) {
264
+ this.failures.push(failure);
265
+ if (this.failures.length > FAILURE_HISTORY)
266
+ this.failures.shift();
267
+ }
268
+ }
269
+ //# sourceMappingURL=compact-content-store.js.map
@@ -0,0 +1,111 @@
1
+ import { type CompactContentStoreOptions, type CompactStorageStatus, type LeafId } from "./compact-content-store.ts";
2
+ /**
3
+ * The in-memory current-state owner over compact storage (senpi#4272, R3.C6.4 first half).
4
+ *
5
+ * `accept` is synchronous: it validates a typed transition completely, then publishes the next
6
+ * sequence, or throws and leaves the prior root untouched. Block text lives in owned bounded leaves
7
+ * that spill asynchronously; settled message rows live in a resident map whose persistence status
8
+ * changes only through `message_status`, never through storage success or failure. The map is
9
+ * exact for every transition: a status change replaces the whole row, a removal deletes it, and a
10
+ * transition addressing a removed or unknown message is refused rather than masked. `freeze` pins
11
+ * an immutable cut synchronously, and `read` materializes it asynchronously from RAM or disk.
12
+ */
13
+ type CompactBlockKind = "text" | "thinking" | "tool_arguments";
14
+ type CompactPersistence = "pending" | "appended" | "failed";
15
+ interface BlockAddress {
16
+ readonly messageId: string;
17
+ readonly contentIndex: number;
18
+ }
19
+ export type CompactTransition = (BlockAddress & {
20
+ readonly type: "block_start";
21
+ readonly kind: CompactBlockKind;
22
+ readonly seed: string;
23
+ /**
24
+ * The whole seed when later deltas replay it from offset 0, or 0 when they continue after it. Partial
25
+ * coverage is refused: the uncovered seed tail would make every later delta overlap or leave a gap.
26
+ */
27
+ readonly coveredCodeUnits: number;
28
+ }) | (BlockAddress & {
29
+ readonly type: "block_delta";
30
+ readonly offset: number;
31
+ readonly text: string;
32
+ }) | (BlockAddress & {
33
+ readonly type: "block_replace";
34
+ readonly text: string;
35
+ }) | {
36
+ readonly type: "message_status";
37
+ readonly messageId: string;
38
+ readonly persistence: CompactPersistence;
39
+ } | {
40
+ readonly type: "message_remove";
41
+ readonly messageId: string;
42
+ };
43
+ interface FrozenBlock {
44
+ readonly contentIndex: number;
45
+ readonly kind: CompactBlockKind;
46
+ readonly leaves: readonly LeafId[];
47
+ /** A cut shares the immutable active tail string (under one leaf) instead of minting a leaf per freeze. */
48
+ readonly tail: string;
49
+ }
50
+ interface SettledRow {
51
+ readonly persistence: CompactPersistence;
52
+ readonly blocks: readonly FrozenBlock[];
53
+ }
54
+ interface FrozenCompactState {
55
+ readonly sequence: number;
56
+ readonly active: ReadonlyMap<string, readonly FrozenBlock[]>;
57
+ readonly settled: ReadonlyMap<string, SettledRow>;
58
+ }
59
+ interface CompactMessageView {
60
+ readonly messageId: string;
61
+ readonly persistence: CompactPersistence | undefined;
62
+ readonly blocks: readonly {
63
+ readonly contentIndex: number;
64
+ readonly kind: CompactBlockKind;
65
+ readonly text: string;
66
+ }[];
67
+ }
68
+ export declare class CompactSessionState {
69
+ private readonly store;
70
+ private readonly settled;
71
+ private readonly active;
72
+ private sequence;
73
+ private frozenTailBytes;
74
+ constructor(options: CompactContentStoreOptions);
75
+ get acceptedSequence(): number;
76
+ accept(transition: CompactTransition): number;
77
+ freeze(): FrozenCompactState;
78
+ release(frozen: FrozenCompactState): void;
79
+ /** Rejects with the storage error when committed content is gone; never returns empty content for it. */
80
+ read(frozen: FrozenCompactState): Promise<{
81
+ sequence: number;
82
+ messages: CompactMessageView[];
83
+ }>;
84
+ storageStatus(): CompactStorageStatus & {
85
+ readonly residentTailBytes: number;
86
+ /** Active tails held by unreleased cuts; may share storage with `residentTailBytes`. */
87
+ readonly frozenTailBytes: number;
88
+ readonly hotWitnessBytes: number;
89
+ /** Settled rows kept resident: S03a-1 has no paged row index, so every row header stays in RAM. */
90
+ readonly residentSettledRows: number;
91
+ };
92
+ idle(): Promise<void>;
93
+ retire(): Promise<void>;
94
+ private start;
95
+ /**
96
+ * Appends only the new suffix. Overlap exists only while a fully covered seed is being replayed
97
+ * (`emitted < covered == units`), so it always lies inside the producer-declared coverage.
98
+ */
99
+ private delta;
100
+ private replace;
101
+ private setPersistence;
102
+ private remove;
103
+ private block;
104
+ private append;
105
+ /** Settling mints the final tail into a spillable leaf; a freeze of an active block shares the tail. */
106
+ private seal;
107
+ private pin;
108
+ private texts;
109
+ }
110
+ export {};
111
+ //# sourceMappingURL=compact-session-state.d.ts.map
@@ -0,0 +1,206 @@
1
+ import { COMPACT_LEAF_CODE_UNITS, CompactContentStore, } from "./compact-content-store.js";
2
+ class CompactTransitionError extends Error {
3
+ constructor(reason, detail) {
4
+ super(`compact transition rejected (${reason}): ${detail}`);
5
+ this.name = "CompactTransitionError";
6
+ this.reason = reason;
7
+ }
8
+ }
9
+ const byContentIndex = ([a], [b]) => a - b;
10
+ export class CompactSessionState {
11
+ constructor(options) {
12
+ this.settled = new Map();
13
+ this.active = new Map();
14
+ this.sequence = 0;
15
+ this.frozenTailBytes = 0;
16
+ this.store = new CompactContentStore(options);
17
+ }
18
+ get acceptedSequence() {
19
+ return this.sequence;
20
+ }
21
+ accept(transition) {
22
+ switch (transition.type) {
23
+ case "block_start":
24
+ this.start(transition);
25
+ break;
26
+ case "block_delta":
27
+ this.delta(this.block(transition), transition.offset, transition.text);
28
+ break;
29
+ case "block_replace":
30
+ this.replace(this.block(transition), transition.text);
31
+ break;
32
+ case "message_status":
33
+ this.setPersistence(transition.messageId, transition.persistence);
34
+ break;
35
+ case "message_remove":
36
+ this.remove(transition.messageId);
37
+ break;
38
+ }
39
+ return ++this.sequence;
40
+ }
41
+ freeze() {
42
+ const active = new Map();
43
+ for (const [messageId, blocks] of this.active)
44
+ active.set(messageId, this.seal(blocks, false));
45
+ const settled = new Map(this.settled);
46
+ for (const blocks of active.values())
47
+ this.pin(blocks, 1);
48
+ for (const row of settled.values())
49
+ this.pin(row.blocks, 1);
50
+ return { sequence: this.sequence, active, settled };
51
+ }
52
+ release(frozen) {
53
+ for (const blocks of frozen.active.values())
54
+ this.pin(blocks, -1);
55
+ for (const row of frozen.settled.values())
56
+ this.pin(row.blocks, -1);
57
+ }
58
+ /** Rejects with the storage error when committed content is gone; never returns empty content for it. */
59
+ async read(frozen) {
60
+ const messages = [];
61
+ for (const [messageId, row] of frozen.settled) {
62
+ messages.push({ messageId, persistence: row.persistence, blocks: await this.texts(row.blocks) });
63
+ }
64
+ for (const [messageId, blocks] of frozen.active) {
65
+ messages.push({ messageId, persistence: undefined, blocks: await this.texts(blocks) });
66
+ }
67
+ return { sequence: frozen.sequence, messages };
68
+ }
69
+ storageStatus() {
70
+ let residentTailBytes = 0;
71
+ let hotWitnessBytes = 0;
72
+ for (const blocks of this.active.values()) {
73
+ for (const block of blocks.values()) {
74
+ residentTailBytes += block.tail.length * 2;
75
+ hotWitnessBytes += block.witness.length * 2;
76
+ }
77
+ }
78
+ return {
79
+ ...this.store.status(),
80
+ residentTailBytes,
81
+ frozenTailBytes: this.frozenTailBytes,
82
+ hotWitnessBytes,
83
+ residentSettledRows: this.settled.size,
84
+ };
85
+ }
86
+ idle() {
87
+ return this.store.idle();
88
+ }
89
+ retire() {
90
+ return this.store.retire();
91
+ }
92
+ start(t) {
93
+ if (this.active.get(t.messageId)?.has(t.contentIndex) || this.settled.has(t.messageId)) {
94
+ throw new CompactTransitionError("duplicate_block", `${t.messageId}#${t.contentIndex}`);
95
+ }
96
+ const covered = t.coveredCodeUnits;
97
+ if (!Number.isInteger(covered) || (covered !== 0 && covered !== t.seed.length)) {
98
+ throw new CompactTransitionError("invalid_coverage", `${covered} of ${t.seed.length} seed code units`);
99
+ }
100
+ const emitted = covered > 0 ? 0 : t.seed.length;
101
+ const block = { kind: t.kind, leaves: [], tail: "", units: 0, emitted, covered, witness: "" };
102
+ block.witness = t.seed.slice(0, covered);
103
+ this.append(block, t.seed);
104
+ const blocks = this.active.get(t.messageId) ?? new Map();
105
+ blocks.set(t.contentIndex, block);
106
+ this.active.set(t.messageId, blocks);
107
+ }
108
+ /**
109
+ * Appends only the new suffix. Overlap exists only while a fully covered seed is being replayed
110
+ * (`emitted < covered == units`), so it always lies inside the producer-declared coverage.
111
+ */
112
+ delta(block, offset, text) {
113
+ if (offset !== block.emitted) {
114
+ throw new CompactTransitionError("non_contiguous", `offset ${offset}, expected ${block.emitted}`);
115
+ }
116
+ const overlapEnd = Math.min(offset + text.length, block.units);
117
+ const overlap = Math.max(0, overlapEnd - offset);
118
+ if (overlap > 0 && text.slice(0, overlap) !== block.witness.slice(offset, overlapEnd)) {
119
+ throw new CompactTransitionError("conflicting_overlap", `[${offset}, ${overlapEnd})`);
120
+ }
121
+ this.append(block, text.slice(overlap));
122
+ block.emitted = offset + text.length;
123
+ if (block.emitted >= block.covered)
124
+ block.witness = "";
125
+ }
126
+ replace(block, text) {
127
+ for (const id of block.leaves.splice(0))
128
+ this.store.adjust(id, -1, 0);
129
+ block.tail = "";
130
+ block.units = 0;
131
+ block.covered = 0;
132
+ block.witness = "";
133
+ this.append(block, text);
134
+ block.emitted = block.units;
135
+ }
136
+ setPersistence(messageId, persistence) {
137
+ const blocks = this.active.get(messageId);
138
+ if (blocks) {
139
+ this.settled.set(messageId, { persistence, blocks: this.seal(blocks, true) });
140
+ this.active.delete(messageId);
141
+ return;
142
+ }
143
+ const row = this.settled.get(messageId);
144
+ if (row) {
145
+ this.settled.set(messageId, { ...row, persistence });
146
+ }
147
+ else {
148
+ throw new CompactTransitionError("unknown_message", messageId);
149
+ }
150
+ }
151
+ remove(messageId) {
152
+ const blocks = this.active.get(messageId);
153
+ const row = this.settled.get(messageId);
154
+ if (!blocks && !row)
155
+ throw new CompactTransitionError("unknown_message", messageId);
156
+ for (const block of blocks?.values() ?? [])
157
+ for (const id of block.leaves)
158
+ this.store.adjust(id, -1, 0);
159
+ for (const block of row?.blocks ?? [])
160
+ for (const id of block.leaves)
161
+ this.store.adjust(id, -1, 0);
162
+ this.active.delete(messageId);
163
+ this.settled.delete(messageId);
164
+ }
165
+ block(address) {
166
+ const block = this.active.get(address.messageId)?.get(address.contentIndex);
167
+ if (!block)
168
+ throw new CompactTransitionError("unknown_block", `${address.messageId}#${address.contentIndex}`);
169
+ return block;
170
+ }
171
+ append(block, text) {
172
+ block.units += text.length;
173
+ let rest = text;
174
+ while (block.tail.length + rest.length >= COMPACT_LEAF_CODE_UNITS) {
175
+ const take = COMPACT_LEAF_CODE_UNITS - block.tail.length;
176
+ block.leaves.push(this.store.createLeaf(block.tail + rest.slice(0, take)));
177
+ block.tail = "";
178
+ rest = rest.slice(take);
179
+ }
180
+ block.tail += rest;
181
+ }
182
+ /** Settling mints the final tail into a spillable leaf; a freeze of an active block shares the tail. */
183
+ seal(blocks, settle) {
184
+ return [...blocks].sort(byContentIndex).map(([contentIndex, block]) => {
185
+ if (settle && block.tail.length > 0) {
186
+ block.leaves.push(this.store.createLeaf(block.tail));
187
+ block.tail = "";
188
+ }
189
+ return { contentIndex, kind: block.kind, leaves: [...block.leaves], tail: block.tail };
190
+ });
191
+ }
192
+ pin(blocks, delta) {
193
+ for (const block of blocks) {
194
+ this.frozenTailBytes += delta * block.tail.length * 2;
195
+ for (const id of block.leaves)
196
+ this.store.adjust(id, 0, delta);
197
+ }
198
+ }
199
+ async texts(blocks) {
200
+ return Promise.all(blocks.map(async ({ contentIndex, kind, leaves, tail }) => {
201
+ const parts = await Promise.all(leaves.map((id) => this.store.readLeaf(id)));
202
+ return { contentIndex, kind, text: parts.join("") + tail };
203
+ }));
204
+ }
205
+ }
206
+ //# sourceMappingURL=compact-session-state.js.map
@@ -0,0 +1,5 @@
1
+ /** Creates a fresh private epoch directory and returns the token its ownership marker holds. */
2
+ export declare function createSpoolDir(dir: string): Promise<string>;
3
+ /** Removes `dir` only while it is still the private, marked directory `createSpoolDir` made. */
4
+ export declare function removeSpoolDir(dir: string, token: string): Promise<void>;
5
+ //# sourceMappingURL=compact-spool-dir.d.ts.map
@@ -0,0 +1,61 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { lstat, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ /**
5
+ * Ownership of the compact spool's epoch directory (senpi#4272). The store creates the directory
6
+ * itself (0700, never adopting an existing one) and writes a private marker into it; removal
7
+ * re-checks the directory and that marker without following symlinks, so retirement never deletes
8
+ * a directory this store did not create and still own.
9
+ */
10
+ const MARKER = ".compact-spool-owner";
11
+ class CompactSpoolRefusedError extends Error {
12
+ constructor(reason, path) {
13
+ super(`compact spool directory refused (${reason}): ${path}`);
14
+ this.name = "CompactSpoolRefusedError";
15
+ this.reason = reason;
16
+ this.path = path;
17
+ }
18
+ }
19
+ /** Creates a fresh private epoch directory and returns the token its ownership marker holds. */
20
+ export async function createSpoolDir(dir) {
21
+ await mkdir(dirname(dir), { recursive: true, mode: 0o700 });
22
+ try {
23
+ await mkdir(dir, { mode: 0o700 });
24
+ }
25
+ catch (cause) {
26
+ if (cause.code === "EEXIST")
27
+ throw new CompactSpoolRefusedError("pre_existing", dir);
28
+ throw cause;
29
+ }
30
+ const token = randomUUID();
31
+ await writeFile(join(dir, MARKER), token, { flag: "wx", mode: 0o600 });
32
+ return token;
33
+ }
34
+ /** Removes `dir` only while it is still the private, marked directory `createSpoolDir` made. */
35
+ export async function removeSpoolDir(dir, token) {
36
+ const refusal = await refusalFor(dir, token);
37
+ if (refusal)
38
+ throw new CompactSpoolRefusedError(refusal, dir);
39
+ await rm(dir, { recursive: true, force: true });
40
+ }
41
+ async function refusalFor(dir, token) {
42
+ const owned = await lstat(dir);
43
+ if (owned.isSymbolicLink())
44
+ return "symlink";
45
+ if (!owned.isDirectory())
46
+ return "not_directory";
47
+ const markerPath = join(dir, MARKER);
48
+ const marker = await lstat(markerPath).catch(() => undefined);
49
+ if (!marker?.isFile() || (await readFile(markerPath, "utf8")) !== token)
50
+ return "marker_missing";
51
+ // Windows has no POSIX owner or mode bits; the marker token and the no-follow checks still apply.
52
+ const uid = process.getuid?.();
53
+ if (uid === undefined)
54
+ return undefined;
55
+ if (owned.uid !== uid || marker.uid !== uid)
56
+ return "foreign_owner";
57
+ if (((owned.mode | marker.mode) & 0o077) !== 0)
58
+ return "wide_mode";
59
+ return undefined;
60
+ }
61
+ //# sourceMappingURL=compact-spool-dir.js.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The asynchronous file seam of compact current-state storage (senpi#4272) and the failures it
3
+ * reports. Every spool access goes through `CompactStorageIo`; nothing here blocks the host loop.
4
+ */
5
+ export type CompactStorageOperation = "spool_create" | "spill_write" | "leaf_read" | "reclaim";
6
+ export interface CompactStorageFailure {
7
+ readonly operation: CompactStorageOperation;
8
+ readonly path: string;
9
+ readonly cause: string;
10
+ /** Accepted bytes this failure leaves resident in memory instead of on disk. */
11
+ readonly retainedInMemoryBytes: number;
12
+ }
13
+ /** Asynchronous file access the host supplies; the default is node's promise filesystem. */
14
+ export interface CompactStorageIo {
15
+ writeFile(path: string, data: Buffer): Promise<void>;
16
+ readFile(path: string): Promise<Buffer>;
17
+ unlink(path: string): Promise<void>;
18
+ }
19
+ export declare const nodeCompactStorageIo: CompactStorageIo;
20
+ export declare function describeCause(cause: unknown): string;
21
+ /** Committed storage that can no longer be read: a real error, never empty content. */
22
+ export declare class CompactStorageReadError extends Error {
23
+ readonly operation: CompactStorageOperation;
24
+ readonly path: string;
25
+ constructor(operation: CompactStorageOperation, path: string, cause: unknown);
26
+ }
27
+ //# sourceMappingURL=compact-storage-io.d.ts.map