@code-yeongyu/senpi 2026.10.25-1 → 2026.10.25-3

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 +36 -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-RHHBXIRN.js → chunk-4WZRVZNX.js} +1 -1
  5. package/dist/bundle/chunks/chunk-6O4SEW2W.js +5 -0
  6. package/dist/bundle/chunks/{chunk-Y5OTHMXO.js → chunk-7CNOCJCN.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-YDCRA5ES.js → chunk-7W7OJDIY.js} +4 -4
  8. package/dist/bundle/chunks/chunk-AX72VNRS.js +27 -0
  9. package/dist/bundle/chunks/{chunk-FWXMMMCV.js → chunk-C5UFJ7B3.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-7IRLFYNM.js → chunk-CNL6QSUB.js} +4 -4
  11. package/dist/bundle/chunks/{chunk-BTNYLNWZ.js → chunk-FFQOEC76.js} +5 -5
  12. package/dist/bundle/chunks/{chunk-VOBD6FQG.js → chunk-FHXR4ZAC.js} +1 -1
  13. package/dist/bundle/chunks/chunk-H3HNMY2W.js +13 -0
  14. package/dist/bundle/chunks/{chunk-BMYNMUVZ.js → chunk-HVJI5T75.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-EWOEVVQT.js → chunk-JA2R2ZSE.js} +1 -1
  16. package/dist/bundle/chunks/chunk-JPGT64RV.js +2 -0
  17. package/dist/bundle/chunks/chunk-KXB5WIS5.js +4 -0
  18. package/dist/bundle/chunks/chunk-L73UYAH5.js +3 -0
  19. package/dist/bundle/chunks/chunk-LTU6UFEX.js +2 -0
  20. package/dist/bundle/chunks/{chunk-EYBDBR7V.js → chunk-MJ5NNWX4.js} +1 -1
  21. package/dist/bundle/chunks/{chunk-RVYSJBZ7.js → chunk-O3Q2ET6W.js} +2 -5
  22. package/dist/bundle/chunks/chunk-O5RYI3IW.js +2 -0
  23. package/dist/bundle/chunks/{chunk-SGDOZT6B.js → chunk-ODTGC6YU.js} +1 -1
  24. package/dist/bundle/chunks/chunk-P3M2PF6K.js +27 -0
  25. package/dist/bundle/chunks/chunk-SWJXRC2I.js +5 -0
  26. package/dist/bundle/chunks/{chunk-QH2HSCMX.js → chunk-TTU2RNVX.js} +3 -3
  27. package/dist/bundle/chunks/{chunk-PXKF2RHD.js → chunk-UFJ2K7Q7.js} +1 -1
  28. package/dist/bundle/chunks/{chunk-7UUOGZKR.js → chunk-UQNRW3VU.js} +1 -1
  29. package/dist/bundle/chunks/{chunk-U67ML6CQ.js → chunk-WD6WBYC5.js} +1 -1
  30. package/dist/bundle/chunks/chunk-WJOT3DFL.js +2 -0
  31. package/dist/bundle/chunks/{chunk-7WAJCESX.js → chunk-WLHNEUGD.js} +1 -1
  32. package/dist/bundle/chunks/chunk-YI7SPFDN.js +5 -0
  33. package/dist/bundle/chunks/{chunk-URBJS24R.js → chunk-YRF4I43T.js} +35 -35
  34. package/dist/bundle/chunks/chunk-Z3UYNSVR.js +3 -0
  35. package/dist/bundle/chunks/cli-main-HEKFFB5S.js +3 -0
  36. package/dist/bundle/chunks/{client-KWTLTKVV.js → client-WTYEICLH.js} +3 -3
  37. package/dist/bundle/chunks/dispatch-NYAULNVW.js +9 -0
  38. package/dist/bundle/chunks/{help-fast-path-ZHZGXAUG.js → help-fast-path-3Z26DQSV.js} +1 -1
  39. package/dist/bundle/chunks/host-command-Z3WPHJOW.js +2 -0
  40. package/dist/bundle/chunks/host-lifecycle-ONQIMWMX.js +3 -0
  41. package/dist/bundle/chunks/host-lifecycle.js +9 -7
  42. package/dist/bundle/chunks/host-session-command-J65ENSV5.js +20 -0
  43. package/dist/bundle/chunks/{host-socket-request-ZDJGSSZO.js → host-socket-request-3CWE4265.js} +1 -1
  44. package/dist/bundle/chunks/interactive-mode-DEPQC6TX.js +2 -0
  45. package/dist/bundle/chunks/mcp-presenter-bootstrap-XUSCQVRC.js +2 -0
  46. package/dist/bundle/chunks/{multi-session-host-M7HVUMRH.js → multi-session-host-YI2WZOET.js} +12 -9
  47. package/dist/bundle/chunks/{package-manager-cli-FSR5P6YL.js → package-manager-cli-GHZPJMRF.js} +1 -1
  48. package/dist/bundle/chunks/{role-M2MTVACJ.js → role-JVSBNKGE.js} +1 -1
  49. package/dist/bundle/chunks/{rotation-stream-U7YQMC7H.js → rotation-stream-ZPVCFOVD.js} +1 -1
  50. package/dist/bundle/chunks/rpc-mode-ORHBGBBV.js +2 -0
  51. package/dist/bundle/chunks/{rpc-monitor-source-OL5KU6NO.js → rpc-monitor-source-FFEQS744.js} +1 -1
  52. package/dist/bundle/chunks/{runtime-session-CLNODAYE.js → runtime-session-V5OO7ZRM.js} +1 -1
  53. package/dist/bundle/chunks/{schedule-command-XBW4EOG5.js → schedule-command-YWG6D6ZF.js} +1 -1
  54. package/dist/bundle/chunks/session-control-endpoint-36HAQSQO.js +3 -0
  55. package/dist/bundle/chunks/{session-picker-KQUFWDQC.js → session-picker-25H5KNYP.js} +1 -1
  56. package/dist/bundle/chunks/session-worker.js +285 -271
  57. package/dist/bundle/chunks/{stdio-IEC635SY.js → stdio-YXDKKKQZ.js} +1 -1
  58. package/dist/bundle/chunks/{streamableHttp-KJI5KE4K.js → streamableHttp-DKIBUWVY.js} +1 -1
  59. package/dist/bundle/chunks/{types-LLNV23LW.js → types-BJUEQ4II.js} +1 -1
  60. package/dist/bundle/chunks/zod-compat-RJDXCU7S.js +2 -0
  61. package/dist/bundle/cli.js +2 -2
  62. package/dist/bundle/host-supervisor.js +9 -7
  63. package/dist/bundle/index.js +1 -1
  64. package/dist/bundle/rpc-entry.js +1 -1
  65. package/dist/bundle/runtime-manifest.json +1 -1
  66. package/dist/cli/args.d.ts +2 -0
  67. package/dist/cli/args.js +9 -0
  68. package/dist/cli/host-command-args.d.ts +1 -0
  69. package/dist/cli/host-command-args.js +7 -1
  70. package/dist/cli/host-command.js +3 -1
  71. package/dist/core/agent-session.d.ts +1 -0
  72. package/dist/core/agent-session.js +50 -16
  73. package/dist/core/engine-turn-limit.d.ts +2 -0
  74. package/dist/core/engine-turn-limit.js +7 -1
  75. package/dist/core/extensions/builtin/anthropic-subscription/auth-attempt.js +5 -0
  76. package/dist/core/extensions/builtin/goal/index.js +1 -0
  77. package/dist/core/extensions/builtin/goal/lifecycle-facts.d.ts +3 -0
  78. package/dist/core/extensions/builtin/goal/stop-pause.js +2 -1
  79. package/dist/core/extensions/builtin/loop/session-hold.js +2 -1
  80. package/dist/core/extensions/builtin/mcp/catalog.d.ts +3 -1
  81. package/dist/core/extensions/builtin/mcp/catalog.js +2 -0
  82. package/dist/core/extensions/builtin/mcp/connection.d.ts +1 -0
  83. package/dist/core/extensions/builtin/mcp/connection.js +46 -1
  84. package/dist/core/extensions/builtin/mcp/elicitation.d.ts +6 -1
  85. package/dist/core/extensions/builtin/mcp/elicitation.js +56 -2
  86. package/dist/core/extensions/builtin/mcp/expose/call.js +6 -2
  87. package/dist/core/extensions/builtin/mcp/expose/session.d.ts +4 -1
  88. package/dist/core/extensions/builtin/mcp/expose/session.js +7 -0
  89. package/dist/core/extensions/builtin/mcp/health.js +5 -0
  90. package/dist/core/extensions/builtin/mcp/host-registry.js +7 -1
  91. package/dist/core/extensions/builtin/mcp/index.js +2 -0
  92. package/dist/core/extensions/builtin/mcp/modern/client.d.ts +5 -0
  93. package/dist/core/extensions/builtin/mcp/modern/client.js +15 -0
  94. package/dist/core/extensions/builtin/mcp/modern/consent-queue.d.ts +2 -0
  95. package/dist/core/extensions/builtin/mcp/modern/consent-queue.js +59 -0
  96. package/dist/core/extensions/builtin/mcp/modern/diagnostics.d.ts +4 -0
  97. package/dist/core/extensions/builtin/mcp/modern/diagnostics.js +39 -0
  98. package/dist/core/extensions/builtin/mcp/modern/dispatch.js +21 -11
  99. package/dist/core/extensions/builtin/mcp/modern/elicitation.d.ts +14 -0
  100. package/dist/core/extensions/builtin/mcp/modern/elicitation.js +145 -0
  101. package/dist/core/extensions/builtin/mcp/modern/errors.d.ts +21 -0
  102. package/dist/core/extensions/builtin/mcp/modern/errors.js +23 -0
  103. package/dist/core/extensions/builtin/mcp/modern/form-schema.d.ts +20 -0
  104. package/dist/core/extensions/builtin/mcp/modern/form-schema.js +153 -0
  105. package/dist/core/extensions/builtin/mcp/modern/http-fetch.d.ts +3 -1
  106. package/dist/core/extensions/builtin/mcp/modern/http-fetch.js +8 -2
  107. package/dist/core/extensions/builtin/mcp/modern/inputs.d.ts +11 -0
  108. package/dist/core/extensions/builtin/mcp/modern/inputs.js +65 -0
  109. package/dist/core/extensions/builtin/mcp/modern/parameter-headers.d.ts +11 -0
  110. package/dist/core/extensions/builtin/mcp/modern/parameter-headers.js +109 -0
  111. package/dist/core/extensions/builtin/mcp/modern/request.d.ts +20 -0
  112. package/dist/core/extensions/builtin/mcp/modern/request.js +292 -0
  113. package/dist/core/extensions/builtin/mcp/modern/stdio-probe.d.ts +1 -1
  114. package/dist/core/extensions/builtin/mcp/modern/stdio-probe.js +54 -6
  115. package/dist/core/extensions/builtin/mcp/modern/transport.js +80 -14
  116. package/dist/core/extensions/builtin/mcp/modern/wire.js +11 -7
  117. package/dist/core/extensions/builtin/mcp/prompts.d.ts +5 -1
  118. package/dist/core/extensions/builtin/mcp/prompts.js +6 -1
  119. package/dist/core/extensions/builtin/mcp/request-origin.d.ts +47 -0
  120. package/dist/core/extensions/builtin/mcp/request-origin.js +59 -0
  121. package/dist/core/extensions/builtin/mcp/resources.d.ts +4 -1
  122. package/dist/core/extensions/builtin/mcp/resources.js +19 -4
  123. package/dist/core/extensions/builtin/mcp/sdk.lazy.d.ts +3 -1
  124. package/dist/core/extensions/builtin/mcp/sdk.lazy.js +3 -1
  125. package/dist/core/extensions/builtin/mcp/service-register.d.ts +4 -1
  126. package/dist/core/extensions/builtin/mcp/service-register.js +3 -0
  127. package/dist/core/extensions/builtin/mcp/service-types.d.ts +2 -1
  128. package/dist/core/extensions/builtin/mcp/service.d.ts +1 -0
  129. package/dist/core/extensions/builtin/mcp/service.js +79 -4
  130. package/dist/core/extensions/builtin/mcp/shared-connection.js +36 -2
  131. package/dist/core/extensions/builtin/mcp/shared-lease.d.ts +1 -0
  132. package/dist/core/extensions/builtin/mcp/shared-lease.js +9 -0
  133. package/dist/core/extensions/builtin/mcp/transport-sdk.d.ts +3 -2
  134. package/dist/core/extensions/builtin/mcp/transport-sdk.js +56 -25
  135. package/dist/core/extensions/builtin/mcp/wrap.d.ts +7 -0
  136. package/dist/core/extensions/builtin/mcp/wrap.js +6 -0
  137. package/dist/core/extensions/runner.js +3 -2
  138. package/dist/core/extensions/types.d.ts +2 -0
  139. package/dist/core/lifecycle-ledger.d.ts +1 -0
  140. package/dist/core/lifecycle-ledger.js +7 -2
  141. package/dist/core/lifecycle-record.js +3 -0
  142. package/dist/core/lifecycle-runtime-facts.js +3 -0
  143. package/dist/core/lifecycle-session-binding.d.ts +11 -1
  144. package/dist/core/lifecycle-session-binding.js +35 -4
  145. package/dist/core/lifecycle-stop.d.ts +41 -0
  146. package/dist/core/lifecycle-stop.js +92 -0
  147. package/dist/core/mcp-form-types.d.ts +89 -0
  148. package/dist/core/mcp-form-types.js +23 -0
  149. package/dist/core/mcp-mrtr-presentations.d.ts +15 -0
  150. package/dist/core/mcp-mrtr-presentations.js +17 -0
  151. package/dist/core/run-resume-turn.d.ts +1 -0
  152. package/dist/core/run-resume-turn.js +4 -3
  153. package/dist/core/session-lifecycle-types.d.ts +15 -0
  154. package/dist/core/session-lifecycle.d.ts +17 -1
  155. package/dist/core/session-lifecycle.js +40 -2
  156. package/dist/core/stop-resume-directive.d.ts +1 -1
  157. package/dist/core/stop-resume-directive.js +10 -1
  158. package/dist/main.js +5 -1
  159. package/dist/modes/interactive/components/extension-selector.d.ts +1 -0
  160. package/dist/modes/interactive/components/extension-selector.js +4 -0
  161. package/dist/modes/interactive/interactive-mode.d.ts +4 -0
  162. package/dist/modes/interactive/interactive-mode.js +35 -8
  163. package/dist/modes/interactive/mcp-local-presenter.d.ts +5 -0
  164. package/dist/modes/interactive/mcp-local-presenter.js +109 -0
  165. package/dist/modes/interactive/pause-footer.d.ts +5 -0
  166. package/dist/modes/interactive/pause-footer.js +2 -0
  167. package/dist/modes/rpc/connection-handler.d.ts +1 -0
  168. package/dist/modes/rpc/connection-handler.js +7 -1
  169. package/dist/modes/rpc/host-capabilities.d.ts +1 -0
  170. package/dist/modes/rpc/host-capabilities.js +2 -1
  171. package/dist/modes/rpc/host-ensure-start.js +13 -2
  172. package/dist/modes/rpc/host-ensure-types.d.ts +2 -0
  173. package/dist/modes/rpc/host-ensure.js +1 -0
  174. package/dist/modes/rpc/host-handoff.d.ts +2 -0
  175. package/dist/modes/rpc/host-handover-wire.d.ts +2 -0
  176. package/dist/modes/rpc/host-handover-wire.js +1 -0
  177. package/dist/modes/rpc/host-lifecycle-launch.d.ts +1 -0
  178. package/dist/modes/rpc/host-lifecycle-launch.js +43 -2
  179. package/dist/modes/rpc/host-reservations.js +3 -2
  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-stalled-evidence.js +2 -1
  183. package/dist/modes/rpc/host-successor.js +11 -1
  184. package/dist/modes/rpc/mcp-form-dialog.d.ts +45 -0
  185. package/dist/modes/rpc/mcp-form-dialog.js +159 -0
  186. package/dist/modes/rpc/mcp-form-worker.d.ts +14 -0
  187. package/dist/modes/rpc/mcp-form-worker.js +167 -0
  188. package/dist/modes/rpc/mcp-presenter-authority.d.ts +53 -0
  189. package/dist/modes/rpc/mcp-presenter-authority.js +309 -0
  190. package/dist/modes/rpc/mcp-presenter-bootstrap.d.ts +8 -0
  191. package/dist/modes/rpc/mcp-presenter-bootstrap.js +111 -0
  192. package/dist/modes/rpc/mcp-presenter-control.d.ts +24 -0
  193. package/dist/modes/rpc/mcp-presenter-control.js +330 -0
  194. package/dist/modes/rpc/multi-session-host.d.ts +3 -0
  195. package/dist/modes/rpc/multi-session-host.js +36 -1
  196. package/dist/modes/rpc/session-binding.js +6 -1
  197. package/dist/modes/rpc/session-event-writer.d.ts +14 -0
  198. package/dist/modes/rpc/session-event-writer.js +128 -1
  199. package/dist/modes/rpc/session-router-routing.js +1 -0
  200. package/dist/modes/rpc/session-worker-client.d.ts +3 -0
  201. package/dist/modes/rpc/session-worker-client.js +89 -0
  202. package/dist/modes/rpc/session-worker-protocol.d.ts +41 -0
  203. package/dist/modes/rpc/session-worker.js +9 -0
  204. package/dist/modes/rpc/tool-child/group-ownership.js +4 -2
  205. package/dist/utils/process-identity.d.ts +7 -0
  206. package/dist/utils/process-identity.js +32 -5
  207. package/docs/extensions.md +5 -0
  208. package/docs/mcp.md +20 -3
  209. package/docs/rpc.md +111 -0
  210. package/package.json +6 -6
  211. package/dist/bundle/chunks/chunk-3VOG6WFU.js +0 -27
  212. package/dist/bundle/chunks/chunk-7ASLQ243.js +0 -2
  213. package/dist/bundle/chunks/chunk-CDRFFHLL.js +0 -4
  214. package/dist/bundle/chunks/chunk-HPCS4HM4.js +0 -27
  215. package/dist/bundle/chunks/chunk-II22E3CV.js +0 -3
  216. package/dist/bundle/chunks/chunk-J5P2VL3Y.js +0 -5
  217. package/dist/bundle/chunks/chunk-SCVRKIFK.js +0 -2
  218. package/dist/bundle/chunks/chunk-XUFUKDKJ.js +0 -5
  219. package/dist/bundle/chunks/cli-main-TABBP55A.js +0 -3
  220. package/dist/bundle/chunks/dispatch-DEP4PEDZ.js +0 -10
  221. package/dist/bundle/chunks/host-command-OQR4RDTM.js +0 -2
  222. package/dist/bundle/chunks/host-lifecycle-U36U7JTE.js +0 -3
  223. package/dist/bundle/chunks/host-session-command-IGB27ZPJ.js +0 -20
  224. package/dist/bundle/chunks/interactive-mode-DGQNSXUA.js +0 -2
  225. package/dist/bundle/chunks/rpc-mode-SZAJNKKL.js +0 -2
  226. package/dist/bundle/chunks/session-control-endpoint-WEF3XTJR.js +0 -3
@@ -55,6 +55,8 @@ export interface Args {
55
55
  grokNeo?: boolean;
56
56
  /** Serve independently routed plain-RPC sessions over one host. */
57
57
  multiSession?: boolean;
58
+ presenterBootstrapFd?: number;
59
+ mcpPresenterEnforcement?: boolean;
58
60
  /**
59
61
  * Opt non-interactive app modes (notably RPC) into engine-side session auto-titling.
60
62
  * Deprecated for shared hosts: prefer per-session `open_session.auto_title`.
package/dist/cli/args.js CHANGED
@@ -297,6 +297,15 @@ export function parseArgs(args, options = {}) {
297
297
  else if (arg === "--multi-session") {
298
298
  result.multiSession = true;
299
299
  }
300
+ else if (arg === "--mcp-presenter-fd") {
301
+ if (args[++i] === "4")
302
+ result.presenterBootstrapFd = 4;
303
+ else
304
+ result.diagnostics.push({ type: "error", message: "Invalid presenter bootstrap descriptor" });
305
+ }
306
+ else if (arg === "--mcp-presenter-enforcement") {
307
+ result.mcpPresenterEnforcement = true;
308
+ }
300
309
  else if (arg === "--auto-title-sessions") {
301
310
  result.autoTitleSessions = true;
302
311
  }
@@ -5,6 +5,7 @@ import { type HostStopGeneration } from "../modes/rpc/host-stop-generation.ts";
5
5
  declare const SUBCOMMANDS: readonly ["ensure", "status", "stop", "handoff", "shard-path", "gc"];
6
6
  type HostSubcommand = (typeof SUBCOMMANDS)[number];
7
7
  export interface ParsedHostArgs {
8
+ readonly presenterBootstrapFd?: number;
8
9
  readonly subcommand: HostSubcommand;
9
10
  readonly specPath?: string;
10
11
  readonly policy: HostDecisionPolicy;
@@ -26,11 +26,16 @@ export function parseHostArgs(args) {
26
26
  let root;
27
27
  let agentDir;
28
28
  let keyRoot;
29
+ let presenterBootstrapFd;
29
30
  const handover = {};
30
31
  for (let index = 0; index < rest.length; index++) {
31
32
  const flag = rest[index];
32
33
  const value = rest[index + 1];
33
- if (flag === "--rpc-key-root" && value !== undefined && !value.startsWith("--")) {
34
+ if (flag === "--mcp-presenter-fd" && value === "4" && (subcommand === "ensure" || subcommand === "handoff")) {
35
+ presenterBootstrapFd = 4;
36
+ index++;
37
+ }
38
+ else if (flag === "--rpc-key-root" && value !== undefined && !value.startsWith("--")) {
34
39
  keyRoot = resolve(value);
35
40
  index++;
36
41
  }
@@ -115,6 +120,7 @@ export function parseHostArgs(args) {
115
120
  if (typeof terms === "string")
116
121
  return terms;
117
122
  return {
123
+ ...(presenterBootstrapFd === undefined ? {} : { presenterBootstrapFd }),
118
124
  subcommand,
119
125
  ...(specPath !== undefined && { specPath }),
120
126
  policy,
@@ -28,6 +28,7 @@ import { DEFAULT_HOST_LAUNCH_SPEC, HostLaunchSpecError, loadHostLaunchSpec, } fr
28
28
  import { HOST_EXIT_ERROR, HOST_EXIT_USAGE, runHostRequest, } from "../modes/rpc/host-runner.js";
29
29
  import { parseHostArgs } from "./host-command-args.js";
30
30
  import { emit } from "./host-emit.js";
31
+ import { presenterBootstrapStream } from "../modes/rpc/mcp-presenter-bootstrap.js";
31
32
  export { parseHostArgs } from "./host-command-args.js";
32
33
  const USAGE = `usage: ${APP_NAME} host <ensure|status|stop|handoff|shard-path|gc> [options]
33
34
 
@@ -55,7 +56,8 @@ export async function runHostCommand(args) {
55
56
  }
56
57
  try {
57
58
  const request = await hostRequest(parsed);
58
- const outcome = await runHostRequest(request, parsed.keyRoot === undefined ? {} : { keyRoot: parsed.keyRoot });
59
+ const presenterBootstrap = parsed.presenterBootstrapFd === undefined ? undefined : presenterBootstrapStream(parsed.presenterBootstrapFd);
60
+ const outcome = await runHostRequest(request, { keyRoot: parsed.keyRoot, presenterBootstrap }).finally(() => presenterBootstrap?.destroy());
59
61
  if (parsed.subcommand === "shard-path" && !parsed.json)
60
62
  writeSync(1, `${String(outcome.payload.socket)}\n`);
61
63
  else
@@ -1708,6 +1708,7 @@ export declare class AgentSession {
1708
1708
  noteAutomaticInput(text: string): boolean;
1709
1709
  private _stopAddedSince;
1710
1710
  private _stopHoldLive;
1711
+ private _resumeCause;
1711
1712
  get pauseView(): {
1712
1713
  pauseSeq: number;
1713
1714
  records: {
@@ -56,7 +56,7 @@ import { assertExpectedUserLeaf, buildEditedUserMessage, UserEditError, userText
56
56
  import { ENGINE_PAUSED_ENTRY_TYPE, enginePauseSinceLastTurn, settledRepetitiveTurnPause } from "./engine-paused.js";
57
57
  import { EngineTurnHold, engineTurnHoldHistory, engineWakeSource } from "./engine-turn-hold.js";
58
58
  import { EngineTurnInput } from "./engine-turn-input.js";
59
- import { ENGINE_TURN_LIMIT_ENTRY_TYPE, ENGINE_TURN_LIMIT_EVENT, ENGINE_TURN_START_ENTRY_TYPE, engineTurnHistory, engineTurnLimitNotice, engineTurnStop, isUserDirectedTurn, } from "./engine-turn-limit.js";
59
+ import { ENGINE_TURN_LIMIT_ENTRY_TYPE, ENGINE_TURN_LIMIT_EVENT, ENGINE_TURN_START_ENTRY_TYPE, engineTurnHistory, engineTurnLimitNotice, engineTurnStop, isUserDirectedTurn, resetsEngineTurnHistory, } from "./engine-turn-limit.js";
60
60
  import { EngineTurnRelease } from "./engine-turn-release.js";
61
61
  import { environmentContextMessageIfChanged, resolveEnvironmentContext, } from "./environment-context.js";
62
62
  import { areExperimentalFeaturesEnabled } from "./experimental.js";
@@ -609,6 +609,9 @@ export class AgentSession {
609
609
  this._handleAgentEvent = (event, signal) => {
610
610
  if (event.type === "agent_start")
611
611
  this._lifecycleBinding.startRun();
612
+ // Bound at emission: the core can start the next run before the queue reaches this end (senpi#3864).
613
+ if (event.type === "agent_end")
614
+ this._lifecycleBinding.bindTurnEnd(event);
612
615
  if (event.type === "message_start") {
613
616
  try {
614
617
  const entryId = this._lifecycleBinding.consume(event.message);
@@ -3113,8 +3116,9 @@ export class AgentSession {
3113
3116
  else if (event.type === "agent_end") {
3114
3117
  const extensionEvent = this._abortProvenance.beginAgentEnd(event.messages, agentEndWillRetry, this._findLastAssistantInMessages(event.messages)?.stopReason === "aborted");
3115
3118
  try {
3119
+ const guardStartLeafId = this.sessionManager.getLeafId();
3116
3120
  await this._extensionRunner.emit(extensionEvent);
3117
- await this._lifecycleBinding.turnEnded(extensionEvent);
3121
+ await this._lifecycleBinding.turnEnded(extensionEvent, event, guardStartLeafId);
3118
3122
  }
3119
3123
  finally {
3120
3124
  this._abortProvenance.endAgentEnd(extensionEvent);
@@ -3538,12 +3542,23 @@ export class AgentSession {
3538
3542
  }
3539
3543
  /** Record a deadline stop durably so the goal engine holds off until admitted user input. */
3540
3544
  _recordDeadlineStop(deadline) {
3541
- this._emitEntryAppended(this.sessionManager.appendCustomEntry(TURN_DEADLINE_ENTRY_TYPE, {
3542
- kind: deadline.kind,
3543
- limitMs: deadline.limitMs,
3544
- elapsedMs: deadline.elapsedMs,
3545
- at: Date.now(),
3546
- }));
3545
+ const at = Date.now();
3546
+ // The lifecycle stop is captured before the write, so a refused write keeps it for this run (senpi#3864).
3547
+ this._lifecycleBinding.deadlineStopped(deadline.kind, at);
3548
+ let entryId;
3549
+ try {
3550
+ entryId = this.sessionManager.appendCustomEntry(TURN_DEADLINE_ENTRY_TYPE, {
3551
+ kind: deadline.kind,
3552
+ limitMs: deadline.limitMs,
3553
+ elapsedMs: deadline.elapsedMs,
3554
+ at,
3555
+ });
3556
+ }
3557
+ catch (error) {
3558
+ this._lifecycleBinding.deadlineStopWriteFailed();
3559
+ throw error;
3560
+ }
3561
+ this._emitEntryAppended(entryId);
3547
3562
  }
3548
3563
  /**
3549
3564
  * Get the names of currently active tools.
@@ -5771,7 +5786,11 @@ export class AgentSession {
5771
5786
  });
5772
5787
  const deliveredNotices = this._engineTurnHold.noticeCount;
5773
5788
  const deliveredHeldInputs = this._heldInputs.pending().length;
5774
- if (this._stopHoldLive() || this._engineTurnHold.fences.length > 0 || deliveredHeldInputs > 0) {
5789
+ // Payload-free notices (including goals) revalidate through their producer after release.
5790
+ // An interruption must not add a directive-only turn when that work has already completed.
5791
+ const resumeHeldTurn = this._stopHoldLive() &&
5792
+ (this._resumeCause() === "stop" || deliveredNotices === 0 || this._engineTurnHold.messages().length > 0);
5793
+ if (resumeHeldTurn || this._engineTurnHold.fences.length > 0 || deliveredHeldInputs > 0) {
5775
5794
  const { turnStarted, settled } = await this._resumeAfterStop(onQueued);
5776
5795
  return {
5777
5796
  answer: { resumed: true, pauseSeq, turnStarted, deliveredNotices, deliveredHeldInputs },
@@ -5810,8 +5829,9 @@ export class AgentSession {
5810
5829
  * resume settlement delivered the turn.
5811
5830
  */
5812
5831
  async _resumeAfterStop(onQueued) {
5832
+ const resumeCause = this._resumeCause();
5813
5833
  const sources = this._engineTurnHold.releaseAll("resume_session");
5814
- const { turnId, delivered } = await this._startResumeTurn(onQueued);
5834
+ const { turnId, delivered } = await this._startResumeTurn(onQueued, resumeCause);
5815
5835
  const settled = (async () => {
5816
5836
  if (!(await delivered)) {
5817
5837
  this._engineTurnHold.rejectInput();
@@ -5833,7 +5853,7 @@ export class AgentSession {
5833
5853
  return this._resumeSettlement;
5834
5854
  }
5835
5855
  /** The resume turn both resume paths start; held input and notices are consumed only if no newer Stop aborted it. */
5836
- async _startResumeTurn(onQueued) {
5856
+ async _startResumeTurn(onQueued, resumeCause = "stop") {
5837
5857
  const settlement = Promise.withResolvers();
5838
5858
  this._resumeSettlement = settlement.promise;
5839
5859
  try {
@@ -5864,7 +5884,7 @@ export class AgentSession {
5864
5884
  }));
5865
5885
  // The hold is already released: include new or re-held input before sending, and omit withdrawals.
5866
5886
  }
5867
- const started = await this._startResumeTurnWith(generation, continues, heldInputs, onQueued);
5887
+ const started = await this._startResumeTurnWith(generation, continues, heldInputs, onQueued, resumeCause);
5868
5888
  started.delivered.then(() => settlement.resolve(), () => settlement.resolve());
5869
5889
  return started;
5870
5890
  }
@@ -5873,8 +5893,9 @@ export class AgentSession {
5873
5893
  throw error;
5874
5894
  }
5875
5895
  }
5876
- _startResumeTurnWith(generation, continues, heldInputs, onQueued) {
5896
+ _startResumeTurnWith(generation, continues, heldInputs, onQueued, resumeCause = "stop") {
5877
5897
  return startRunResumeTurn({
5898
+ resumeCause,
5878
5899
  heldInputs: () => heldInputs,
5879
5900
  continueInputs: () => [...continues],
5880
5901
  heldWakes: () => this._engineTurnHold.messages(),
@@ -5906,7 +5927,7 @@ export class AgentSession {
5906
5927
  return;
5907
5928
  this._releaseExtrasGeneration = this._userAbortGeneration;
5908
5929
  if (this._stopHoldLive())
5909
- this._releaseExtras.push({ role: "custom", ...stopResumeDirective(), timestamp: Date.now() });
5930
+ this._releaseExtras.push({ role: "custom", ...stopResumeDirective(this._resumeCause()), timestamp: Date.now() });
5910
5931
  const { messages, commands } = this._takeHeldInputs();
5911
5932
  this._releaseExtras.push(...messages);
5912
5933
  // A later capture that finds no commands keeps the run an earlier one (a question answer's) still owes.
@@ -6019,7 +6040,7 @@ export class AgentSession {
6019
6040
  });
6020
6041
  }
6021
6042
  noteAutomaticInput(text) {
6022
- const userHold = this._engineTurnHold.holds.some((hold) => hold.kind === "session_hold" || hold.kind === "inherited_hold");
6043
+ const userHold = this._engineTurnHold.holds.some((hold) => (hold.kind === "session_hold" && hold.key !== "interrupted-run") || hold.kind === "inherited_hold");
6023
6044
  if (!userHold)
6024
6045
  return false;
6025
6046
  const message = {
@@ -6039,6 +6060,13 @@ export class AgentSession {
6039
6060
  _stopHoldLive() {
6040
6061
  return this._engineTurnHold.holds.some((hold) => hold.kind === "session_hold");
6041
6062
  }
6063
+ _resumeCause() {
6064
+ const holds = this._engineTurnHold.holds;
6065
+ return holds.some((hold) => hold.kind === "session_hold" && hold.key === "interrupted-run") &&
6066
+ !holds.some((hold) => hold.kind === "session_hold" && hold.key !== "interrupted-run")
6067
+ ? "interrupted"
6068
+ : "stop";
6069
+ }
6042
6070
  get pauseView() {
6043
6071
  return {
6044
6072
  pauseSeq: this._engineTurnHold.currentPauseSeq,
@@ -6150,7 +6178,8 @@ export class AgentSession {
6150
6178
  }
6151
6179
  /** Keeps this process's engine-turn record: a user message clears it, a tool call marks the running engine turn. */
6152
6180
  _observeEngineTurnMessage(message) {
6153
- if (message.role === "user" || (message.role === "custom" && isUserDirectedTurn(message.customType))) {
6181
+ if (message.role === "user" ||
6182
+ (message.role === "custom" && resetsEngineTurnHistory(message.customType, message.details))) {
6154
6183
  this._engineTurnStarts = [];
6155
6184
  this._engineResumeWindow = false;
6156
6185
  this._engineTurnPauseRecorded = false;
@@ -6615,6 +6644,7 @@ export class AgentSession {
6615
6644
  /** A user Stop: hold automatic turns until human input or resume_session, then stop the running turn (senpi#3811). */
6616
6645
  async stop() {
6617
6646
  this._runHolds.stamp();
6647
+ this.refreshLifecycle();
6618
6648
  this._stopInterrupt = { pauseSeq: this._engineTurnHold.currentPauseSeq, at: new Date().toISOString() };
6619
6649
  try {
6620
6650
  await this.abort();
@@ -9211,6 +9241,10 @@ export class AgentSession {
9211
9241
  this._extensionErrorListener = bindings.onError;
9212
9242
  }
9213
9243
  this._applyExtensionBindings(this._extensionRunner);
9244
+ // Opening a session is not explicit Resume. Install the same hold used by Stop before
9245
+ // session_start producers run, so automatic work waits quietly in notice custody.
9246
+ if (this._lifecycleBinding.controller.requiresHumanContinuation)
9247
+ this._engineTurnHold.addHold("session_hold", { key: "interrupted-run" });
9214
9248
  this._engineTurnHold.start();
9215
9249
  this.syncPromptCacheSafeWaitEnv();
9216
9250
  await this._extensionRunner.emit({
@@ -27,6 +27,8 @@ export interface EngineTurnStop {
27
27
  export declare function engineTurnLimitNotice(stop: EngineTurnStop): string;
28
28
  /** A `.` manual continue or a continue-from-leaf is the user's own request, not an automatic turn. */
29
29
  export declare function isUserDirectedTurn(customType: string): boolean;
30
+ /** Recovering an interrupted run is not a new message that resets the unattended turn budget. */
31
+ export declare function resetsEngineTurnHistory(customType: string, details?: unknown): boolean;
30
32
  /** The session's engine turns since the user's last message, in order: when each started and whether its reply called a tool. */
31
33
  export interface EngineTurnHistory {
32
34
  readonly starts: EngineTurnStartRecord[];
@@ -23,9 +23,15 @@ export function isUserDirectedTurn(customType) {
23
23
  customType === CONTINUE_FROM_LEAF_CUSTOM_TYPE ||
24
24
  customType === STOP_RESUME_CUSTOM_TYPE);
25
25
  }
26
+ /** Recovering an interrupted run is not a new message that resets the unattended turn budget. */
27
+ export function resetsEngineTurnHistory(customType, details) {
28
+ return (isUserDirectedTurn(customType) &&
29
+ !(customType === STOP_RESUME_CUSTOM_TYPE &&
30
+ details?.resumeCause === "interrupted"));
31
+ }
26
32
  function isUserMessage(entry) {
27
33
  if (entry.type === "custom_message")
28
- return isUserDirectedTurn(entry.customType);
34
+ return resetsEngineTurnHistory(entry.customType, entry.details);
29
35
  return entry.type === "message" && entry.message.role === "user";
30
36
  }
31
37
  function isEngineTurnStart(entry) {
@@ -24,6 +24,11 @@ async function* closingQueryMessages(query) {
24
24
  }
25
25
  }
26
26
  export async function createAttemptMessages(input, auth) {
27
+ // Claude Code loads auto memory independently of settingSources. Keep its private
28
+ // notes out of every subscription session, after all auth and caller overrides.
29
+ auth.options.env = { ...auth.options.env, CLAUDE_CODE_DISABLE_AUTO_MEMORY: "1" };
30
+ if (typeof auth.options.settings !== "string")
31
+ auth.options.settings = { ...auth.options.settings, autoMemoryEnabled: false };
27
32
  if (input.createAttempt)
28
33
  return recordingLimitWindows(retainedAttemptMessages(await input.createAttempt(auth)), auth);
29
34
  const query = input.query({ prompt: input.prompt, options: auth.options });
@@ -137,6 +137,7 @@ export default function goalExtension(pi) {
137
137
  const end = lastAgentEnd ?? restoredEnd;
138
138
  return {
139
139
  goal,
140
+ ...(lastTodoOutcome === undefined ? {} : { todoOutcome: lastTodoOutcome }),
140
141
  ...(end
141
142
  ? {
142
143
  input: collectTurnEndLivenessInput(ctx, end, {
@@ -1,9 +1,12 @@
1
1
  import type { AgentEndEvent } from "../../types.ts";
2
+ import type { TodoOwedOutcome } from "./todo-owed-backstop.ts";
2
3
  import type { TurnEndLivenessInput } from "./turn-end-liveness.ts";
3
4
  import type { Goal } from "./types.ts";
4
5
  export interface GoalLifecycleFacts {
5
6
  readonly goal: Goal | null;
6
7
  readonly input?: TurnEndLivenessInput;
8
+ /** What the todo backstop returned at the last turn end; a repeated `cap` emits no event. */
9
+ readonly todoOutcome?: TodoOwedOutcome;
7
10
  }
8
11
  type Collector = (restoredEnd?: AgentEndEvent, goal?: Goal | null) => Promise<GoalLifecycleFacts>;
9
12
  export declare function onGoalLifecycleChange(owner: object, listener: (goal: Goal | null) => void): () => void;
@@ -1,7 +1,8 @@
1
1
  export const GOAL_STOP_PAUSED_ENTRY = "goal-stop-paused";
2
2
  export const GOAL_STOP_RESUMED_ENTRY = "goal-stop-resumed";
3
3
  export function stopHoldLive(ctx) {
4
- return ctx.getPauseState?.().holds.some((hold) => hold.kind === "session_hold") === true;
4
+ return (ctx.getPauseState?.().holds.some((hold) => hold.kind === "session_hold" && hold.key !== "interrupted-run") ===
5
+ true);
5
6
  }
6
7
  export function markStopPaused(pi, goalId) {
7
8
  pi.appendEntry(GOAL_STOP_PAUSED_ENTRY, { goalId });
@@ -2,6 +2,7 @@ export function sessionHeld(ctx) {
2
2
  return ctx.getPauseState?.().holds.some((hold) => hold.kind !== "run_fence") === true;
3
3
  }
4
4
  export function userStopped(ctx) {
5
- return ctx.getPauseState?.().holds.some((hold) => hold.kind === "session_hold") === true;
5
+ return (ctx.getPauseState?.().holds.some((hold) => hold.kind === "session_hold" && hold.key !== "interrupted-run") ===
6
+ true);
6
7
  }
7
8
  //# sourceMappingURL=session-hold.js.map
@@ -5,8 +5,10 @@ import type { ServerConnection } from "./connection.ts";
5
5
  import type { McpOutputArtifacts } from "./guard/output-guard.ts";
6
6
  import type { McpEnsureFreshAuth } from "./health.ts";
7
7
  import type { McpInvocationResolver } from "./invocation.ts";
8
+ import type { McpRequestOrigin } from "./request-origin.ts";
8
9
  type ListedTool = Awaited<ReturnType<Client["listTools"]>>["tools"][number];
9
10
  export interface McpToolCatalogEntry {
11
+ requestOrigin?: () => McpRequestOrigin | undefined;
10
12
  server: string;
11
13
  tool: string;
12
14
  schema: ListedTool["inputSchema"];
@@ -21,7 +23,7 @@ export interface McpToolCatalogEntry {
21
23
  outputGuard?: McpSettings["outputGuard"];
22
24
  invocation?: McpInvocationResolver;
23
25
  }
24
- type McpToolCatalogOptions = Pick<McpToolCatalogEntry, "agentDir" | "artifacts" | "ensureFresh" | "outputGuard" | "invocation">;
26
+ type McpToolCatalogOptions = Pick<McpToolCatalogEntry, "agentDir" | "artifacts" | "ensureFresh" | "outputGuard" | "invocation" | "requestOrigin">;
25
27
  export declare function collectToolCatalog(server: string, connection: ServerConnection, config: McpServerDeclaredConfig, options?: McpToolCatalogOptions): Promise<McpToolCatalogEntry[]>;
26
28
  /** What registering these tools with this catalog's resources and prompts exposes;
27
29
  * equal identities register identical definitions. */
@@ -2,6 +2,7 @@ import { collectAllPages } from "./expose/pagination.js";
2
2
  export async function collectToolCatalog(server, connection, config, options = {}) {
3
3
  const result = await collectAllPages((cursor) => connection.client.listTools(cursor === undefined ? {} : { cursor }, { timeout: config.requestTimeoutMs }));
4
4
  return result.items.map((tool) => ({
5
+ requestOrigin: options.requestOrigin,
5
6
  annotations: tool.annotations,
6
7
  agentDir: options.agentDir,
7
8
  artifacts: options.artifacts,
@@ -27,6 +28,7 @@ export function mcpRegistrationIdentity(tools, catalog) {
27
28
  }
28
29
  export function cachedToolsToCatalogEntries(server, catalog, connection, requestTimeoutMs, ensureConnected, options = {}) {
29
30
  return catalog.tools.map((tool) => ({
31
+ requestOrigin: options.requestOrigin,
30
32
  annotations: tool.annotations,
31
33
  agentDir: options.agentDir,
32
34
  artifacts: options.artifacts,
@@ -4,6 +4,7 @@ export type { ServerConnectionOptions, ServerConnectionState, ServerConnectionSt
4
4
  export declare class ServerConnection {
5
5
  #private;
6
6
  readonly serverName: string;
7
+ get requestLifetimeSignal(): AbortSignal;
7
8
  constructor(options: ServerConnectionOptions);
8
9
  get state(): ServerConnectionState;
9
10
  get declarationIdentity(): string | undefined;
@@ -2,6 +2,8 @@ import { dirname } from "node:path";
2
2
  import { resolveServerAuth } from "./auth/context.js";
3
3
  import { diagnoseCapturedMcpConnectFailure, diagnoseMcpConnectFailure } from "./diagnose.js";
4
4
  import { ConnectError } from "./errors.js";
5
+ import { legacyMcpFormEligible } from "./elicitation.js";
6
+ import { isModernMcpClient, legacyMcpFormsAdvertised } from "./modern/request.js";
5
7
  import { subscribeMcpServerLogging } from "./logging.js";
6
8
  import { isMcpNeedsAuthError } from "./needs-auth.js";
7
9
  import { subscribeMcpListChanged } from "./notifications.js";
@@ -18,12 +20,18 @@ export class ServerConnection {
18
20
  #resolveConfig;
19
21
  #projectPolicy;
20
22
  #policyAbort;
23
+ #requestLifetime = new AbortController();
24
+ get requestLifetimeSignal() {
25
+ return this.#requestLifetime.signal;
26
+ }
21
27
  #startedGrantNames = [];
22
28
  #state;
23
29
  #generation = 0;
24
30
  #connection;
25
31
  #pendingConnection;
26
32
  #pendingConnect;
33
+ #renegotiatingForms;
34
+ #stopFormReadiness;
27
35
  #lastError;
28
36
  #stateListeners = new Set();
29
37
  #toolsListeners = new Set();
@@ -87,9 +95,33 @@ export class ServerConnection {
87
95
  return this.#connection?.getRootPid() ?? null;
88
96
  }
89
97
  connect() {
98
+ return this.#renegotiatingForms ?? this.#connect();
99
+ }
100
+ #legacyFormsChanged(client) {
101
+ return (this.#elicitationUiProvider?.enforced?.() === true &&
102
+ !isModernMcpClient(client) &&
103
+ legacyMcpFormsAdvertised(client) !==
104
+ legacyMcpFormEligible(this.#elicitationUiProvider, this.#elicitationEnabled !== false));
105
+ }
106
+ #refreshLegacyForms() {
107
+ if (this.#state !== "connected" || this.#renegotiatingForms || !this.#legacyFormsChanged(this.client))
108
+ return;
109
+ // Publish the single flight before renewing: connect callers join it while the old transport closes.
110
+ this.#renegotiatingForms = Promise.resolve()
111
+ .then(() => this.renew())
112
+ .finally(() => {
113
+ this.#renegotiatingForms = undefined;
114
+ });
115
+ void this.#renegotiatingForms.catch((error) => {
116
+ this.markDegraded(error instanceof Error ? error : new Error(String(error)));
117
+ });
118
+ }
119
+ #connect() {
90
120
  if (this.#state === "disabled") {
91
121
  return Promise.reject(this.#connectError(`MCP server ${this.serverName} is disabled`, "connect"));
92
122
  }
123
+ if (this.#elicitationUiProvider?.enforced?.())
124
+ this.#stopFormReadiness ??= this.#elicitationUiProvider.subscribe?.(() => this.#refreshLegacyForms());
93
125
  if (this.#state === "connected")
94
126
  return Promise.resolve(this.client);
95
127
  if (this.#pendingConnect)
@@ -153,13 +185,17 @@ export class ServerConnection {
153
185
  throw this.#connectError(`MCP server ${this.serverName} is disabled`, "renew");
154
186
  }
155
187
  this.#policyAbort?.abort();
188
+ this.#requestLifetime.abort();
189
+ this.#requestLifetime = new AbortController();
156
190
  this.#generation++;
157
191
  this.#pendingConnect = undefined;
158
192
  this.#setState("idle");
159
193
  await this.#disposeOwnedConnections();
160
- return this.connect();
194
+ return this.#connect();
161
195
  }
162
196
  bumpGeneration() {
197
+ this.#requestLifetime.abort();
198
+ this.#requestLifetime = new AbortController();
163
199
  this.#policyAbort?.abort();
164
200
  this.#generation++;
165
201
  this.#pendingConnect = undefined;
@@ -169,6 +205,9 @@ export class ServerConnection {
169
205
  return this.#disposeOwnedConnections();
170
206
  }
171
207
  disable() {
208
+ this.#stopFormReadiness?.();
209
+ this.#stopFormReadiness = undefined;
210
+ this.#requestLifetime.abort();
172
211
  this.#policyAbort?.abort();
173
212
  this.#generation++;
174
213
  this.#pendingConnect = undefined;
@@ -177,6 +216,9 @@ export class ServerConnection {
177
216
  return this.#disposeOwnedConnections();
178
217
  }
179
218
  async dispose() {
219
+ this.#stopFormReadiness?.();
220
+ this.#stopFormReadiness = undefined;
221
+ this.#requestLifetime.abort();
180
222
  this.#policyAbort?.abort();
181
223
  this.#generation++;
182
224
  this.#pendingConnect = undefined;
@@ -320,6 +362,9 @@ export class ServerConnection {
320
362
  // Server logging notifications (todo 42): RFC-5424 → the per-server
321
363
  // logger, filtered by config.logLevel, burst-capped.
322
364
  subscribeMcpServerLogging(connection.client, { logLevel: this.#config.logLevel, logger: this.#logger });
365
+ // Readiness can change while initialize is in flight; never publish that stale negotiation as connected.
366
+ if (this.#legacyFormsChanged(connection.client))
367
+ return this.renew();
323
368
  this.#setState("connected");
324
369
  this.markToolsChanged("connect");
325
370
  return connection.client;
@@ -9,7 +9,11 @@ export declare function mcpClientCapabilities(elicitationEnabled?: boolean): {
9
9
  };
10
10
  export declare const MCP_ELICITATION_TIMEOUT_MS: number;
11
11
  type ElicitationUi = Pick<ExtensionUIContext, "input" | "select">;
12
- export type McpElicitationUiProvider = () => ElicitationUi | undefined;
12
+ export type McpElicitationUiProvider = (() => ElicitationUi | undefined) & {
13
+ readonly eligible?: () => boolean;
14
+ readonly enforced?: () => boolean;
15
+ readonly subscribe?: (listener: () => void) => () => void;
16
+ };
13
17
  export interface McpElicitationUiOwner {
14
18
  getMcpElicitationUi(): ElicitationUi | undefined;
15
19
  }
@@ -26,6 +30,7 @@ interface ElicitProperty {
26
30
  }
27
31
  /** Legacy classic-session seam. Session-owned services pass themselves directly. */
28
32
  export declare function setMcpElicitationUiProvider(provider: McpElicitationUiProvider | undefined): void;
33
+ export declare function legacyMcpFormEligible(provider: McpElicitationUiProvider | undefined, enabled: boolean): boolean;
29
34
  /**
30
35
  * Wire the elicitation/create handler onto a fresh client. Async because the
31
36
  * request schema comes from the lazily loaded SDK; the caller (`buildMcpClient`
@@ -9,6 +9,9 @@
9
9
  // The whole form is bounded by a timeout that resolves to cancel so a wedged
10
10
  // dialog can never wedge the tool-call pipeline. URL mode is deliberately not
11
11
  // implemented in v1.
12
+ import { privateMcpForms } from "../../../mcp-form-types.js";
13
+ import { legacyMcpOperation } from "./request-origin.js";
14
+ import { isRecord, validateMrtrForm } from "./modern/form-schema.js";
12
15
  import { createMcpLogger } from "./log.js";
13
16
  import { loadMcpSdkTypes } from "./sdk.lazy.js";
14
17
  import { safeTimer } from "./wrap.js";
@@ -27,6 +30,48 @@ async function rejectElicitation() {
27
30
  const { ErrorCode, McpError } = await loadMcpSdkTypes();
28
31
  throw new McpError(ErrorCode.InternalError, "Elicitation cannot be processed");
29
32
  }
33
+ export function legacyMcpFormEligible(provider, enabled) {
34
+ if (!enabled)
35
+ return false;
36
+ if (provider?.eligible)
37
+ return provider.eligible();
38
+ const ui = provider?.();
39
+ const binding = ui === undefined ? undefined : privateMcpForms(ui);
40
+ return binding === undefined || binding.capture()?.ready() === true;
41
+ }
42
+ async function rejectPrivateElicitation() {
43
+ const { ErrorCode, McpError } = await loadMcpSdkTypes();
44
+ throw new McpError(ErrorCode.InternalError, "Elicitation processing unavailable");
45
+ }
46
+ async function privateElicitation(client, params, signal, requestId, timeoutMs) {
47
+ const operation = legacyMcpOperation(client);
48
+ if (operation === "ambiguous") {
49
+ const { ErrorCode, McpError } = await loadMcpSdkTypes();
50
+ throw new McpError(ErrorCode.InternalError, "Elicitation cannot be processed: ambiguous origin from concurrent legacy tools/call requests. Run these calls sequentially or use an MRTR server.", { reason: "ambiguous_legacy_origin" });
51
+ }
52
+ const presenter = operation?.origin.privateForm;
53
+ if (!isRecord(params) ||
54
+ typeof params.message !== "string" ||
55
+ (params.mode !== undefined && params.mode !== "form") ||
56
+ !operation ||
57
+ !presenter?.ready() ||
58
+ !validateMrtrForm(params.requestedSchema))
59
+ return rejectPrivateElicitation();
60
+ const outcome = await presenter.present({
61
+ operationId: String(requestId),
62
+ inputId: String(requestId),
63
+ round: 0,
64
+ server: { label: operation.origin.serverLabel, source: operation.origin.serverSource ?? "user" },
65
+ message: params.message,
66
+ schema: params.requestedSchema,
67
+ operationSignal: AbortSignal.any([operation.signal, signal]),
68
+ presentationSignal: presenter.signal,
69
+ deadlineMs: timeoutMs,
70
+ });
71
+ if (outcome.kind !== "result" || operation.signal.aborted || !operation.origin.isCurrent())
72
+ return rejectPrivateElicitation();
73
+ return outcome.result;
74
+ }
30
75
  /**
31
76
  * Wire the elicitation/create handler onto a fresh client. Async because the
32
77
  * request schema comes from the lazily loaded SDK; the caller (`buildMcpClient`
@@ -40,18 +85,27 @@ export async function configureMcpElicitation(client, owner = legacyUiProvider,
40
85
  const { ErrorCode, McpError } = await loadMcpSdkTypes();
41
86
  client.fallbackRequestHandler = async (request) => {
42
87
  if (request.method === "elicitation/create")
43
- return rejectElicitation();
88
+ return typeof owner === "function" && owner.enforced?.() ? rejectPrivateElicitation() : rejectElicitation();
44
89
  throw new McpError(ErrorCode.MethodNotFound, "Method not found");
45
90
  };
46
91
  return;
47
92
  }
48
93
  const { ElicitRequestSchema } = await loadMcpSdkTypes();
49
- client.setRequestHandler(ElicitRequestSchema, async (request) => {
94
+ if (typeof owner === "function" && owner.enforced?.()) {
95
+ // Private validation prevents SDK error.data from echoing malformed form bodies.
96
+ client.setRequestHandler(ElicitRequestSchema.omit({ params: true }).passthrough(), (request, extra) => privateElicitation(client, request.params, extra.signal, extra.requestId, timeoutMs));
97
+ return;
98
+ }
99
+ client.setRequestHandler(ElicitRequestSchema, async (request, extra) => {
50
100
  const params = request.params;
51
101
  // URL mode is deliberately unsupported in v1 (form mode only).
52
102
  if (!("requestedSchema" in params))
53
103
  return rejectElicitation();
54
104
  const ui = typeof owner === "function" ? owner() : owner?.getMcpElicitationUi();
105
+ const enforced = (typeof owner === "function" && owner.enforced?.()) || (ui !== undefined && privateMcpForms(ui) !== undefined);
106
+ if (enforced) {
107
+ return privateElicitation(client, params, extra.signal, extra.requestId, timeoutMs);
108
+ }
55
109
  if (ui === undefined)
56
110
  return rejectElicitation();
57
111
  return await runElicitationForm(ui, params.message, params.requestedSchema, timeoutMs);
@@ -2,6 +2,8 @@ import { redactResolvedReferenceData, redactResolvedReferenceValues } from "../.
2
2
  import { ToolExecError } from "../errors.js";
3
3
  import { ensureMcpToolCallConnection, withMcpRetriableFailedSendRetry, withMcpSessionExpiryRetry } from "../health.js";
4
4
  import { runMcpConnectionLifecycleCall } from "../idle.js";
5
+ import { mcpRequestOptions } from "../request-origin.js";
6
+ import { McpMrtrError } from "../modern/errors.js";
5
7
  export async function callMcpTool(entry, args, signal, onUpdate, label, invocation) {
6
8
  try {
7
9
  return await runMcpConnectionLifecycleCall(entry.connection, () => withMcpSessionExpiryRetry(entry.connection, async () => {
@@ -29,7 +31,7 @@ export async function callMcpTool(entry, args, signal, onUpdate, label, invocati
29
31
  if (prepared !== undefined && !prepared.isCurrent())
30
32
  continue;
31
33
  const current = prepared?.entry ?? entry;
32
- const result = await current.connection.client.callTool({ name: current.tool, arguments: args }, undefined, {
34
+ const result = await current.connection.client.callTool({ name: current.tool, arguments: args }, undefined, mcpRequestOptions(current.requestOrigin?.(), {
33
35
  onprogress: (progress) => {
34
36
  onUpdate?.(redactResolvedReferenceData({
35
37
  content: [{ type: "text", text: formatProgress(label, progress) }],
@@ -38,13 +40,15 @@ export async function callMcpTool(entry, args, signal, onUpdate, label, invocati
38
40
  },
39
41
  signal,
40
42
  timeout: current.requestTimeoutMs,
41
- });
43
+ }));
42
44
  return { kind: "called", entry: current, result };
43
45
  }
44
46
  });
45
47
  }));
46
48
  }
47
49
  catch (error) {
50
+ if (error instanceof McpMrtrError)
51
+ throw error;
48
52
  const label = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
49
53
  throw new ToolExecError(redactResolvedReferenceValues(`ToolExecError: ${label}`), {
50
54
  cause: error,