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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/bundle/chunks/{anthropic-messages-WBASBTIW.js → anthropic-messages-M235FJQV.js} +3 -3
  3. package/dist/bundle/chunks/{args-B444OQBK.js → args-FBNJOJSR.js} +1 -1
  4. package/dist/bundle/chunks/azure-openai-responses-YQRTQ7H6.js +2 -0
  5. package/dist/bundle/chunks/bedrock-converse-stream.js +3 -3
  6. package/dist/bundle/chunks/{chunk-4MOZXOJX.js → chunk-3A3XCLNH.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-E6D4RIT7.js → chunk-3LPWILUZ.js} +2 -2
  8. package/dist/bundle/chunks/{chunk-3BTN337S.js → chunk-3WP5RFQR.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-2RB5EL4X.js → chunk-5FQMNHXV.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-STMXIBXF.js → chunk-5KZH6EYQ.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-4P7WARQZ.js → chunk-5LWUUXMH.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-5JJDR6XI.js → chunk-6U3TBFBA.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-BWA4FVFW.js → chunk-7KOUQSBX.js} +2 -2
  14. package/dist/bundle/chunks/{chunk-7CNOCJCN.js → chunk-ADQAPDPV.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-ITGYN22E.js → chunk-CDQPA4FP.js} +1 -1
  16. package/dist/bundle/chunks/{chunk-WDPQ6CZJ.js → chunk-DMFAVE5N.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-XF3LJC3O.js → chunk-DYWIFF2Q.js} +1 -1
  18. package/dist/bundle/chunks/{chunk-C2MQ4QXN.js → chunk-EZP6AYW2.js} +35 -29
  19. package/dist/bundle/chunks/chunk-GQJYNFMO.js +2 -0
  20. package/dist/bundle/chunks/{chunk-SBGIXMX3.js → chunk-GWGNVYPB.js} +1 -1
  21. package/dist/bundle/chunks/{chunk-KKD4CYCI.js → chunk-H4WJ4LY3.js} +1 -1
  22. package/dist/bundle/chunks/{chunk-V2UYXTRR.js → chunk-HWTD4QF3.js} +1 -1
  23. package/dist/bundle/chunks/{chunk-WD6WBYC5.js → chunk-I7QWTWVZ.js} +1 -1
  24. package/dist/bundle/chunks/{chunk-BZ7726QO.js → chunk-J2YAYTB2.js} +1 -1
  25. package/dist/bundle/chunks/{chunk-TTU2RNVX.js → chunk-J5FCH7TI.js} +1 -1
  26. package/dist/bundle/chunks/chunk-JFQUWEN7.js +13 -0
  27. package/dist/bundle/chunks/{chunk-KNVNKDER.js → chunk-L2O6K3SJ.js} +1 -1
  28. package/dist/bundle/chunks/{chunk-4AOMNOGY.js → chunk-LO5MYLGF.js} +1 -1
  29. package/dist/bundle/chunks/{chunk-N7IN46Q5.js → chunk-MSHRIZGT.js} +8 -6
  30. package/dist/bundle/chunks/{chunk-T36LJ6MW.js → chunk-MY4ZD3EW.js} +1 -1
  31. package/dist/bundle/chunks/{chunk-UFJ2K7Q7.js → chunk-OQ3WDMB3.js} +1 -1
  32. package/dist/bundle/chunks/{chunk-7GZUFOM3.js → chunk-PH3JDRB2.js} +1 -1
  33. package/dist/bundle/chunks/{chunk-E6WYH3ZL.js → chunk-QREKAXKX.js} +7 -5
  34. package/dist/bundle/chunks/{chunk-3QCYBOKN.js → chunk-QYFOHFPS.js} +1 -1
  35. package/dist/bundle/chunks/{chunk-JMOUU75H.js → chunk-SOXQ42VS.js} +1 -1
  36. package/dist/bundle/chunks/{chunk-FHXR4ZAC.js → chunk-UDM7VO6O.js} +1 -1
  37. package/dist/bundle/chunks/{chunk-OLCDIITS.js → chunk-UW6M3EXY.js} +1 -1
  38. package/dist/bundle/chunks/chunk-VICDRG3J.js +410 -0
  39. package/dist/bundle/chunks/chunk-VL7O5PO4.js +5 -0
  40. package/dist/bundle/chunks/{chunk-PT6JJUN2.js → chunk-XMNPK472.js} +1 -1
  41. package/dist/bundle/chunks/{chunk-GSSD3R5X.js → chunk-XQ45JMGN.js} +1 -1
  42. package/dist/bundle/chunks/{client-WTYEICLH.js → chunk-Y3TMJV3J.js} +3 -3
  43. package/dist/bundle/chunks/{chunk-KXB5WIS5.js → chunk-Z6SAQ5O3.js} +1 -1
  44. package/dist/bundle/chunks/{chunk-LOIF6PHI.js → chunk-ZAUVY6ZC.js} +3 -3
  45. package/dist/bundle/chunks/{chunk-E4LBKOZG.js → chunk-ZLIVZQDY.js} +4 -4
  46. package/dist/bundle/chunks/{chunk-6AOVUTUR.js → chunk-ZXHLH4MW.js} +2 -2
  47. package/dist/bundle/chunks/cli-main-55EQA5PS.js +3 -0
  48. package/dist/bundle/chunks/client-LOS6GSEW.js +2 -0
  49. package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-K5H6BJP3.js → cloudflare-workers-ai-system-one-ZJQGIIRC.js} +1 -1
  50. package/dist/bundle/chunks/cursor-agent.js +3 -3
  51. package/dist/bundle/chunks/devin-agent.js +3 -3
  52. package/dist/bundle/chunks/dispatch-C2IBP5VV.js +9 -0
  53. package/dist/bundle/chunks/google-generative-ai-5G4RIQA6.js +2 -0
  54. package/dist/bundle/chunks/google-vertex-ALD4P27O.js +2 -0
  55. package/dist/bundle/chunks/{help-fast-path-3Z26DQSV.js → help-fast-path-PEIKGCKZ.js} +1 -1
  56. package/dist/bundle/chunks/host-command-657VCQ34.js +2 -0
  57. package/dist/bundle/chunks/{host-lifecycle-3VHHIGDL.js → host-lifecycle-HX3EGRRW.js} +1 -1
  58. package/dist/bundle/chunks/host-lifecycle.js +2 -2
  59. package/dist/bundle/chunks/{host-session-command-56XF6ACS.js → host-session-command-OV5XDOJK.js} +1 -1
  60. package/dist/bundle/chunks/{host-socket-request-QL2WEZTN.js → host-socket-request-K4LCUOUD.js} +1 -1
  61. package/dist/bundle/chunks/interactive-mode-MLXSYK2H.js +2 -0
  62. package/dist/bundle/chunks/{mcp-presenter-bootstrap-XUSCQVRC.js → mcp-presenter-bootstrap-FOMKF4TU.js} +1 -1
  63. package/dist/bundle/chunks/{mistral-conversations-DH44NOYV.js → mistral-conversations-RAE4K2XY.js} +1 -1
  64. package/dist/bundle/chunks/{multi-session-host-Z2CW4SDM.js → multi-session-host-774MACJU.js} +5 -6
  65. package/dist/bundle/chunks/{openai-codex-responses-SERAFKAS.js → openai-codex-responses-3CW4R7TG.js} +1 -1
  66. package/dist/bundle/chunks/{openai-completions-BZB5O45A.js → openai-completions-5UIJXQUF.js} +1 -1
  67. package/dist/bundle/chunks/openai-responses-2BO44MRW.js +3 -0
  68. package/dist/bundle/chunks/{package-manager-cli-N55U6PEO.js → package-manager-cli-MZHVCX2S.js} +1 -1
  69. package/dist/bundle/chunks/{pi-messages-VXATX45B.js → pi-messages-RF6GER4R.js} +2 -2
  70. package/dist/bundle/chunks/{role-JVSBNKGE.js → role-ETSFSDTS.js} +1 -1
  71. package/dist/bundle/chunks/{rotation-stream-CBGCBYHV.js → rotation-stream-ZMQB4GPO.js} +1 -1
  72. package/dist/bundle/chunks/rpc-mode-UJXOIZCW.js +2 -0
  73. package/dist/bundle/chunks/{rpc-monitor-source-AE3C6EQV.js → rpc-monitor-source-DRZXJHTL.js} +1 -1
  74. package/dist/bundle/chunks/{schedule-command-VS7CZC2J.js → schedule-command-KA7LFFNK.js} +1 -1
  75. package/dist/bundle/chunks/session-control-endpoint-MFWXUR7Z.js +3 -0
  76. package/dist/bundle/chunks/{session-picker-E4KUJDLO.js → session-picker-BP33NRSL.js} +1 -1
  77. package/dist/bundle/chunks/session-worker.js +199 -190
  78. package/dist/bundle/chunks/{typesafe-system-one-P75WJ767.js → typesafe-system-one-GNNZ5O5U.js} +1 -1
  79. package/dist/bundle/cli.js +2 -2
  80. package/dist/bundle/host-supervisor.js +2 -2
  81. package/dist/bundle/index.js +1 -1
  82. package/dist/bundle/rpc-entry.js +1 -1
  83. package/dist/bundle/runtime-manifest.json +1 -1
  84. package/dist/cli/host-command-args.d.ts +1 -0
  85. package/dist/cli/host-command-args.js +5 -0
  86. package/dist/cli/host-command.js +7 -1
  87. package/dist/cli.js +21 -3
  88. package/dist/core/extensions/builtin/mcp/catalog-cache.d.ts +2 -1
  89. package/dist/core/extensions/builtin/mcp/catalog-cache.js +19 -6
  90. package/dist/core/extensions/builtin/mcp/catalog.d.ts +3 -2
  91. package/dist/core/extensions/builtin/mcp/catalog.js +20 -3
  92. package/dist/core/extensions/builtin/mcp/expose/call.d.ts +2 -1
  93. package/dist/core/extensions/builtin/mcp/expose/call.js +2 -1
  94. package/dist/core/extensions/builtin/mcp/expose/register.js +1 -1
  95. package/dist/core/extensions/builtin/mcp/expose/schema-compat.d.ts +2 -2
  96. package/dist/core/extensions/builtin/mcp/expose/schema-compat.js +11 -1
  97. package/dist/core/extensions/builtin/mcp/invocation.js +16 -2
  98. package/dist/core/extensions/builtin/mcp/modern/request.d.ts +1 -1
  99. package/dist/core/extensions/builtin/mcp/modern/request.js +1 -1
  100. package/dist/core/extensions/builtin/mcp/modern/sdk-bridge.js +18 -3
  101. package/dist/core/extensions/builtin/mcp/modern/stdio-probe.js +23 -0
  102. package/dist/core/extensions/builtin/mcp/modern/subscriptions.d.ts +14 -0
  103. package/dist/core/extensions/builtin/mcp/modern/subscriptions.js +226 -0
  104. package/dist/core/extensions/builtin/mcp/modern/tool-schema.d.ts +13 -0
  105. package/dist/core/extensions/builtin/mcp/modern/tool-schema.js +75 -0
  106. package/dist/core/extensions/builtin/mcp/modern/tools.d.ts +107 -0
  107. package/dist/core/extensions/builtin/mcp/modern/tools.js +53 -0
  108. package/dist/core/extensions/builtin/mcp/resources.js +4 -0
  109. package/dist/core/extensions/builtin/mcp/tool-client.d.ts +13 -0
  110. package/dist/core/extensions/builtin/mcp/tool-client.js +17 -0
  111. package/dist/core/extensions/builtin/mcp/tool-contracts.d.ts +19 -0
  112. package/dist/core/extensions/builtin/mcp/tool-contracts.js +2 -0
  113. package/dist/core/extensions/builtin/mcp/wrap.js +2 -2
  114. package/dist/core/extensions/builtin/permission-system/bash-command-segments.d.ts +37 -0
  115. package/dist/core/extensions/builtin/permission-system/bash-command-segments.js +899 -0
  116. package/dist/core/extensions/builtin/permission-system/parsers.d.ts +2 -0
  117. package/dist/core/extensions/builtin/permission-system/parsers.js +44 -10
  118. package/dist/modes/interactive/session-control-commands.js +21 -4
  119. package/dist/modes/interactive/session-control-rpc-commands.d.ts +1 -1
  120. package/dist/modes/interactive/session-control-rpc-commands.js +19 -0
  121. package/dist/modes/rpc/compact-content-store.d.ts +96 -0
  122. package/dist/modes/rpc/compact-content-store.js +269 -0
  123. package/dist/modes/rpc/compact-session-state.d.ts +111 -0
  124. package/dist/modes/rpc/compact-session-state.js +206 -0
  125. package/dist/modes/rpc/compact-spool-dir.d.ts +5 -0
  126. package/dist/modes/rpc/compact-spool-dir.js +61 -0
  127. package/dist/modes/rpc/compact-storage-io.d.ts +27 -0
  128. package/dist/modes/rpc/compact-storage-io.js +19 -0
  129. package/dist/modes/rpc/connection-handler.d.ts +2 -0
  130. package/dist/modes/rpc/connection-handler.js +39 -7
  131. package/dist/modes/rpc/connection-question-bridge.d.ts +2 -1
  132. package/dist/modes/rpc/connection-question-bridge.js +6 -2
  133. package/dist/modes/rpc/extension-ui-response.d.ts +7 -2
  134. package/dist/modes/rpc/extension-ui-response.js +27 -7
  135. package/dist/modes/rpc/get-snapshot.d.ts +18 -0
  136. package/dist/modes/rpc/get-snapshot.js +43 -0
  137. package/dist/modes/rpc/host-capabilities.js +3 -0
  138. package/dist/modes/rpc/host-ensure-start.js +1 -0
  139. package/dist/modes/rpc/host-ensure-types.d.ts +2 -0
  140. package/dist/modes/rpc/host-ensure.js +1 -0
  141. package/dist/modes/rpc/host-handoff.d.ts +2 -0
  142. package/dist/modes/rpc/host-handover-request.d.ts +1 -1
  143. package/dist/modes/rpc/host-handover-request.js +4 -3
  144. package/dist/modes/rpc/host-handover-wire.js +6 -0
  145. package/dist/modes/rpc/host-idle-handover.d.ts +2 -0
  146. package/dist/modes/rpc/host-idle-handover.js +3 -1
  147. package/dist/modes/rpc/host-observe-request.js +1 -0
  148. package/dist/modes/rpc/host-runner.d.ts +1 -0
  149. package/dist/modes/rpc/host-runner.js +9 -7
  150. package/dist/modes/rpc/host-successor.js +4 -0
  151. package/dist/modes/rpc/json-normal-value.d.ts +6 -0
  152. package/dist/modes/rpc/json-normal-value.js +100 -0
  153. package/dist/modes/rpc/mcp-presenter-bootstrap.js +5 -0
  154. package/dist/modes/rpc/media-placeholders.d.ts +9 -3
  155. package/dist/modes/rpc/media-placeholders.js +12 -4
  156. package/dist/modes/rpc/rpc-host-command-types.d.ts +2 -0
  157. package/dist/modes/rpc/rpc-question-types.d.ts +1 -0
  158. package/dist/modes/rpc/rpc-read-response-types.d.ts +7 -0
  159. package/dist/modes/rpc/rpc-session-command-types.d.ts +6 -0
  160. package/dist/modes/rpc/rpc-session-state-types.d.ts +3 -0
  161. package/dist/modes/rpc/rpc-session-state.js +2 -0
  162. package/dist/modes/rpc/rpc-types.d.ts +1 -0
  163. package/dist/modes/rpc/session-attachment-gate.js +1 -0
  164. package/dist/modes/rpc/session-binding.js +21 -18
  165. package/dist/modes/rpc/session-command-activity.js +1 -0
  166. package/dist/modes/rpc/session-event-fanout.d.ts +1 -1
  167. package/dist/modes/rpc/session-event-fanout.js +17 -9
  168. package/dist/modes/rpc/session-event-writer.js +22 -5
  169. package/dist/modes/rpc/session-extension-ui-requests.d.ts +2 -1
  170. package/dist/modes/rpc/session-extension-ui-requests.js +15 -10
  171. package/dist/modes/rpc/session-held.js +1 -0
  172. package/dist/modes/rpc/session-status-stream.d.ts +2 -1
  173. package/dist/modes/rpc/session-status-stream.js +8 -2
  174. package/dist/modes/rpc/session-status-subscriptions.d.ts +2 -1
  175. package/dist/modes/rpc/session-status-subscriptions.js +4 -0
  176. package/dist/modes/rpc/session-worker.js +5 -2
  177. package/dist/modes/rpc/snapshot-types.d.ts +29 -0
  178. package/dist/modes/rpc/snapshot-types.js +2 -0
  179. package/dist/modes/rpc/ui-answer-ledger.d.ts +13 -0
  180. package/dist/modes/rpc/ui-answer-ledger.js +34 -0
  181. package/dist/senpi +21 -3
  182. package/docs/mcp.md +20 -0
  183. package/docs/rpc.md +44 -0
  184. package/package.json +7 -6
  185. package/dist/bundle/chunks/azure-openai-responses-7I45DI72.js +0 -2
  186. package/dist/bundle/chunks/chunk-DMI55M2N.js +0 -2
  187. package/dist/bundle/chunks/chunk-DODH3N6U.js +0 -410
  188. package/dist/bundle/chunks/chunk-J37GRED2.js +0 -5
  189. package/dist/bundle/chunks/chunk-TBU6VYHN.js +0 -13
  190. package/dist/bundle/chunks/cli-main-YYH3IBC6.js +0 -3
  191. package/dist/bundle/chunks/dispatch-MXG5YWLV.js +0 -9
  192. package/dist/bundle/chunks/google-generative-ai-UM5Y7T6B.js +0 -2
  193. package/dist/bundle/chunks/google-vertex-OKU2VQHL.js +0 -2
  194. package/dist/bundle/chunks/host-command-TDLLHXVI.js +0 -2
  195. package/dist/bundle/chunks/interactive-mode-OEPULLZK.js +0 -2
  196. package/dist/bundle/chunks/openai-responses-TNHQ6SJS.js +0 -3
  197. package/dist/bundle/chunks/rpc-mode-ETXQWEB2.js +0 -2
  198. package/dist/bundle/chunks/session-control-endpoint-4LEIDIVI.js +0 -3
@@ -0,0 +1,206 @@
1
+ import { COMPACT_LEAF_CODE_UNITS, CompactContentStore, } from "./compact-content-store.js";
2
+ class CompactTransitionError extends Error {
3
+ constructor(reason, detail) {
4
+ super(`compact transition rejected (${reason}): ${detail}`);
5
+ this.name = "CompactTransitionError";
6
+ this.reason = reason;
7
+ }
8
+ }
9
+ const byContentIndex = ([a], [b]) => a - b;
10
+ export class CompactSessionState {
11
+ constructor(options) {
12
+ this.settled = new Map();
13
+ this.active = new Map();
14
+ this.sequence = 0;
15
+ this.frozenTailBytes = 0;
16
+ this.store = new CompactContentStore(options);
17
+ }
18
+ get acceptedSequence() {
19
+ return this.sequence;
20
+ }
21
+ accept(transition) {
22
+ switch (transition.type) {
23
+ case "block_start":
24
+ this.start(transition);
25
+ break;
26
+ case "block_delta":
27
+ this.delta(this.block(transition), transition.offset, transition.text);
28
+ break;
29
+ case "block_replace":
30
+ this.replace(this.block(transition), transition.text);
31
+ break;
32
+ case "message_status":
33
+ this.setPersistence(transition.messageId, transition.persistence);
34
+ break;
35
+ case "message_remove":
36
+ this.remove(transition.messageId);
37
+ break;
38
+ }
39
+ return ++this.sequence;
40
+ }
41
+ freeze() {
42
+ const active = new Map();
43
+ for (const [messageId, blocks] of this.active)
44
+ active.set(messageId, this.seal(blocks, false));
45
+ const settled = new Map(this.settled);
46
+ for (const blocks of active.values())
47
+ this.pin(blocks, 1);
48
+ for (const row of settled.values())
49
+ this.pin(row.blocks, 1);
50
+ return { sequence: this.sequence, active, settled };
51
+ }
52
+ release(frozen) {
53
+ for (const blocks of frozen.active.values())
54
+ this.pin(blocks, -1);
55
+ for (const row of frozen.settled.values())
56
+ this.pin(row.blocks, -1);
57
+ }
58
+ /** Rejects with the storage error when committed content is gone; never returns empty content for it. */
59
+ async read(frozen) {
60
+ const messages = [];
61
+ for (const [messageId, row] of frozen.settled) {
62
+ messages.push({ messageId, persistence: row.persistence, blocks: await this.texts(row.blocks) });
63
+ }
64
+ for (const [messageId, blocks] of frozen.active) {
65
+ messages.push({ messageId, persistence: undefined, blocks: await this.texts(blocks) });
66
+ }
67
+ return { sequence: frozen.sequence, messages };
68
+ }
69
+ storageStatus() {
70
+ let residentTailBytes = 0;
71
+ let hotWitnessBytes = 0;
72
+ for (const blocks of this.active.values()) {
73
+ for (const block of blocks.values()) {
74
+ residentTailBytes += block.tail.length * 2;
75
+ hotWitnessBytes += block.witness.length * 2;
76
+ }
77
+ }
78
+ return {
79
+ ...this.store.status(),
80
+ residentTailBytes,
81
+ frozenTailBytes: this.frozenTailBytes,
82
+ hotWitnessBytes,
83
+ residentSettledRows: this.settled.size,
84
+ };
85
+ }
86
+ idle() {
87
+ return this.store.idle();
88
+ }
89
+ retire() {
90
+ return this.store.retire();
91
+ }
92
+ start(t) {
93
+ if (this.active.get(t.messageId)?.has(t.contentIndex) || this.settled.has(t.messageId)) {
94
+ throw new CompactTransitionError("duplicate_block", `${t.messageId}#${t.contentIndex}`);
95
+ }
96
+ const covered = t.coveredCodeUnits;
97
+ if (!Number.isInteger(covered) || (covered !== 0 && covered !== t.seed.length)) {
98
+ throw new CompactTransitionError("invalid_coverage", `${covered} of ${t.seed.length} seed code units`);
99
+ }
100
+ const emitted = covered > 0 ? 0 : t.seed.length;
101
+ const block = { kind: t.kind, leaves: [], tail: "", units: 0, emitted, covered, witness: "" };
102
+ block.witness = t.seed.slice(0, covered);
103
+ this.append(block, t.seed);
104
+ const blocks = this.active.get(t.messageId) ?? new Map();
105
+ blocks.set(t.contentIndex, block);
106
+ this.active.set(t.messageId, blocks);
107
+ }
108
+ /**
109
+ * Appends only the new suffix. Overlap exists only while a fully covered seed is being replayed
110
+ * (`emitted < covered == units`), so it always lies inside the producer-declared coverage.
111
+ */
112
+ delta(block, offset, text) {
113
+ if (offset !== block.emitted) {
114
+ throw new CompactTransitionError("non_contiguous", `offset ${offset}, expected ${block.emitted}`);
115
+ }
116
+ const overlapEnd = Math.min(offset + text.length, block.units);
117
+ const overlap = Math.max(0, overlapEnd - offset);
118
+ if (overlap > 0 && text.slice(0, overlap) !== block.witness.slice(offset, overlapEnd)) {
119
+ throw new CompactTransitionError("conflicting_overlap", `[${offset}, ${overlapEnd})`);
120
+ }
121
+ this.append(block, text.slice(overlap));
122
+ block.emitted = offset + text.length;
123
+ if (block.emitted >= block.covered)
124
+ block.witness = "";
125
+ }
126
+ replace(block, text) {
127
+ for (const id of block.leaves.splice(0))
128
+ this.store.adjust(id, -1, 0);
129
+ block.tail = "";
130
+ block.units = 0;
131
+ block.covered = 0;
132
+ block.witness = "";
133
+ this.append(block, text);
134
+ block.emitted = block.units;
135
+ }
136
+ setPersistence(messageId, persistence) {
137
+ const blocks = this.active.get(messageId);
138
+ if (blocks) {
139
+ this.settled.set(messageId, { persistence, blocks: this.seal(blocks, true) });
140
+ this.active.delete(messageId);
141
+ return;
142
+ }
143
+ const row = this.settled.get(messageId);
144
+ if (row) {
145
+ this.settled.set(messageId, { ...row, persistence });
146
+ }
147
+ else {
148
+ throw new CompactTransitionError("unknown_message", messageId);
149
+ }
150
+ }
151
+ remove(messageId) {
152
+ const blocks = this.active.get(messageId);
153
+ const row = this.settled.get(messageId);
154
+ if (!blocks && !row)
155
+ throw new CompactTransitionError("unknown_message", messageId);
156
+ for (const block of blocks?.values() ?? [])
157
+ for (const id of block.leaves)
158
+ this.store.adjust(id, -1, 0);
159
+ for (const block of row?.blocks ?? [])
160
+ for (const id of block.leaves)
161
+ this.store.adjust(id, -1, 0);
162
+ this.active.delete(messageId);
163
+ this.settled.delete(messageId);
164
+ }
165
+ block(address) {
166
+ const block = this.active.get(address.messageId)?.get(address.contentIndex);
167
+ if (!block)
168
+ throw new CompactTransitionError("unknown_block", `${address.messageId}#${address.contentIndex}`);
169
+ return block;
170
+ }
171
+ append(block, text) {
172
+ block.units += text.length;
173
+ let rest = text;
174
+ while (block.tail.length + rest.length >= COMPACT_LEAF_CODE_UNITS) {
175
+ const take = COMPACT_LEAF_CODE_UNITS - block.tail.length;
176
+ block.leaves.push(this.store.createLeaf(block.tail + rest.slice(0, take)));
177
+ block.tail = "";
178
+ rest = rest.slice(take);
179
+ }
180
+ block.tail += rest;
181
+ }
182
+ /** Settling mints the final tail into a spillable leaf; a freeze of an active block shares the tail. */
183
+ seal(blocks, settle) {
184
+ return [...blocks].sort(byContentIndex).map(([contentIndex, block]) => {
185
+ if (settle && block.tail.length > 0) {
186
+ block.leaves.push(this.store.createLeaf(block.tail));
187
+ block.tail = "";
188
+ }
189
+ return { contentIndex, kind: block.kind, leaves: [...block.leaves], tail: block.tail };
190
+ });
191
+ }
192
+ pin(blocks, delta) {
193
+ for (const block of blocks) {
194
+ this.frozenTailBytes += delta * block.tail.length * 2;
195
+ for (const id of block.leaves)
196
+ this.store.adjust(id, 0, delta);
197
+ }
198
+ }
199
+ async texts(blocks) {
200
+ return Promise.all(blocks.map(async ({ contentIndex, kind, leaves, tail }) => {
201
+ const parts = await Promise.all(leaves.map((id) => this.store.readLeaf(id)));
202
+ return { contentIndex, kind, text: parts.join("") + tail };
203
+ }));
204
+ }
205
+ }
206
+ //# sourceMappingURL=compact-session-state.js.map
@@ -0,0 +1,5 @@
1
+ /** Creates a fresh private epoch directory and returns the token its ownership marker holds. */
2
+ export declare function createSpoolDir(dir: string): Promise<string>;
3
+ /** Removes `dir` only while it is still the private, marked directory `createSpoolDir` made. */
4
+ export declare function removeSpoolDir(dir: string, token: string): Promise<void>;
5
+ //# sourceMappingURL=compact-spool-dir.d.ts.map
@@ -0,0 +1,61 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { lstat, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ /**
5
+ * Ownership of the compact spool's epoch directory (senpi#4272). The store creates the directory
6
+ * itself (0700, never adopting an existing one) and writes a private marker into it; removal
7
+ * re-checks the directory and that marker without following symlinks, so retirement never deletes
8
+ * a directory this store did not create and still own.
9
+ */
10
+ const MARKER = ".compact-spool-owner";
11
+ class CompactSpoolRefusedError extends Error {
12
+ constructor(reason, path) {
13
+ super(`compact spool directory refused (${reason}): ${path}`);
14
+ this.name = "CompactSpoolRefusedError";
15
+ this.reason = reason;
16
+ this.path = path;
17
+ }
18
+ }
19
+ /** Creates a fresh private epoch directory and returns the token its ownership marker holds. */
20
+ export async function createSpoolDir(dir) {
21
+ await mkdir(dirname(dir), { recursive: true, mode: 0o700 });
22
+ try {
23
+ await mkdir(dir, { mode: 0o700 });
24
+ }
25
+ catch (cause) {
26
+ if (cause.code === "EEXIST")
27
+ throw new CompactSpoolRefusedError("pre_existing", dir);
28
+ throw cause;
29
+ }
30
+ const token = randomUUID();
31
+ await writeFile(join(dir, MARKER), token, { flag: "wx", mode: 0o600 });
32
+ return token;
33
+ }
34
+ /** Removes `dir` only while it is still the private, marked directory `createSpoolDir` made. */
35
+ export async function removeSpoolDir(dir, token) {
36
+ const refusal = await refusalFor(dir, token);
37
+ if (refusal)
38
+ throw new CompactSpoolRefusedError(refusal, dir);
39
+ await rm(dir, { recursive: true, force: true });
40
+ }
41
+ async function refusalFor(dir, token) {
42
+ const owned = await lstat(dir);
43
+ if (owned.isSymbolicLink())
44
+ return "symlink";
45
+ if (!owned.isDirectory())
46
+ return "not_directory";
47
+ const markerPath = join(dir, MARKER);
48
+ const marker = await lstat(markerPath).catch(() => undefined);
49
+ if (!marker?.isFile() || (await readFile(markerPath, "utf8")) !== token)
50
+ return "marker_missing";
51
+ // Windows has no POSIX owner or mode bits; the marker token and the no-follow checks still apply.
52
+ const uid = process.getuid?.();
53
+ if (uid === undefined)
54
+ return undefined;
55
+ if (owned.uid !== uid || marker.uid !== uid)
56
+ return "foreign_owner";
57
+ if (((owned.mode | marker.mode) & 0o077) !== 0)
58
+ return "wide_mode";
59
+ return undefined;
60
+ }
61
+ //# sourceMappingURL=compact-spool-dir.js.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The asynchronous file seam of compact current-state storage (senpi#4272) and the failures it
3
+ * reports. Every spool access goes through `CompactStorageIo`; nothing here blocks the host loop.
4
+ */
5
+ export type CompactStorageOperation = "spool_create" | "spill_write" | "leaf_read" | "reclaim";
6
+ export interface CompactStorageFailure {
7
+ readonly operation: CompactStorageOperation;
8
+ readonly path: string;
9
+ readonly cause: string;
10
+ /** Accepted bytes this failure leaves resident in memory instead of on disk. */
11
+ readonly retainedInMemoryBytes: number;
12
+ }
13
+ /** Asynchronous file access the host supplies; the default is node's promise filesystem. */
14
+ export interface CompactStorageIo {
15
+ writeFile(path: string, data: Buffer): Promise<void>;
16
+ readFile(path: string): Promise<Buffer>;
17
+ unlink(path: string): Promise<void>;
18
+ }
19
+ export declare const nodeCompactStorageIo: CompactStorageIo;
20
+ export declare function describeCause(cause: unknown): string;
21
+ /** Committed storage that can no longer be read: a real error, never empty content. */
22
+ export declare class CompactStorageReadError extends Error {
23
+ readonly operation: CompactStorageOperation;
24
+ readonly path: string;
25
+ constructor(operation: CompactStorageOperation, path: string, cause: unknown);
26
+ }
27
+ //# sourceMappingURL=compact-storage-io.d.ts.map
@@ -0,0 +1,19 @@
1
+ import { readFile, unlink, writeFile } from "node:fs/promises";
2
+ export const nodeCompactStorageIo = {
3
+ writeFile: (path, data) => writeFile(path, data, { flag: "wx", mode: 0o600 }),
4
+ readFile: (path) => readFile(path),
5
+ unlink: (path) => unlink(path),
6
+ };
7
+ export function describeCause(cause) {
8
+ return cause instanceof Error ? cause.message : String(cause);
9
+ }
10
+ /** Committed storage that can no longer be read: a real error, never empty content. */
11
+ export class CompactStorageReadError extends Error {
12
+ constructor(operation, path, cause) {
13
+ super(`compact storage ${operation} failed for ${path}: ${describeCause(cause)}`, { cause });
14
+ this.name = "CompactStorageReadError";
15
+ this.operation = operation;
16
+ this.path = path;
17
+ }
18
+ }
19
+ //# sourceMappingURL=compact-storage-io.js.map
@@ -48,6 +48,8 @@ export interface RpcConnectionOptions {
48
48
  */
49
49
  export interface RpcConnectionSink {
50
50
  writeRaw(chunk: string): void;
51
+ /** Shared bindings accept correlated records synchronously, before JSONL buffering. */
52
+ writeRecord?(record: object): void;
51
53
  waitForBackpressure(): Promise<void>;
52
54
  /** Tears the transport down once its event queue can no longer deliver (overflow, write failure). */
53
55
  close?(): void;
@@ -57,6 +57,8 @@ import { protocolIdentity } from "./protocol-identity.js";
57
57
  import { buildRpcCommandsForSession, createCommandsChangedEvent, rpcCommandListDigest } from "./rpc-command-surface.js";
58
58
  import { rpcCommandPayloadError, rpcCommandShapeError, rpcMessageLengthError, sessionRetryFallbackError, } from "./rpc-input-validation.js";
59
59
  import { buildRpcSessionState } from "./rpc-session-state.js";
60
+ import { getSnapshot } from "./get-snapshot.js";
61
+ import { sessionUiAnswerLedger } from "./ui-answer-ledger.js";
60
62
  import { RPC_ERROR_EVAL_CELL_NOT_FOUND, RPC_ERROR_EVAL_CELLS_UNAVAILABLE, RPC_ERROR_INVALID_ARGUMENT, RPC_ERROR_INVALID_EVAL_CELL_REQUEST, RPC_ERROR_MEDIA_NOT_FOUND, RPC_ERROR_MODEL_NOT_FOUND, STEER_WHEN_IDLE_CAPABILITY, USER_ACTIVITY_CAPABILITY, } from "./rpc-types.js";
61
63
  import { runPauseForRun, runResumeForRun } from "./run-hold-commands.js";
62
64
  import { availableModelsData, interruptRunningTurn } from "./session-control-actions-data.js";
@@ -197,7 +199,9 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
197
199
  let loadedSurfacesDigest;
198
200
  let rpcCommandsDigest;
199
201
  let suppressLoadedSurfaceEvents = false;
200
- const eventOutput = createRpcEventOutputBuffer(sink.writeRaw, options.eventFlushScheduler);
202
+ const eventOutput = sink.writeRecord
203
+ ? undefined
204
+ : createRpcEventOutputBuffer(sink.writeRaw, options.eventFlushScheduler);
201
205
  const effectContext = () => ({
202
206
  session,
203
207
  by: { connectionId: options.clientInfo?.connectionId() ?? "local" },
@@ -205,10 +209,18 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
205
209
  });
206
210
  const tagSessionRecord = (value) => routingSessionId === undefined ? value : { ...value, sessionId: routingSessionId };
207
211
  const output = (obj) => {
208
- eventOutput.writeImmediate(tagSessionRecord(obj));
212
+ const record = tagSessionRecord(obj);
213
+ if (sink.writeRecord)
214
+ sink.writeRecord(record);
215
+ else
216
+ eventOutput?.writeImmediate(record);
209
217
  };
210
218
  const outputEvent = (event) => {
211
- eventOutput.enqueueEvent(tagSessionRecord(event));
219
+ const record = tagSessionRecord(event);
220
+ if (sink.writeRecord)
221
+ sink.writeRecord(record);
222
+ else
223
+ eventOutput?.enqueueEvent(record);
212
224
  };
213
225
  const currentLoadedSurfaces = () => {
214
226
  const extensions = loadedExtensions(session);
@@ -268,7 +280,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
268
280
  });
269
281
  });
270
282
  const waitForRpcBackpressure = async () => {
271
- eventOutput.flushEvents();
283
+ eventOutput?.flushEvents();
272
284
  await sink.waitForBackpressure();
273
285
  };
274
286
  const success = (id, command, data) => {
@@ -292,10 +304,15 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
292
304
  };
293
305
  };
294
306
  // Pending extension UI requests waiting for response
295
- const pendingExtensionRequests = new SessionExtensionUiRequests(() => session.refreshLifecycle());
307
+ const uiRequestChanged = (change) => {
308
+ session.refreshLifecycle();
309
+ if (change && routingSessionId !== undefined)
310
+ output(change);
311
+ };
312
+ const pendingExtensionRequests = new SessionExtensionUiRequests(uiRequestChanged);
296
313
  const grantRequests = new Map();
297
314
  sessionDialogRequests.set(session, pendingExtensionRequests);
298
- const questions = new ConnectionQuestionBridge(output, () => session.refreshLifecycle());
315
+ const questions = new ConnectionQuestionBridge(output, uiRequestChanged);
299
316
  let shutdownRequested = false;
300
317
  // In-flight OAuth logins, keyed by provider. login_start registers one; the
301
318
  // login promise clears it on completion; login_cancel aborts it. The flow is
@@ -858,6 +875,8 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
858
875
  capabilities: [
859
876
  ...new Set([
860
877
  "multi_session",
878
+ "snapshot_cursor",
879
+ "ui_answer_ledger",
861
880
  "session_lifecycle",
862
881
  AUTO_TITLE_SESSIONS_CAPABILITY,
863
882
  MEDIA_PLACEHOLDERS_CAPABILITY,
@@ -1080,6 +1099,18 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
1080
1099
  refreshGrantPresentation();
1081
1100
  return success(id, "set_client_info");
1082
1101
  }
1102
+ case "get_snapshot": {
1103
+ const snapshot = getSnapshot(session, command, {
1104
+ lastAbortSource,
1105
+ enqueue: (markerId) => {
1106
+ if (routingSessionId !== undefined)
1107
+ output({ type: "snapshot_marker", markerId });
1108
+ },
1109
+ });
1110
+ return "error" in snapshot
1111
+ ? error(id, "get_snapshot", snapshot.error, RPC_ERROR_INVALID_ARGUMENT)
1112
+ : success(id, "get_snapshot", snapshot.data);
1113
+ }
1083
1114
  case "get_state":
1084
1115
  return success(id, "get_state", buildRpcSessionState(session, lastAbortSource));
1085
1116
  case "request_self_settle":
@@ -1688,6 +1719,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
1688
1719
  questions,
1689
1720
  dialogs: pendingExtensionRequests,
1690
1721
  routed: routingSessionId !== undefined,
1722
+ ledger: sessionUiAnswerLedger(session),
1691
1723
  // An answer is a person's input by its entry point, whatever the connection role.
1692
1724
  beforeAnswer: () => session.admitHumanAnswer(),
1693
1725
  });
@@ -1719,7 +1751,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
1719
1751
  // A single-session connection hears its account's new limit windows as they are reported. A host
1720
1752
  // session's handler (it has a routing `sessionId`) does not: the host broadcasts the record once.
1721
1753
  const stopLimitEvents = options.sessionId === undefined
1722
- ? subscriptionLimits.subscribe((limits) => eventOutput.enqueueEvent({ type: "subscription_limits", ...limits }))
1754
+ ? subscriptionLimits.subscribe((limits) => outputEvent({ type: "subscription_limits", ...limits }))
1723
1755
  : () => { };
1724
1756
  const dispose = async (disposeOptions = {}) => {
1725
1757
  unbindMcpForms?.();
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionUIContext, QuestionRequest, QuestionResponse } from "../../core/extensions/types.ts";
2
+ import type { UiRequestChange } from "./snapshot-types.ts";
2
3
  import type { RpcExtensionUIProgress, RpcExtensionUIResponse, RpcQuestionUiRequest } from "./rpc-types.ts";
3
4
  type Options = Parameters<NonNullable<ExtensionUIContext["question"]>>[1];
4
5
  export declare const sessionQuestionBridges: WeakMap<object, ConnectionQuestionBridge>;
@@ -7,7 +8,7 @@ export declare class ConnectionQuestionBridge {
7
8
  private readonly resolved;
8
9
  private readonly output;
9
10
  private readonly changed;
10
- constructor(output: (record: object) => void, changed?: () => void);
11
+ constructor(output: (record: object) => void, changed?: (change?: UiRequestChange) => void);
11
12
  pendingQuestions(): RpcQuestionUiRequest[];
12
13
  ask(request: QuestionRequest, opts?: Options): Promise<QuestionResponse>;
13
14
  respond(response: RpcExtensionUIResponse, beforeAnswer?: () => void): boolean | "question_incomplete" | "question_already_resolved";
@@ -43,7 +43,11 @@ export class ConnectionQuestionBridge {
43
43
  clearTimeout(timer);
44
44
  opts?.signal?.removeEventListener("abort", cancel);
45
45
  this.pending.delete(id);
46
- this.changed?.();
46
+ this.changed?.({
47
+ type: "ui_request_settled",
48
+ uiRequestId: id,
49
+ disposition: status === "answered" || status === "comment-submitted" ? "answered" : "cancelled",
50
+ });
47
51
  this.resolved.add(id);
48
52
  const result = {
49
53
  status,
@@ -121,7 +125,7 @@ export class ConnectionQuestionBridge {
121
125
  },
122
126
  });
123
127
  arm();
124
- this.changed?.();
128
+ this.changed?.({ type: "ui_request_pending", uiRequestId: id, method: "question", request: frame });
125
129
  this.output(frame);
126
130
  opts?.signal?.addEventListener("abort", cancel, { once: true });
127
131
  if (opts?.signal?.aborted)
@@ -1,5 +1,6 @@
1
1
  import type { QuestionRequest, QuestionResponse } from "../../core/extensions/types.ts";
2
2
  import type { RpcExtensionUIResponse } from "./rpc-types.ts";
3
+ import type { UiAnswerLedger } from "./ui-answer-ledger.ts";
3
4
  /**
4
5
  * An `extension_ui_response` carries two ids: `id`, the frame's own correlation id that its reply
5
6
  * echoes, and `uiRequestId`, the extension UI request it answers. The short form every older client
@@ -23,6 +24,8 @@ export type ExtensionUiResponseReply = {
23
24
  readonly id: string;
24
25
  readonly type: "response";
25
26
  readonly command: "extension_ui_response";
27
+ /** The answer's outcome for its request; absent when a malformed answer was refused before settling anything. */
28
+ readonly disposition?: "applied" | "replay" | "already_answered" | "unknown";
26
29
  } & ({
27
30
  readonly success: true;
28
31
  } | {
@@ -37,13 +40,15 @@ type DialogAnswers = {
37
40
  };
38
41
  /**
39
42
  * Settles one response on a host connection and returns its reply, keyed by the frame `id`.
40
- * `undefined` only on an unrouted (single-session stdio) connection for a response no request here
41
- * matches: that connection has always ignored such a response, and still does.
43
+ * `undefined` only on an unrouted (single-session stdio) connection for an unnamed response no
44
+ * request here matches: that connection has always ignored such a response, and still does. A named
45
+ * response (`answerId`) asks for a receipt, so every connection answers it.
42
46
  */
43
47
  export declare function settleExtensionUiResponse(response: RpcExtensionUIResponse, targets: {
44
48
  readonly questions: QuestionAnswers;
45
49
  readonly dialogs: DialogAnswers;
46
50
  readonly routed: boolean;
51
+ readonly ledger?: UiAnswerLedger;
47
52
  readonly beforeAnswer?: () => void;
48
53
  }): ExtensionUiResponseReply | undefined;
49
54
  export {};
@@ -28,25 +28,45 @@ export function unansweredQuestionIds(questions, answers) {
28
28
  }
29
29
  /**
30
30
  * Settles one response on a host connection and returns its reply, keyed by the frame `id`.
31
- * `undefined` only on an unrouted (single-session stdio) connection for a response no request here
32
- * matches: that connection has always ignored such a response, and still does.
31
+ * `undefined` only on an unrouted (single-session stdio) connection for an unnamed response no
32
+ * request here matches: that connection has always ignored such a response, and still does. A named
33
+ * response (`answerId`) asks for a receipt, so every connection answers it.
33
34
  */
34
35
  export function settleExtensionUiResponse(response, targets) {
36
+ if (response.answerId !== undefined &&
37
+ (typeof response.answerId !== "string" || response.answerId.length < 1 || response.answerId.length > 256))
38
+ return refused(response.id, "invalid_answer_id");
35
39
  const requestId = answeredUiRequestId(response) ?? response.id;
40
+ const receipt = response.answerId === undefined ? undefined : targets.ledger?.lookup(requestId, response.answerId);
41
+ if (receipt === "replay")
42
+ return { ...reply(response.id), success: true, disposition: "replay" };
43
+ if (receipt === "already_answered")
44
+ return { ...reply(response.id), success: false, error: "already_answered", disposition: receipt };
36
45
  const answer = requestId === response.id ? response : { ...response, id: requestId };
37
46
  const question = targets.questions.respond(answer, targets.beforeAnswer);
38
47
  if (typeof question === "string")
39
- return refused(response.id, question);
40
- if (question || targets.dialogs.resolve(answer, targets.beforeAnswer))
41
- return { ...reply(response.id), success: true };
48
+ return refused(response.id, response.answerId !== undefined && question === "question_already_resolved"
49
+ ? "unknown_extension_ui_request"
50
+ : question);
51
+ if (question || targets.dialogs.resolve(answer, targets.beforeAnswer)) {
52
+ targets.ledger?.record(requestId, response.answerId);
53
+ return { ...reply(response.id), success: true, disposition: "applied" };
54
+ }
42
55
  // A routed binding owns exactly one session's request map: an unmatched response is a protocol
43
56
  // error for its sender, never a cross-session match.
44
- return targets.routed ? refused(response.id, "unknown_extension_ui_request") : undefined;
57
+ return targets.routed || response.answerId !== undefined
58
+ ? refused(response.id, "unknown_extension_ui_request")
59
+ : undefined;
45
60
  }
46
61
  function reply(id) {
47
62
  return { id, type: "response", command: "extension_ui_response" };
48
63
  }
49
64
  function refused(id, error) {
50
- return { ...reply(id), success: false, error };
65
+ const disposition = error === "question_already_resolved"
66
+ ? "already_answered"
67
+ : error === "unknown_extension_ui_request"
68
+ ? "unknown"
69
+ : undefined;
70
+ return { ...reply(id), success: false, error, ...(disposition === undefined ? {} : { disposition }) };
51
71
  }
52
72
  //# sourceMappingURL=extension-ui-response.js.map
@@ -0,0 +1,18 @@
1
+ import type { AgentAbortSource } from "../../core/agent-abort-provenance.ts";
2
+ import type { AgentSession } from "../../core/agent-session.ts";
3
+ import type { RpcSnapshot } from "./snapshot-types.ts";
4
+ type SnapshotRequest = {
5
+ readonly tail?: unknown;
6
+ readonly maxBytes?: unknown;
7
+ };
8
+ /** Capture and mark on the source loop, before any response-writing await. */
9
+ export declare function getSnapshot(session: AgentSession, request: SnapshotRequest, cursor: {
10
+ readonly enqueue: (markerId: string) => void;
11
+ readonly lastAbortSource?: AgentAbortSource;
12
+ }): {
13
+ readonly data: RpcSnapshot;
14
+ } | {
15
+ readonly error: string;
16
+ };
17
+ export {};
18
+ //# sourceMappingURL=get-snapshot.d.ts.map
@@ -0,0 +1,43 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { describeUsageLimitEntry } from "../../core/retry-fallback/usage-limit.js";
3
+ import { currentResponseByteBudget, DEFAULT_ENTRY_PAGE_BYTES } from "./get-entries-page.js";
4
+ import { buildRpcSessionState } from "./rpc-session-state.js";
5
+ /** Capture and mark on the source loop, before any response-writing await. */
6
+ export function getSnapshot(session, request, cursor) {
7
+ const tail = request.tail === undefined ? 100 : request.tail;
8
+ const maxBytes = request.maxBytes === undefined ? DEFAULT_ENTRY_PAGE_BYTES : request.maxBytes;
9
+ if (typeof tail !== "number" || !Number.isSafeInteger(tail) || tail < 0)
10
+ return { error: "tail must be a non-negative integer" };
11
+ if (typeof maxBytes !== "number" || !Number.isSafeInteger(maxBytes) || maxBytes <= 0)
12
+ return { error: "maxBytes must be a positive integer" };
13
+ const state = buildRpcSessionState(session, cursor.lastAbortSource);
14
+ const history = session.sessionManager.getEntries();
15
+ const entries = [];
16
+ let bytes = 2;
17
+ for (let i = history.length - 1; i >= 0 && entries.length < tail; i--) {
18
+ const entry = history[i];
19
+ if (entry === undefined)
20
+ break;
21
+ const projected = describeUsageLimitEntry(entry);
22
+ const size = Buffer.byteLength(JSON.stringify(projected)) + (entries.length > 0 ? 1 : 0);
23
+ if (bytes + size > Math.min(maxBytes, currentResponseByteBudget()))
24
+ break;
25
+ entries.push(projected);
26
+ bytes += size;
27
+ }
28
+ entries.reverse();
29
+ const history_truncated = entries.length < history.length;
30
+ const markerId = randomUUID();
31
+ // Some state fields and entry payloads are live references: detach them before yielding, too.
32
+ const data = structuredClone({
33
+ state,
34
+ entries,
35
+ history_truncated,
36
+ next_before: history_truncated ? (entries[0]?.id ?? null) : null,
37
+ markerId,
38
+ });
39
+ session.releaseFullHistoryView();
40
+ cursor.enqueue(markerId);
41
+ return { data };
42
+ }
43
+ //# sourceMappingURL=get-snapshot.js.map
@@ -20,6 +20,9 @@ export function multiSessionHostCapabilities(options) {
20
20
  return [
21
21
  ...new Set([
22
22
  "multi_session",
23
+ "snapshot_cursor",
24
+ "ui_answer_ledger",
25
+ "dialog_observer",
23
26
  ...(options.mcpPresenters ? ["mcp_form_presenter_v1"] : []),
24
27
  "session_lifecycle",
25
28
  "expected_host_instance",
@@ -63,6 +63,7 @@ export async function startHost(paths, socket, options, generation = 0) {
63
63
  socket,
64
64
  ...(options.owner ? ["--owner-fd", String(OWNER_WATCH_FD)] : []),
65
65
  ...(options.presenterBootstrap ? ["--mcp-presenter-fd", "4"] : []),
66
+ ...(options.mcpPresenterEnforcement ? ["--mcp-presenter-enforcement"] : []),
66
67
  ...(options.hostArgs ?? []),
67
68
  ];
68
69
  const launch = testOptions?.spawn ?? testOptions?.launch?.(supervisorArgs) ?? defaultHostLaunch(supervisorArgs);