@code-yeongyu/senpi 2026.10.25-2 → 2026.10.25-4

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 (226) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/bundle/chunks/{args-JXULI4EH.js → args-B444OQBK.js} +1 -1
  3. package/dist/bundle/chunks/{auth-4NUNUSTF.js → auth-C77UQA55.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-XBUSVKWC.js → chunk-2RB5EL4X.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-KUE4AQN6.js → chunk-3QCYBOKN.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-2EFVQWXZ.js → chunk-4MOZXOJX.js} +5 -5
  7. package/dist/bundle/chunks/{chunk-MXZPAUFB.js → chunk-5JJDR6XI.js} +1 -1
  8. package/dist/bundle/chunks/chunk-6VWIMHMH.js +2 -0
  9. package/dist/bundle/chunks/{chunk-Y5OTHMXO.js → chunk-7CNOCJCN.js} +1 -1
  10. package/dist/bundle/chunks/chunk-AX72VNRS.js +27 -0
  11. package/dist/bundle/chunks/{chunk-VNN2V74Y.js → chunk-BZ7726QO.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-J2U5VDG7.js → chunk-C2MQ4QXN.js} +30 -30
  13. package/dist/bundle/chunks/{chunk-JRXKLIEQ.js → chunk-DODH3N6U.js} +5 -5
  14. package/dist/bundle/chunks/{chunk-EHQXLHRB.js → chunk-E6D4RIT7.js} +2 -2
  15. package/dist/bundle/chunks/chunk-J37GRED2.js +5 -0
  16. package/dist/bundle/chunks/chunk-JMOUU75H.js +2 -0
  17. package/dist/bundle/chunks/chunk-JPGT64RV.js +2 -0
  18. package/dist/bundle/chunks/chunk-KKD4CYCI.js +3 -0
  19. package/dist/bundle/chunks/{chunk-YN7ELQ43.js → chunk-KNVNKDER.js} +1 -1
  20. package/dist/bundle/chunks/chunk-KXB5WIS5.js +4 -0
  21. package/dist/bundle/chunks/chunk-L73UYAH5.js +3 -0
  22. package/dist/bundle/chunks/{chunk-G2UETS2R.js → chunk-LOIF6PHI.js} +4 -4
  23. package/dist/bundle/chunks/chunk-LTU6UFEX.js +2 -0
  24. package/dist/bundle/chunks/chunk-N7IN46Q5.js +27 -0
  25. package/dist/bundle/chunks/{chunk-LYGAV6KU.js → chunk-PT6JJUN2.js} +1 -1
  26. package/dist/bundle/chunks/{chunk-WFXUCVIR.js → chunk-SBGIXMX3.js} +2 -5
  27. package/dist/bundle/chunks/{chunk-W2BKZBYJ.js → chunk-STMXIBXF.js} +2 -2
  28. package/dist/bundle/chunks/chunk-TBU6VYHN.js +13 -0
  29. package/dist/bundle/chunks/{chunk-RXKNOEQQ.js → chunk-V2UYXTRR.js} +1 -1
  30. package/dist/bundle/chunks/{chunk-U67ML6CQ.js → chunk-WD6WBYC5.js} +1 -1
  31. package/dist/bundle/chunks/chunk-WJOT3DFL.js +2 -0
  32. package/dist/bundle/chunks/chunk-XMJKZK5H.js +2 -0
  33. package/dist/bundle/chunks/chunk-YI7SPFDN.js +5 -0
  34. package/dist/bundle/chunks/cli-main-YYH3IBC6.js +3 -0
  35. package/dist/bundle/chunks/{client-KWTLTKVV.js → client-WTYEICLH.js} +3 -3
  36. package/dist/bundle/chunks/dispatch-MXG5YWLV.js +9 -0
  37. package/dist/bundle/chunks/{help-fast-path-ZHZGXAUG.js → help-fast-path-3Z26DQSV.js} +1 -1
  38. package/dist/bundle/chunks/host-command-TDLLHXVI.js +2 -0
  39. package/dist/bundle/chunks/host-lifecycle-3VHHIGDL.js +3 -0
  40. package/dist/bundle/chunks/host-lifecycle.js +8 -6
  41. package/dist/bundle/chunks/host-session-command-56XF6ACS.js +20 -0
  42. package/dist/bundle/chunks/{host-socket-request-AS2OCRLP.js → host-socket-request-QL2WEZTN.js} +1 -1
  43. package/dist/bundle/chunks/interactive-mode-OEPULLZK.js +2 -0
  44. package/dist/bundle/chunks/mcp-presenter-bootstrap-XUSCQVRC.js +2 -0
  45. package/dist/bundle/chunks/{multi-session-host-VTTY5MFT.js → multi-session-host-Z2CW4SDM.js} +12 -9
  46. package/dist/bundle/chunks/{package-manager-cli-DZFOC3MF.js → package-manager-cli-N55U6PEO.js} +1 -1
  47. package/dist/bundle/chunks/rotation-stream-CBGCBYHV.js +2 -0
  48. package/dist/bundle/chunks/rpc-mode-ETXQWEB2.js +2 -0
  49. package/dist/bundle/chunks/{rpc-monitor-source-B5NQ4ZVH.js → rpc-monitor-source-AE3C6EQV.js} +1 -1
  50. package/dist/bundle/chunks/{schedule-command-SLISTG4V.js → schedule-command-VS7CZC2J.js} +1 -1
  51. package/dist/bundle/chunks/session-control-endpoint-4LEIDIVI.js +3 -0
  52. package/dist/bundle/chunks/session-picker-E4KUJDLO.js +2 -0
  53. package/dist/bundle/chunks/session-worker.js +292 -278
  54. package/dist/bundle/chunks/{stdio-IEC635SY.js → stdio-YXDKKKQZ.js} +1 -1
  55. package/dist/bundle/chunks/{streamableHttp-KJI5KE4K.js → streamableHttp-DKIBUWVY.js} +1 -1
  56. package/dist/bundle/chunks/{types-LLNV23LW.js → types-BJUEQ4II.js} +1 -1
  57. package/dist/bundle/chunks/zod-compat-RJDXCU7S.js +2 -0
  58. package/dist/bundle/cli.js +2 -2
  59. package/dist/bundle/host-supervisor.js +8 -6
  60. package/dist/bundle/index.js +1 -1
  61. package/dist/bundle/rpc-entry.js +1 -1
  62. package/dist/bundle/runtime-manifest.json +1 -1
  63. package/dist/cli/args.d.ts +2 -0
  64. package/dist/cli/args.js +9 -0
  65. package/dist/cli/host-command-args.d.ts +1 -0
  66. package/dist/cli/host-command-args.js +7 -1
  67. package/dist/cli/host-command.js +3 -1
  68. package/dist/core/agent-session.js +22 -7
  69. package/dist/core/extensions/builtin/goal/index.js +1 -0
  70. package/dist/core/extensions/builtin/goal/lifecycle-facts.d.ts +3 -0
  71. package/dist/core/extensions/builtin/mcp/catalog.d.ts +3 -1
  72. package/dist/core/extensions/builtin/mcp/catalog.js +2 -0
  73. package/dist/core/extensions/builtin/mcp/connection.d.ts +1 -0
  74. package/dist/core/extensions/builtin/mcp/connection.js +46 -1
  75. package/dist/core/extensions/builtin/mcp/elicitation.d.ts +6 -1
  76. package/dist/core/extensions/builtin/mcp/elicitation.js +54 -2
  77. package/dist/core/extensions/builtin/mcp/expose/call.js +6 -2
  78. package/dist/core/extensions/builtin/mcp/expose/session.d.ts +4 -1
  79. package/dist/core/extensions/builtin/mcp/expose/session.js +7 -0
  80. package/dist/core/extensions/builtin/mcp/health.js +5 -0
  81. package/dist/core/extensions/builtin/mcp/host-registry.js +7 -1
  82. package/dist/core/extensions/builtin/mcp/index.js +2 -0
  83. package/dist/core/extensions/builtin/mcp/modern/client.d.ts +5 -0
  84. package/dist/core/extensions/builtin/mcp/modern/client.js +15 -0
  85. package/dist/core/extensions/builtin/mcp/modern/consent-queue.d.ts +2 -0
  86. package/dist/core/extensions/builtin/mcp/modern/consent-queue.js +59 -0
  87. package/dist/core/extensions/builtin/mcp/modern/diagnostics.d.ts +4 -0
  88. package/dist/core/extensions/builtin/mcp/modern/diagnostics.js +39 -0
  89. package/dist/core/extensions/builtin/mcp/modern/dispatch.js +21 -11
  90. package/dist/core/extensions/builtin/mcp/modern/elicitation.d.ts +14 -0
  91. package/dist/core/extensions/builtin/mcp/modern/elicitation.js +145 -0
  92. package/dist/core/extensions/builtin/mcp/modern/errors.d.ts +21 -0
  93. package/dist/core/extensions/builtin/mcp/modern/errors.js +23 -0
  94. package/dist/core/extensions/builtin/mcp/modern/form-schema.d.ts +20 -0
  95. package/dist/core/extensions/builtin/mcp/modern/form-schema.js +153 -0
  96. package/dist/core/extensions/builtin/mcp/modern/http-fetch.d.ts +3 -1
  97. package/dist/core/extensions/builtin/mcp/modern/http-fetch.js +8 -2
  98. package/dist/core/extensions/builtin/mcp/modern/inputs.d.ts +11 -0
  99. package/dist/core/extensions/builtin/mcp/modern/inputs.js +65 -0
  100. package/dist/core/extensions/builtin/mcp/modern/parameter-headers.d.ts +11 -0
  101. package/dist/core/extensions/builtin/mcp/modern/parameter-headers.js +109 -0
  102. package/dist/core/extensions/builtin/mcp/modern/request.d.ts +20 -0
  103. package/dist/core/extensions/builtin/mcp/modern/request.js +292 -0
  104. package/dist/core/extensions/builtin/mcp/modern/stdio-probe.d.ts +1 -1
  105. package/dist/core/extensions/builtin/mcp/modern/stdio-probe.js +54 -6
  106. package/dist/core/extensions/builtin/mcp/modern/transport.js +80 -14
  107. package/dist/core/extensions/builtin/mcp/modern/wire.js +11 -7
  108. package/dist/core/extensions/builtin/mcp/prompts.d.ts +5 -1
  109. package/dist/core/extensions/builtin/mcp/prompts.js +6 -1
  110. package/dist/core/extensions/builtin/mcp/request-origin.d.ts +47 -0
  111. package/dist/core/extensions/builtin/mcp/request-origin.js +59 -0
  112. package/dist/core/extensions/builtin/mcp/resources.d.ts +4 -1
  113. package/dist/core/extensions/builtin/mcp/resources.js +19 -4
  114. package/dist/core/extensions/builtin/mcp/sdk.lazy.d.ts +3 -1
  115. package/dist/core/extensions/builtin/mcp/sdk.lazy.js +3 -1
  116. package/dist/core/extensions/builtin/mcp/service-register.d.ts +4 -1
  117. package/dist/core/extensions/builtin/mcp/service-register.js +3 -0
  118. package/dist/core/extensions/builtin/mcp/service-types.d.ts +2 -1
  119. package/dist/core/extensions/builtin/mcp/service.d.ts +1 -0
  120. package/dist/core/extensions/builtin/mcp/service.js +79 -4
  121. package/dist/core/extensions/builtin/mcp/shared-connection.js +36 -2
  122. package/dist/core/extensions/builtin/mcp/shared-lease.d.ts +1 -0
  123. package/dist/core/extensions/builtin/mcp/shared-lease.js +9 -0
  124. package/dist/core/extensions/builtin/mcp/transport-sdk.d.ts +3 -2
  125. package/dist/core/extensions/builtin/mcp/transport-sdk.js +56 -25
  126. package/dist/core/extensions/builtin/mcp/wrap.d.ts +7 -0
  127. package/dist/core/extensions/builtin/mcp/wrap.js +6 -0
  128. package/dist/core/extensions/runner.js +3 -2
  129. package/dist/core/extensions/types.d.ts +2 -0
  130. package/dist/core/lifecycle-ledger.d.ts +1 -0
  131. package/dist/core/lifecycle-ledger.js +7 -2
  132. package/dist/core/lifecycle-record.js +3 -0
  133. package/dist/core/lifecycle-session-binding.d.ts +11 -1
  134. package/dist/core/lifecycle-session-binding.js +29 -2
  135. package/dist/core/lifecycle-stop.d.ts +41 -0
  136. package/dist/core/lifecycle-stop.js +92 -0
  137. package/dist/core/mcp-form-types.d.ts +89 -0
  138. package/dist/core/mcp-form-types.js +23 -0
  139. package/dist/core/mcp-mrtr-presentations.d.ts +15 -0
  140. package/dist/core/mcp-mrtr-presentations.js +17 -0
  141. package/dist/core/session-control-actions.js +0 -2
  142. package/dist/core/session-lifecycle-types.d.ts +15 -0
  143. package/dist/core/session-lifecycle.d.ts +17 -1
  144. package/dist/core/session-lifecycle.js +40 -2
  145. package/dist/main.js +5 -1
  146. package/dist/modes/interactive/components/extension-selector.d.ts +1 -0
  147. package/dist/modes/interactive/components/extension-selector.js +4 -0
  148. package/dist/modes/interactive/interactive-mode.d.ts +4 -0
  149. package/dist/modes/interactive/interactive-mode.js +34 -8
  150. package/dist/modes/interactive/mcp-local-presenter.d.ts +5 -0
  151. package/dist/modes/interactive/mcp-local-presenter.js +109 -0
  152. package/dist/modes/interactive/session-control-attach.d.ts +20 -0
  153. package/dist/modes/interactive/session-control-attach.js +13 -0
  154. package/dist/modes/interactive/session-control-commands.js +16 -6
  155. package/dist/modes/interactive/session-control-registry.d.ts +2 -1
  156. package/dist/modes/interactive/session-control-registry.js +3 -2
  157. package/dist/modes/interactive/session-control-rpc-commands.d.ts +4 -2
  158. package/dist/modes/interactive/session-control-rpc-commands.js +114 -4
  159. package/dist/modes/interactive/session-control-server.js +9 -3
  160. package/dist/modes/rpc/connection-handler.d.ts +1 -0
  161. package/dist/modes/rpc/connection-handler.js +13 -41
  162. package/dist/modes/rpc/custom-capability.d.ts +6 -0
  163. package/dist/modes/rpc/custom-capability.js +6 -0
  164. package/dist/modes/rpc/get-entries-command.d.ts +13 -0
  165. package/dist/modes/rpc/get-entries-command.js +61 -0
  166. package/dist/modes/rpc/get-entries-page.d.ts +22 -0
  167. package/dist/modes/rpc/get-entries-page.js +41 -2
  168. package/dist/modes/rpc/host-capabilities.d.ts +1 -0
  169. package/dist/modes/rpc/host-capabilities.js +6 -2
  170. package/dist/modes/rpc/host-ensure-start.js +13 -2
  171. package/dist/modes/rpc/host-ensure-types.d.ts +2 -0
  172. package/dist/modes/rpc/host-ensure.js +1 -0
  173. package/dist/modes/rpc/host-handoff.d.ts +2 -0
  174. package/dist/modes/rpc/host-handover-wire.d.ts +2 -0
  175. package/dist/modes/rpc/host-handover-wire.js +1 -0
  176. package/dist/modes/rpc/host-lifecycle-launch.d.ts +1 -0
  177. package/dist/modes/rpc/host-lifecycle-launch.js +43 -2
  178. package/dist/modes/rpc/host-observe-request.d.ts +2 -1
  179. package/dist/modes/rpc/host-observe-request.js +4 -1
  180. package/dist/modes/rpc/host-runner.d.ts +2 -0
  181. package/dist/modes/rpc/host-runner.js +8 -6
  182. package/dist/modes/rpc/host-successor.js +11 -1
  183. package/dist/modes/rpc/mcp-form-dialog.d.ts +45 -0
  184. package/dist/modes/rpc/mcp-form-dialog.js +159 -0
  185. package/dist/modes/rpc/mcp-form-worker.d.ts +14 -0
  186. package/dist/modes/rpc/mcp-form-worker.js +167 -0
  187. package/dist/modes/rpc/mcp-presenter-authority.d.ts +53 -0
  188. package/dist/modes/rpc/mcp-presenter-authority.js +309 -0
  189. package/dist/modes/rpc/mcp-presenter-bootstrap.d.ts +8 -0
  190. package/dist/modes/rpc/mcp-presenter-bootstrap.js +111 -0
  191. package/dist/modes/rpc/mcp-presenter-control.d.ts +24 -0
  192. package/dist/modes/rpc/mcp-presenter-control.js +330 -0
  193. package/dist/modes/rpc/multi-session-host.d.ts +3 -0
  194. package/dist/modes/rpc/multi-session-host.js +36 -1
  195. package/dist/modes/rpc/rpc-read-response-types.d.ts +2 -0
  196. package/dist/modes/rpc/rpc-session-command-types.d.ts +8 -1
  197. package/dist/modes/rpc/session-binding.js +6 -1
  198. package/dist/modes/rpc/session-event-writer.d.ts +14 -0
  199. package/dist/modes/rpc/session-event-writer.js +128 -1
  200. package/dist/modes/rpc/session-router-routing.js +1 -0
  201. package/dist/modes/rpc/session-worker-client.d.ts +3 -0
  202. package/dist/modes/rpc/session-worker-client.js +89 -0
  203. package/dist/modes/rpc/session-worker-protocol.d.ts +41 -0
  204. package/dist/modes/rpc/session-worker.js +9 -0
  205. package/docs/extensions.md +5 -0
  206. package/docs/mcp.md +20 -3
  207. package/docs/rpc.md +151 -8
  208. package/package.json +6 -6
  209. package/dist/bundle/chunks/chunk-3LNSLJ7N.js +0 -2
  210. package/dist/bundle/chunks/chunk-B6TPQDBW.js +0 -5
  211. package/dist/bundle/chunks/chunk-CDRFFHLL.js +0 -4
  212. package/dist/bundle/chunks/chunk-HPCS4HM4.js +0 -27
  213. package/dist/bundle/chunks/chunk-KJZAY5YH.js +0 -27
  214. package/dist/bundle/chunks/chunk-MSQCTH6V.js +0 -2
  215. package/dist/bundle/chunks/chunk-OI4NMCKP.js +0 -3
  216. package/dist/bundle/chunks/chunk-SCVRKIFK.js +0 -2
  217. package/dist/bundle/chunks/cli-main-SWKFMTZQ.js +0 -3
  218. package/dist/bundle/chunks/dispatch-DEP4PEDZ.js +0 -10
  219. package/dist/bundle/chunks/host-command-BC6UWP33.js +0 -2
  220. package/dist/bundle/chunks/host-lifecycle-TN5NEGNF.js +0 -3
  221. package/dist/bundle/chunks/host-session-command-MGVBLBRV.js +0 -20
  222. package/dist/bundle/chunks/interactive-mode-SCSROHD5.js +0 -2
  223. package/dist/bundle/chunks/rotation-stream-U7YQMC7H.js +0 -2
  224. package/dist/bundle/chunks/rpc-mode-TOXYD4XA.js +0 -2
  225. package/dist/bundle/chunks/session-control-endpoint-HFFMBALG.js +0 -3
  226. package/dist/bundle/chunks/session-picker-465FVXQG.js +0 -2
@@ -1,4 +1,6 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { bindPrivateMcpForms } from "../../core/mcp-form-types.js";
3
+ import { McpPresenterAuthority } from "./mcp-presenter-authority.js";
2
4
  import { MEDIA_PLACEHOLDERS_CAPABILITY } from "./custom-capability.js";
3
5
  import { serializeJsonLine } from "./jsonl.js";
4
6
  import { omitInlineMedia } from "./media-placeholders.js";
@@ -58,6 +60,79 @@ function toolUpdateKey(value) {
58
60
  * scheduling boundary and violate D9.
59
61
  */
60
62
  export class SessionEventWriter {
63
+ /** Deployment switch. Ordinary RPC behavior is unchanged unless the host explicitly enables it. */
64
+ enableMcpPresenters() {
65
+ this.presenterAuthority ??= new McpPresenterAuthority();
66
+ return this.presenterAuthority;
67
+ }
68
+ get mcpPresenters() {
69
+ return this.presenterAuthority;
70
+ }
71
+ getMcpPresenterConnection(id) {
72
+ return this.presenterConnections.get(id);
73
+ }
74
+ findMcpPresenterConnection(nonce) {
75
+ return [...this.presenterConnections.values()].find((connection) => connection.nonce === nonce);
76
+ }
77
+ getMcpFormBinding(sessionId) {
78
+ return this.presenterBindings.get(sessionId);
79
+ }
80
+ bindMcpForms(sessionId, durableId, ui, admit) {
81
+ const authority = this.presenterAuthority;
82
+ if (!authority)
83
+ return undefined;
84
+ const binding = authority.bind(sessionId, durableId, admit);
85
+ this.presenterBindings.set(sessionId, binding);
86
+ for (const id of this.fanout.attachedConnections(sessionId)) {
87
+ const connection = this.presenterConnections.get(id);
88
+ if (connection)
89
+ authority.attach(connection, binding);
90
+ }
91
+ bindPrivateMcpForms(ui, binding);
92
+ return () => {
93
+ binding.dispose();
94
+ if (this.presenterBindings.get(sessionId) === binding)
95
+ this.presenterBindings.delete(sessionId);
96
+ };
97
+ }
98
+ /** Private records bypass event queues, snapshots, mirrors, replay, and overflow preview errors. */
99
+ handleMcpForm(value) {
100
+ if (!this.presenterAuthority)
101
+ return false;
102
+ if (typeof value !== "object" ||
103
+ value === null ||
104
+ !("type" in value) ||
105
+ typeof value.type !== "string" ||
106
+ (!value.type.startsWith("mcp_form_") && value.type !== "mcp_presenter_context"))
107
+ return false;
108
+ const id = this.currentConnection();
109
+ const sender = id === undefined ? undefined : this.presenterConnections.get(id);
110
+ const context = value.type === "mcp_presenter_context" && "sessionId" in value && typeof value.sessionId === "string" && sender
111
+ ? this.presenterAuthority?.connectionContext(sender, value.sessionId)
112
+ : undefined;
113
+ const reply = value.type === "mcp_presenter_context"
114
+ ? context
115
+ ? { ok: true, context }
116
+ : { error: "presenter_not_authorized" }
117
+ : sender && this.presenterAuthority
118
+ ? this.presenterAuthority.receive(sender, value)
119
+ : { error: "presenter_not_authorized" };
120
+ const connection = id === undefined ? undefined : this.fanout.get(id)?.connection;
121
+ if (connection) {
122
+ try {
123
+ connection.writeRaw(serializeJsonLine({
124
+ type: "mcp_form_reply",
125
+ ...("id" in value && typeof value.id === "string" ? { id: value.id } : {}),
126
+ ...reply,
127
+ }));
128
+ }
129
+ catch {
130
+ if (sender)
131
+ this.presenterAuthority?.detach(sender);
132
+ }
133
+ }
134
+ return true;
135
+ }
61
136
  get pendingCloseRecordCount() {
62
137
  return this.reservedCloseRecords;
63
138
  }
@@ -65,6 +140,8 @@ export class SessionEventWriter {
65
140
  return this.reservedCloseBytes;
66
141
  }
67
142
  constructor(writeRaw, waitOrSchedule, scheduleFlush) {
143
+ this.presenterConnections = new Map();
144
+ this.presenterBindings = new Map();
68
145
  this.records = new SessionRecordAccounting();
69
146
  this.queues = new Map();
70
147
  this.fanout = new SessionEventFanout();
@@ -99,7 +176,13 @@ export class SessionEventWriter {
99
176
  this.closeOverflowActors = new WeakSet();
100
177
  this.reservedCloseRecords = 0;
101
178
  this.reservedCloseBytes = 0;
102
- this.fanout.onConnectionClosed = (id) => this.securityPresentations.dropConnection(id);
179
+ this.fanout.onConnectionClosed = (id) => {
180
+ this.securityPresentations.dropConnection(id);
181
+ const sender = this.presenterConnections.get(id);
182
+ if (sender)
183
+ this.presenterAuthority?.detach(sender);
184
+ this.presenterConnections.delete(id);
185
+ };
103
186
  this.writeRaw = writeRaw;
104
187
  if (waitOrSchedule && scheduleFlush === undefined && waitOrSchedule.length > 0) {
105
188
  this.scheduleFlush = waitOrSchedule;
@@ -119,9 +202,33 @@ export class SessionEventWriter {
119
202
  return this.sealedSessions.has(sessionId);
120
203
  }
121
204
  registerConnection(id, connection, options = {}) {
205
+ const previousPresenter = this.presenterConnections.get(id);
206
+ if (previousPresenter)
207
+ this.presenterAuthority?.detach(previousPresenter);
122
208
  this.fanout.registerConnection(id, connection, options);
209
+ if (this.presenterAuthority) {
210
+ const sender = this.presenterAuthority.connect(id, (frame) => {
211
+ if (!this.fanout.get(id))
212
+ return false;
213
+ try {
214
+ connection.writeRaw(serializeJsonLine(frame));
215
+ // A pending drain does not undo an accepted write or drop the next resolution.
216
+ void connection.waitForBackpressure().catch(() => this.presenterAuthority?.detach(sender));
217
+ return true;
218
+ }
219
+ catch {
220
+ this.presenterAuthority?.detach(sender);
221
+ return false;
222
+ }
223
+ });
224
+ this.presenterConnections.set(id, sender);
225
+ }
123
226
  }
124
227
  unregisterConnection(id) {
228
+ const sender = this.presenterConnections.get(id);
229
+ if (sender)
230
+ this.presenterAuthority?.detach(sender);
231
+ this.presenterConnections.delete(id);
125
232
  this.securityPresentations.dropConnection(id);
126
233
  this.fanout.unregisterConnection(id);
127
234
  this.status.dropConnection(id);
@@ -131,8 +238,16 @@ export class SessionEventWriter {
131
238
  this.fanout.attachConnectionToSession(id, sessionId);
132
239
  if (this.hasConnection(id))
133
240
  this.securityPresentations.attach(id, sessionId);
241
+ const sender = this.presenterConnections.get(id);
242
+ const binding = this.presenterBindings.get(sessionId);
243
+ if (sender && binding)
244
+ this.presenterAuthority?.attach(sender, binding);
134
245
  }
135
246
  detachConnectionFromSession(id, sessionId) {
247
+ const sender = this.presenterConnections.get(id);
248
+ const binding = this.presenterBindings.get(sessionId);
249
+ if (sender && binding)
250
+ this.presenterAuthority?.detach(sender, binding);
136
251
  this.fanout.detachConnectionFromSession(id, sessionId);
137
252
  this.securityPresentations.detach(id, sessionId);
138
253
  }
@@ -142,8 +257,16 @@ export class SessionEventWriter {
142
257
  setConnectionCapabilities(id, capabilities) {
143
258
  this.fanout.setConnectionCapabilities(id, capabilities);
144
259
  this.securityPresentations.capabilitiesChanged(id);
260
+ if (!capabilities.includes("interactive_user") || !capabilities.includes("mcp_form_presenter_v1")) {
261
+ const sender = this.presenterConnections.get(id);
262
+ if (sender)
263
+ this.presenterAuthority?.withdraw(sender);
264
+ }
145
265
  }
146
266
  clearConnectionCapabilities(id) {
267
+ const sender = this.presenterConnections.get(id);
268
+ if (sender)
269
+ this.presenterAuthority?.withdraw(sender);
147
270
  this.fanout.clearConnectionCapabilities(id);
148
271
  this.securityPresentations.capabilitiesChanged(id);
149
272
  }
@@ -459,6 +582,8 @@ export class SessionEventWriter {
459
582
  * session's final stdout record.
460
583
  */
461
584
  closeSession(sessionId, response, reason, sessionPath) {
585
+ this.presenterBindings.get(sessionId)?.dispose();
586
+ this.presenterBindings.delete(sessionId);
462
587
  if (!this.settleBeforeSeal(sessionId))
463
588
  return;
464
589
  this.sealedSessions.add(sessionId);
@@ -526,6 +651,8 @@ export class SessionEventWriter {
526
651
  });
527
652
  }
528
653
  sealLifecycle(sessionId, lifecycle, attachedOnly = false) {
654
+ this.presenterBindings.get(sessionId)?.dispose();
655
+ this.presenterBindings.delete(sessionId);
529
656
  if (!this.settleBeforeSeal(sessionId))
530
657
  return;
531
658
  this.sealedSessions.add(sessionId);
@@ -69,6 +69,7 @@ export async function dispatch(host, command) {
69
69
  warm: host.registry.warm !== undefined,
70
70
  toolRuntime: host.defaults.toolRuntime ?? "in-process",
71
71
  negotiated: host.connectionOptions?.capabilities ?? [],
72
+ mcpPresenters: host.writer.mcpPresenters !== undefined,
72
73
  });
73
74
  return {
74
75
  id: command.id,
@@ -16,6 +16,9 @@ export interface SessionWorkerCallbacks {
16
16
  failure: (error: string) => void;
17
17
  }
18
18
  export declare class SessionWorkerClient {
19
+ private privateBinding?;
20
+ private stopPrivateBinding?;
21
+ private readonly privateOperations;
19
22
  /** The worker's own route to the main-thread Bun.WebView service; released when the worker exits. */
20
23
  private readonly webviewBroker;
21
24
  readonly worker: Worker;
@@ -8,11 +8,13 @@ import { roleConnection } from "./connection-roles.js";
8
8
  import { subscriptionLimits } from "../../core/subscription-limits/limit-store.js";
9
9
  import { SessionWorkerRequests } from "./session-worker-requests.js";
10
10
  import { acknowledge, acknowledgeGrant } from "./session-worker-signals.js";
11
+ import { randomUUID } from "node:crypto";
11
12
  const compiledWorkerEntry = typeof SENPI_RPC_SESSION_WORKER_ENTRY === "string"
12
13
  ? SENPI_RPC_SESSION_WORKER_ENTRY
13
14
  : "./src/modes/rpc/session-worker.ts";
14
15
  export class SessionWorkerClient {
15
16
  constructor(callbacks) {
17
+ this.privateOperations = new Map();
16
18
  /** The worker's own route to the main-thread Bun.WebView service; released when the worker exits. */
17
19
  this.webviewBroker = createWebViewBroker();
18
20
  this.worker = new Worker(isBunBinary
@@ -35,6 +37,11 @@ export class SessionWorkerClient {
35
37
  this.callbacks = callbacks;
36
38
  this.exited = new Promise((resolve, reject) => {
37
39
  this.worker.once("exit", () => {
40
+ this.stopPrivateBinding?.();
41
+ this.privateBinding?.dispose();
42
+ for (const operation of this.privateOperations.values())
43
+ operation.operation.abort();
44
+ this.privateOperations.clear();
38
45
  if (!this.stopped && !this.closeTimer)
39
46
  this.fail("session_worker_exited");
40
47
  this.stopped = true;
@@ -111,9 +118,12 @@ export class SessionWorkerClient {
111
118
  capabilities: options.capabilities ?? [],
112
119
  connection: writer.currentConnection(),
113
120
  roles: options.connectionRoles?.rolesOf(roleConnection(writer.currentConnection())),
121
+ mcpFormsEnforced: writer.mcpPresenters !== undefined,
114
122
  });
115
123
  return {
116
124
  handle: async (command) => {
125
+ if (writer.handleMcpForm(command))
126
+ return;
117
127
  const connection = writer.currentConnection();
118
128
  if ("type" in command &&
119
129
  command.type === "extension_ui_response" &&
@@ -177,6 +187,85 @@ export class SessionWorkerClient {
177
187
  return;
178
188
  }
179
189
  switch (message.type) {
190
+ case "mcp_form_bind": {
191
+ this.stopPrivateBinding?.();
192
+ this.privateBinding?.dispose();
193
+ const writer = this.writer;
194
+ const sessionId = this.sessionId;
195
+ if (!writer?.mcpPresenters || !sessionId) {
196
+ this.fail("private_form_unavailable");
197
+ return;
198
+ }
199
+ const bound = writer.bindMcpForms(sessionId, message.durableId, {}, () => { });
200
+ if (bound instanceof Promise) {
201
+ this.fail("private_form_unavailable");
202
+ return;
203
+ }
204
+ this.privateBinding = writer.getMcpFormBinding(sessionId);
205
+ const binding = this.privateBinding;
206
+ if (!binding) {
207
+ this.fail("private_form_unavailable");
208
+ return;
209
+ }
210
+ const unsubscribe = binding.subscribe?.(() => this.post({
211
+ type: "mcp_form_availability",
212
+ epoch: binding.epoch,
213
+ version: binding.version?.(),
214
+ }));
215
+ this.stopPrivateBinding = () => {
216
+ unsubscribe?.();
217
+ bound?.();
218
+ };
219
+ this.post({ type: "mcp_form_bound", id: message.id, epoch: binding.epoch, version: binding.version?.() });
220
+ return;
221
+ }
222
+ case "mcp_form_ready": {
223
+ const binding = this.privateBinding;
224
+ this.post({
225
+ type: "mcp_form_availability",
226
+ id: message.id,
227
+ epoch: message.epoch,
228
+ version: binding?.epoch === message.epoch ? binding.version?.() : undefined,
229
+ });
230
+ return;
231
+ }
232
+ case "mcp_form_release":
233
+ if (this.privateBinding?.epoch === message.epoch)
234
+ this.stopPrivateBinding?.();
235
+ return;
236
+ case "mcp_form_cancel": {
237
+ const operation = this.privateOperations.get(message.id);
238
+ if (operation?.epoch === message.epoch)
239
+ operation[message.lifetime].abort();
240
+ return;
241
+ }
242
+ case "mcp_form_create": {
243
+ const binding = this.privateBinding;
244
+ const presenter = binding?.epoch === message.epoch && binding.version?.() === message.version
245
+ ? binding.capture()
246
+ : undefined;
247
+ const operation = new AbortController();
248
+ const presentation = new AbortController();
249
+ this.privateOperations.set(message.id, { operation, presentation, epoch: message.epoch });
250
+ const fence = new Int32Array(message.operationFence);
251
+ const result = presenter?.present({
252
+ ...message.input,
253
+ operationSignal: operation.signal,
254
+ presentationSignal: presentation.signal,
255
+ operationCurrent: () => Atomics.load(fence, 0) === 0,
256
+ });
257
+ void Promise.resolve(result).then((outcome) => {
258
+ this.privateOperations.delete(message.id);
259
+ this.post({
260
+ type: "mcp_form_settlement",
261
+ id: message.id,
262
+ epoch: message.epoch,
263
+ settlementId: randomUUID(),
264
+ outcome: outcome ?? { kind: "processing_failure", source: "host_termination", humanAdmission: false },
265
+ });
266
+ });
267
+ return;
268
+ }
180
269
  case "prepared":
181
270
  case "ready":
182
271
  case "result": {
@@ -1,4 +1,5 @@
1
1
  import type { BrowserEngine } from "../../core/browser-engine.ts";
2
+ import type { McpFormInput, McpFormOutcome } from "../../core/mcp-form-types.ts";
2
3
  import type { PromptSurface } from "../../core/dynamic-prompt/types.ts";
3
4
  import type { MirroredLimits } from "../../core/subscription-limits/limit-store.ts";
4
5
  import type { CliRuntimeConfiguration } from "../../main.ts";
@@ -52,6 +53,23 @@ export type HostToSessionWorker = {
52
53
  connection?: string;
53
54
  /** Roles the connection explicitly declared on the host (senpi#3811); the isolate adopts them. */
54
55
  roles?: readonly string[];
56
+ mcpFormsEnforced?: boolean;
57
+ } | {
58
+ type: "mcp_form_bound";
59
+ id: string;
60
+ epoch: string;
61
+ version?: string;
62
+ } | {
63
+ type: "mcp_form_availability";
64
+ epoch: string;
65
+ version?: string;
66
+ id?: string;
67
+ } | {
68
+ type: "mcp_form_settlement";
69
+ id: string;
70
+ epoch: string;
71
+ settlementId: string;
72
+ outcome: McpFormOutcome;
55
73
  } | {
56
74
  type: "command";
57
75
  request: number;
@@ -84,6 +102,29 @@ export type HostToSessionWorker = {
84
102
  type: "close";
85
103
  };
86
104
  export type SessionWorkerToHost = {
105
+ type: "mcp_form_bind";
106
+ id: string;
107
+ durableId: string;
108
+ } | {
109
+ type: "mcp_form_ready";
110
+ id: string;
111
+ epoch: string;
112
+ } | {
113
+ type: "mcp_form_release";
114
+ epoch: string;
115
+ } | {
116
+ type: "mcp_form_cancel";
117
+ id: string;
118
+ epoch: string;
119
+ lifetime: "operation" | "presentation";
120
+ } | {
121
+ type: "mcp_form_create";
122
+ id: string;
123
+ epoch: string;
124
+ version: string;
125
+ operationFence: SharedArrayBuffer;
126
+ input: Omit<McpFormInput, "operationSignal" | "operationCurrent" | "presentationSignal">;
127
+ } | {
87
128
  type: "prepared";
88
129
  request: number;
89
130
  sessionPath: string;
@@ -23,6 +23,7 @@ import { resolveMovedProfile } from "./session-registry-moved-path.js";
23
23
  import { createWorkerCredit } from "./session-worker-credit.js";
24
24
  import { SESSION_WORKER_LIMITS, } from "./session-worker-protocol.js";
25
25
  import { createSessionWorkerRegistry } from "./session-worker-registry.js";
26
+ import { McpFormWorker } from "./mcp-form-worker.js";
26
27
  if (isBunBinary) {
27
28
  const { registerBunRuntimeModules } = await import("../../bun/runtime-modules.js");
28
29
  registerBunRuntimeModules();
@@ -33,6 +34,8 @@ const port = parentPort;
33
34
  if (!port)
34
35
  throw new Error("Session worker requires a parent port");
35
36
  const send = (message) => port.postMessage(message);
37
+ const privateForms = new McpFormWorker(send);
38
+ let privateFormsEnforced = false;
36
39
  function failWorker(error) {
37
40
  send({ type: "failure", error });
38
41
  process.exit(1);
@@ -41,6 +44,9 @@ const { exchange, installWriteReservation } = createWorkerCredit(send, failWorke
41
44
  // The host's key for the same file, so a deleted directory neither throws here nor misses (senpi#2285).
42
45
  installWriteReservation(canonicalSessionPath);
43
46
  class WorkerEventWriter extends SessionEventWriter {
47
+ bindMcpForms(_sessionId, durableId, ui, admit) {
48
+ return privateFormsEnforced ? privateForms.bind(durableId, ui, admit) : undefined;
49
+ }
44
50
  constructor() {
45
51
  super(() => { });
46
52
  this.workerSecurityBridge = true;
@@ -174,6 +180,7 @@ async function handle(message) {
174
180
  if (message.roles !== undefined)
175
181
  connectionRoles.adopt(roleConnection(message.connection), message.roles);
176
182
  const bindingEntry = entry;
183
+ privateFormsEnforced = message.mcpFormsEnforced === true;
177
184
  const createBinding = () => createRpcSessionBinding(message.sessionId, bindingEntry, writer, () => send({ type: "request_close" }), {
178
185
  capabilities: message.capabilities,
179
186
  connectionRoles,
@@ -262,6 +269,8 @@ async function handle(message) {
262
269
  // A turn in this isolate reports into its own store; the host mirrors it so its clients see it (senpi#3695).
263
270
  subscriptionLimits.subscribeMirror((limits) => send({ type: "subscription_limits", limits }));
264
271
  port.on("message", (message) => {
272
+ if (privateForms.receive(message))
273
+ return;
265
274
  void handle(message).catch((cause) => {
266
275
  const error = cause instanceof Error ? cause.message : String(cause);
267
276
  if (error.startsWith("session_worker_")) {
@@ -3507,6 +3507,11 @@ if (confirmed) {
3507
3507
 
3508
3508
  #### Manual Dismissal with AbortSignal
3509
3509
 
3510
+ Local terminal `select`, `confirm`, and `input` dialogs also accept
3511
+ `ephemeral: true`. Their visible contents remain local; they emit no generic
3512
+ blocked events, replacement labels, identifiers, or pending markers. This option is
3513
+ not an RPC field or permission to present MCP input on another session.
3514
+
3510
3515
  For more control (e.g., to distinguish timeout from user cancel), use `AbortSignal`:
3511
3516
 
3512
3517
  ```typescript
package/docs/mcp.md CHANGED
@@ -393,9 +393,26 @@ above.
393
393
  notice.
394
394
  - Every server prompt registers as `/mcp:<server>:<prompt>`; invoking it
395
395
  collects the prompt's arguments and drops the rendered text into the editor.
396
- - Servers may ask questions mid-call (elicitation, form mode): senpi walks the
397
- requested fields through input dialogs; in non-interactive runs the request
398
- is declined cleanly.
396
+ - Legacy servers may ask form questions mid-call through the existing terminal
397
+ or RPC dialog channel.
398
+ - On MCP 2026-07-28 connections, `tools/call`, `resources/read`, and `prompts/get`
399
+ can return `input_required`. Senpi collects form input privately in the
400
+ originating terminal session, lets you review and edit it, and continues the
401
+ operation with a new request ID. Only the final result reaches the model or editor.
402
+ Continue does not submit a form: even empty forms require Submit. Decline is
403
+ an explicit human refusal; dismissal of a presented form is Cancel.
404
+ - RPC, print, and other runs without an originating terminal presenter do not
405
+ advertise modern elicitation. An unsolicited input request terminates the
406
+ operation locally without fabricating an answer or retrying incomplete input.
407
+ State-only continuations need no human and remain supported.
408
+ - Each operation allows eight input rounds and five minutes of total consent
409
+ time, including queueing. The overall deadline also includes network time
410
+ and cannot be extended by progress. Caller cancellation, session closure,
411
+ or observed authorization invalidation dismisses pending input without a retry.
412
+ Authorization uses the same observed-revocation boundary as ordinary MCP
413
+ requests. Unsupported client features terminate locally.
414
+ - Continuation state and drafts are not persisted or resumed after restart.
415
+ MRTR does not enable the Tasks extension.
399
416
 
400
417
  ## Auth
401
418