@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,3 +1,4 @@
1
+ import { getAssistantCapture, } from "@earendil-works/pi-ai";
1
2
  function toJsonAssistantMessageEvent(event) {
2
3
  if (event.type === "toolcall_start") {
3
4
  const toolCall = event.partial.content[event.contentIndex];
@@ -13,7 +14,84 @@ function toJsonAssistantMessageEvent(event) {
13
14
  const { partial: _partial, ...deltaEvent } = event;
14
15
  return deltaEvent;
15
16
  }
16
- export function toJsonEvent(event) {
17
+ /**
18
+ * Builds a seeded assistant event from the producer-time capture only: an ordinary delta reads its index, its
19
+ * delta string and the capture, never the shared partial, the block or the parsed arguments.
20
+ */
21
+ function toSeededAssistantEvent(event, capture) {
22
+ if (event.type === "start")
23
+ return capture.message && { type: "start", seed: { message: capture.message } };
24
+ if (event.type === "done")
25
+ return { type: "done", reason: event.reason, message: event.message };
26
+ if (event.type === "error")
27
+ return { type: "error", reason: event.reason, error: event.error };
28
+ const contentIndex = capture.contentIndex ?? event.contentIndex;
29
+ if (event.type === "text_delta" || event.type === "thinking_delta" || event.type === "toolcall_delta") {
30
+ const { offset, parsedThrough } = capture;
31
+ if (offset === undefined)
32
+ return undefined;
33
+ if (event.type !== "toolcall_delta")
34
+ return { type: event.type, contentIndex, delta: event.delta, offset };
35
+ if (parsedThrough === undefined)
36
+ return undefined;
37
+ return { type: event.type, contentIndex, delta: event.delta, offset, parsedThrough };
38
+ }
39
+ const { block, coveredCodeUnits } = capture;
40
+ if (event.type === "toolcall_end")
41
+ return block?.type === "toolCall" ? { type: event.type, contentIndex, toolCall: block } : undefined;
42
+ if (event.type === "text_end")
43
+ return block?.type === "text" ? { type: event.type, contentIndex, content: event.content, block } : undefined;
44
+ if (event.type === "thinking_end")
45
+ return block?.type === "thinking" ? { type: event.type, contentIndex, content: event.content, block } : undefined;
46
+ if (coveredCodeUnits === undefined)
47
+ return undefined;
48
+ if (event.type === "text_start")
49
+ return block?.type === "text" ? { type: event.type, contentIndex, seed: { block, coveredCodeUnits } } : undefined;
50
+ if (event.type === "thinking_start")
51
+ return block?.type === "thinking"
52
+ ? { type: event.type, contentIndex, seed: { block, coveredCodeUnits } }
53
+ : undefined;
54
+ const { rawArgumentPrefix, parsedThrough } = capture;
55
+ if (block?.type !== "toolCall" || rawArgumentPrefix === undefined || parsedThrough === undefined)
56
+ return undefined;
57
+ return {
58
+ type: event.type,
59
+ contentIndex,
60
+ id: block.id,
61
+ toolName: block.name,
62
+ seed: { block, rawArgumentPrefix, parsedThrough, coveredCodeUnits },
63
+ };
64
+ }
65
+ /**
66
+ * An assistant record with no capture has no seeded form (`undefined`): its producer captured nothing, so any
67
+ * seed would be read from the live partial and could already hold later bytes. Identity is never invented.
68
+ */
69
+ function toSeededJsonEvent(event) {
70
+ if ((event.type === "message_start" || event.type === "message_end") && event.message.role === "assistant") {
71
+ const message = getAssistantCapture(event)?.message;
72
+ if (message)
73
+ return { ...event, message };
74
+ // The terminal message is complete when emitted; only an uncaptured start may already hold later bytes.
75
+ return event.type === "message_end" ? event : undefined;
76
+ }
77
+ if (event.type !== "message_update")
78
+ return event;
79
+ const capture = getAssistantCapture(event);
80
+ const assistantMessageEvent = capture && toSeededAssistantEvent(event.assistantMessageEvent, capture);
81
+ if (!capture || !assistantMessageEvent)
82
+ return undefined;
83
+ const { message: _message, assistantMessageEvent: _event, ...outer } = event;
84
+ const changed = capture.metadata !== undefined && Object.keys(capture.metadata).length > 0;
85
+ return {
86
+ ...outer,
87
+ usage: capture.usage,
88
+ ...(changed ? { messageMetadata: capture.metadata } : {}),
89
+ assistantMessageEvent,
90
+ };
91
+ }
92
+ export function toJsonEvent(event, options) {
93
+ if (options)
94
+ return toSeededJsonEvent(event);
17
95
  if (event.type !== "message_update") {
18
96
  return event;
19
97
  }
@@ -0,0 +1,85 @@
1
+ import type { CompactContentStore, LeafId } from "./compact-content-store.ts";
2
+ /**
3
+ * A spillable keyed collection: an immutable paged disk base plus one resident override slot per key
4
+ * (senpi#4272). Updates coalesce into that slot, so N updates to one key hold one slot, and a cold row
5
+ * changes without reading its page. Background compaction folds eligible overrides into new pages and
6
+ * publishes them in one synchronous swap that never removes an override newer than the one it read.
7
+ * A failed compaction leaves the previous base in place and the next one free to run.
8
+ * Pages are internal storage only; nothing here is a wire record.
9
+ */
10
+ type Override<Row> = {
11
+ readonly kind: "row";
12
+ readonly row: Row;
13
+ } | {
14
+ readonly kind: "patch";
15
+ readonly patch: Partial<Row>;
16
+ } | {
17
+ readonly kind: "tombstone";
18
+ };
19
+ interface Page {
20
+ readonly path: string;
21
+ readonly bytes: number;
22
+ /** Published keys; a row skipped at publication stays in the file but is never read back. */
23
+ readonly keys: readonly string[];
24
+ readonly leaves: readonly LeafId[];
25
+ inBase: boolean;
26
+ pins: number;
27
+ }
28
+ interface IndexBase {
29
+ readonly pages: readonly Page[];
30
+ readonly pageOf: ReadonlyMap<string, Page>;
31
+ }
32
+ export interface FrozenIndex<Row> {
33
+ readonly base: IndexBase;
34
+ readonly overrides: ReadonlyMap<string, Override<Row>>;
35
+ }
36
+ export declare class CompactContentIndex<Row extends object> {
37
+ private readonly store;
38
+ private readonly leavesOf;
39
+ private readonly compactAboveRows;
40
+ private base;
41
+ private readonly overrides;
42
+ /** Uncommitted leaves per row override; only overrides absent here are eligible to fold. */
43
+ private readonly unsettledLeaves;
44
+ private readonly leafKey;
45
+ private compacting;
46
+ private tombstones;
47
+ private tombstoneKeyUnits;
48
+ constructor(store: CompactContentStore, leavesOf: (row: Row) => readonly LeafId[], compactAboveRows: number);
49
+ has(key: string): boolean;
50
+ set(key: string, row: Row): void;
51
+ patch(key: string, fields: Partial<Row>): void;
52
+ /** A key only the overlay ever held leaves no tombstone; a committed one is masked until compaction. */
53
+ delete(key: string): void;
54
+ freeze(): FrozenIndex<Row>;
55
+ release(frozen: FrozenIndex<Row>): void;
56
+ /** Every row visible at a frozen root, reading only that root's own pages. */
57
+ entries(frozen: FrozenIndex<Row>): Promise<Map<string, Row>>;
58
+ status(): {
59
+ overrideSlots: number;
60
+ deletionMasks: number;
61
+ deletionMaskBytes: number;
62
+ basePages: number;
63
+ baseRows: number;
64
+ };
65
+ /** The acceptance path checks counts only: an override is eligible once none of its leaves is uncommitted. */
66
+ maybeCompact(): void;
67
+ private committed;
68
+ /** Folds eligible overrides and every small base page; false leaves the previous base untouched. */
69
+ private compact;
70
+ /**
71
+ * The publication point: one synchronous step, so no acceptance observes a half-swapped index. A folded
72
+ * row is published only while its value is still observable (its override is the folded one, or a newer
73
+ * patch over it); a row removed or wholly replaced meanwhile is skipped, so every published leaf is still
74
+ * owned by the folded override or a touched page when its reference is added.
75
+ */
76
+ private swap;
77
+ private readPage;
78
+ private reclaimPage;
79
+ private put;
80
+ private drop;
81
+ private countTombstone;
82
+ private pinRow;
83
+ }
84
+ export {};
85
+ //# sourceMappingURL=compact-content-index.d.ts.map
@@ -0,0 +1,254 @@
1
+ import { isSmallPage, packPages } from "./compact-index-pages.js";
2
+ export class CompactContentIndex {
3
+ constructor(store, leavesOf, compactAboveRows) {
4
+ this.base = { pages: [], pageOf: new Map() };
5
+ this.overrides = new Map();
6
+ /** Uncommitted leaves per row override; only overrides absent here are eligible to fold. */
7
+ this.unsettledLeaves = new Map();
8
+ this.leafKey = new Map();
9
+ this.compacting = false;
10
+ this.tombstones = 0;
11
+ this.tombstoneKeyUnits = 0;
12
+ this.store = store;
13
+ this.leavesOf = leavesOf;
14
+ this.compactAboveRows = compactAboveRows;
15
+ store.onCommitted = (id) => this.committed(id);
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
+ return { base: this.base, overrides: new Map(this.overrides) };
46
+ }
47
+ release(frozen) {
48
+ for (const override of frozen.overrides.values())
49
+ this.pinRow(override, -1);
50
+ for (const page of frozen.base.pages) {
51
+ page.pins--;
52
+ if (!page.inBase && page.pins === 0)
53
+ this.reclaimPage(page, 0, -1);
54
+ }
55
+ }
56
+ /** Every row visible at a frozen root, reading only that root's own pages. */
57
+ async entries(frozen) {
58
+ const rows = new Map();
59
+ for (const page of frozen.base.pages)
60
+ for (const [key, row] of await this.readPage(page))
61
+ rows.set(key, row);
62
+ applyOverrides(rows, frozen.overrides);
63
+ return rows;
64
+ }
65
+ status() {
66
+ return {
67
+ overrideSlots: this.overrides.size,
68
+ deletionMasks: this.tombstones,
69
+ deletionMaskBytes: this.tombstoneKeyUnits * 2,
70
+ basePages: this.base.pages.length,
71
+ baseRows: this.base.pageOf.size,
72
+ };
73
+ }
74
+ /** The acceptance path checks counts only: an override is eligible once none of its leaves is uncommitted. */
75
+ maybeCompact() {
76
+ if (this.compacting || !this.store.spillAllowed)
77
+ return;
78
+ if (this.overrides.size - this.unsettledLeaves.size < this.compactAboveRows)
79
+ return;
80
+ this.compacting = true;
81
+ this.store.enqueue("page_write", async () => {
82
+ let progressed = false;
83
+ try {
84
+ progressed = await this.compact();
85
+ }
86
+ finally {
87
+ this.compacting = false;
88
+ }
89
+ if (progressed)
90
+ this.maybeCompact();
91
+ });
92
+ }
93
+ committed(id) {
94
+ const key = this.leafKey.get(id);
95
+ if (key === undefined)
96
+ return;
97
+ this.leafKey.delete(id);
98
+ const left = (this.unsettledLeaves.get(key) ?? 0) - 1;
99
+ if (left > 0)
100
+ this.unsettledLeaves.set(key, left);
101
+ else
102
+ this.unsettledLeaves.delete(key);
103
+ this.maybeCompact();
104
+ }
105
+ /** Folds eligible overrides and every small base page; false leaves the previous base untouched. */
106
+ async compact() {
107
+ const base = this.base;
108
+ const selected = [...this.overrides].filter(([key]) => !this.unsettledLeaves.has(key));
109
+ if (selected.length < this.compactAboveRows)
110
+ return false;
111
+ const touched = new Set(base.pages.filter(isSmallPage));
112
+ for (const [key] of selected) {
113
+ const page = base.pageOf.get(key);
114
+ if (page)
115
+ touched.add(page);
116
+ }
117
+ const rows = new Map();
118
+ try {
119
+ for (const page of touched)
120
+ for (const [key, row] of await this.readPage(page))
121
+ rows.set(key, row);
122
+ }
123
+ catch {
124
+ return false;
125
+ }
126
+ applyOverrides(rows, new Map(selected));
127
+ const written = [];
128
+ for (const chunk of packPages([...rows])) {
129
+ const data = Buffer.from(JSON.stringify(chunk));
130
+ const path = await this.store.writeFile(data, "page_write", data.byteLength);
131
+ if (path === undefined || !this.store.spillAllowed) {
132
+ for (const page of written)
133
+ this.store.discard(page.path, page.bytes);
134
+ if (path !== undefined)
135
+ this.store.discard(path, data.byteLength);
136
+ return false;
137
+ }
138
+ written.push({ path, bytes: data.byteLength, rows: chunk });
139
+ }
140
+ this.swap(base, touched, written, new Map(selected));
141
+ return true;
142
+ }
143
+ /**
144
+ * The publication point: one synchronous step, so no acceptance observes a half-swapped index. A folded
145
+ * row is published only while its value is still observable (its override is the folded one, or a newer
146
+ * patch over it); a row removed or wholly replaced meanwhile is skipped, so every published leaf is still
147
+ * owned by the folded override or a touched page when its reference is added.
148
+ */
149
+ swap(base, touched, written, selected) {
150
+ const pageOf = new Map(base.pageOf);
151
+ for (const page of touched)
152
+ for (const key of page.keys)
153
+ pageOf.delete(key);
154
+ const published = [];
155
+ for (const { path, bytes, rows } of written) {
156
+ const kept = rows.filter(([key]) => {
157
+ const folded = selected.get(key);
158
+ const current = this.overrides.get(key);
159
+ return folded === undefined || current === folded || current?.kind === "patch";
160
+ });
161
+ if (kept.length === 0) {
162
+ this.store.discard(path, bytes);
163
+ continue;
164
+ }
165
+ const leaves = kept.flatMap(([, row]) => this.leavesOf(row));
166
+ const page = { path, bytes, keys: kept.map(([key]) => key), leaves, inBase: true, pins: 0 };
167
+ for (const key of page.keys)
168
+ pageOf.set(key, page);
169
+ for (const id of leaves)
170
+ this.store.adjust(id, 1, 0);
171
+ published.push(page);
172
+ }
173
+ this.base = { pages: [...base.pages.filter((page) => !touched.has(page)), ...published], pageOf };
174
+ for (const [key, override] of selected) {
175
+ const current = this.overrides.get(key);
176
+ if (current === override || (current?.kind === "tombstone" && !pageOf.has(key)))
177
+ this.drop(key);
178
+ }
179
+ for (const page of touched) {
180
+ page.inBase = false;
181
+ if (page.pins === 0)
182
+ this.reclaimPage(page, -1, 0);
183
+ else
184
+ for (const id of page.leaves)
185
+ this.store.adjust(id, -1, 1);
186
+ }
187
+ }
188
+ async readPage(page) {
189
+ const data = await this.store.readFile(page.path, page.bytes, "page_read");
190
+ const entries = JSON.parse(data.toString("utf8"));
191
+ if (entries.length === page.keys.length)
192
+ return entries;
193
+ const keys = new Set(page.keys);
194
+ return entries.filter(([key]) => keys.has(key));
195
+ }
196
+ reclaimPage(page, live, pins) {
197
+ for (const id of page.leaves)
198
+ this.store.adjust(id, live, pins);
199
+ this.store.discard(page.path, page.bytes);
200
+ }
201
+ put(key, override) {
202
+ const leaves = override.kind === "row" ? this.leavesOf(override.row) : [];
203
+ for (const id of leaves)
204
+ this.store.adjust(id, 1, 0);
205
+ this.drop(key);
206
+ this.overrides.set(key, override);
207
+ const unsettled = leaves.filter((id) => !this.store.isCommitted(id));
208
+ for (const id of unsettled)
209
+ this.leafKey.set(id, key);
210
+ if (unsettled.length > 0)
211
+ this.unsettledLeaves.set(key, unsettled.length);
212
+ if (override.kind === "tombstone")
213
+ this.countTombstone(key, 1);
214
+ this.maybeCompact();
215
+ }
216
+ drop(key) {
217
+ const old = this.overrides.get(key);
218
+ if (!old)
219
+ return;
220
+ this.overrides.delete(key);
221
+ this.unsettledLeaves.delete(key);
222
+ if (old.kind === "row") {
223
+ for (const id of this.leavesOf(old.row)) {
224
+ this.leafKey.delete(id);
225
+ this.store.adjust(id, -1, 0);
226
+ }
227
+ }
228
+ if (old.kind === "tombstone")
229
+ this.countTombstone(key, -1);
230
+ }
231
+ countTombstone(key, delta) {
232
+ this.tombstones += delta;
233
+ this.tombstoneKeyUnits += delta * key.length;
234
+ }
235
+ pinRow(override, delta) {
236
+ if (override.kind === "row")
237
+ for (const id of this.leavesOf(override.row))
238
+ this.store.adjust(id, 0, delta);
239
+ }
240
+ }
241
+ function applyOverrides(rows, overrides) {
242
+ for (const [key, override] of overrides) {
243
+ if (override.kind === "tombstone")
244
+ rows.delete(key);
245
+ else if (override.kind === "row")
246
+ rows.set(key, override.row);
247
+ else {
248
+ const row = rows.get(key);
249
+ if (row)
250
+ rows.set(key, { ...row, ...override.patch });
251
+ }
252
+ }
253
+ }
254
+ //# sourceMappingURL=compact-content-index.js.map
@@ -1,3 +1,4 @@
1
+ import { CompactSpoolStorage } from "./compact-spool-storage.ts";
1
2
  import { type CompactStorageFailure, type CompactStorageIo } from "./compact-storage-io.ts";
2
3
  /**
3
4
  * Owned bounded leaves for compact current state (senpi#4272, R3.C6.4 / R3.C2.4).
@@ -30,67 +31,29 @@ export interface CompactContentStoreOptions {
30
31
  readonly spillAboveBytes?: number;
31
32
  readonly spillTargetBytes?: number;
32
33
  }
33
- export declare class CompactContentStore {
34
- readonly dir: string;
35
- private readonly io;
34
+ export declare class CompactContentStore extends CompactSpoolStorage {
36
35
  private readonly spillAboveBytes;
37
36
  private readonly spillTargetBytes;
38
37
  private readonly leaves;
39
38
  /** Resident leaves in creation order, so each spill takes the oldest without rescanning committed ones. */
40
39
  private readonly ramLeaves;
41
- private readonly failures;
42
40
  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
41
  private residentPayloadBytes;
52
- private pendingIoCopyBytes;
53
42
  private pinnedOnlyBytes;
54
43
  private spilledLiveBytes;
55
- private spillBytesWritten;
56
- private reclaimDebtBytes;
44
+ /** Called after a leaf commits, so index compaction can include rows that now reference only disk. */
45
+ onCommitted: ((id: LeafId) => void) | undefined;
57
46
  constructor(options: CompactContentStoreOptions);
58
47
  /** Takes ownership of an immutable value with one live reference. */
59
48
  createLeaf(text: string): LeafId;
60
49
  /** Moves live and cut-pin references; the last reference drops RAM or reclaims the file. */
61
50
  adjust(id: LeafId, live: number, pins: number): void;
51
+ isCommitted(id: LeafId): boolean;
62
52
  /** Reads a leaf the caller still references, from RAM or its committed file. */
63
53
  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
54
  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;
55
+ protected scheduleSpill(): void;
88
56
  /** Spills one oldest resident leaf per job, so other storage work interleaves fairly. */
89
57
  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
58
  }
96
59
  //# sourceMappingURL=compact-content-store.d.ts.map