@code-yeongyu/senpi 2026.10.26-1 → 2026.10.27-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 (179) hide show
  1. package/CHANGELOG.md +39 -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-DMFAVE5N.js → chunk-4QVORFQ2.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-ZXHLH4MW.js → chunk-5W3DTM2J.js} +1 -1
  8. package/dist/bundle/chunks/{chunk-QYFOHFPS.js → chunk-6CJC6JY4.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-LO5MYLGF.js → chunk-FQDARFHT.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-7KOUQSBX.js → chunk-GPPOPK76.js} +3 -3
  18. package/dist/bundle/chunks/{chunk-I7QWTWVZ.js → chunk-IHKHCWWG.js} +1 -1
  19. package/dist/bundle/chunks/{chunk-MY4ZD3EW.js → chunk-IY4G44MN.js} +1 -1
  20. package/dist/bundle/chunks/{chunk-OQ3WDMB3.js → chunk-JH7MVRNJ.js} +1 -1
  21. package/dist/bundle/chunks/chunk-KBSFTXAT.js +2 -0
  22. package/dist/bundle/chunks/{chunk-UW6M3EXY.js → chunk-KRO63T4Q.js} +1 -1
  23. package/dist/bundle/chunks/{chunk-J5FCH7TI.js → chunk-KTDIIF2F.js} +1 -1
  24. package/dist/bundle/chunks/{chunk-3WP5RFQR.js → chunk-NDNIMC5W.js} +1 -1
  25. package/dist/bundle/chunks/chunk-NXQFBAUI.js +2 -0
  26. package/dist/bundle/chunks/{chunk-CDQPA4FP.js → chunk-ODRGPC5S.js} +1 -1
  27. package/dist/bundle/chunks/{chunk-5FQMNHXV.js → chunk-P5E4EVQF.js} +1 -1
  28. package/dist/bundle/chunks/{chunk-3A3XCLNH.js → chunk-PDG4XWO2.js} +1 -1
  29. package/dist/bundle/chunks/{chunk-5LWUUXMH.js → chunk-PY4ZQHTN.js} +1 -1
  30. package/dist/bundle/chunks/chunk-Q2A2V6NT.js +5 -0
  31. package/dist/bundle/chunks/{chunk-SOXQ42VS.js → chunk-RQ5JTXUC.js} +1 -1
  32. package/dist/bundle/chunks/{chunk-JPGT64RV.js → chunk-TV3UI63P.js} +1 -1
  33. package/dist/bundle/chunks/{chunk-JFQUWEN7.js → chunk-TWHF52G6.js} +4 -4
  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-ZAS4NBVK.js +410 -0
  38. package/dist/bundle/chunks/{chunk-ADQAPDPV.js → chunk-ZP6JJDYV.js} +1 -1
  39. package/dist/bundle/chunks/{chunk-EZP6AYW2.js → chunk-ZXIAVLJV.js} +88 -88
  40. package/dist/bundle/chunks/{cli-main-55EQA5PS.js → cli-main-36FRZ7ZB.js} +1 -1
  41. package/dist/bundle/chunks/client-CBDPMWZJ.js +2 -0
  42. package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-ZJQGIIRC.js → cloudflare-workers-ai-system-one-WDHT7BBV.js} +1 -1
  43. package/dist/bundle/chunks/cursor-agent.js +1 -1
  44. package/dist/bundle/chunks/devin-agent.js +1 -1
  45. package/dist/bundle/chunks/{dispatch-C2IBP5VV.js → dispatch-W5PENPZM.js} +3 -3
  46. package/dist/bundle/chunks/{google-generative-ai-5G4RIQA6.js → google-generative-ai-FHYFXKLA.js} +1 -1
  47. package/dist/bundle/chunks/{google-vertex-ALD4P27O.js → google-vertex-5DWE5U5K.js} +1 -1
  48. package/dist/bundle/chunks/{help-fast-path-PEIKGCKZ.js → help-fast-path-FETLIF3W.js} +1 -1
  49. package/dist/bundle/chunks/{host-command-657VCQ34.js → host-command-XEB7SHT3.js} +1 -1
  50. package/dist/bundle/chunks/{host-session-command-OV5XDOJK.js → host-session-command-FFN52LXJ.js} +1 -1
  51. package/dist/bundle/chunks/{interactive-mode-MLXSYK2H.js → interactive-mode-5S7NNV2Z.js} +1 -1
  52. package/dist/bundle/chunks/{mistral-conversations-RAE4K2XY.js → mistral-conversations-I2PI7DDF.js} +1 -1
  53. package/dist/bundle/chunks/{multi-session-host-774MACJU.js → multi-session-host-E3OPE6HJ.js} +3 -3
  54. package/dist/bundle/chunks/{openai-codex-responses-3CW4R7TG.js → openai-codex-responses-WSEX6SH7.js} +1 -1
  55. package/dist/bundle/chunks/{openai-completions-5UIJXQUF.js → openai-completions-YCBHTBSS.js} +1 -1
  56. package/dist/bundle/chunks/{openai-responses-2BO44MRW.js → openai-responses-GXNSQF4A.js} +1 -1
  57. package/dist/bundle/chunks/{package-manager-cli-MZHVCX2S.js → package-manager-cli-3XO456VE.js} +1 -1
  58. package/dist/bundle/chunks/{pi-messages-RF6GER4R.js → pi-messages-O6GISIRW.js} +1 -1
  59. package/dist/bundle/chunks/protocol-GLNWYI5G.js +2 -0
  60. package/dist/bundle/chunks/{role-ETSFSDTS.js → role-SHIZQKK4.js} +1 -1
  61. package/dist/bundle/chunks/{rotation-stream-ZMQB4GPO.js → rotation-stream-2PCZWNKF.js} +1 -1
  62. package/dist/bundle/chunks/{rpc-mode-UJXOIZCW.js → rpc-mode-GLWKLTMA.js} +1 -1
  63. package/dist/bundle/chunks/{schedule-command-KA7LFFNK.js → schedule-command-B7RRSANU.js} +1 -1
  64. package/dist/bundle/chunks/{session-control-endpoint-MFWXUR7Z.js → session-control-endpoint-KLE2X4RR.js} +1 -1
  65. package/dist/bundle/chunks/{session-picker-BP33NRSL.js → session-picker-DTGBOXDR.js} +1 -1
  66. package/dist/bundle/chunks/session-worker.js +131 -131
  67. package/dist/bundle/chunks/{typesafe-system-one-GNNZ5O5U.js → typesafe-system-one-XGYJJVSE.js} +1 -1
  68. package/dist/bundle/cli.js +2 -2
  69. package/dist/bundle/index.js +1 -1
  70. package/dist/bundle/rpc-entry.js +1 -1
  71. package/dist/bundle/runtime-manifest.json +1 -1
  72. package/dist/core/agent-session.d.ts +11 -1
  73. package/dist/core/agent-session.js +19 -2
  74. package/dist/core/extensions/builtin/anthropic-subscription/account-context.d.ts +15 -0
  75. package/dist/core/extensions/builtin/anthropic-subscription/account-context.js +53 -0
  76. package/dist/core/extensions/builtin/anthropic-subscription/auth-attempt.d.ts +3 -0
  77. package/dist/core/extensions/builtin/anthropic-subscription/auth-attempt.js +26 -9
  78. package/dist/core/extensions/builtin/anthropic-subscription/auth-lane-boundary.d.ts +16 -0
  79. package/dist/core/extensions/builtin/anthropic-subscription/auth-lane-boundary.js +27 -0
  80. package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.d.ts +2 -13
  81. package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js +3 -26
  82. package/dist/core/extensions/builtin/anthropic-subscription/errors.js +3 -2
  83. package/dist/core/extensions/builtin/anthropic-subscription/guidance.d.ts +6 -1
  84. package/dist/core/extensions/builtin/anthropic-subscription/guidance.js +1 -1
  85. package/dist/core/extensions/builtin/anthropic-subscription/stream-guidance.js +13 -2
  86. package/dist/core/extensions/builtin/compaction/index.js +7 -1
  87. package/dist/core/extensions/builtin/herdr/index.js +9 -5
  88. package/dist/core/extensions/builtin/mcp/auth/oauth-issuer.d.ts +6 -0
  89. package/dist/core/extensions/builtin/mcp/auth/oauth-issuer.js +25 -0
  90. package/dist/core/extensions/builtin/mcp/auth/oauth-provider.d.ts +3 -3
  91. package/dist/core/extensions/builtin/mcp/auth/oauth-provider.js +50 -19
  92. package/dist/core/extensions/builtin/mcp/auth/oauth-refresh.js +4 -3
  93. package/dist/core/extensions/builtin/mcp/auth/oauth.js +19 -12
  94. package/dist/core/extensions/builtin/mcp/modern/consent-queue.js +1 -0
  95. package/dist/core/extensions/builtin/mcp/modern/dispatch.js +1 -0
  96. package/dist/core/extensions/builtin/mcp/modern/parameter-headers.d.ts +4 -1
  97. package/dist/core/extensions/builtin/mcp/modern/parameter-headers.js +33 -35
  98. package/dist/core/extensions/builtin/mcp/modern/request.js +6 -2
  99. package/dist/core/extensions/builtin/mcp/modern/transport.d.ts +2 -0
  100. package/dist/core/extensions/builtin/mcp/modern/transport.js +4 -2
  101. package/dist/core/extensions/builtin/mcp/sdk.lazy.d.ts +2 -0
  102. package/dist/core/extensions/builtin/mcp/sdk.lazy.js +2 -0
  103. package/dist/core/extensions/builtin/mcp/transport-sdk.js +12 -1
  104. package/dist/core/extensions/builtin/terminal/extension-state.js +2 -1
  105. package/dist/core/extensions/builtin/vertex-model-garden/auth.d.ts +9 -0
  106. package/dist/core/extensions/builtin/vertex-model-garden/auth.js +17 -1
  107. package/dist/core/extensions/builtin/websearch/websearch/providers/google.d.ts +6 -0
  108. package/dist/core/extensions/builtin/websearch/websearch/providers/google.js +6 -1
  109. package/dist/core/extensions/types.d.ts +2 -0
  110. package/dist/core/lifecycle-children.d.ts +3 -1
  111. package/dist/core/lifecycle-children.js +79 -15
  112. package/dist/core/lifecycle-record.js +1 -24
  113. package/dist/core/lifecycle-session-binding.js +2 -1
  114. package/dist/core/lifecycle-wait-items.d.ts +17 -0
  115. package/dist/core/lifecycle-wait-items.js +72 -0
  116. package/dist/core/model-runtime.js +6 -1
  117. package/dist/core/provider-api-key-auth.d.ts +6 -1
  118. package/dist/core/provider-api-key-auth.js +11 -4
  119. package/dist/core/provider-composer.d.ts +8 -1
  120. package/dist/core/provider-composer.js +7 -3
  121. package/dist/core/sdk.js +4 -1
  122. package/dist/core/session-activity.js +2 -0
  123. package/dist/core/session-lifecycle-types.d.ts +20 -4
  124. package/dist/core/session-lifecycle.js +35 -4
  125. package/dist/core/structured-session-lane.d.ts +33 -0
  126. package/dist/core/structured-session-lane.js +62 -0
  127. package/dist/core/work-custody-record.d.ts +2 -1
  128. package/dist/core/work-custody-record.js +2 -0
  129. package/dist/core/work-obligations.d.ts +1 -1
  130. package/dist/core/work-obligations.js +8 -2
  131. package/dist/modes/interactive/interactive-mode.d.ts +2 -1
  132. package/dist/modes/interactive/interactive-mode.js +2 -1
  133. package/dist/modes/json-event.d.ts +79 -1
  134. package/dist/modes/json-event.js +79 -1
  135. package/dist/modes/rpc/compact-commit-dependencies.d.ts +13 -0
  136. package/dist/modes/rpc/compact-commit-dependencies.js +48 -0
  137. package/dist/modes/rpc/compact-content-index.d.ts +84 -0
  138. package/dist/modes/rpc/compact-content-index.js +243 -0
  139. package/dist/modes/rpc/compact-content-store.d.ts +10 -43
  140. package/dist/modes/rpc/compact-content-store.js +61 -163
  141. package/dist/modes/rpc/compact-index-pages.d.ts +12 -0
  142. package/dist/modes/rpc/compact-index-pages.js +31 -0
  143. package/dist/modes/rpc/compact-read-lease.d.ts +15 -0
  144. package/dist/modes/rpc/compact-read-lease.js +40 -0
  145. package/dist/modes/rpc/compact-session-state.d.ts +14 -63
  146. package/dist/modes/rpc/compact-session-state.js +69 -51
  147. package/dist/modes/rpc/compact-spool-dir.d.ts +4 -1
  148. package/dist/modes/rpc/compact-spool-dir.js +41 -8
  149. package/dist/modes/rpc/compact-spool-storage.d.ts +58 -0
  150. package/dist/modes/rpc/compact-spool-storage.js +195 -0
  151. package/dist/modes/rpc/compact-storage-io.d.ts +2 -1
  152. package/dist/modes/rpc/compact-storage-io.js +7 -1
  153. package/dist/modes/rpc/compact-text-roots.d.ts +22 -0
  154. package/dist/modes/rpc/compact-text-roots.js +138 -0
  155. package/dist/modes/rpc/compact-text-view.d.ts +22 -0
  156. package/dist/modes/rpc/compact-text-view.js +16 -0
  157. package/dist/modes/rpc/compact-transition.d.ts +41 -0
  158. package/dist/modes/rpc/compact-transition.js +12 -0
  159. package/dist/modes/rpc/connection-handler.d.ts +2 -0
  160. package/dist/modes/rpc/connection-handler.js +12 -6
  161. package/dist/modes/rpc/host-ensure-start.js +5 -3
  162. package/dist/modes/rpc/security-presentations.js +2 -1
  163. package/dist/modes/rpc/session-binding.js +10 -0
  164. package/dist/modes/rpc/session-event-writer.d.ts +2 -0
  165. package/dist/modes/rpc/session-event-writer.js +2 -0
  166. package/dist/modes/rpc/session-record-stream.d.ts +3 -3
  167. package/dist/modes/rpc/session-record-stream.js +3 -3
  168. package/dist/modes/rpc/session-structured-lanes.d.ts +12 -0
  169. package/dist/modes/rpc/session-structured-lanes.js +24 -0
  170. package/dist/modes/rpc/session-worker-client.js +6 -5
  171. package/dist/modes/rpc/structured-session-records.d.ts +12 -0
  172. package/dist/modes/rpc/structured-session-records.js +88 -0
  173. package/docs/extensions.md +7 -1
  174. package/docs/rpc.md +17 -2
  175. package/package.json +6 -6
  176. package/dist/bundle/chunks/chunk-GQJYNFMO.js +0 -2
  177. package/dist/bundle/chunks/chunk-VICDRG3J.js +0 -410
  178. package/dist/bundle/chunks/chunk-VL7O5PO4.js +0 -5
  179. package/dist/bundle/chunks/client-LOS6GSEW.js +0 -2
@@ -0,0 +1,243 @@
1
+ import { CompactCommitDependencies } from "./compact-commit-dependencies.js";
2
+ import { isSmallPage, packPages } from "./compact-index-pages.js";
3
+ import { CompactReadError } from "./compact-read-lease.js";
4
+ export class CompactContentIndex {
5
+ constructor(store, leavesOf, compactAboveRows) {
6
+ this.base = { pages: [], pageOf: new Map() };
7
+ this.overrides = new Map();
8
+ this.compacting = false;
9
+ this.tombstones = 0;
10
+ this.tombstoneKeyUnits = 0;
11
+ this.frozen = new WeakSet();
12
+ this.store = store;
13
+ this.leavesOf = leavesOf;
14
+ this.compactAboveRows = compactAboveRows;
15
+ this.unsettledLeaves = new CompactCommitDependencies(store, () => this.maybeCompact());
16
+ }
17
+ has(key) {
18
+ const override = this.overrides.get(key);
19
+ return override ? override.kind !== "tombstone" : this.base.pageOf.has(key);
20
+ }
21
+ set(key, row) {
22
+ this.put(key, { kind: "row", row });
23
+ }
24
+ patch(key, fields) {
25
+ const current = this.overrides.get(key);
26
+ if (current?.kind === "row")
27
+ this.put(key, { kind: "row", row: { ...current.row, ...fields } });
28
+ else if (current?.kind === "patch")
29
+ this.put(key, { kind: "patch", patch: { ...current.patch, ...fields } });
30
+ else
31
+ this.put(key, { kind: "patch", patch: fields });
32
+ }
33
+ /** A key only the overlay ever held leaves no tombstone; a committed one is masked until compaction. */
34
+ delete(key) {
35
+ if (this.base.pageOf.has(key))
36
+ this.put(key, { kind: "tombstone" });
37
+ else
38
+ this.drop(key);
39
+ }
40
+ freeze() {
41
+ for (const page of this.base.pages)
42
+ page.pins++;
43
+ for (const override of this.overrides.values())
44
+ this.pinRow(override, 1);
45
+ const frozen = { base: this.base, overrides: new Map(this.overrides) };
46
+ this.frozen.add(frozen);
47
+ return frozen;
48
+ }
49
+ release(frozen) {
50
+ if (!this.frozen.delete(frozen))
51
+ return;
52
+ for (const override of frozen.overrides.values())
53
+ this.pinRow(override, -1);
54
+ for (const page of frozen.base.pages) {
55
+ page.pins--;
56
+ if (!page.inBase && page.pins === 0)
57
+ this.reclaimPage(page, 0, -1);
58
+ }
59
+ }
60
+ /** Every row visible at a frozen root, reading only that root's own pages. */
61
+ async entries(frozen) {
62
+ if (!this.frozen.has(frozen))
63
+ throw new CompactReadError("released");
64
+ const rows = new Map();
65
+ for (const page of frozen.base.pages)
66
+ for (const [key, row] of await this.readPage(page))
67
+ rows.set(key, row);
68
+ applyOverrides(rows, frozen.overrides);
69
+ return rows;
70
+ }
71
+ status() {
72
+ return {
73
+ overrideSlots: this.overrides.size,
74
+ deletionMasks: this.tombstones,
75
+ deletionMaskBytes: this.tombstoneKeyUnits * 2,
76
+ basePages: this.base.pages.length,
77
+ baseRows: this.base.pageOf.size,
78
+ };
79
+ }
80
+ /** The acceptance path checks counts only: an override is eligible once none of its leaves is uncommitted. */
81
+ maybeCompact() {
82
+ if (this.compacting || !this.store.spillAllowed)
83
+ return;
84
+ if (this.overrides.size - this.unsettledLeaves.size < this.compactAboveRows)
85
+ return;
86
+ this.compacting = true;
87
+ this.store.enqueue("page_write", async () => {
88
+ let progressed = false;
89
+ try {
90
+ progressed = await this.compact();
91
+ }
92
+ finally {
93
+ this.compacting = false;
94
+ }
95
+ if (progressed)
96
+ this.maybeCompact();
97
+ });
98
+ }
99
+ /** Folds eligible overrides and every small base page; false leaves the previous base untouched. */
100
+ async compact() {
101
+ const base = this.base;
102
+ const selected = [...this.overrides].filter(([key]) => !this.unsettledLeaves.has(key));
103
+ if (selected.length < this.compactAboveRows)
104
+ return false;
105
+ const touched = new Set(base.pages.filter(isSmallPage));
106
+ for (const [key] of selected) {
107
+ const page = base.pageOf.get(key);
108
+ if (page)
109
+ touched.add(page);
110
+ }
111
+ const rows = new Map();
112
+ try {
113
+ for (const page of touched)
114
+ for (const [key, row] of await this.readPage(page))
115
+ rows.set(key, row);
116
+ }
117
+ catch {
118
+ return false;
119
+ }
120
+ applyOverrides(rows, new Map(selected));
121
+ const written = [];
122
+ for (const chunk of packPages([...rows])) {
123
+ const data = Buffer.from(JSON.stringify(chunk));
124
+ const path = await this.store.writeFile(data, "page_write", data.byteLength);
125
+ if (path === undefined || !this.store.spillAllowed) {
126
+ for (const page of written)
127
+ this.store.discard(page.path, page.bytes);
128
+ if (path !== undefined)
129
+ this.store.discard(path, data.byteLength);
130
+ return false;
131
+ }
132
+ written.push({ path, bytes: data.byteLength, rows: chunk });
133
+ }
134
+ this.swap(base, touched, written, new Map(selected));
135
+ return true;
136
+ }
137
+ /**
138
+ * The publication point: one synchronous step, so no acceptance observes a half-swapped index. A folded
139
+ * row is published only while its value is still observable (its override is the folded one, or a newer
140
+ * patch over it); a row removed or wholly replaced meanwhile is skipped, so every published leaf is still
141
+ * owned by the folded override or a touched page when its reference is added.
142
+ */
143
+ swap(base, touched, written, selected) {
144
+ const pageOf = new Map(base.pageOf);
145
+ for (const page of touched)
146
+ for (const key of page.keys)
147
+ pageOf.delete(key);
148
+ const published = [];
149
+ for (const { path, bytes, rows } of written) {
150
+ const kept = rows.filter(([key]) => {
151
+ const folded = selected.get(key);
152
+ const current = this.overrides.get(key);
153
+ return folded === undefined || current === folded || current?.kind === "patch";
154
+ });
155
+ if (kept.length === 0) {
156
+ this.store.discard(path, bytes);
157
+ continue;
158
+ }
159
+ const leaves = kept.flatMap(([, row]) => this.leavesOf(row));
160
+ const page = { path, bytes, keys: kept.map(([key]) => key), leaves, inBase: true, pins: 0 };
161
+ for (const key of page.keys)
162
+ pageOf.set(key, page);
163
+ for (const id of leaves)
164
+ this.store.adjust(id, 1, 0);
165
+ published.push(page);
166
+ }
167
+ this.base = { pages: [...base.pages.filter((page) => !touched.has(page)), ...published], pageOf };
168
+ for (const [key, override] of selected) {
169
+ const current = this.overrides.get(key);
170
+ if (current === override || (current?.kind === "tombstone" && !pageOf.has(key)))
171
+ this.drop(key);
172
+ }
173
+ for (const page of touched) {
174
+ page.inBase = false;
175
+ if (page.pins === 0)
176
+ this.reclaimPage(page, -1, 0);
177
+ else
178
+ for (const id of page.leaves)
179
+ this.store.adjust(id, -1, 1);
180
+ }
181
+ }
182
+ async readPage(page) {
183
+ const data = await this.store.readFile(page.path, page.bytes, "page_read");
184
+ const entries = JSON.parse(data.toString("utf8"));
185
+ if (entries.length === page.keys.length)
186
+ return entries;
187
+ const keys = new Set(page.keys);
188
+ return entries.filter(([key]) => keys.has(key));
189
+ }
190
+ reclaimPage(page, live, pins) {
191
+ for (const id of page.leaves)
192
+ this.store.adjust(id, live, pins);
193
+ this.store.discard(page.path, page.bytes);
194
+ }
195
+ put(key, override) {
196
+ const leaves = override.kind === "row" ? this.leavesOf(override.row) : [];
197
+ for (const id of leaves)
198
+ this.store.adjust(id, 1, 0);
199
+ this.drop(key);
200
+ this.overrides.set(key, override);
201
+ this.unsettledLeaves.set(key, leaves);
202
+ if (override.kind === "tombstone")
203
+ this.countTombstone(key, 1);
204
+ this.maybeCompact();
205
+ }
206
+ drop(key) {
207
+ const old = this.overrides.get(key);
208
+ if (!old)
209
+ return;
210
+ this.overrides.delete(key);
211
+ this.unsettledLeaves.delete(key);
212
+ if (old.kind === "row") {
213
+ for (const id of this.leavesOf(old.row)) {
214
+ this.store.adjust(id, -1, 0);
215
+ }
216
+ }
217
+ if (old.kind === "tombstone")
218
+ this.countTombstone(key, -1);
219
+ }
220
+ countTombstone(key, delta) {
221
+ this.tombstones += delta;
222
+ this.tombstoneKeyUnits += delta * key.length;
223
+ }
224
+ pinRow(override, delta) {
225
+ if (override.kind === "row")
226
+ for (const id of this.leavesOf(override.row))
227
+ this.store.adjust(id, 0, delta);
228
+ }
229
+ }
230
+ function applyOverrides(rows, overrides) {
231
+ for (const [key, override] of overrides) {
232
+ if (override.kind === "tombstone")
233
+ rows.delete(key);
234
+ else if (override.kind === "row")
235
+ rows.set(key, override.row);
236
+ else {
237
+ const row = rows.get(key);
238
+ if (row)
239
+ rows.set(key, { ...row, ...override.patch });
240
+ }
241
+ }
242
+ }
243
+ //# sourceMappingURL=compact-content-index.js.map
@@ -1,4 +1,6 @@
1
+ import { CompactSpoolStorage } from "./compact-spool-storage.ts";
1
2
  import { type CompactStorageFailure, type CompactStorageIo } from "./compact-storage-io.ts";
3
+ import { CompactTextRoots } from "./compact-text-roots.ts";
2
4
  /**
3
5
  * Owned bounded leaves for compact current state (senpi#4272, R3.C6.4 / R3.C2.4).
4
6
  *
@@ -30,67 +32,32 @@ export interface CompactContentStoreOptions {
30
32
  readonly spillAboveBytes?: number;
31
33
  readonly spillTargetBytes?: number;
32
34
  }
33
- export declare class CompactContentStore {
34
- readonly dir: string;
35
- private readonly io;
35
+ export declare class CompactContentStore extends CompactSpoolStorage {
36
36
  private readonly spillAboveBytes;
37
37
  private readonly spillTargetBytes;
38
38
  private readonly leaves;
39
39
  /** Resident leaves in creation order, so each spill takes the oldest without rescanning committed ones. */
40
40
  private readonly ramLeaves;
41
- private readonly failures;
42
41
  private nextLeafId;
43
- private nextFile;
44
- private dirReady;
45
- private spoolToken;
46
- private chain;
47
- private queued;
48
- private spillQueued;
49
- private halted;
50
- private retired;
51
42
  private residentPayloadBytes;
52
- private pendingIoCopyBytes;
53
43
  private pinnedOnlyBytes;
54
44
  private spilledLiveBytes;
55
- private spillBytesWritten;
56
- private reclaimDebtBytes;
45
+ private readonly subscribers;
46
+ readonly text: CompactTextRoots;
47
+ subscribeCommitted(listener: (id: LeafId) => void): void;
48
+ committed(id: LeafId): void;
49
+ leafUnits(id: LeafId): number;
57
50
  constructor(options: CompactContentStoreOptions);
58
51
  /** Takes ownership of an immutable value with one live reference. */
59
52
  createLeaf(text: string): LeafId;
60
53
  /** Moves live and cut-pin references; the last reference drops RAM or reclaims the file. */
61
54
  adjust(id: LeafId, live: number, pins: number): void;
55
+ isCommitted(id: LeafId): boolean;
62
56
  /** Reads a leaf the caller still references, from RAM or its committed file. */
63
57
  readLeaf(id: LeafId): Promise<string>;
64
- /** Resolves once every queued storage job, including the ones they queued, has completed. */
65
- idle(): Promise<void>;
66
- /**
67
- * Invalidates spilling, joins owned I/O, then removes the epoch directory only when this store
68
- * created it and it still passes the ownership checks; otherwise rejects with `CompactSpoolRefusedError`.
69
- */
70
- retire(): Promise<void>;
71
58
  status(): CompactStorageStatus;
72
- /** Reads a committed file and rejects a missing, unreadable or short one. */
73
- private readFile;
74
- /** Writes one new spool file; undefined means the write failed, was reported, and spilling halted. */
75
- private writeFile;
76
- /**
77
- * An unreferenced (or partially written) spool file: reclamation debt until its unlink completes or
78
- * finds it gone. Files dropped after retirement started belong to the directory retirement removes.
79
- */
80
- private discard;
81
- /** A failed unlink keeps its debt and is retried with bounded backoff outside the job chain. */
82
- private reclaim;
83
- private get spillAllowed();
84
- /** True while a storage failure or unreclaimed debt above the work allowance holds spilling back. */
85
- private get spillStopped();
86
- private enqueue;
87
- private scheduleSpill;
59
+ protected scheduleSpill(): void;
88
60
  /** Spills one oldest resident leaf per job, so other storage work interleaves fairly. */
89
61
  private spillNext;
90
- /** Creates the private epoch directory once; a refusal or failure halts spilling and is reported. */
91
- private ensureDir;
92
- private readFailure;
93
- private fail;
94
- private record;
95
62
  }
96
63
  //# sourceMappingURL=compact-content-store.d.ts.map
@@ -1,6 +1,7 @@
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";
3
+ import { CompactReadError } from "./compact-read-lease.js";
4
+ import { CompactTextRoots } from "./compact-text-roots.js";
4
5
  /**
5
6
  * Owned bounded leaves for compact current state (senpi#4272, R3.C6.4 / R3.C2.4).
6
7
  *
@@ -11,35 +12,39 @@ import { CompactStorageReadError, describeCause, nodeCompactStorageIo, } from ".
11
12
  */
12
13
  /** 32,768 UTF-16 code units (64 KiB as UTF-16LE), so a split surrogate survives a leaf boundary exactly. */
13
14
  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 {
15
+ export class CompactContentStore extends CompactSpoolStorage {
16
+ subscribeCommitted(listener) {
17
+ this.subscribers.add(listener);
18
+ }
19
+ committed(id) {
20
+ for (const listener of this.subscribers)
21
+ listener(id);
22
+ }
23
+ leafUnits(id) {
24
+ const leaf = this.leaves.get(id);
25
+ if (!leaf)
26
+ throw new CompactReadError("ownership");
27
+ return leaf.units;
28
+ }
18
29
  constructor(options) {
30
+ super(options.dir, options.io ?? nodeCompactStorageIo);
19
31
  this.leaves = new Map();
20
32
  /** Resident leaves in creation order, so each spill takes the oldest without rescanning committed ones. */
21
33
  this.ramLeaves = new Set();
22
- this.failures = [];
23
34
  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
35
  this.residentPayloadBytes = 0;
31
- this.pendingIoCopyBytes = 0;
32
36
  this.pinnedOnlyBytes = 0;
33
37
  this.spilledLiveBytes = 0;
34
- this.spillBytesWritten = 0;
35
- this.reclaimDebtBytes = 0;
36
- this.dir = options.dir;
37
- this.io = options.io ?? nodeCompactStorageIo;
38
+ this.subscribers = new Set();
39
+ this.text = new CompactTextRoots(this);
38
40
  this.spillAboveBytes = options.spillAboveBytes ?? 4 * 1024 * 1024;
39
41
  this.spillTargetBytes = options.spillTargetBytes ?? 2 * 1024 * 1024;
40
42
  }
41
43
  /** Takes ownership of an immutable value with one live reference. */
42
44
  createLeaf(text) {
45
+ this.assertReadable();
46
+ if (text.length > COMPACT_LEAF_CODE_UNITS)
47
+ throw new CompactReadError("size_limit");
43
48
  const id = this.nextLeafId++;
44
49
  this.leaves.set(id, { text, units: text.length, live: 1, pins: 0, state: "ram" });
45
50
  this.ramLeaves.add(id);
@@ -49,9 +54,13 @@ export class CompactContentStore {
49
54
  }
50
55
  /** Moves live and cut-pin references; the last reference drops RAM or reclaims the file. */
51
56
  adjust(id, live, pins) {
57
+ if (id === 0)
58
+ return;
59
+ if (id < 0)
60
+ return this.text.adjust(id, live, pins);
52
61
  const leaf = this.leaves.get(id);
53
- if (!leaf)
54
- throw new Error(`compact leaf ${id} is not owned`);
62
+ if (!leaf || leaf.live + live < 0 || leaf.pins + pins < 0)
63
+ throw new CompactReadError("ownership");
55
64
  const pinnedOnly = leaf.live === 0 && leaf.pins > 0;
56
65
  leaf.live += live;
57
66
  leaf.pins += pins;
@@ -68,31 +77,26 @@ export class CompactContentStore {
68
77
  }
69
78
  this.residentPayloadBytes -= leaf.units * 2;
70
79
  }
80
+ isCommitted(id) {
81
+ if (id <= 0)
82
+ return id === 0 || this.text.isCommitted(id);
83
+ return this.leaves.get(id)?.state === "committed";
84
+ }
71
85
  /** Reads a leaf the caller still references, from RAM or its committed file. */
72
86
  async readLeaf(id) {
87
+ this.assertReadable();
73
88
  const leaf = this.leaves.get(id);
74
89
  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);
90
+ throw new CompactReadError("ownership");
91
+ this.adjust(id, 0, 1);
92
+ try {
93
+ if (leaf.text !== undefined)
94
+ return leaf.text;
95
+ return (await this.readFile(leaf.path ?? "", leaf.units * 2, "leaf_read")).toString("utf16le");
96
+ }
97
+ finally {
98
+ this.adjust(id, 0, -1);
99
+ }
96
100
  }
97
101
  status() {
98
102
  return {
@@ -109,94 +113,6 @@ export class CompactContentStore {
109
113
  failures: [...this.failures],
110
114
  };
111
115
  }
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
116
  scheduleSpill() {
201
117
  if (this.spillQueued || !this.spillAllowed || this.residentPayloadBytes <= this.spillAboveBytes)
202
118
  return;
@@ -216,22 +132,29 @@ export class CompactContentStore {
216
132
  const bytes = leaf.units * 2;
217
133
  leaf.state = "pending";
218
134
  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);
135
+ let path;
136
+ try {
137
+ path = await this.writeFile(Buffer.from(leaf.text, "utf16le"), "spill_write", bytes);
138
+ }
139
+ finally {
140
+ // A failed write, or a failure report that threw, keeps the leaf resident and spillable.
141
+ if (path === undefined) {
142
+ leaf.state = "ram";
143
+ if (this.leaves.get(id) === leaf)
144
+ this.ramLeaves.add(id);
145
+ }
225
146
  }
226
- else if (!owned || this.retired) {
147
+ const owned = this.leaves.get(id) === leaf;
148
+ if (path !== undefined && (!owned || this.retired)) {
227
149
  this.discard(path, bytes);
228
150
  }
229
- else {
151
+ else if (path !== undefined) {
230
152
  leaf.state = "committed";
231
153
  leaf.path = path;
232
154
  leaf.text = undefined;
233
155
  this.residentPayloadBytes -= bytes;
234
156
  this.spilledLiveBytes += bytes;
157
+ this.committed(id);
235
158
  }
236
159
  this.spillQueued = false;
237
160
  if (this.residentPayloadBytes > this.spillTargetBytes) {
@@ -240,30 +163,5 @@ export class CompactContentStore {
240
163
  this.enqueue("spill_write", () => this.spillNext());
241
164
  }
242
165
  }
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
166
  }
269
167
  //# 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
@@ -0,0 +1,15 @@
1
+ export declare class CompactReadError extends Error {
2
+ readonly reason: "released" | "retired" | "range" | "size_limit" | "count_limit" | "ownership";
3
+ constructor(reason: CompactReadError["reason"]);
4
+ }
5
+ /** Invalidates immediately, but holds the owner's references until its last operation unwinds. */
6
+ export declare class CompactReadLease {
7
+ private released;
8
+ private operations;
9
+ private readonly dispose;
10
+ constructor(dispose: () => void);
11
+ assert(): void;
12
+ run<T>(read: () => Promise<T>): Promise<T>;
13
+ release(): void;
14
+ }
15
+ //# sourceMappingURL=compact-read-lease.d.ts.map