@code-yeongyu/senpi 2026.10.26-1 → 2026.10.26-2

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 (148) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/bundle/chunks/{anthropic-messages-M235FJQV.js → anthropic-messages-ODSFJCXR.js} +1 -1
  3. package/dist/bundle/chunks/{args-FBNJOJSR.js → args-SWKJAYQD.js} +1 -1
  4. package/dist/bundle/chunks/{azure-openai-responses-YQRTQ7H6.js → azure-openai-responses-REGXS4XO.js} +1 -1
  5. package/dist/bundle/chunks/bedrock-converse-stream.js +1 -1
  6. package/dist/bundle/chunks/chunk-2XF6W6F6.js +410 -0
  7. package/dist/bundle/chunks/{chunk-DMFAVE5N.js → chunk-4QVORFQ2.js} +1 -1
  8. package/dist/bundle/chunks/{chunk-ZXHLH4MW.js → chunk-5W3DTM2J.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-QREKAXKX.js → chunk-6GAMRAVM.js} +5 -5
  10. package/dist/bundle/chunks/{chunk-MSHRIZGT.js → chunk-7IZH2KQX.js} +3 -3
  11. package/dist/bundle/chunks/{chunk-PH3JDRB2.js → chunk-ARSENVDY.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-5KZH6EYQ.js → chunk-CNDPCYNT.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-DYWIFF2Q.js → chunk-CYOHFREV.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-3LPWILUZ.js → chunk-DNDZPKIQ.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-ZLIVZQDY.js → chunk-EQNUL2WB.js} +1 -1
  16. package/dist/bundle/chunks/{chunk-5FQMNHXV.js → chunk-ESNSRUW4.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-LO5MYLGF.js → chunk-FQDARFHT.js} +1 -1
  18. package/dist/bundle/chunks/{chunk-7KOUQSBX.js → chunk-GPPOPK76.js} +3 -3
  19. package/dist/bundle/chunks/chunk-I2WFLKZ2.js +5 -0
  20. package/dist/bundle/chunks/{chunk-I7QWTWVZ.js → chunk-IHKHCWWG.js} +1 -1
  21. package/dist/bundle/chunks/{chunk-MY4ZD3EW.js → chunk-IY4G44MN.js} +1 -1
  22. package/dist/bundle/chunks/{chunk-OQ3WDMB3.js → chunk-JH7MVRNJ.js} +1 -1
  23. package/dist/bundle/chunks/chunk-KBSFTXAT.js +2 -0
  24. package/dist/bundle/chunks/{chunk-UW6M3EXY.js → chunk-KRO63T4Q.js} +1 -1
  25. package/dist/bundle/chunks/{chunk-J5FCH7TI.js → chunk-KTDIIF2F.js} +1 -1
  26. package/dist/bundle/chunks/{chunk-3WP5RFQR.js → chunk-NDNIMC5W.js} +1 -1
  27. package/dist/bundle/chunks/{chunk-CDQPA4FP.js → chunk-ODRGPC5S.js} +1 -1
  28. package/dist/bundle/chunks/{chunk-3A3XCLNH.js → chunk-PDG4XWO2.js} +1 -1
  29. package/dist/bundle/chunks/{chunk-QYFOHFPS.js → chunk-PPECSSWX.js} +1 -1
  30. package/dist/bundle/chunks/{chunk-5LWUUXMH.js → chunk-PY4ZQHTN.js} +1 -1
  31. package/dist/bundle/chunks/{chunk-EZP6AYW2.js → chunk-RPW3XQW3.js} +22 -22
  32. package/dist/bundle/chunks/{chunk-SOXQ42VS.js → chunk-RQ5JTXUC.js} +1 -1
  33. package/dist/bundle/chunks/{chunk-JFQUWEN7.js → chunk-TQ67PB7U.js} +2 -2
  34. package/dist/bundle/chunks/{chunk-6U3TBFBA.js → chunk-UJPG4ROT.js} +1 -1
  35. package/dist/bundle/chunks/{chunk-UDM7VO6O.js → chunk-XM36IFP3.js} +1 -1
  36. package/dist/bundle/chunks/{chunk-XQ45JMGN.js → chunk-Z6BREXV3.js} +1 -1
  37. package/dist/bundle/chunks/{chunk-ADQAPDPV.js → chunk-ZP6JJDYV.js} +1 -1
  38. package/dist/bundle/chunks/{cli-main-55EQA5PS.js → cli-main-Q2GA53XM.js} +1 -1
  39. package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-ZJQGIIRC.js → cloudflare-workers-ai-system-one-WDHT7BBV.js} +1 -1
  40. package/dist/bundle/chunks/cursor-agent.js +1 -1
  41. package/dist/bundle/chunks/devin-agent.js +1 -1
  42. package/dist/bundle/chunks/{dispatch-C2IBP5VV.js → dispatch-5R3TYAYB.js} +3 -3
  43. package/dist/bundle/chunks/{google-generative-ai-5G4RIQA6.js → google-generative-ai-FHYFXKLA.js} +1 -1
  44. package/dist/bundle/chunks/{google-vertex-ALD4P27O.js → google-vertex-5DWE5U5K.js} +1 -1
  45. package/dist/bundle/chunks/{help-fast-path-PEIKGCKZ.js → help-fast-path-FETLIF3W.js} +1 -1
  46. package/dist/bundle/chunks/{host-command-657VCQ34.js → host-command-XEB7SHT3.js} +1 -1
  47. package/dist/bundle/chunks/{host-session-command-OV5XDOJK.js → host-session-command-FFN52LXJ.js} +1 -1
  48. package/dist/bundle/chunks/{interactive-mode-MLXSYK2H.js → interactive-mode-5DEKY3YW.js} +1 -1
  49. package/dist/bundle/chunks/{mistral-conversations-RAE4K2XY.js → mistral-conversations-I2PI7DDF.js} +1 -1
  50. package/dist/bundle/chunks/{multi-session-host-774MACJU.js → multi-session-host-OPR5VBFO.js} +3 -3
  51. package/dist/bundle/chunks/{openai-codex-responses-3CW4R7TG.js → openai-codex-responses-WSEX6SH7.js} +1 -1
  52. package/dist/bundle/chunks/{openai-completions-5UIJXQUF.js → openai-completions-YCBHTBSS.js} +1 -1
  53. package/dist/bundle/chunks/{openai-responses-2BO44MRW.js → openai-responses-GXNSQF4A.js} +1 -1
  54. package/dist/bundle/chunks/{package-manager-cli-MZHVCX2S.js → package-manager-cli-W55E3LHP.js} +1 -1
  55. package/dist/bundle/chunks/{pi-messages-RF6GER4R.js → pi-messages-O6GISIRW.js} +1 -1
  56. package/dist/bundle/chunks/{role-ETSFSDTS.js → role-SHIZQKK4.js} +1 -1
  57. package/dist/bundle/chunks/{rotation-stream-ZMQB4GPO.js → rotation-stream-2PCZWNKF.js} +1 -1
  58. package/dist/bundle/chunks/{rpc-mode-UJXOIZCW.js → rpc-mode-IDUJTSYS.js} +1 -1
  59. package/dist/bundle/chunks/{schedule-command-KA7LFFNK.js → schedule-command-B7RRSANU.js} +1 -1
  60. package/dist/bundle/chunks/{session-control-endpoint-MFWXUR7Z.js → session-control-endpoint-LMZH5JLH.js} +1 -1
  61. package/dist/bundle/chunks/{session-picker-BP33NRSL.js → session-picker-DTGBOXDR.js} +1 -1
  62. package/dist/bundle/chunks/session-worker.js +41 -41
  63. package/dist/bundle/chunks/{typesafe-system-one-GNNZ5O5U.js → typesafe-system-one-XGYJJVSE.js} +1 -1
  64. package/dist/bundle/cli.js +2 -2
  65. package/dist/bundle/index.js +1 -1
  66. package/dist/bundle/rpc-entry.js +1 -1
  67. package/dist/bundle/runtime-manifest.json +1 -1
  68. package/dist/core/agent-session.d.ts +11 -1
  69. package/dist/core/agent-session.js +19 -2
  70. package/dist/core/extensions/builtin/compaction/index.js +7 -1
  71. package/dist/core/extensions/builtin/herdr/index.js +9 -5
  72. package/dist/core/extensions/builtin/mcp/auth/oauth-issuer.d.ts +6 -0
  73. package/dist/core/extensions/builtin/mcp/auth/oauth-issuer.js +25 -0
  74. package/dist/core/extensions/builtin/mcp/auth/oauth-provider.d.ts +3 -3
  75. package/dist/core/extensions/builtin/mcp/auth/oauth-provider.js +50 -19
  76. package/dist/core/extensions/builtin/mcp/auth/oauth-refresh.js +4 -3
  77. package/dist/core/extensions/builtin/mcp/auth/oauth.js +19 -12
  78. package/dist/core/extensions/builtin/mcp/modern/dispatch.js +1 -0
  79. package/dist/core/extensions/builtin/mcp/modern/parameter-headers.d.ts +4 -1
  80. package/dist/core/extensions/builtin/mcp/modern/parameter-headers.js +33 -35
  81. package/dist/core/extensions/builtin/mcp/modern/request.js +3 -0
  82. package/dist/core/extensions/builtin/mcp/modern/transport.d.ts +2 -0
  83. package/dist/core/extensions/builtin/mcp/modern/transport.js +4 -2
  84. package/dist/core/extensions/builtin/terminal/extension-state.js +2 -1
  85. package/dist/core/extensions/builtin/vertex-model-garden/auth.d.ts +9 -0
  86. package/dist/core/extensions/builtin/vertex-model-garden/auth.js +17 -1
  87. package/dist/core/extensions/builtin/websearch/websearch/providers/google.d.ts +6 -0
  88. package/dist/core/extensions/builtin/websearch/websearch/providers/google.js +6 -1
  89. package/dist/core/extensions/types.d.ts +2 -0
  90. package/dist/core/lifecycle-children.d.ts +3 -1
  91. package/dist/core/lifecycle-children.js +79 -15
  92. package/dist/core/lifecycle-record.js +1 -24
  93. package/dist/core/lifecycle-session-binding.js +2 -1
  94. package/dist/core/lifecycle-wait-items.d.ts +17 -0
  95. package/dist/core/lifecycle-wait-items.js +72 -0
  96. package/dist/core/model-runtime.js +6 -1
  97. package/dist/core/provider-api-key-auth.d.ts +6 -1
  98. package/dist/core/provider-api-key-auth.js +11 -4
  99. package/dist/core/provider-composer.d.ts +8 -1
  100. package/dist/core/provider-composer.js +7 -3
  101. package/dist/core/sdk.js +4 -1
  102. package/dist/core/session-activity.js +2 -0
  103. package/dist/core/session-lifecycle-types.d.ts +20 -4
  104. package/dist/core/session-lifecycle.js +35 -4
  105. package/dist/core/structured-session-lane.d.ts +33 -0
  106. package/dist/core/structured-session-lane.js +62 -0
  107. package/dist/core/work-custody-record.d.ts +2 -1
  108. package/dist/core/work-custody-record.js +2 -0
  109. package/dist/core/work-obligations.d.ts +1 -1
  110. package/dist/core/work-obligations.js +8 -2
  111. package/dist/modes/json-event.d.ts +79 -1
  112. package/dist/modes/json-event.js +79 -1
  113. package/dist/modes/rpc/compact-content-index.d.ts +85 -0
  114. package/dist/modes/rpc/compact-content-index.js +254 -0
  115. package/dist/modes/rpc/compact-content-store.d.ts +6 -43
  116. package/dist/modes/rpc/compact-content-store.js +22 -157
  117. package/dist/modes/rpc/compact-index-pages.d.ts +12 -0
  118. package/dist/modes/rpc/compact-index-pages.js +31 -0
  119. package/dist/modes/rpc/compact-session-state.d.ts +11 -39
  120. package/dist/modes/rpc/compact-session-state.js +17 -26
  121. package/dist/modes/rpc/compact-spool-dir.d.ts +4 -1
  122. package/dist/modes/rpc/compact-spool-dir.js +41 -8
  123. package/dist/modes/rpc/compact-spool-storage.d.ts +53 -0
  124. package/dist/modes/rpc/compact-spool-storage.js +162 -0
  125. package/dist/modes/rpc/compact-storage-io.d.ts +2 -1
  126. package/dist/modes/rpc/compact-storage-io.js +7 -1
  127. package/dist/modes/rpc/compact-transition.d.ts +41 -0
  128. package/dist/modes/rpc/compact-transition.js +12 -0
  129. package/dist/modes/rpc/connection-handler.d.ts +2 -0
  130. package/dist/modes/rpc/connection-handler.js +12 -6
  131. package/dist/modes/rpc/host-ensure-start.js +5 -3
  132. package/dist/modes/rpc/security-presentations.js +2 -1
  133. package/dist/modes/rpc/session-binding.js +10 -0
  134. package/dist/modes/rpc/session-event-writer.d.ts +2 -0
  135. package/dist/modes/rpc/session-event-writer.js +2 -0
  136. package/dist/modes/rpc/session-record-stream.d.ts +3 -3
  137. package/dist/modes/rpc/session-record-stream.js +3 -3
  138. package/dist/modes/rpc/session-structured-lanes.d.ts +12 -0
  139. package/dist/modes/rpc/session-structured-lanes.js +24 -0
  140. package/dist/modes/rpc/session-worker-client.js +6 -5
  141. package/dist/modes/rpc/structured-session-records.d.ts +12 -0
  142. package/dist/modes/rpc/structured-session-records.js +88 -0
  143. package/docs/extensions.md +7 -1
  144. package/docs/rpc.md +17 -2
  145. package/package.json +6 -6
  146. package/dist/bundle/chunks/chunk-GQJYNFMO.js +0 -2
  147. package/dist/bundle/chunks/chunk-VICDRG3J.js +0 -410
  148. package/dist/bundle/chunks/chunk-VL7O5PO4.js +0 -5
@@ -1,6 +1,5 @@
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";
1
+ import { CompactSpoolStorage, RECLAIM_WORK_ALLOWANCE_BYTES } from "./compact-spool-storage.js";
2
+ import { nodeCompactStorageIo } from "./compact-storage-io.js";
4
3
  /**
5
4
  * Owned bounded leaves for compact current state (senpi#4272, R3.C6.4 / R3.C2.4).
6
5
  *
@@ -11,30 +10,16 @@ import { CompactStorageReadError, describeCause, nodeCompactStorageIo, } from ".
11
10
  */
12
11
  /** 32,768 UTF-16 code units (64 KiB as UTF-16LE), so a split surrogate survives a leaf boundary exactly. */
13
12
  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 {
13
+ export class CompactContentStore extends CompactSpoolStorage {
18
14
  constructor(options) {
15
+ super(options.dir, options.io ?? nodeCompactStorageIo);
19
16
  this.leaves = new Map();
20
17
  /** Resident leaves in creation order, so each spill takes the oldest without rescanning committed ones. */
21
18
  this.ramLeaves = new Set();
22
- this.failures = [];
23
19
  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
20
  this.residentPayloadBytes = 0;
31
- this.pendingIoCopyBytes = 0;
32
21
  this.pinnedOnlyBytes = 0;
33
22
  this.spilledLiveBytes = 0;
34
- this.spillBytesWritten = 0;
35
- this.reclaimDebtBytes = 0;
36
- this.dir = options.dir;
37
- this.io = options.io ?? nodeCompactStorageIo;
38
23
  this.spillAboveBytes = options.spillAboveBytes ?? 4 * 1024 * 1024;
39
24
  this.spillTargetBytes = options.spillTargetBytes ?? 2 * 1024 * 1024;
40
25
  }
@@ -68,6 +53,9 @@ export class CompactContentStore {
68
53
  }
69
54
  this.residentPayloadBytes -= leaf.units * 2;
70
55
  }
56
+ isCommitted(id) {
57
+ return this.leaves.get(id)?.state === "committed";
58
+ }
71
59
  /** Reads a leaf the caller still references, from RAM or its committed file. */
72
60
  async readLeaf(id) {
73
61
  const leaf = this.leaves.get(id);
@@ -77,23 +65,6 @@ export class CompactContentStore {
77
65
  return leaf.text;
78
66
  return (await this.readFile(leaf.path ?? "", leaf.units * 2, "leaf_read")).toString("utf16le");
79
67
  }
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
68
  status() {
98
69
  return {
99
70
  residentPayloadBytes: this.residentPayloadBytes,
@@ -109,94 +80,6 @@ export class CompactContentStore {
109
80
  failures: [...this.failures],
110
81
  };
111
82
  }
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
83
  scheduleSpill() {
201
84
  if (this.spillQueued || !this.spillAllowed || this.residentPayloadBytes <= this.spillAboveBytes)
202
85
  return;
@@ -216,22 +99,29 @@ export class CompactContentStore {
216
99
  const bytes = leaf.units * 2;
217
100
  leaf.state = "pending";
218
101
  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);
102
+ let path;
103
+ try {
104
+ path = await this.writeFile(Buffer.from(leaf.text, "utf16le"), "spill_write", bytes);
225
105
  }
226
- else if (!owned || this.retired) {
106
+ finally {
107
+ // A failed write, or a failure report that threw, keeps the leaf resident and spillable.
108
+ if (path === undefined) {
109
+ leaf.state = "ram";
110
+ if (this.leaves.get(id) === leaf)
111
+ this.ramLeaves.add(id);
112
+ }
113
+ }
114
+ const owned = this.leaves.get(id) === leaf;
115
+ if (path !== undefined && (!owned || this.retired)) {
227
116
  this.discard(path, bytes);
228
117
  }
229
- else {
118
+ else if (path !== undefined) {
230
119
  leaf.state = "committed";
231
120
  leaf.path = path;
232
121
  leaf.text = undefined;
233
122
  this.residentPayloadBytes -= bytes;
234
123
  this.spilledLiveBytes += bytes;
124
+ this.onCommitted?.(id);
235
125
  }
236
126
  this.spillQueued = false;
237
127
  if (this.residentPayloadBytes > this.spillTargetBytes) {
@@ -240,30 +130,5 @@ export class CompactContentStore {
240
130
  this.enqueue("spill_write", () => this.spillNext());
241
131
  }
242
132
  }
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
133
  }
269
134
  //# sourceMappingURL=compact-content-store.js.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Page layout of the compact settled-row index (senpi#4272): how folded rows are packed into
3
+ * copy-on-write page files, and which base pages are small enough to merge on the next compaction.
4
+ */
5
+ /** A page under half full in both rows and bytes is rewritten by the next compaction, so pages merge. */
6
+ export declare function isSmallPage(page: {
7
+ readonly keys: readonly string[];
8
+ readonly bytes: number;
9
+ }): boolean;
10
+ /** Packs entries into pages of at most `PAGE_ROWS` rows and `PAGE_BYTES` UTF-8 bytes (one oversized row stands alone). */
11
+ export declare function packPages<Row>(rows: readonly [string, Row][]): [string, Row][][];
12
+ //# sourceMappingURL=compact-index-pages.d.ts.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Page layout of the compact settled-row index (senpi#4272): how folded rows are packed into
3
+ * copy-on-write page files, and which base pages are small enough to merge on the next compaction.
4
+ */
5
+ const PAGE_ROWS = 128;
6
+ const PAGE_BYTES = 64 * 1024;
7
+ /** A page under half full in both rows and bytes is rewritten by the next compaction, so pages merge. */
8
+ export function isSmallPage(page) {
9
+ return page.keys.length < PAGE_ROWS / 2 && page.bytes < PAGE_BYTES / 2;
10
+ }
11
+ /** Packs entries into pages of at most `PAGE_ROWS` rows and `PAGE_BYTES` UTF-8 bytes (one oversized row stands alone). */
12
+ export function packPages(rows) {
13
+ const pages = [];
14
+ let page = [];
15
+ // UTF-8 bytes of the serialized page: the brackets, plus each entry and its separator.
16
+ let bytes = 1;
17
+ for (const entry of rows) {
18
+ const size = Buffer.byteLength(JSON.stringify(entry)) + 1;
19
+ if (page.length > 0 && (page.length === PAGE_ROWS || bytes + size > PAGE_BYTES)) {
20
+ pages.push(page);
21
+ page = [];
22
+ bytes = 1;
23
+ }
24
+ page.push(entry);
25
+ bytes += size;
26
+ }
27
+ if (page.length > 0)
28
+ pages.push(page);
29
+ return pages;
30
+ }
31
+ //# sourceMappingURL=compact-index-pages.js.map
@@ -1,45 +1,15 @@
1
+ import { CompactContentIndex, type FrozenIndex } from "./compact-content-index.ts";
1
2
  import { type CompactContentStoreOptions, type CompactStorageStatus, type LeafId } from "./compact-content-store.ts";
3
+ import { type CompactBlockKind, type CompactPersistence, type CompactTransition } from "./compact-transition.ts";
2
4
  /**
3
5
  * The in-memory current-state owner over compact storage (senpi#4272, R3.C6.4 first half).
4
6
  *
5
7
  * `accept` is synchronous: it validates a typed transition completely, then publishes the next
6
8
  * 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
9
+ * that spill asynchronously; settled messages live in a spillable index whose persistence status
10
+ * changes only through `message_status`, never through storage success or failure. `freeze` pins
11
11
  * an immutable cut synchronously, and `read` materializes it asynchronously from RAM or disk.
12
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
13
  interface FrozenBlock {
44
14
  readonly contentIndex: number;
45
15
  readonly kind: CompactBlockKind;
@@ -54,7 +24,7 @@ interface SettledRow {
54
24
  interface FrozenCompactState {
55
25
  readonly sequence: number;
56
26
  readonly active: ReadonlyMap<string, readonly FrozenBlock[]>;
57
- readonly settled: ReadonlyMap<string, SettledRow>;
27
+ readonly settled: FrozenIndex<SettledRow>;
58
28
  }
59
29
  interface CompactMessageView {
60
30
  readonly messageId: string;
@@ -65,13 +35,17 @@ interface CompactMessageView {
65
35
  readonly text: string;
66
36
  }[];
67
37
  }
38
+ export interface CompactSessionStateOptions extends CompactContentStoreOptions {
39
+ /** Index compaction is considered once this many override slots exist (one page by default). */
40
+ readonly compactAboveRows?: number;
41
+ }
68
42
  export declare class CompactSessionState {
69
43
  private readonly store;
70
44
  private readonly settled;
71
45
  private readonly active;
72
46
  private sequence;
73
47
  private frozenTailBytes;
74
- constructor(options: CompactContentStoreOptions);
48
+ constructor(options: CompactSessionStateOptions);
75
49
  get acceptedSequence(): number;
76
50
  accept(transition: CompactTransition): number;
77
51
  freeze(): FrozenCompactState;
@@ -81,13 +55,11 @@ export declare class CompactSessionState {
81
55
  sequence: number;
82
56
  messages: CompactMessageView[];
83
57
  }>;
84
- storageStatus(): CompactStorageStatus & {
58
+ storageStatus(): CompactStorageStatus & ReturnType<CompactContentIndex<SettledRow>["status"]> & {
85
59
  readonly residentTailBytes: number;
86
60
  /** Active tails held by unreleased cuts; may share storage with `residentTailBytes`. */
87
61
  readonly frozenTailBytes: number;
88
62
  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
63
  };
92
64
  idle(): Promise<void>;
93
65
  retire(): Promise<void>;
@@ -1,19 +1,15 @@
1
+ import { CompactContentIndex } from "./compact-content-index.js";
1
2
  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
- }
3
+ import { CompactTransitionError, } from "./compact-transition.js";
9
4
  const byContentIndex = ([a], [b]) => a - b;
10
5
  export class CompactSessionState {
11
6
  constructor(options) {
12
- this.settled = new Map();
13
7
  this.active = new Map();
14
8
  this.sequence = 0;
15
9
  this.frozenTailBytes = 0;
16
10
  this.store = new CompactContentStore(options);
11
+ const leavesOf = (row) => row.blocks.flatMap((block) => block.leaves);
12
+ this.settled = new CompactContentIndex(this.store, leavesOf, options.compactAboveRows ?? 128);
17
13
  }
18
14
  get acceptedSequence() {
19
15
  return this.sequence;
@@ -42,23 +38,19 @@ export class CompactSessionState {
42
38
  const active = new Map();
43
39
  for (const [messageId, blocks] of this.active)
44
40
  active.set(messageId, this.seal(blocks, false));
45
- const settled = new Map(this.settled);
46
41
  for (const blocks of active.values())
47
42
  this.pin(blocks, 1);
48
- for (const row of settled.values())
49
- this.pin(row.blocks, 1);
50
- return { sequence: this.sequence, active, settled };
43
+ return { sequence: this.sequence, active, settled: this.settled.freeze() };
51
44
  }
52
45
  release(frozen) {
53
46
  for (const blocks of frozen.active.values())
54
47
  this.pin(blocks, -1);
55
- for (const row of frozen.settled.values())
56
- this.pin(row.blocks, -1);
48
+ this.settled.release(frozen.settled);
57
49
  }
58
50
  /** Rejects with the storage error when committed content is gone; never returns empty content for it. */
59
51
  async read(frozen) {
60
52
  const messages = [];
61
- for (const [messageId, row] of frozen.settled) {
53
+ for (const [messageId, row] of await this.settled.entries(frozen.settled)) {
62
54
  messages.push({ messageId, persistence: row.persistence, blocks: await this.texts(row.blocks) });
63
55
  }
64
56
  for (const [messageId, blocks] of frozen.active) {
@@ -77,10 +69,10 @@ export class CompactSessionState {
77
69
  }
78
70
  return {
79
71
  ...this.store.status(),
72
+ ...this.settled.status(),
80
73
  residentTailBytes,
81
74
  frozenTailBytes: this.frozenTailBytes,
82
75
  hotWitnessBytes,
83
- residentSettledRows: this.settled.size,
84
76
  };
85
77
  }
86
78
  idle() {
@@ -137,12 +129,13 @@ export class CompactSessionState {
137
129
  const blocks = this.active.get(messageId);
138
130
  if (blocks) {
139
131
  this.settled.set(messageId, { persistence, blocks: this.seal(blocks, true) });
132
+ for (const block of blocks.values())
133
+ for (const id of block.leaves)
134
+ this.store.adjust(id, -1, 0);
140
135
  this.active.delete(messageId);
141
- return;
142
136
  }
143
- const row = this.settled.get(messageId);
144
- if (row) {
145
- this.settled.set(messageId, { ...row, persistence });
137
+ else if (this.settled.has(messageId)) {
138
+ this.settled.patch(messageId, { persistence });
146
139
  }
147
140
  else {
148
141
  throw new CompactTransitionError("unknown_message", messageId);
@@ -150,17 +143,15 @@ export class CompactSessionState {
150
143
  }
151
144
  remove(messageId) {
152
145
  const blocks = this.active.get(messageId);
153
- const row = this.settled.get(messageId);
154
- if (!blocks && !row)
146
+ const settled = this.settled.has(messageId);
147
+ if (!blocks && !settled)
155
148
  throw new CompactTransitionError("unknown_message", messageId);
156
149
  for (const block of blocks?.values() ?? [])
157
150
  for (const id of block.leaves)
158
151
  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
152
  this.active.delete(messageId);
163
- this.settled.delete(messageId);
153
+ if (settled)
154
+ this.settled.delete(messageId);
164
155
  }
165
156
  block(address) {
166
157
  const block = this.active.get(address.messageId)?.get(address.contentIndex);
@@ -1,5 +1,8 @@
1
1
  /** Creates a fresh private epoch directory and returns the token its ownership marker holds. */
2
2
  export declare function createSpoolDir(dir: string): Promise<string>;
3
- /** Removes `dir` only while it is still the private, marked directory `createSpoolDir` made. */
3
+ /**
4
+ * Removes `dir` only while it is still the private, marked directory `createSpoolDir` made. A
5
+ * directory that is already gone leaves nothing to remove, so retirement settles as a no-op.
6
+ */
4
7
  export declare function removeSpoolDir(dir: string, token: string): Promise<void>;
5
8
  //# sourceMappingURL=compact-spool-dir.d.ts.map
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { lstat, mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { lstat, mkdir, readFile, rm, rmdir, writeFile } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
+ import { describeCause } from "./compact-storage-io.js";
4
5
  /**
5
6
  * Ownership of the compact spool's epoch directory (senpi#4272). The store creates the directory
6
7
  * itself (0700, never adopting an existing one) and writes a private marker into it; removal
@@ -9,8 +10,10 @@ import { dirname, join } from "node:path";
9
10
  */
10
11
  const MARKER = ".compact-spool-owner";
11
12
  class CompactSpoolRefusedError extends Error {
12
- constructor(reason, path) {
13
- super(`compact spool directory refused (${reason}): ${path}`);
13
+ constructor(reason, path, cause, cleanupFailure) {
14
+ const detail = cause === undefined ? "" : `: ${describeCause(cause)}`;
15
+ const kept = cleanupFailure === undefined ? "" : `; removing the directory failed: ${describeCause(cleanupFailure)}`;
16
+ super(`compact spool directory refused (${reason}): ${path}${detail}${kept}`, { cause });
14
17
  this.name = "CompactSpoolRefusedError";
15
18
  this.reason = reason;
16
19
  this.path = path;
@@ -28,18 +31,48 @@ export async function createSpoolDir(dir) {
28
31
  throw cause;
29
32
  }
30
33
  const token = randomUUID();
31
- await writeFile(join(dir, MARKER), token, { flag: "wx", mode: 0o600 });
34
+ try {
35
+ await writeFile(join(dir, MARKER), token, { flag: "wx", mode: 0o600 });
36
+ }
37
+ catch (cause) {
38
+ throw await abandonSpoolDir(dir, cause);
39
+ }
32
40
  return token;
33
41
  }
34
- /** Removes `dir` only while it is still the private, marked directory `createSpoolDir` made. */
42
+ /**
43
+ * Removes a directory `createSpoolDir` just made but could not mark, so a later store creates it
44
+ * fresh instead of refusing it as pre-existing. Only a marker this call's exclusive create made (any
45
+ * failure but EEXIST) is unlinked, then the directory itself, which `rmdir` removes only while empty.
46
+ */
47
+ async function abandonSpoolDir(dir, cause) {
48
+ try {
49
+ if (cause.code !== "EEXIST")
50
+ await rm(join(dir, MARKER), { force: true });
51
+ await rmdir(dir);
52
+ return new CompactSpoolRefusedError("marker_write_failed", dir, cause);
53
+ }
54
+ catch (cleanupFailure) {
55
+ return new CompactSpoolRefusedError("marker_write_failed", dir, cause, cleanupFailure);
56
+ }
57
+ }
58
+ /**
59
+ * Removes `dir` only while it is still the private, marked directory `createSpoolDir` made. A
60
+ * directory that is already gone leaves nothing to remove, so retirement settles as a no-op.
61
+ */
35
62
  export async function removeSpoolDir(dir, token) {
36
- const refusal = await refusalFor(dir, token);
63
+ const owned = await lstat(dir).catch((cause) => {
64
+ if (cause.code === "ENOENT")
65
+ return undefined;
66
+ throw cause;
67
+ });
68
+ if (owned === undefined)
69
+ return;
70
+ const refusal = await refusalFor(dir, owned, token);
37
71
  if (refusal)
38
72
  throw new CompactSpoolRefusedError(refusal, dir);
39
73
  await rm(dir, { recursive: true, force: true });
40
74
  }
41
- async function refusalFor(dir, token) {
42
- const owned = await lstat(dir);
75
+ async function refusalFor(dir, owned, token) {
43
76
  if (owned.isSymbolicLink())
44
77
  return "symlink";
45
78
  if (!owned.isDirectory())
@@ -0,0 +1,53 @@
1
+ import { type CompactStorageFailure, type CompactStorageIo, type CompactStorageOperation } from "./compact-storage-io.ts";
2
+ /**
3
+ * The spool file path of compact current-state storage (senpi#4272, R3.C6.4): one serial
4
+ * asynchronous job chain, the private epoch directory, spool file writes, reads and reclamation,
5
+ * and the failure ledger. `CompactContentStore` adds leaf ownership and spilling on top.
6
+ */
7
+ export declare const RECLAIM_WORK_ALLOWANCE_BYTES: number;
8
+ export declare abstract class CompactSpoolStorage {
9
+ readonly dir: string;
10
+ private readonly io;
11
+ protected readonly failures: CompactStorageFailure[];
12
+ private nextFile;
13
+ private dirReady;
14
+ private spoolToken;
15
+ private chain;
16
+ private queued;
17
+ protected spillQueued: boolean;
18
+ private halted;
19
+ protected retired: boolean;
20
+ protected pendingIoCopyBytes: number;
21
+ protected spillBytesWritten: number;
22
+ protected reclaimDebtBytes: number;
23
+ constructor(dir: string, io: CompactStorageIo);
24
+ /** Resolves once every queued storage job, including the ones they queued, has completed. */
25
+ idle(): Promise<void>;
26
+ /**
27
+ * Invalidates spilling, joins owned I/O, then removes the epoch directory only when this store
28
+ * created it and it still passes the ownership checks; otherwise rejects with `CompactSpoolRefusedError`.
29
+ */
30
+ retire(): Promise<void>;
31
+ /** Reads a committed file and rejects a missing, unreadable or short one. */
32
+ readFile(path: string, bytes: number, operation: CompactStorageOperation): Promise<Buffer>;
33
+ /** Writes one new spool file; undefined means the write failed and was reported (a spill write also halts spilling). */
34
+ writeFile(data: Buffer, operation: CompactStorageOperation, retainedInMemoryBytes: number): Promise<string | undefined>;
35
+ /**
36
+ * An unreferenced (or partially written) spool file: reclamation debt until its unlink completes or
37
+ * finds it gone. Files dropped after retirement started belong to the directory retirement removes.
38
+ */
39
+ discard(path: string, bytes: number): void;
40
+ /** A failed unlink keeps its debt and is retried with bounded backoff outside the job chain. */
41
+ private reclaim;
42
+ get spillAllowed(): boolean;
43
+ /** True while a storage failure or unreclaimed debt above the work allowance holds spilling back. */
44
+ protected get spillStopped(): boolean;
45
+ enqueue(operation: CompactStorageOperation, job: () => Promise<void>): void;
46
+ protected abstract scheduleSpill(): void;
47
+ /** Creates the private epoch directory once; a refusal or failure halts spilling and is reported. */
48
+ private ensureDir;
49
+ private readFailure;
50
+ private fail;
51
+ private record;
52
+ }
53
+ //# sourceMappingURL=compact-spool-storage.d.ts.map