@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
@@ -1,6 +1,6 @@
1
1
  import { MEDIA_PLACEHOLDERS_CAPABILITY } from "./custom-capability.js";
2
2
  import { serializeJsonLine } from "./jsonl.js";
3
- import { omitInlineMedia } from "./media-placeholders.js";
3
+ import { carriesMedia, omitNormalInlineMedia } from "./media-placeholders.js";
4
4
  import { SocketEventSinkActor } from "./socket-event-fanout.js";
5
5
  import { EVENT_RESYNC_CAPABILITY } from "./socket-peer-recovery.js";
6
6
  const BROADCAST_LIFECYCLE_RECORDS = new Set([
@@ -15,9 +15,11 @@ const BROADCAST_LIFECYCLE_RECORDS = new Set([
15
15
  function snapshotPlaceholderLine(record) {
16
16
  if (record.placeholderLine !== undefined)
17
17
  return record.placeholderLine;
18
- const redacted = record.source === undefined ? undefined : omitInlineMedia(record.source);
19
- record.placeholderLine =
20
- redacted === undefined || redacted === record.source ? record.line : serializeJsonLine(redacted);
18
+ if (!record.mayCarryMedia)
19
+ return record.line;
20
+ const source = JSON.parse(record.line);
21
+ const redacted = omitNormalInlineMedia(source);
22
+ record.placeholderLine = redacted === source ? record.line : serializeJsonLine(redacted);
21
23
  return record.placeholderLine;
22
24
  }
23
25
  export class SessionEventFanout {
@@ -147,13 +149,20 @@ export class SessionEventFanout {
147
149
  if (this.connectionSessions.get(id)?.has(sessionId))
148
150
  actor.enqueue(line);
149
151
  }
150
- rememberSnapshot(sessionId, value, line, placeholderLine, source) {
152
+ rememberSnapshot(sessionId, value, line, placeholderLine) {
153
+ const record = {
154
+ line,
155
+ placeholderLine,
156
+ hasMessage: value.message != null,
157
+ mayCarryMedia: carriesMedia(value),
158
+ };
151
159
  if (value.type === "session_lifecycle") {
152
- this.lifecycleSnapshots.set(sessionId, { line, placeholderLine, source });
160
+ this.lifecycleSnapshots.set(sessionId, record);
153
161
  return;
154
162
  }
155
163
  if (value.type === "extension_ui_request" && value.method === "question" && typeof value.id === "string") {
156
164
  const pending = this.pendingQuestions.get(sessionId) ?? new Map();
165
+ // Writer acceptance already detached and JSON-normalized the frame.
157
166
  pending.set(value.id, { ...value });
158
167
  this.pendingQuestions.set(sessionId, pending);
159
168
  return;
@@ -169,7 +178,6 @@ export class SessionEventFanout {
169
178
  return;
170
179
  }
171
180
  const event = value.assistantMessageEvent;
172
- const record = { line, placeholderLine, source };
173
181
  if (value.type === "message_start" || (value.type === "message_update" && event?.type === "text_start")) {
174
182
  this.sessionSnapshots.set(sessionId, [record]);
175
183
  }
@@ -200,7 +208,7 @@ export class SessionEventFanout {
200
208
  const current = this.connectionSessions.get(id) ?? new Set();
201
209
  return [...new Set([...attached, ...current])].map((sessionId) => {
202
210
  const records = this.sessionSnapshots.get(sessionId) ?? [];
203
- const latest = records.findLast((record) => record.source?.message != null);
211
+ const latest = records.findLast((record) => record.hasMessage);
204
212
  // The producer may mutate its partial object after emission. The serialized
205
213
  // record, not that object, fixes the recovery cut before later deltas enqueue.
206
214
  const frozen = latest === undefined ? undefined : JSON.parse(latest.line);
@@ -211,7 +219,7 @@ export class SessionEventFanout {
211
219
  message: typeof frozen === "object" && frozen !== null && "message" in frozen ? frozen.message : null,
212
220
  ...(!current.has(sessionId) ? { closed: true } : {}),
213
221
  };
214
- return placeholders ? omitInlineMedia(record) : record;
222
+ return placeholders ? omitNormalInlineMedia(record) : record;
215
223
  });
216
224
  }
217
225
  }
@@ -2,8 +2,9 @@ import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { bindPrivateMcpForms } from "../../core/mcp-form-types.js";
3
3
  import { McpPresenterAuthority } from "./mcp-presenter-authority.js";
4
4
  import { MEDIA_PLACEHOLDERS_CAPABILITY } from "./custom-capability.js";
5
+ import { normalizeJsonValue } from "./json-normal-value.js";
5
6
  import { serializeJsonLine } from "./jsonl.js";
6
- import { omitInlineMedia } from "./media-placeholders.js";
7
+ import { omitNormalInlineMedia } from "./media-placeholders.js";
7
8
  import { RpcSecurityPresentations } from "./security-presentations.js";
8
9
  import { SessionEventFanout } from "./session-event-fanout.js";
9
10
  import { SessionOpenTurns } from "./session-open-turns.js";
@@ -330,7 +331,7 @@ export class SessionEventWriter {
330
331
  if (this.sealedSessions.has(sessionId))
331
332
  return false;
332
333
  const targetId = this.connectionContext.getStore();
333
- const record = value;
334
+ const record = normalizeJsonValue(value).value;
334
335
  const isTargeted = record.type === "response" ||
335
336
  record.type === "bash_execution_update" ||
336
337
  // Relocation records belong to the one connection that offers it (session-relocation-channel.ts).
@@ -338,8 +339,22 @@ export class SessionEventWriter {
338
339
  record.type === "relocation_due" ||
339
340
  (record.type === "extension_ui_request" &&
340
341
  ["select", "confirm", "input", "editor"].includes(String(record.method)));
341
- const tagged = { ...value, sessionId };
342
+ const tagged = { ...record, sessionId };
342
343
  const line = serializeJsonLine(tagged);
344
+ // Observation records are per-subscription opt-ins on the subscriber's own actor FIFO: never attachments,
345
+ // the attach replay, response broadcast, or the stdio lane, which cannot subscribe.
346
+ if (record.type === "snapshot_marker" ||
347
+ record.type === "ui_request_pending" ||
348
+ record.type === "ui_request_settled") {
349
+ const targets = record.type === "snapshot_marker"
350
+ ? this.status.mirrorTargets(sessionId, "snapshot_marker").filter((id) => id === targetId)
351
+ : this.status.dialogTargets(sessionId);
352
+ for (const target of targets)
353
+ this.fanout.get(target)?.actor.enqueue(line);
354
+ if (targets.length > 0)
355
+ this.requestFlush();
356
+ return true;
357
+ }
343
358
  if (this.fanout.isEmpty() && this.exceedsStdioCapacity(line)) {
344
359
  this.closeSession(sessionId, {
345
360
  type: "response",
@@ -357,10 +372,12 @@ export class SessionEventWriter {
357
372
  // A record is only walked and re-serialized when a target asked for placeholders;
358
373
  // otherwise this is byte-for-byte today's path, with serializeJsonLine called once.
359
374
  const hasPlaceholderTarget = targets.some((target) => this.fanout.connectionHas(target, MEDIA_PLACEHOLDERS_CAPABILITY));
360
- const redacted = hasPlaceholderTarget ? omitInlineMedia(tagged, this.mediaPersisters.get(sessionId)) : tagged;
375
+ const redacted = hasPlaceholderTarget
376
+ ? omitNormalInlineMedia(tagged, this.mediaPersisters.get(sessionId))
377
+ : tagged;
361
378
  const placeholderLine = redacted === tagged ? undefined : serializeJsonLine(redacted);
362
379
  if (!isTargeted)
363
- this.fanout.rememberSnapshot(sessionId, tagged, line, placeholderLine, tagged);
380
+ this.fanout.rememberSnapshot(sessionId, tagged, line, placeholderLine);
364
381
  for (const target of targets) {
365
382
  if (target !== undefined && !this.fanout.get(target))
366
383
  continue;
@@ -1,3 +1,4 @@
1
+ import type { UiRequestChange } from "./snapshot-types.ts";
1
2
  import type { RpcExtensionUIRequest, RpcExtensionUIResponse } from "./rpc-types.ts";
2
3
  type PendingRequest = {
3
4
  resolve: (value: RpcExtensionUIResponse) => void;
@@ -9,7 +10,7 @@ export declare const sessionDialogRequests: WeakMap<object, SessionExtensionUiRe
9
10
  export declare class SessionExtensionUiRequests {
10
11
  private readonly pending;
11
12
  private readonly changed;
12
- constructor(changed?: () => void);
13
+ constructor(changed?: (change?: UiRequestChange) => void);
13
14
  set(id: string, request: PendingRequest): void;
14
15
  delete(id: string): void;
15
16
  snapshot(): RpcExtensionUIRequest[];
@@ -7,11 +7,13 @@ export class SessionExtensionUiRequests {
7
7
  }
8
8
  set(id, request) {
9
9
  this.pending.set(id, request);
10
- this.changed?.();
10
+ this.changed?.(request.frame
11
+ ? { type: "ui_request_pending", uiRequestId: id, method: request.frame.method, request: request.frame }
12
+ : undefined);
11
13
  }
12
14
  delete(id) {
13
- this.pending.delete(id);
14
- this.changed?.();
15
+ if (this.pending.delete(id))
16
+ this.changed?.({ type: "ui_request_settled", uiRequestId: id, disposition: "cancelled" });
15
17
  }
16
18
  snapshot() {
17
19
  return [...this.pending.values()].flatMap((request) => (request.frame ? [request.frame] : []));
@@ -22,22 +24,25 @@ export class SessionExtensionUiRequests {
22
24
  return false;
23
25
  beforeAnswer?.();
24
26
  this.pending.delete(response.id);
25
- this.changed?.();
27
+ this.changed?.({
28
+ type: "ui_request_settled",
29
+ uiRequestId: response.id,
30
+ disposition: "cancelled" in response ? "cancelled" : "answered",
31
+ });
26
32
  request.resolve(response);
27
33
  return true;
28
34
  }
29
35
  cancelAll() {
30
- for (const [id, request] of this.pending)
36
+ for (const [id, request] of this.pending) {
37
+ this.delete(id);
31
38
  request.resolve({ type: "extension_ui_response", id, cancelled: true });
32
- this.pending.clear();
33
- this.changed?.();
39
+ }
34
40
  }
35
41
  close() {
36
- for (const request of this.pending.values()) {
42
+ for (const [id, request] of this.pending) {
43
+ this.delete(id);
37
44
  request.reject(new Error("Extension UI request cancelled: session closed"));
38
45
  }
39
- this.pending.clear();
40
- this.changed?.();
41
46
  }
42
47
  }
43
48
  //# sourceMappingURL=session-extension-ui-requests.js.map
@@ -9,6 +9,7 @@ import { RpcSessionRegistryError } from "./session-registry-types.js";
9
9
  // a read or a control needed to finish/cancel an already-admitted turn.
10
10
  const READ_ONLY_OR_CONTROL = new Set([
11
11
  "get_state",
12
+ "get_snapshot",
12
13
  "get_effect_status",
13
14
  "get_pending_inputs",
14
15
  "get_steering_messages",
@@ -67,7 +67,8 @@ export declare class SessionStatusStream {
67
67
  publishEffect(sessionId: string, status: EffectStatus): void;
68
68
  seal(sessionId: string, terminal: SessionStatusTerminal, cause: SessionStatusCause, by: SessionStatusActor): void;
69
69
  forget(sessionId: string): void;
70
- mirrorTargets(sessionId: string, recordType: string): readonly string[];
70
+ dialogTargets(sessionId: string): readonly string[];
71
+ mirrorTargets(sessionId: string, recordType: string, dialogs?: boolean): readonly string[];
71
72
  matchingConnections(sessionId: string): ReadonlyMap<string, boolean>;
72
73
  /** The live view, falling back to the last one seen once the registry has dropped a sealed entry. */
73
74
  private currentView;
@@ -171,8 +171,14 @@ export class SessionStatusStream {
171
171
  this.lastViews.delete(sessionId);
172
172
  this.lastChanges.delete(sessionId);
173
173
  }
174
- mirrorTargets(sessionId, recordType) {
175
- const subscriptions = [...this.pendings.values(), ...this.subscriptions.values()].filter((subscription) => mirrorsRecord(subscription.options.mirror, recordType));
174
+ dialogTargets(sessionId) {
175
+ return this.mirrorTargets(sessionId, "ui_request_pending", true);
176
+ }
177
+ mirrorTargets(sessionId, recordType, dialogs = false) {
178
+ const subscriptions = [...this.pendings.values(), ...this.subscriptions.values()].filter((subscription) => (dialogs ? subscription.options.dialogs === true : true) &&
179
+ (recordType === "snapshot_marker" || dialogs
180
+ ? subscription.options.mirror.kind !== "none"
181
+ : mirrorsRecord(subscription.options.mirror, recordType)));
176
182
  if (subscriptions.length === 0)
177
183
  return [];
178
184
  const entry = this.source?.entryView(sessionId) ?? this.lastViews.get(sessionId)?.entry;
@@ -29,6 +29,7 @@ export interface SubscriptionOptions {
29
29
  readonly includeContext: boolean;
30
30
  readonly mirror: MirrorSelection;
31
31
  readonly effects: boolean;
32
+ readonly dialogs?: boolean;
32
33
  }
33
34
  export interface SubscriptionRefusal {
34
35
  readonly reason: string;
@@ -36,7 +37,7 @@ export interface SubscriptionRefusal {
36
37
  }
37
38
  export declare function parseSubscription(request: Readonly<Pick<Extract<RpcCommand, {
38
39
  type: "subscribe_sessions";
39
- }>, "sessions" | "mirror" | "include_workers" | "include_context" | "effects">>): {
40
+ }>, "sessions" | "mirror" | "include_workers" | "include_context" | "effects" | "dialogs">>): {
40
41
  readonly options: SubscriptionOptions;
41
42
  } | {
42
43
  readonly refusal: SubscriptionRefusal;
@@ -59,12 +59,16 @@ export function parseSubscription(request) {
59
59
  const includeContext = optionalBoolean(request.include_context, "include_context");
60
60
  if (typeof includeContext === "object")
61
61
  return { refusal: includeContext };
62
+ const dialogs = optionalBoolean(request.dialogs, "dialogs");
63
+ if (typeof dialogs === "object")
64
+ return { refusal: dialogs };
62
65
  const effects = optionalBoolean(request.effects, "effects");
63
66
  if (typeof effects === "object")
64
67
  return { refusal: effects };
65
68
  return {
66
69
  options: {
67
70
  selector: selector.selector,
71
+ dialogs: dialogs ?? false,
68
72
  // A childrenOf subscription exists to watch machine-driven children, which open as workers.
69
73
  includeWorkers: includeWorkers ?? selector.selector.kind === "childrenOf",
70
74
  includeContext: includeContext ?? false,
@@ -76,7 +76,8 @@ class WorkerEventWriter extends SessionEventWriter {
76
76
  }));
77
77
  }
78
78
  enqueue(_sessionId, record) {
79
- if (Buffer.byteLength(JSON.stringify(record)) > SESSION_WORKER_LIMITS.outputBytes)
79
+ const line = JSON.stringify(record);
80
+ if (Buffer.byteLength(line) > SESSION_WORKER_LIMITS.outputBytes)
80
81
  failWorker("session_worker_output_limit");
81
82
  const session = entry?.runtime?.session;
82
83
  if (!session)
@@ -97,7 +98,9 @@ class WorkerEventWriter extends SessionEventWriter {
97
98
  failWorker("session_worker_snapshot_limit");
98
99
  exchange((signal) => ({
99
100
  type: "output",
100
- record,
101
+ // Keep one JSON-normal transfer format, including toJSON and non-cloneable fields.
102
+ // S12 replaces this whole-record serialization with compact records and permits.
103
+ record: JSON.parse(line),
101
104
  connection: this.currentConnection(),
102
105
  signal,
103
106
  activity,
@@ -0,0 +1,29 @@
1
+ import type { SessionEntry } from "../../core/session-manager.ts";
2
+ import type { RpcExtensionUIRequest } from "./rpc-question-types.ts";
3
+ import type { RpcSessionState } from "./rpc-session-state-types.ts";
4
+ export type RpcSnapshot = {
5
+ readonly state: RpcSessionState;
6
+ readonly entries: SessionEntry[];
7
+ readonly next_before: string | null;
8
+ readonly history_truncated: boolean;
9
+ readonly markerId: string;
10
+ };
11
+ export type RpcSnapshotMarker = {
12
+ readonly type: "snapshot_marker";
13
+ readonly sessionId: string;
14
+ readonly markerId: string;
15
+ };
16
+ export type UiRequestChange = {
17
+ readonly type: "ui_request_pending";
18
+ readonly uiRequestId: string;
19
+ readonly method: RpcExtensionUIRequest["method"];
20
+ readonly request: RpcExtensionUIRequest;
21
+ } | {
22
+ readonly type: "ui_request_settled";
23
+ readonly uiRequestId: string;
24
+ readonly disposition: "answered" | "cancelled";
25
+ };
26
+ export type RpcUiRequestEvent = UiRequestChange & {
27
+ readonly sessionId: string;
28
+ };
29
+ //# sourceMappingURL=snapshot-types.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=snapshot-types.js.map
@@ -0,0 +1,13 @@
1
+ /** Session-generation receipts: deliberately never persisted to the transcript. */
2
+ export type UiAnswerReceipt = {
3
+ readonly uiRequestId: string;
4
+ readonly answerId?: string;
5
+ };
6
+ export declare class UiAnswerLedger {
7
+ private readonly answers;
8
+ lookup(uiRequestId: string, answerId: string | undefined): "replay" | "already_answered" | undefined;
9
+ record(uiRequestId: string, answerId: string | undefined): void;
10
+ snapshot(): UiAnswerReceipt[];
11
+ }
12
+ export declare function sessionUiAnswerLedger(session: object): UiAnswerLedger;
13
+ //# sourceMappingURL=ui-answer-ledger.d.ts.map
@@ -0,0 +1,34 @@
1
+ export class UiAnswerLedger {
2
+ constructor() {
3
+ this.answers = new Map();
4
+ }
5
+ lookup(uiRequestId, answerId) {
6
+ const receipt = this.answers.get(uiRequestId);
7
+ if (!receipt)
8
+ return undefined;
9
+ return answerId !== undefined && answerId === receipt.answerId ? "replay" : "already_answered";
10
+ }
11
+ record(uiRequestId, answerId) {
12
+ if (this.answers.has(uiRequestId))
13
+ return;
14
+ this.answers.set(uiRequestId, { uiRequestId, ...(answerId === undefined ? {} : { answerId }) });
15
+ if (this.answers.size > 1024) {
16
+ const oldest = this.answers.keys().next();
17
+ if (!oldest.done)
18
+ this.answers.delete(oldest.value);
19
+ }
20
+ }
21
+ snapshot() {
22
+ return [...this.answers.values()].slice(-64).map((receipt) => ({ ...receipt }));
23
+ }
24
+ }
25
+ const ledgers = new WeakMap();
26
+ export function sessionUiAnswerLedger(session) {
27
+ let ledger = ledgers.get(session);
28
+ if (!ledger) {
29
+ ledger = new UiAnswerLedger();
30
+ ledgers.set(session, ledger);
31
+ }
32
+ return ledger;
33
+ }
34
+ //# sourceMappingURL=ui-answer-ledger.js.map
package/dist/senpi CHANGED
@@ -10,7 +10,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
10
10
  import "./valid-cwd.js";
11
11
  import "./internal-role.js";
12
12
  import { spawn, spawnSync } from "node:child_process";
13
- import { existsSync, realpathSync } from "node:fs";
13
+ import { existsSync, fstatSync, realpathSync } from "node:fs";
14
14
  import { dirname, join } from "node:path";
15
15
  import { fileURLToPath } from "node:url";
16
16
  import { processBunRuntimeOptions, resolveBunReexec } from "./bun-runtime.js";
@@ -20,6 +20,24 @@ import { APP_NAME, DISPLAY_VERSION, findNodePackageDir, getAgentDir, getInstallP
20
20
  import { hasInheritedInspectorOption, releaseInheritedInspectorForChild } from "./inspector-policy.js";
21
21
  import { prepareRuntimeSnapshot } from "./runtime-snapshot/enter.js";
22
22
  import { handleBootstrapSelfUpdate } from "./self-update-bootstrap.js";
23
+ /** Only a presenter-enabled launch may carry its reserved private channel through CLI re-execution. */
24
+ function reexecStdio() {
25
+ const argv = process.argv.slice(2);
26
+ const presenter = argv.indexOf("--mcp-presenter-fd");
27
+ if (presenter < 0 || argv[presenter + 1] !== "4")
28
+ return "inherit";
29
+ try {
30
+ const stat = fstatSync(4);
31
+ if (!stat.isSocket() && !stat.isFIFO())
32
+ return "inherit";
33
+ }
34
+ catch {
35
+ return "inherit";
36
+ }
37
+ const owner = argv.indexOf("--owner-fd");
38
+ const lifetime = (owner >= 0 && argv[owner + 1] === "3") || process.env.SENPI_RPC_HOST_WATCH_FD === "3";
39
+ return ["inherit", "inherit", "inherit", lifetime ? 3 : "ignore", 4];
40
+ }
23
41
  // Upstream's `cli/setup.ts` helper is deliberately not used here: this launcher only decides the
24
42
  // runtime and process structure, and `cli-main.ts` performs the equivalent process/title/env/http
25
43
  // setup for both entry paths (Node launcher and the Bun binary).
@@ -52,7 +70,7 @@ function reexecUnderBunIfNeeded() {
52
70
  return false;
53
71
  }
54
72
  const result = spawnSync(decision.bunPath, [scriptRealPath, ...process.argv.slice(2)], {
55
- stdio: "inherit",
73
+ stdio: reexecStdio(),
56
74
  windowsHide: true,
57
75
  });
58
76
  if (result.signal) {
@@ -125,7 +143,7 @@ async function spawnFullCli() {
125
143
  return await new Promise((resolve, reject) => {
126
144
  const child = spawn(process.execPath, [...process.execArgv, fullCliPath, ...args], {
127
145
  env: childEnvironment,
128
- stdio: "inherit",
146
+ stdio: reexecStdio(),
129
147
  });
130
148
  child.on("error", (error) => {
131
149
  reject(error);
package/docs/mcp.md CHANGED
@@ -386,6 +386,26 @@ above.
386
386
 
387
387
  ## Resources and prompts
388
388
 
389
+ MCP 2026-07-28 tool schemas use JSON Schema 2020-12 by default. Explicit
390
+ 2020-12 declarations are supported; other dialect declarations produce
391
+ `MCP_SCHEMA_UNSUPPORTED_DIALECT`. Tool discovery, the disk cache, and MCP
392
+ exposure retain the full schema, including `$defs`, `$ref` siblings, and
393
+ `additionalProperties`. Boolean roots use an equivalent `allOf` container
394
+ at the object-shaped model-tool API boundary.
395
+
396
+ Modern structured results may be any JSON value, including `null`, `false`,
397
+ and `0`. Successful results with an `outputSchema` must contain conforming
398
+ `structuredContent`; execution-error results are exempt. Validation never
399
+ coerces values, inserts defaults, removes properties, or fetches references
400
+ from the network. Unresolved references are rejected. Schemas are bounded
401
+ to 64 nesting levels and 16,384 JSON nodes; `format` is an annotation.
402
+
403
+ Object-only model providers may require a lossy schema adaptation in the AI
404
+ provider layer. That wire adaptation does not change MCP's source schema
405
+ or the arguments sent to the server: modern calls validate the model's
406
+ arguments without coercion. Legacy connections retain the SDK's existing
407
+ schema validation and compatibility conversion.
408
+
389
409
  - `mcp_list_resources` / `mcp_read_resource` register automatically when a
390
410
  connected server lists resources.
391
411
  - Mention `@mcp:<server>/<uri>` in your prompt to inline a resource's content
package/docs/rpc.md CHANGED
@@ -1644,6 +1644,7 @@ A multi-session host advertising the capability `session_status` pushes one `ses
1644
1644
  - `include_context`: carry each session's published `context` on its records; default `false`.
1645
1645
  - `mirror`: `"none"` (default), `"all"` or an array of record types (`["message_update", "tool_execution_start"]`). The matched sessions' records of those types are also delivered to this connection. Mirroring is an explicit opt-in open to every subscriber of the host (the socket is per user, mode 0600): it is not an attachment and does not keep a session alive. Worker content is mirrored only with `include_workers: true`.
1646
1646
  - `effects`: also deliver `effect_status` receipts of matched sessions; default `true` for `childrenOf`, `false` otherwise. Each `effect_status` is enqueued after the record that caused the transition and shares the subscription's `seq`.
1647
+ - `dialogs`: with capability `dialog_observer`, also deliver the `ui_request_pending` and `ui_request_settled` records of matched sessions; default `false`. It takes effect only together with a `mirror` selection, stays observing, and grants no answering authority (see "UI-answer receipts and dialog observation").
1647
1648
 
1648
1649
  Response:
1649
1650
 
@@ -3425,6 +3426,14 @@ If an extension cancelled:
3425
3426
 
3426
3427
  ### State
3427
3428
 
3429
+ #### get_snapshot
3430
+
3431
+ Atomic snapshot cursor (capability `snapshot_cursor`). Subscribe with `mirror` before taking a snapshot to cut the live record stream at a source-issued cursor rather than at response arrival. Terminal control endpoints support the same command.
3432
+
3433
+ `get_snapshot {sessionId, tail?, maxBytes?, observe?: true}` returns `{state, entries, next_before, history_truncated, markerId}`. `state` uses the existing `get_state` projection, including `pendingDialogs` and `pendingQuestions`. `entries` is the newest tail, in chronological order. `tail` defaults to 100 and accepts non-negative integers; `maxBytes` defaults to 8 MiB and accepts positive integers, capped at 48 MiB. The byte bound applies to serialized history entries. An entry that does not fit ends the tail; it is never silently skipped. `history_truncated` reports omitted history; `next_before` is the oldest returned entry's id when older history remains, otherwise null (also null when no entry fits).
3434
+
3435
+ State capture, history capture, and enqueueing `{type: "snapshot_marker", sessionId, markerId}` are one synchronous operation. The marker goes only to the requesting connection, only when a matching subscription has `mirror` enabled (including a list of selected record types). Other subscribers never receive that marker. Apply the snapshot and then records after its marker; a post-marker record can arrive **before** the correlated response. A reader without a mirrored subscription receives the snapshot response but no marker record. `observe: true` does not attach or refresh activity.
3436
+
3428
3437
  #### get_state
3429
3438
 
3430
3439
  Get current session state.
@@ -5490,6 +5499,22 @@ With enforcement, only the authority's winning whole-form action admits once.
5490
5499
  The host advertises `mcp_form_presenter_v1` only when enforcement is enabled;
5491
5500
  the capability reports protocol support, not authority to present or answer a form.
5492
5501
 
5502
+ The public `host ensure` and `host handoff` commands accept the same deployment option,
5503
+ separately from `--launch-spec`. Add `--mcp-presenter-fd 4` with a live inherited duplex
5504
+ channel to enroll the launch owner of a new host. The runtime-selecting CLI re-exec,
5505
+ supervisor and host child preserve that channel. The option alone enables enforcement,
5506
+ not a presenter; fd 4 alone does not enable enforcement.
5507
+
5508
+ An enforcing predecessor keeps enforcement in its successor, including when an older
5509
+ handoff caller omits the option. Conditional `handoff --when idle` carries the non-secret
5510
+ `mcp_presenter_enforcement` boolean in `begin_handover`, independently of the launch
5511
+ specification and environment. It uses only the existing authenticated owner's channel
5512
+ for fresh successor enrollment; the CLI cannot enroll itself into the predecessor.
5513
+ Reuse and already-current handover answers report the serving host's actual capabilities.
5514
+ Neither a successful CLI exit nor an equal `runtimeBuildId` proves enforcement or
5515
+ enrollment. A deployment requesting enforcement against an unenforced existing host must
5516
+ coordinate a replacement, not force-restart another owner's busy host.
5517
+
5493
5518
  The native launch owner enrolls an Ed25519 public key through an inherited private
5494
5519
  bootstrap channel. Descriptor 4 is reserved for this channel; descriptor 3 remains
5495
5520
  the separate owner-lifetime channel. Enrollment and management proofs sign the
@@ -5901,6 +5926,25 @@ Dismiss any dialog method. The extension receives `undefined` (for select/input/
5901
5926
  {"type": "extension_ui_response", "id": "uuid-3", "cancelled": true}
5902
5927
  ```
5903
5928
 
5929
+ ### UI-answer receipts and dialog observation
5930
+
5931
+ With capability `ui_answer_ledger`, `extension_ui_response` accepts an optional `answerId` string of 1-256 characters. The response preserves the frame's correlation `id` and adds `disposition`:
5932
+
5933
+ - `applied`: success; the pending request was settled once.
5934
+ - `replay`: success; this `(uiRequestId, answerId)` pair was already applied, with no repeated delivery.
5935
+ - `already_answered`: failure; the request was already settled with a different answer id.
5936
+ - `unknown`: failure; no pending request or receipt exists (`unknown_extension_ui_request`).
5937
+
5938
+ A malformed answer (for example `invalid_answer_id` or `question_incomplete`) is refused without a `disposition` and leaves its request pending. A single-session stdio connection still ignores an unnamed response that matches no request; a named response always gets a reply.
5939
+
5940
+ `get_state.answeredUiRequests` contains the latest 64 receipts, oldest first, as `{uiRequestId, answerId?}`. Receipts are session-local and memory-only, retained for that session's life in the host generation. The ledger evicts FIFO beyond 1,024 requests; replay does not extend retention. Omitting `answerId` keeps the short-form answer contract but provides no named replay identity. A restart, unloaded session, or evicted receipt is not proof of non-execution.
5941
+
5942
+ With capability `dialog_observer`, `subscribe_sessions` accepts `dialogs?: boolean` (default false). A matching `dialogs: true` subscription must also enable `mirror`. It receives these new records on its connection FIFO:
5943
+
5944
+ `{type: "ui_request_pending", sessionId, uiRequestId, method, request}` when a dialog or question is registered, with its original request frame; `{type: "ui_request_settled", sessionId, uiRequestId, disposition: "answered" | "cancelled"}` when it settles. Timeout and lifecycle teardown count as cancellation. The feed survives the command connection's departure while the session remains loaded. A dialog already present in the snapshot is not emitted again.
5945
+
5946
+ The subscription remains observing: it creates no attachment and carries no correlated `response` records. The original select, confirm, input, and editor `extension_ui_request` records remain targeted, not mirrored. Use a separate attached command connection for answers; dialog observation grants no answering authority.
5947
+
5904
5948
  ## Error Handling
5905
5949
 
5906
5950
  Failed commands return a response with `success: false`:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi",
3
- "version": "2026.10.25-4",
3
+ "version": "2026.10.26-1",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -71,10 +71,11 @@
71
71
  },
72
72
  "dependencies": {
73
73
  "@earendil-works/chord": "0.99.1",
74
- "@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.10.25-4",
75
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.25-4",
76
- "@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.25-4",
74
+ "@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.10.26-1",
75
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.26-1",
76
+ "@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.26-1",
77
77
  "@silvia-odwyer/photon-node": "0.3.4",
78
+ "ajv": "8.20.0",
78
79
  "chalk": "6.0.0",
79
80
  "cross-spawn": "7.0.6",
80
81
  "diff": "9.0.0",
@@ -92,8 +93,8 @@
92
93
  "yaml": "2.9.1",
93
94
  "@anthropic-ai/claude-agent-sdk": "0.3.306",
94
95
  "@anthropic-ai/sdk": "0.127.0",
95
- "@code-yeongyu/senpi-codemode": "2026.10.25-4",
96
- "@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.25-4",
96
+ "@code-yeongyu/senpi-codemode": "2026.10.26-1",
97
+ "@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.26-1",
97
98
  "@modelcontextprotocol/sdk": "1.32.1",
98
99
  "@mozilla/readability": "0.6.0",
99
100
  "linkedom": "0.18.13",
@@ -1,2 +0,0 @@
1
- import { createRequire as __piCreateRequire } from "node:module"; const require = __piCreateRequire(import.meta.url); { const __piWorkerThreads = require("node:worker_threads"); if (typeof __piWorkerThreads.markAsUncloneable !== "function") { __piWorkerThreads.markAsUncloneable = () => {}; } }
2
- import{AzureOpenAI}from"./chunk-CD2HJK4C.js";import{getPiUserAgent}from"./chunk-QTG7MY2X.js";import{buildBaseOptions}from"./chunk-GSSD3R5X.js";import{retryProviderRequest}from"./chunk-EDOW5W5H.js";import{deriveOpenAIPromptCacheKey}from"./chunk-3BTN337S.js";import{formatProviderError,normalizeProviderError}from"./chunk-TAV6QZFM.js";import"./chunk-PELNNWGO.js";import{convertResponsesMessages,convertResponsesTools,processResponsesStream}from"./chunk-E4LBKOZG.js";import"./chunk-GVSBCJFI.js";import"./chunk-HK46JMSC.js";import{createGrammarToolInputProperties}from"./chunk-4P7WARQZ.js";import"./chunk-WDTJ6JKE.js";import{getProviderEnvValue}from"./chunk-5ROMKDLK.js";import"./chunk-3W47LTXQ.js";import{clampThinkingLevel,getDeclaredTools,resolveTranscript,resolveTranscriptTools,supportsMax}from"./chunk-6AOVUTUR.js";import"./chunk-45SZOLCU.js";import{AssistantMessageEventStream}from"./chunk-DMI55M2N.js";import{headersToRecord}from"./chunk-PJ34QVL5.js";import"./chunk-IKOFCODM.js";var DEFAULT_AZURE_API_VERSION="v1",AZURE_TOOL_CALL_PROVIDERS=new Set(["openai","chatgpt-subscription","opencode","azure-openai-responses"]),OPENAI_RESPONSES_MIN_OUTPUT_TOKENS=16;function parseDeploymentNameMap(value){let map=new Map;if(!value)return map;for(let entry of value.split(",")){let trimmed=entry.trim();if(!trimmed)continue;let[modelId,deploymentName]=trimmed.split("=",2);!modelId||!deploymentName||map.set(modelId.trim(),deploymentName.trim())}return map}function resolveDeploymentName(model,options){return options?.azureDeploymentName?options.azureDeploymentName:parseDeploymentNameMap(getProviderEnvValue("AZURE_OPENAI_DEPLOYMENT_NAME_MAP",options?.env)).get(model.id)||model.id}function formatAzureOpenAIError(error){return formatProviderError(normalizeProviderError(error),"Azure OpenAI API error")}var stream=(model,context,options)=>{let stream2=new AssistantMessageEventStream,normalizedContext=resolveTranscript(context,model.compat?.supportsMidConvoSystemMessages);return(async()=>{let deploymentName=resolveDeploymentName(model,options),output={role:"assistant",content:[],api:"azure-openai-responses",provider:model.provider,model:model.id,usage:{input:0,output:0,cacheRead:0,cacheWrite:0,totalTokens:0,cost:{input:0,output:0,cacheRead:0,cacheWrite:0,total:0}},stopReason:"pending",timestamp:Date.now()};try{let apiKey=options?.apiKey;if(!apiKey)throw new Error(`No API key for provider: ${model.provider}`);let client=createClient(model,apiKey,options),grammarToolInputProperties=createGrammarToolInputProperties(getDeclaredTools(normalizedContext.messages),model.compat?.supportsOpenAIGrammarTools??!1),params=buildParams(model,normalizedContext,options,deploymentName,grammarToolInputProperties),nextParams=await options?.onPayload?.(params,model);nextParams!==void 0&&(params=nextParams);let requestOptions={...options?.signal?{signal:options.signal}:{},...options?.timeoutMs!==void 0?{timeout:options.timeoutMs}:{},maxRetries:0},{data:openaiStream,response}=await retryProviderRequest(()=>client.responses.create(params,requestOptions).withResponse(),{maxRetries:options?.maxRetries,maxRetryDelayMs:options?.maxRetryDelayMs,signal:options?.signal});if(await options?.onResponse?.({status:response.status,headers:headersToRecord(response.headers)},model),stream2.push({type:"start",partial:output}),await processResponsesStream(openaiStream,output,stream2,model,{onProviderStreamEvent:options?.onProviderStreamEvent,grammarToolInputProperties}),options?.signal?.aborted)throw new Error("Request was aborted");if(output.stopReason==="pending")throw new Error("Azure OpenAI Responses stream ended without a stop reason");if(output.stopReason==="aborted"||output.stopReason==="error")throw new Error(output.errorMessage||"An unknown error occurred");stream2.push({type:"done",reason:output.stopReason,message:output}),stream2.end()}catch(error){for(let block of output.content)delete block.index,delete block.partialJson,delete block.customInput;output.stopReason=options?.signal?.aborted?"aborted":"error",output.errorMessage=formatAzureOpenAIError(error),stream2.push({type:"error",reason:output.stopReason,error:output}),stream2.end()}})(),stream2},streamSimple=(model,context,options)=>{let apiKey=options?.apiKey;if(!apiKey)throw new Error(`No API key for provider: ${model.provider}`);let base={...buildBaseOptions(model,context,options,apiKey),toolChoice:options?.toolChoice},clampedReasoning=options?.reasoning?clampThinkingLevel(model,options.reasoning):void 0,reasoningEffort=clampedReasoning==="off"?void 0:clampedReasoning==="max"&&supportsMax(model)?"max":clampedReasoning==="max"?"high":clampedReasoning;return stream(model,context,{...base,reasoningEffort})};function normalizeAzureBaseUrl(baseUrl){let trimmed=baseUrl.trim().replace(/\/+$/,""),url;try{url=new URL(trimmed)}catch{throw new Error(`Invalid Azure OpenAI base URL: ${baseUrl}`)}let isAzureHost=url.hostname.endsWith(".openai.azure.com")||url.hostname.endsWith(".cognitiveservices.azure.com")||url.hostname.endsWith(".ai.azure.com"),normalizedPath=url.pathname.replace(/\/+$/,"");return isAzureHost&&(normalizedPath===""||normalizedPath==="/"||normalizedPath==="/openai"||normalizedPath==="/openai/v1/responses")&&(url.pathname="/openai/v1",url.search=""),url.toString().replace(/\/+$/,"")}function buildDefaultBaseUrl(resourceName){return`https://${resourceName}.openai.azure.com/openai/v1`}function resolveAzureConfig(model,options){let apiVersion=options?.azureApiVersion||getProviderEnvValue("AZURE_OPENAI_API_VERSION",options?.env)||DEFAULT_AZURE_API_VERSION,baseUrl=options?.azureBaseUrl?.trim()||getProviderEnvValue("AZURE_OPENAI_BASE_URL",options?.env)?.trim()||void 0,resourceName=options?.azureResourceName||getProviderEnvValue("AZURE_OPENAI_RESOURCE_NAME",options?.env),resolvedBaseUrl=baseUrl;if(!resolvedBaseUrl&&resourceName&&(resolvedBaseUrl=buildDefaultBaseUrl(resourceName)),!resolvedBaseUrl&&model.baseUrl&&(resolvedBaseUrl=model.baseUrl),!resolvedBaseUrl)throw new Error("Azure OpenAI base URL is required. Set AZURE_OPENAI_BASE_URL or AZURE_OPENAI_RESOURCE_NAME, or pass azureBaseUrl, azureResourceName, or model.baseUrl.");return{baseUrl:normalizeAzureBaseUrl(resolvedBaseUrl),apiVersion}}function createClient(model,apiKey,options){let headers={"User-Agent":getPiUserAgent(),...model.headers};options?.headers&&Object.assign(headers,options.headers);let{baseUrl,apiVersion}=resolveAzureConfig(model,options);return new AzureOpenAI({apiKey,apiVersion,dangerouslyAllowBrowser:!0,fetch:options?.fetch,defaultHeaders:headers,baseURL:baseUrl})}function buildParams(model,context,options,deploymentName,grammarToolInputProperties=createGrammarToolInputProperties(getDeclaredTools(context.messages),model.compat?.supportsOpenAIGrammarTools??!1)){let requestedReasoningEffort=options?.reasoningEffort??(options?.reasoningSummary?"medium":void 0),mappedReasoningEffort=requestedReasoningEffort===void 0?void 0:model.thinkingLevelMap?.[requestedReasoningEffort],reasoningEffort=mappedReasoningEffort===void 0?requestedReasoningEffort:mappedReasoningEffort,reasoningRequested=reasoningEffort!=null,reasoningUnavailable=reasoningEffort===null,supportsAdditionalTools=model.compat?.supportsAdditionalTools??!1,supportsToolSearch=model.compat?.supportsToolSearch??!1,transcriptTools=resolveTranscriptTools(context.messages,supportsAdditionalTools||supportsToolSearch),messages=convertResponsesMessages(model,context,AZURE_TOOL_CALL_PROVIDERS,{grammarToolInputProperties,supportsMidConvoSystemMessages:model.compat?.supportsMidConvoSystemMessages??!1,supportsAdditionalTools,supportsToolSearch,toolOptions:{supportsStrictMode:model.compat?.supportsStrictMode??!0,supportsOpenAIGrammarTools:model.compat?.supportsOpenAIGrammarTools??!1}}),params={model:deploymentName,input:messages,stream:!0,...(options?.cacheRetention??model.cacheRetention)==="none"?{}:{prompt_cache_key:deriveOpenAIPromptCacheKey(model.id,context)},store:!1};return options?.maxTokens&&(params.max_output_tokens=Math.max(options.maxTokens,OPENAI_RESPONSES_MIN_OUTPUT_TOKENS)),options?.temperature!==void 0&&(params.temperature=options?.temperature),transcriptTools.requestTools.length>0&&(params.tools=convertResponsesTools(transcriptTools.requestTools,{supportsStrictMode:model.compat?.supportsStrictMode??!0,supportsOpenAIGrammarTools:model.compat?.supportsOpenAIGrammarTools??!1})),options?.toolChoice!==void 0&&(params.tool_choice=options.toolChoice),model.reasoning&&(reasoningRequested?(params.reasoning={effort:reasoningEffort,...options?.reasoningSummary===null?{}:{summary:options?.reasoningSummary||"auto"}},params.include=["reasoning.encrypted_content"]):!reasoningUnavailable&&model.thinkingLevelMap?.off!==null&&(params.reasoning={effort:model.thinkingLevelMap?.off??"none"})),Object.assign(params,model.samplingParams,options?.samplingParams),params}export{stream,streamSimple};
@@ -1,2 +0,0 @@
1
- import { createRequire as __piCreateRequire } from "node:module"; const require = __piCreateRequire(import.meta.url); { const __piWorkerThreads = require("node:worker_threads"); if (typeof __piWorkerThreads.markAsUncloneable !== "function") { __piWorkerThreads.markAsUncloneable = () => {}; } }
2
- var FifoQueue=class{constructor(){this.incoming=[],this.outgoing=[]}get length(){return this.incoming.length+this.outgoing.length}enqueue(value){this.incoming.push(value)}dequeue(){if(this.outgoing.length===0)for(;this.incoming.length>0;)this.outgoing.push(this.incoming.pop());return this.outgoing.pop()}},EventStream=class{#queue;#queueHead;#failed;#error;#localWorkDepth;constructor(isComplete,extractResult){this.#queue=[],this.#queueHead=0,this.waiting=new FifoQueue,this.done=!1,this.#failed=!1,this.#localWorkDepth=0,this.isComplete=isComplete,this.extractResult=extractResult,this.finalResultPromise=new Promise((resolve,reject)=>{this.resolveFinalResult=resolve,this.rejectFinalResult=reject}),this.finalResultPromise.catch(()=>{})}get queue(){return this.#queue.slice(this.#queueHead)}#enqueue(event){this.#queue.push(event)}#dequeue(){let event=this.#queue[this.#queueHead];if(event===void 0)throw new Error("EventStream queue underflow");return this.#queueHead++,this.#queueHead>1024&&this.#queueHead*2>=this.#queue.length&&(this.#queue=this.#queue.slice(this.#queueHead),this.#queueHead=0),event}push(event){if(this.done)return;this.isComplete(event)&&(this.done=!0,this.resolveFinalResult(this.extractResult(event)));let waiter=this.waiting.dequeue();waiter?waiter.resolve({value:event,done:!1}):this.#enqueue(event)}end(result){for(this.done=!0,result!==void 0&&this.resolveFinalResult(result);this.waiting.length>0;){let waiter=this.waiting.dequeue();waiter&&waiter.resolve({value:void 0,done:!0})}}fail(error){if(!this.done)for(this.done=!0,this.#failed=!0,this.#error=error,this.rejectFinalResult(error);this.waiting.length>0;){let waiter=this.waiting.dequeue();waiter&&waiter.reject(error)}}[Symbol.asyncIterator](){return{next:()=>this.#queueHead<this.#queue.length?Promise.resolve({value:this.#dequeue(),done:!1}):this.#failed?Promise.reject(this.#error):this.done?Promise.resolve({value:void 0,done:!0}):new Promise((resolve,reject)=>this.waiting.enqueue({resolve,reject}))}}result(){return this.finalResultPromise}async trackLocalWork(work){this.#localWorkDepth++;try{return await work}finally{this.#localWorkDepth--}}hasPendingLocalWork(){return this.#localWorkDepth>0}},AssistantMessageEventStream=class extends EventStream{constructor(){super(event=>event.type==="done"||event.type==="error",event=>{if(event.type==="done")return event.message;if(event.type==="error")return event.error;throw new Error("Unexpected event type for final result")})}};function createAssistantMessageEventStream(){return new AssistantMessageEventStream}function formatThrownValue(value){return value instanceof Error?value.message||value.name:typeof value=="string"?value:String(value)}function extractDiagnosticError(error){if(!(error instanceof Error))return{name:"ThrownValue",message:formatThrownValue(error)};let code=error.code;return{name:error.name||void 0,message:error.message||error.name,stack:error.stack,code:typeof code=="string"||typeof code=="number"?code:void 0}}function createAssistantMessageDiagnostic(type,error,details){return{type,timestamp:Date.now(),error:extractDiagnosticError(error),details}}function appendAssistantMessageDiagnostic(message,diagnostic){message.diagnostics=[...message.diagnostics??[],diagnostic]}export{EventStream,AssistantMessageEventStream,createAssistantMessageEventStream,formatThrownValue,extractDiagnosticError,createAssistantMessageDiagnostic,appendAssistantMessageDiagnostic};