@gajae-code/coding-agent 0.16.3 → 0.16.6

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 (277) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/cli/args.d.ts +2 -2
  4. package/dist/types/cli/customize-doctor.d.ts +22 -0
  5. package/dist/types/cli/root-flags.d.ts +4 -2
  6. package/dist/types/cli.d.ts +1 -1
  7. package/dist/types/commands/launch.d.ts +1 -1
  8. package/dist/types/config/autorouting-tier-map.d.ts +3 -0
  9. package/dist/types/config/file-lock.d.ts +31 -3
  10. package/dist/types/config/model-registry.d.ts +4 -0
  11. package/dist/types/config/model-resolver.d.ts +9 -0
  12. package/dist/types/config/settings-schema.d.ts +29 -1
  13. package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
  14. package/dist/types/coordinator-mcp/server.d.ts +7 -0
  15. package/dist/types/discovery/claude-plugins.d.ts +3 -1
  16. package/dist/types/discovery/helpers.d.ts +35 -0
  17. package/dist/types/exec/bash-executor.d.ts +17 -1
  18. package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
  19. package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
  20. package/dist/types/exec/bash-shell-signals.d.ts +2 -0
  21. package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
  22. package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
  23. package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
  24. package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
  25. package/dist/types/exec/bash-shell-worker.d.ts +1 -0
  26. package/dist/types/exec/isolated-shell.d.ts +19 -0
  27. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
  28. package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
  29. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  30. package/dist/types/extensibility/extensions/runner.d.ts +46 -6
  31. package/dist/types/extensibility/extensions/types.d.ts +16 -0
  32. package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
  33. package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
  34. package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
  35. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
  36. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  37. package/dist/types/extensibility/hooks/types.d.ts +4 -0
  38. package/dist/types/extensibility/runtime-skill-discovery.d.ts +5 -2
  39. package/dist/types/extensibility/shared-events.d.ts +1 -1
  40. package/dist/types/extensibility/skill-management.d.ts +9 -4
  41. package/dist/types/extensibility/skills.d.ts +2 -0
  42. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
  43. package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
  44. package/dist/types/gjc-runtime/session-state-lock.d.ts +14 -0
  45. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
  46. package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
  47. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  48. package/dist/types/modes/components/bash-execution.d.ts +1 -0
  49. package/dist/types/modes/components/execution-shared.d.ts +1 -0
  50. package/dist/types/modes/components/settings-selector.d.ts +6 -1
  51. package/dist/types/modes/components/status-line/command.d.ts +35 -0
  52. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  53. package/dist/types/modes/components/status-line/types.d.ts +5 -0
  54. package/dist/types/modes/components/tool-status-header.d.ts +14 -0
  55. package/dist/types/modes/controllers/event-controller.d.ts +6 -0
  56. package/dist/types/modes/interactive-mode.d.ts +3 -0
  57. package/dist/types/modes/types.d.ts +7 -0
  58. package/dist/types/runtime-mcp/client.d.ts +9 -1
  59. package/dist/types/runtime-mcp/manager.d.ts +7 -1
  60. package/dist/types/runtime-mcp/pool.d.ts +22 -3
  61. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  62. package/dist/types/runtime-mcp/types.d.ts +22 -0
  63. package/dist/types/sdk/broker/ensure.d.ts +32 -0
  64. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
  65. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  66. package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
  67. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
  68. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
  69. package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
  70. package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
  71. package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
  72. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  73. package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
  74. package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
  75. package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
  76. package/dist/types/sdk/client/client.d.ts +6 -0
  77. package/dist/types/sdk/host/host.d.ts +3 -1
  78. package/dist/types/sdk/host/session-runtime.d.ts +26 -1
  79. package/dist/types/sdk/lifecycle/service.d.ts +1 -0
  80. package/dist/types/sdk/router/session-router.d.ts +10 -2
  81. package/dist/types/sdk/session-api-key.d.ts +26 -0
  82. package/dist/types/sdk/session.d.ts +5 -1
  83. package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
  84. package/dist/types/session/agent-session.d.ts +11 -0
  85. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  86. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  87. package/dist/types/session/messages.d.ts +1 -0
  88. package/dist/types/session/session-manager.d.ts +3 -1
  89. package/dist/types/setup/paseo/announce.d.ts +112 -0
  90. package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
  91. package/dist/types/skill-state/canonical-skills.d.ts +4 -0
  92. package/dist/types/system-prompt.d.ts +10 -0
  93. package/dist/types/task/executor.d.ts +5 -1
  94. package/dist/types/tools/bash.d.ts +1 -0
  95. package/dist/types/tools/fetch.d.ts +7 -0
  96. package/dist/types/tools/github-cache.d.ts +2 -1
  97. package/dist/types/tools/index.d.ts +10 -0
  98. package/dist/types/tools/path-utils.d.ts +2 -0
  99. package/dist/types/tools/search.d.ts +6 -0
  100. package/package.json +9 -7
  101. package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
  102. package/scripts/generate-sdk-operation-inventory.ts +11 -0
  103. package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
  104. package/src/capability/index.ts +14 -2
  105. package/src/capability/types.ts +4 -0
  106. package/src/cli/args.ts +20 -9
  107. package/src/cli/customize-doctor.ts +155 -12
  108. package/src/cli/root-flags.ts +5 -3
  109. package/src/cli.ts +22 -0
  110. package/src/commands/sdk.ts +74 -31
  111. package/src/config/autorouting-tier-map.ts +1 -0
  112. package/src/config/file-lock-gc.ts +40 -2
  113. package/src/config/file-lock.ts +379 -31
  114. package/src/config/model-preset-registry.ts +120 -19
  115. package/src/config/model-profiles.ts +40 -0
  116. package/src/config/model-registry.ts +73 -23
  117. package/src/config/model-resolver.ts +48 -0
  118. package/src/config/settings-schema.ts +33 -1
  119. package/src/config/settings.ts +1 -0
  120. package/src/coordinator-mcp/question-state.ts +33 -2
  121. package/src/coordinator-mcp/server.ts +477 -101
  122. package/src/crash/sanitize.ts +1 -1
  123. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
  124. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
  125. package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
  126. package/src/discovery/builtin.ts +52 -55
  127. package/src/discovery/claude-plugins.ts +12 -4
  128. package/src/discovery/helpers.ts +307 -34
  129. package/src/exec/bash-executor.ts +330 -50
  130. package/src/exec/bash-shell-guardian.ts +388 -0
  131. package/src/exec/bash-shell-runtime-entry.ts +5 -0
  132. package/src/exec/bash-shell-signals.ts +29 -0
  133. package/src/exec/bash-shell-supervisor-entry.ts +3 -0
  134. package/src/exec/bash-shell-supervisor.ts +359 -0
  135. package/src/exec/bash-shell-worker-entry.ts +5 -0
  136. package/src/exec/bash-shell-worker-protocol.ts +36 -0
  137. package/src/exec/bash-shell-worker.ts +116 -0
  138. package/src/exec/isolated-shell.ts +393 -0
  139. package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
  140. package/src/extensibility/extensions/function-hooks.ts +959 -0
  141. package/src/extensibility/extensions/index.ts +22 -0
  142. package/src/extensibility/extensions/loader.ts +53 -2
  143. package/src/extensibility/extensions/runner.ts +895 -264
  144. package/src/extensibility/extensions/types.ts +42 -0
  145. package/src/extensibility/extensions/wrapper.ts +56 -15
  146. package/src/extensibility/gjc-plugins/compiler.ts +228 -60
  147. package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
  148. package/src/extensibility/gjc-plugins/installer.ts +13 -12
  149. package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
  150. package/src/extensibility/gjc-plugins/paths.ts +13 -0
  151. package/src/extensibility/gjc-plugins/registry.ts +113 -1
  152. package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
  153. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
  154. package/src/extensibility/gjc-plugins/schema.ts +121 -4
  155. package/src/extensibility/gjc-plugins/types.ts +25 -0
  156. package/src/extensibility/hooks/loader.ts +133 -16
  157. package/src/extensibility/hooks/types.ts +28 -0
  158. package/src/extensibility/runtime-skill-discovery.ts +149 -190
  159. package/src/extensibility/shared-events.ts +1 -1
  160. package/src/extensibility/skill-management.ts +200 -52
  161. package/src/extensibility/skills.ts +73 -25
  162. package/src/gjc-runtime/empty-delete-gc.ts +94 -4
  163. package/src/gjc-runtime/gc-runtime.ts +29 -0
  164. package/src/gjc-runtime/linux-proc.ts +14 -0
  165. package/src/gjc-runtime/repository-binding.ts +1 -1
  166. package/src/gjc-runtime/session-state-lock.ts +143 -10
  167. package/src/gjc-runtime/session-state-sidecar.ts +27 -11
  168. package/src/gjc-runtime/state-writer.ts +30 -29
  169. package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
  170. package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
  171. package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
  172. package/src/gjc-runtime/workflow-manifest.ts +16 -0
  173. package/src/harness-control-plane/session-lease.ts +2 -2
  174. package/src/internal-urls/docs-index.generated.ts +12 -12
  175. package/src/lsp/config.ts +10 -3
  176. package/src/lsp/index.ts +1 -1
  177. package/src/lsp/path-trust.ts +69 -14
  178. package/src/main.ts +78 -35
  179. package/src/modes/DESIGN.md +54 -0
  180. package/src/modes/acp/acp-agent.ts +16 -3
  181. package/src/modes/components/assistant-message.ts +22 -9
  182. package/src/modes/components/bash-execution.ts +4 -1
  183. package/src/modes/components/execution-shared.ts +4 -1
  184. package/src/modes/components/model-selector.ts +166 -33
  185. package/src/modes/components/session-observer-overlay.ts +1 -1
  186. package/src/modes/components/settings-selector.ts +1075 -235
  187. package/src/modes/components/status-line/command.ts +190 -0
  188. package/src/modes/components/status-line/index.ts +1 -0
  189. package/src/modes/components/status-line/segments.ts +32 -5
  190. package/src/modes/components/status-line/types.ts +5 -0
  191. package/src/modes/components/tool-status-header.ts +396 -22
  192. package/src/modes/controllers/command-controller.ts +3 -0
  193. package/src/modes/controllers/event-controller.ts +151 -7
  194. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  195. package/src/modes/controllers/input-controller.ts +6 -3
  196. package/src/modes/controllers/selector-controller.ts +21 -0
  197. package/src/modes/interactive-mode.ts +53 -6
  198. package/src/modes/print-mode.ts +5 -1
  199. package/src/modes/prompt-action-autocomplete.ts +54 -27
  200. package/src/modes/runtime-init.ts +2 -0
  201. package/src/modes/types.ts +17 -3
  202. package/src/modes/utils/ui-helpers.ts +2 -0
  203. package/src/prompts/system/system-prompt.md +8 -0
  204. package/src/prompts/tools/search.md +2 -0
  205. package/src/prompts/tools/subagent.md +1 -0
  206. package/src/runtime-mcp/client.ts +60 -17
  207. package/src/runtime-mcp/manager.ts +189 -14
  208. package/src/runtime-mcp/pool.test.ts +218 -0
  209. package/src/runtime-mcp/pool.ts +224 -23
  210. package/src/runtime-mcp/transports/stdio.ts +158 -24
  211. package/src/runtime-mcp/types.ts +24 -0
  212. package/src/sdk/broker/broker.ts +124 -32
  213. package/src/sdk/broker/ensure.ts +248 -41
  214. package/src/sdk/broker/lifecycle-ledger.ts +7 -17
  215. package/src/sdk/broker/lifecycle.ts +56 -6
  216. package/src/sdk/broker/startup-failure.ts +15 -6
  217. package/src/sdk/bus/chat-daemon-control.ts +3 -1
  218. package/src/sdk/bus/chat-daemon-runtime.ts +72 -6
  219. package/src/sdk/bus/chat-effect-journal.ts +132 -7
  220. package/src/sdk/bus/conversation-store.ts +29 -0
  221. package/src/sdk/bus/lifecycle-commands.ts +17 -11
  222. package/src/sdk/bus/slack-conversation.ts +1 -0
  223. package/src/sdk/bus/slack-daemon.ts +478 -49
  224. package/src/sdk/bus/slack-live-provider.ts +12 -0
  225. package/src/sdk/bus/slack-provider.ts +5 -0
  226. package/src/sdk/bus/telegram-daemon.ts +16 -2
  227. package/src/sdk/bus/topic-registry.ts +19 -0
  228. package/src/sdk/cli/master-cli.ts +1 -1
  229. package/src/sdk/cli/session-cli.ts +11 -8
  230. package/src/sdk/client/client.ts +66 -17
  231. package/src/sdk/host/host.ts +29 -9
  232. package/src/sdk/host/query/revision-store.ts +19 -13
  233. package/src/sdk/host/reverse-leases.ts +7 -4
  234. package/src/sdk/host/session-runtime.test.ts +52 -10
  235. package/src/sdk/host/session-runtime.ts +383 -44
  236. package/src/sdk/lifecycle/service.ts +4 -0
  237. package/src/sdk/protocol/operation-inventory.generated.json +77 -0
  238. package/src/sdk/router/session-router.ts +89 -38
  239. package/src/sdk/session-api-key.ts +94 -0
  240. package/src/sdk/session.ts +228 -55
  241. package/src/sdk/transport/relay.ts +35 -13
  242. package/src/sdk/transport/serve-cli.ts +3 -3
  243. package/src/session/agent-session.ts +240 -90
  244. package/src/session/contribution-prep.ts +214 -21
  245. package/src/session/fallback-chain-controller.ts +38 -0
  246. package/src/session/internal/managed-session-storage.ts +19 -5
  247. package/src/session/messages.ts +5 -0
  248. package/src/session/session-manager.ts +29 -5
  249. package/src/session-import/redact.ts +5 -1
  250. package/src/setup/paseo/announce.ts +456 -0
  251. package/src/setup/paseo/setup-deps.ts +15 -3
  252. package/src/skill-state/canonical-skills.ts +12 -0
  253. package/src/skills/skills.test.ts +41 -21
  254. package/src/slash-commands/builtin-registry.ts +1 -0
  255. package/src/system-prompt.ts +29 -3
  256. package/src/task/executor.ts +7 -1
  257. package/src/task/index.ts +21 -4
  258. package/src/tools/bash.ts +63 -19
  259. package/src/tools/cron.ts +1 -1
  260. package/src/tools/fetch.ts +29 -1
  261. package/src/tools/gh.ts +26 -3
  262. package/src/tools/github-cache.ts +68 -16
  263. package/src/tools/index.ts +10 -0
  264. package/src/tools/job.ts +21 -4
  265. package/src/tools/monitor.ts +8 -2
  266. package/src/tools/path-utils.ts +7 -0
  267. package/src/tools/read.ts +14 -2
  268. package/src/tools/render-utils.ts +1 -3
  269. package/src/tools/resource-gc.ts +28 -8
  270. package/src/tools/search.ts +116 -58
  271. package/src/tools/skill-discovery.ts +5 -0
  272. package/src/tools/skill.ts +37 -2
  273. package/src/tools/subagent.ts +6 -3
  274. package/src/tools/tool-catalog.generated.ts +10 -3
  275. package/src/tools/ultragoal-ask-guard.ts +2 -2
  276. package/src/web/insane/bridge.ts +113 -23
  277. package/src/web/search/providers/perplexity.ts +102 -41
@@ -74,6 +74,20 @@ export type ActivityIndicatorStopOptions = Readonly<{
74
74
  foregroundSettled?: boolean;
75
75
  }>;
76
76
 
77
+ export function clearInteractiveActivityIndicator(ctx: {
78
+ loadingAnimation?: Loader;
79
+ statusContainer?: PartialActivityStatusContainer;
80
+ clearLoadingAnimation?: () => void;
81
+ }): void {
82
+ if (ctx.clearLoadingAnimation) {
83
+ ctx.clearLoadingAnimation();
84
+ return;
85
+ }
86
+ ctx.loadingAnimation?.stop();
87
+ ctx.loadingAnimation = undefined;
88
+ ctx.statusContainer?.clear?.();
89
+ }
90
+
77
91
  export function stopInteractiveActivityIndicator(
78
92
  ctx: {
79
93
  loadingAnimation?: Loader;
@@ -86,9 +100,7 @@ export function stopInteractiveActivityIndicator(
86
100
  ctx.stopLoadingAnimation(options);
87
101
  return;
88
102
  }
89
- ctx.loadingAnimation?.stop();
90
- ctx.loadingAnimation = undefined;
91
- ctx.statusContainer?.clear?.();
103
+ clearInteractiveActivityIndicator(ctx);
92
104
  }
93
105
 
94
106
  export function clearInteractiveActivityLoaders(
@@ -285,6 +297,7 @@ export interface InteractiveModeContext {
285
297
  setWorkingMessage(message?: string): void;
286
298
  applyPendingWorkingMessage(): void;
287
299
  ensureLoadingAnimation(): void;
300
+ clearLoadingAnimation(): void;
288
301
  syncActivityIndicator(): void;
289
302
  suspendActivityIndicator(): () => void;
290
303
  stopLoadingAnimation(options?: ActivityIndicatorStopOptions): void;
@@ -355,6 +368,7 @@ export interface InteractiveModeContext {
355
368
  extractAssistantText(message: AssistantMessage): string;
356
369
  /** Records one semantic visible-transcript mutation for the sticky viewport. */
357
370
  recordVisibleTranscriptMutation?(): void;
371
+ resetAssistantTextPresentation(): void;
358
372
  updateEditorTopBorder(): void;
359
373
  updateEditorBorderColor(): void;
360
374
  rebuildChatFromMessages(policy: TranscriptRebuildPolicy): void;
@@ -530,6 +530,7 @@ export class UiHelpers {
530
530
  component.appendOutput(message.output);
531
531
  }
532
532
  component.setComplete(message.exitCode, message.cancelled, {
533
+ signal: message.signal,
533
534
  truncation: message.meta?.truncation,
534
535
  });
535
536
  addChatChild(this.ctx, component);
@@ -944,6 +945,7 @@ export class UiHelpers {
944
945
  this.ctx.pendingPythonComponents = this.ctx.pendingPythonComponents.filter(component =>
945
946
  runningExecutionComponents.includes(component),
946
947
  );
948
+ this.ctx.resetAssistantTextPresentation();
947
949
  this.ctx.chatContainer.clear();
948
950
 
949
951
  // Reuse a pre-built context when available (e.g. from navigateTree) to avoid a second O(N) walk.
@@ -47,6 +47,14 @@ Optimize for correctness first, maintainability second, and brevity third. Prefe
47
47
  - Do not defer actionable work. Underpromise and overdeliver: report only what is done or in progress, never announce remaining work instead of doing it.
48
48
  </communication>
49
49
 
50
+ {{#if reasoningLanguageEnglish}}
51
+ <reasoning-language>
52
+ - Reason through development and technical problem-solving in English.
53
+ - Keep user-facing answers in the language the user requested or used.
54
+ - This changes reasoning language only; it does not relax correctness, safety, or communication requirements.
55
+ </reasoning-language>
56
+ {{/if}}
57
+
50
58
  <completion-contract>
51
59
  - Never present partial work as complete.
52
60
  - Never suppress tests or warnings to make code pass.
@@ -5,6 +5,7 @@ Searches files using powerful regex matching.
5
5
  - `paths` accepts an array of files, directories, globs, or internal URLs; when omitted, the whole working directory is searched
6
6
  - `paths` is an array; do not embed commas or spaces inside a single entry. Pass `["src", "tests"]` not `["src,tests"]`.
7
7
  - Cross-line patterns are detected from literal `\n` or escaped `\\n` in `pattern`
8
+ - `timeout` is in seconds (default 5, range 0.5–60). Increase it only for intentionally broad searches; narrow `paths` first
8
9
  </instruction>
9
10
 
10
11
  <output>
@@ -20,4 +21,5 @@ Searches files using powerful regex matching.
20
21
  <critical>
21
22
  - Search paths are an array; pass separate entries rather than comma-joined paths.
22
23
  - Use a cross-line pattern only when the match actually spans lines.
24
+ - Do not retry a timed-out search unchanged; narrow `paths`/`pattern` or explicitly increase `timeout`.
23
25
  </critical>
@@ -15,6 +15,7 @@ Inspect selected subagents by `ids`; omit `ids` to inspect current running subag
15
15
  ## `action: "await"`
16
16
  Wait for selected subagents by `ids`; omit `ids` to wait for current running subagents.
17
17
  - Always set `timeout_ms` when the result is not immediately required forever.
18
+ - `heartbeat_ms` accepts integer intervals from 100 to 60,000 milliseconds (for example, 10,000 for ten seconds); `0` disables heartbeat updates and omission retains the 500 ms default. Terminal completion is event-driven, not delayed until the next heartbeat.
18
19
  - Await timeout only bounds this tool call's wait; it does not stop the subagent and is not a failure reason.
19
20
  - On timeout, inspect progress and keep doing independent work. Never cancel just because an await timed out; cancel only if the subagent has actually failed, gone off-track, or become unrecoverably wrong.
20
21
  - Completed results are receipt-first by default: bounded preview plus `agent://<id>` output ref when available, not full retained output.
@@ -24,8 +24,8 @@ import {
24
24
  normalizeMcpCacheHints,
25
25
  resolveMCPProtocolPreference,
26
26
  } from "./protocol";
27
- import { createHttpTransport, HttpTransport } from "./transports/http";
28
- import { createStdioTransport } from "./transports/stdio";
27
+ import { HttpTransport } from "./transports/http";
28
+ import { StdioTransport } from "./transports/stdio";
29
29
  import type {
30
30
  MCPDiscoverResultLite,
31
31
  MCPGetPromptParams,
@@ -188,18 +188,18 @@ async function defaultRequestHandler(method: string, _params: unknown): Promise<
188
188
  /**
189
189
  * Create a transport for the given server config.
190
190
  */
191
- async function createTransport(config: MCPServerConfig): Promise<MCPTransport> {
191
+ function createTransport(config: MCPServerConfig): StdioTransport | HttpTransport {
192
192
  const serverType = config.type ?? "stdio";
193
193
 
194
194
  switch (serverType) {
195
195
  case "stdio":
196
- return createStdioTransport(config as MCPStdioServerConfig);
196
+ return new StdioTransport(config as MCPStdioServerConfig);
197
197
  case "http":
198
- return createHttpTransport(config as MCPHttpServerConfig);
198
+ return new HttpTransport(config as MCPHttpServerConfig);
199
199
  case "sse":
200
200
  // Compatibility: `sse` configs use Streamable HTTP, not the legacy MCP SSE
201
201
  // protocol. The configured URL receives JSON-RPC POST requests directly.
202
- return createHttpTransport(config as MCPSseServerConfig);
202
+ return new HttpTransport(config as MCPSseServerConfig);
203
203
  default:
204
204
  throw new Error(`Unknown server type: ${serverType}`);
205
205
  }
@@ -295,6 +295,42 @@ function unwrapTransportFailure(error: unknown): unknown {
295
295
  return error instanceof MCPExpectedFailure && error.cause !== undefined ? error.cause : error;
296
296
  }
297
297
 
298
+ function transportFailureCauses(error: unknown): unknown[] {
299
+ const failure = unwrapTransportFailure(error);
300
+ return failure instanceof AggregateError ? failure.errors : [failure];
301
+ }
302
+
303
+ function connectionAndCleanupCause(connectionError: unknown, cleanupError: unknown): AggregateError {
304
+ return new AggregateError(
305
+ [...transportFailureCauses(connectionError), ...transportFailureCauses(cleanupError)],
306
+ "MCP connection failed and transport cleanup also failed",
307
+ );
308
+ }
309
+
310
+ function combinedConnectionAndCleanupFailure(connectionError: unknown, cleanupError: unknown): MCPExpectedFailure {
311
+ return new MCPExpectedFailure(connectionAndCleanupCause(connectionError, cleanupError));
312
+ }
313
+
314
+ /** Failed connection whose still-owned transport cleanup can be retried explicitly. */
315
+ export class MCPConnectionCleanupFailure extends MCPExpectedFailure {
316
+ #transport: MCPTransport;
317
+
318
+ constructor(cause: unknown, transport: MCPTransport) {
319
+ super(cause);
320
+ this.name = "MCPConnectionCleanupFailure";
321
+ this.#transport = transport;
322
+ }
323
+
324
+ /** Retry the same retained transport cleanup; failures preserve all prior causes. */
325
+ async close(): Promise<void> {
326
+ try {
327
+ await this.#transport.close();
328
+ } catch (cleanupError) {
329
+ throw new MCPConnectionCleanupFailure(connectionAndCleanupCause(this, cleanupError), this.#transport);
330
+ }
331
+ }
332
+ }
333
+
298
334
  type ModernNegotiation =
299
335
  | { outcome: "modern"; discovery: MCPDiscoverResultLite | null; discoverState: "yes" | "no"; versionRetry: boolean }
300
336
  | { outcome: "legacy-fallback"; reason: MCPDowngradeReason };
@@ -505,14 +541,14 @@ export async function connectToServer(
505
541
  },
506
542
  ): Promise<MCPServerConnection> {
507
543
  const timeoutMs = config.timeout ?? CONNECTION_TIMEOUT_MS;
508
- let transport: MCPTransport | undefined;
544
+ let transport: StdioTransport | HttpTransport | undefined;
509
545
  const connectAbort = new AbortController();
510
546
  const connectSignal = options?.signal ? AbortSignal.any([options.signal, connectAbort.signal]) : connectAbort.signal;
511
547
 
512
548
  const preference = resolveMCPProtocolPreference(config.protocol);
513
549
 
514
550
  const connect = async (): Promise<MCPServerConnection> => {
515
- transport = await createTransport(config);
551
+ transport = createTransport(config);
516
552
  if (options?.onNotification) {
517
553
  transport.onNotification = options.onNotification;
518
554
  }
@@ -561,6 +597,7 @@ export async function connectToServer(
561
597
  };
562
598
 
563
599
  try {
600
+ await transport.connect();
564
601
  // stdio has no per-request HTTP era signals; it stays on the legacy handshake.
565
602
  if ((config.type ?? "stdio") === "stdio") {
566
603
  return await connectLegacy("legacy-forced", "stdio-transport");
@@ -609,8 +646,8 @@ export async function connectToServer(
609
646
  } catch (error) {
610
647
  try {
611
648
  await transport.close();
612
- } catch {
613
- // Preserve the initialization failure when cleanup also fails.
649
+ } catch (cleanupError) {
650
+ throw combinedConnectionAndCleanupFailure(error, cleanupError);
614
651
  }
615
652
  throw error;
616
653
  }
@@ -625,20 +662,26 @@ export async function connectToServer(
625
662
  // abort initialization and wait for transport cleanup before returning.
626
663
  const aborted = options?.signal?.aborted === true;
627
664
  connectAbort.abort(error);
665
+ let failure = error;
666
+ let cleanupIncomplete = false;
628
667
  if (transport) {
629
668
  try {
630
669
  await transport.close();
631
- } catch {
632
- // Preserve the primary connection failure when cleanup also fails.
670
+ } catch (cleanupError) {
671
+ failure = combinedConnectionAndCleanupFailure(failure, cleanupError);
672
+ cleanupIncomplete = true;
633
673
  }
634
674
  }
635
- if (error instanceof MCPExpectedFailure) {
636
- throw error;
675
+ if (cleanupIncomplete && transport) {
676
+ throw new MCPConnectionCleanupFailure(unwrapTransportFailure(failure), transport);
637
677
  }
638
- if (aborted || (error instanceof Error && error.message === connectionTimeoutMessage)) {
639
- throw new MCPExpectedFailure(error);
678
+ if (failure instanceof MCPExpectedFailure) {
679
+ throw failure;
640
680
  }
641
- throw error;
681
+ if (aborted || (failure instanceof Error && failure.message === connectionTimeoutMessage)) {
682
+ throw new MCPExpectedFailure(failure);
683
+ }
684
+ throw failure;
642
685
  }
643
686
  }
644
687
 
@@ -20,6 +20,7 @@ import {
20
20
  listResources,
21
21
  listResourceTemplates,
22
22
  listTools,
23
+ MCPConnectionCleanupFailure,
23
24
  readResource,
24
25
  serverSupportsPrompts,
25
26
  serverSupportsResources,
@@ -344,6 +345,8 @@ export class MCPManager {
344
345
  #pendingConnections = new Map<string, Promise<MCPServerConnection>>();
345
346
  #pendingConnectionControllers = new Map<string, AbortController>();
346
347
  #pendingToolLoads = new Map<string, Promise<ToolLoadResult>>();
348
+ #pendingConnectionCleanups = new Map<string, MCPConnectionCleanupFailure>();
349
+ #pendingAcquireCleanups = new Map<string, Set<MCPPoolAcquireAbortError>>();
347
350
  #sources = new Map<string, SourceMeta>();
348
351
  #authStorage: AuthStorage | null = null;
349
352
  #inputRequestHandler: MCPInputRequestHandler | null = null;
@@ -373,6 +376,88 @@ export class MCPManager {
373
376
  #serverError(message: string): string {
374
377
  return this.#toolsOnly ? "MCP server unavailable" : message;
375
378
  }
379
+
380
+ #removePendingAcquireCleanup(name: string, error: MCPPoolAcquireAbortError): void {
381
+ const pending = this.#pendingAcquireCleanups.get(name);
382
+ pending?.delete(error);
383
+ if (pending?.size === 0) this.#pendingAcquireCleanups.delete(name);
384
+ }
385
+ #pruneSettledPendingAcquireCleanups(name: string): void {
386
+ for (const error of this.#pendingAcquireCleanups.get(name) ?? []) {
387
+ if (!this.#pool.hasPendingAcquireCleanup(error)) this.#removePendingAcquireCleanup(name, error);
388
+ }
389
+ }
390
+
391
+ #retainPendingAcquireCleanup(name: string, error: MCPPoolAcquireAbortError): void {
392
+ if (error.cleanupGeneration === undefined || !error.cleanup || !this.#pool.hasPendingAcquireCleanup(error))
393
+ return;
394
+ let pending = this.#pendingAcquireCleanups.get(name);
395
+ if (!pending) {
396
+ pending = new Set();
397
+ this.#pendingAcquireCleanups.set(name, pending);
398
+ }
399
+ if (pending.has(error)) return;
400
+ pending.add(error);
401
+ void error.cleanup
402
+ .finally(() => {
403
+ if (!this.#pool.hasPendingAcquireCleanup(error)) this.#removePendingAcquireCleanup(name, error);
404
+ })
405
+ .catch(() => {});
406
+ }
407
+
408
+ #retainConnectionCleanupFailure(name: string, error: unknown): MCPConnectionCleanupFailure | undefined {
409
+ if (error instanceof MCPPoolAcquireAbortError) {
410
+ this.#retainPendingAcquireCleanup(name, error);
411
+ return undefined;
412
+ }
413
+ if (!(error instanceof MCPConnectionCleanupFailure)) return undefined;
414
+ if (!this.#pool.ownsPendingAcquireCleanupFailure(error)) this.#pendingConnectionCleanups.set(name, error);
415
+ return error;
416
+ }
417
+ async #closePendingConnectionCleanup(name: string, waitForAcquireFlight = true): Promise<void> {
418
+ const failures: unknown[] = [];
419
+ const pendingAcquires = [...(this.#pendingAcquireCleanups.get(name) ?? [])];
420
+ const acquireResults = await Promise.allSettled(
421
+ pendingAcquires.map(error => {
422
+ if (!error.cleanup) return Promise.resolve();
423
+ if (!waitForAcquireFlight && !this.#pool.getPendingAcquireCleanupFailure(error)) return Promise.resolve();
424
+ return error.cleanup.then(
425
+ () => undefined,
426
+ () =>
427
+ this.#pool.closePendingAcquireCleanup(error.poolKey, error.cleanupGeneration!).then(() => undefined),
428
+ );
429
+ }),
430
+ );
431
+ for (const [index, result] of acquireResults.entries()) {
432
+ const pendingAcquire = pendingAcquires[index];
433
+ if (!pendingAcquire) continue;
434
+ if (result.status === "rejected") failures.push(result.reason);
435
+ if (!this.#pool.hasPendingAcquireCleanup(pendingAcquire)) {
436
+ this.#removePendingAcquireCleanup(name, pendingAcquire);
437
+ }
438
+ }
439
+
440
+ const cleanup = this.#pendingConnectionCleanups.get(name);
441
+ if (cleanup) {
442
+ let cleanupFailed = false;
443
+ try {
444
+ await cleanup.close();
445
+ } catch (error) {
446
+ cleanupFailed = true;
447
+ const retained = error instanceof MCPConnectionCleanupFailure ? error : cleanup;
448
+ if (this.#pendingConnectionCleanups.get(name) === cleanup) {
449
+ this.#pendingConnectionCleanups.set(name, retained);
450
+ }
451
+ failures.push(error);
452
+ }
453
+ if (this.#pendingConnectionCleanups.get(name) === cleanup && !cleanupFailed) {
454
+ this.#pendingConnectionCleanups.delete(name);
455
+ }
456
+ }
457
+ if (failures.length === 1) throw failures[0];
458
+ if (failures.length > 1) throw new AggregateError(failures, `MCP server cleanup failed: ${name}`);
459
+ }
460
+
376
461
  #assertRawMCPAccessAllowed(): void {
377
462
  if (this.#toolsOnly) throw new Error("Tools-only MCP manager does not allow raw MCP access");
378
463
  }
@@ -413,6 +498,11 @@ export class MCPManager {
413
498
  connectionAbort: AbortController,
414
499
  trackManagerLease = true,
415
500
  ): Promise<MCPPoolLease> {
501
+ const pendingCleanup = this.#pendingConnectionCleanups.get(name);
502
+ if (pendingCleanup) throw pendingCleanup;
503
+ if (connectionAbort.signal.aborted) {
504
+ throw connectionAbort.signal.reason ?? new Error(`MCP connection acquisition aborted: ${name}`);
505
+ }
416
506
  const sharedConfig = originalConfig.sharing === "shared";
417
507
  const sharedEligible =
418
508
  sharedConfig &&
@@ -435,7 +525,9 @@ export class MCPManager {
435
525
  effectiveHeaders:
436
526
  resolvedConfig.type === "http" || resolvedConfig.type === "sse" ? resolvedConfig.headers : undefined,
437
527
  onRequest: (method, params) => this.#handleServerRequest(method, params),
528
+ onCleanupPending: error => this.#retainPendingAcquireCleanup(name, error),
438
529
  });
530
+ this.#pruneSettledPendingAcquireCleanups(name);
439
531
  if (trackManagerLease) this.#leaseByConnection.set(lease.connection, lease);
440
532
  if (!this.#toolsOnly) lease.updateRoots(this.#getRoots().roots);
441
533
  return lease;
@@ -958,6 +1050,25 @@ export class MCPManager {
958
1050
  const connectionTasks: ConnectionTask[] = [];
959
1051
 
960
1052
  for (const [name, config] of Object.entries(configs)) {
1053
+ // Another manager sharing the pool may have retried and completed an
1054
+ // abandoned-acquire cleanup. Retire this manager's observation before
1055
+ // applying its reconnect fence; pool ownership remains authoritative.
1056
+ this.#pruneSettledPendingAcquireCleanups(name);
1057
+ const pendingAcquire = this.#pendingAcquireCleanups.get(name);
1058
+ const pendingCleanup = this.#pendingConnectionCleanups.get(name);
1059
+ if (pendingCleanup || pendingAcquire) {
1060
+ const message =
1061
+ pendingCleanup?.message ??
1062
+ (pendingAcquire
1063
+ ? [...pendingAcquire]
1064
+ .map(error => this.#pool.getPendingAcquireCleanupFailure(error)?.message ?? error.message)
1065
+ .at(0)
1066
+ : undefined) ??
1067
+ "MCP connection cleanup pending";
1068
+ errors.set(name, this.#serverError(message));
1069
+ reportedErrors.add(name);
1070
+ continue;
1071
+ }
961
1072
  if (sources[name]) {
962
1073
  this.#sources.set(name, sources[name]);
963
1074
  const existing = this.#connections.get(name);
@@ -1136,6 +1247,7 @@ export class MCPManager {
1136
1247
  if (!this.#toolsOnly) await this.#loadServerResourcesAndPrompts(name, connection);
1137
1248
  })
1138
1249
  .catch(error => {
1250
+ this.#retainConnectionCleanupFailure(name, error);
1139
1251
  if (this.#pendingToolLoads.get(name) !== toolsPromise) return;
1140
1252
  this.#pendingToolLoads.delete(name);
1141
1253
  if (!allowBackgroundLogging || reportedErrors.has(name) || this.#toolsOnly) return;
@@ -1270,6 +1382,7 @@ export class MCPManager {
1270
1382
  }
1271
1383
  } else if (task.tracked.status === "rejected") {
1272
1384
  const reason = task.tracked.reason;
1385
+ this.#retainConnectionCleanupFailure(name, reason);
1273
1386
  const message = reason instanceof Error ? reason.message : String(reason);
1274
1387
  errors.set(name, this.#serverError(message));
1275
1388
  reportedErrors.add(name);
@@ -1582,6 +1695,7 @@ export class MCPManager {
1582
1695
  });
1583
1696
  result = await Promise.race([callbackPromise, abortPromise]);
1584
1697
  } catch (error) {
1698
+ this.#retainConnectionCleanupFailure(operation.name, error);
1585
1699
  failed = true;
1586
1700
  primaryError = error;
1587
1701
  } finally {
@@ -1609,6 +1723,28 @@ export class MCPManager {
1609
1723
  get retiredLeaseReleaseCountForTests(): number {
1610
1724
  return this.#retiredLeaseReleases.size;
1611
1725
  }
1726
+
1727
+ /** Read-only test seam for retained failed-connection cleanup owners. */
1728
+ get pendingConnectionCleanupCountForTests(): number {
1729
+ return (
1730
+ this.#pendingConnectionCleanups.size +
1731
+ [...this.#pendingAcquireCleanups.values()].reduce((count, pending) => count + pending.size, 0)
1732
+ );
1733
+ }
1734
+
1735
+ /** Read-only test seam for retained failed-connection cleanup causes. */
1736
+ getPendingConnectionCleanupFailureForTests(name: string): MCPConnectionCleanupFailure | undefined {
1737
+ const pendingAcquire = this.#pendingAcquireCleanups.get(name);
1738
+ return (
1739
+ this.#pendingConnectionCleanups.get(name) ??
1740
+ (pendingAcquire
1741
+ ? [...pendingAcquire]
1742
+ .map(error => this.#pool.getPendingAcquireCleanupFailure(error))
1743
+ .find((error): error is MCPConnectionCleanupFailure => error !== undefined)
1744
+ : undefined)
1745
+ );
1746
+ }
1747
+
1612
1748
  /**
1613
1749
  * Get all connected server names.
1614
1750
  */
@@ -1621,7 +1757,13 @@ export class MCPManager {
1621
1757
  */
1622
1758
  getAllServerNames(): string[] {
1623
1759
  return Array.from(
1624
- new Set([...this.#sources.keys(), ...this.#connections.keys(), ...this.#pendingConnections.keys()]),
1760
+ new Set([
1761
+ ...this.#sources.keys(),
1762
+ ...this.#connections.keys(),
1763
+ ...this.#pendingConnections.keys(),
1764
+ ...this.#pendingConnectionCleanups.keys(),
1765
+ ...this.#pendingAcquireCleanups.keys(),
1766
+ ]),
1625
1767
  );
1626
1768
  }
1627
1769
 
@@ -1652,11 +1794,24 @@ export class MCPManager {
1652
1794
  this.#subscribedResources.delete(name);
1653
1795
 
1654
1796
  let closeError: unknown;
1797
+ try {
1798
+ // A transport may ignore its abort signal while the initial connection
1799
+ // is still opening. The pool owns that abandoned flight and closes any
1800
+ // late connection, so disconnect must not wait for the uncooperative
1801
+ // opener. Already-materialized cleanup failures are still retried here.
1802
+ await this.#closePendingConnectionCleanup(name, false);
1803
+ } catch (error) {
1804
+ closeError = error;
1805
+ }
1655
1806
  if (connection) {
1656
1807
  try {
1657
1808
  await this.#releaseLease(name);
1658
1809
  } catch (error) {
1659
- closeError = this.#logLeaseReleaseFailure(name, connection, error);
1810
+ const leaseError = this.#logLeaseReleaseFailure(name, connection, error);
1811
+ closeError =
1812
+ closeError !== undefined
1813
+ ? new AggregateError([closeError, leaseError], `MCP server cleanup failed: ${name}`)
1814
+ : leaseError;
1660
1815
  }
1661
1816
  if (this.#connections.get(name) === connection) this.#connections.delete(name);
1662
1817
  }
@@ -1672,7 +1827,7 @@ export class MCPManager {
1672
1827
 
1673
1828
  // Notify prompt consumers so stale commands are cleared
1674
1829
  if (connection?.prompts?.length) this.#onPromptsChanged?.(name);
1675
- if (closeError) throw closeError;
1830
+ if (closeError !== undefined) throw closeError;
1676
1831
  }
1677
1832
 
1678
1833
  #abortConnectionTask(task: ConnectionTask): void {
@@ -1692,12 +1847,8 @@ export class MCPManager {
1692
1847
  }
1693
1848
 
1694
1849
  async #terminateConnectionTask(task: ConnectionTask): Promise<void> {
1695
- const connection = await task.connectionPromise.catch(async error => {
1696
- if (error instanceof MCPPoolAcquireAbortError && error.cleanup) {
1697
- await error.cleanup.catch(cleanupError => {
1698
- logger.error("MCP aborted acquire cleanup failed", { path: `mcp:${task.name}`, error: cleanupError });
1699
- });
1700
- }
1850
+ const connection = await task.connectionPromise.catch(error => {
1851
+ this.#retainConnectionCleanupFailure(task.name, error);
1701
1852
  logger.debug("MCP connection task did not publish before cleanup", { path: `mcp:${task.name}`, error });
1702
1853
  return undefined;
1703
1854
  });
@@ -1731,6 +1882,14 @@ export class MCPManager {
1731
1882
  const lifecycleEpoch = this.#beginScopedLifecycle("disconnecting");
1732
1883
  this.#deferredSharedRebinds.clear();
1733
1884
  try {
1885
+ const cleanupNames = new Set([
1886
+ ...this.#pendingConnectionCleanups.keys(),
1887
+ ...this.#pendingAcquireCleanups.keys(),
1888
+ ...this.#pendingConnectionControllers.keys(),
1889
+ ...this.#pendingConnections.keys(),
1890
+ ...this.#serverConfigs.keys(),
1891
+ ...[...this.#scopedOperations.values()].map(operation => operation.name),
1892
+ ]);
1734
1893
  // Invalidate any in-flight reconnection attempts that outlive this call.
1735
1894
  // They captured the old epoch; after increment they'll detect staleness.
1736
1895
  this.#epoch++;
@@ -1757,6 +1916,11 @@ export class MCPManager {
1757
1916
  }
1758
1917
  this.#reconnectBackoffs.clear();
1759
1918
  this.#pendingReconnections.clear();
1919
+ for (const name of this.#pendingConnectionCleanups.keys()) cleanupNames.add(name);
1920
+ for (const name of this.#pendingAcquireCleanups.keys()) cleanupNames.add(name);
1921
+ const pendingCleanupResults = await Promise.allSettled(
1922
+ [...cleanupNames].map(name => this.#closePendingConnectionCleanup(name, false)),
1923
+ );
1760
1924
  const retiredReleaseFailures = await this.#drainRetiredLeaseReleases();
1761
1925
  this.#deferredSharedRebinds.clear();
1762
1926
  this.#pendingResourceRefresh.clear();
@@ -1770,6 +1934,9 @@ export class MCPManager {
1770
1934
  ...releaseResults
1771
1935
  .filter((result): result is PromiseRejectedResult => result.status === "rejected")
1772
1936
  .map(result => result.reason),
1937
+ ...pendingCleanupResults
1938
+ .filter((result): result is PromiseRejectedResult => result.status === "rejected")
1939
+ .map(result => result.reason),
1773
1940
  ...retiredReleaseFailures,
1774
1941
  ];
1775
1942
  if (releaseFailures.length > 0) {
@@ -1793,7 +1960,8 @@ export class MCPManager {
1793
1960
  * Tears down the stale connection, re-resolves auth, establishes a new
1794
1961
  * connection, reloads tools, and notifies consumers.
1795
1962
  * Concurrent calls for the same server share one reconnection attempt.
1796
- * Returns the new connection, or null if reconnection failed.
1963
+ * Returns the new connection, or null if reconnection fails after cleanup.
1964
+ * Throws a retryable cleanup failure while the previous transport owner remains incomplete.
1797
1965
  */
1798
1966
  async reconnectServer(name: string): Promise<MCPServerConnection | null> {
1799
1967
  if (this.#connectionSetSealed) return null;
@@ -1801,6 +1969,14 @@ export class MCPManager {
1801
1969
  const pending = this.#pendingReconnections.get(name);
1802
1970
  if (pending) return pending;
1803
1971
 
1972
+ const attempt = this.#closePendingConnectionCleanup(name).then(() => this.#startReconnect(name));
1973
+ this.#pendingReconnections.set(name, attempt);
1974
+ return attempt.finally(() => {
1975
+ if (this.#pendingReconnections.get(name) === attempt) this.#pendingReconnections.delete(name);
1976
+ });
1977
+ }
1978
+
1979
+ #startReconnect(name: string): Promise<MCPServerConnection | null> {
1804
1980
  const lease = this.#leases.get(name);
1805
1981
  const sharedKey = lease?.sharingMode === "shared" ? lease.key : undefined;
1806
1982
  let attempt: Promise<MCPServerConnection | null>;
@@ -1831,10 +2007,7 @@ export class MCPManager {
1831
2007
  .finally(() => this.#pool.releaseRestart(sharedKey));
1832
2008
  }
1833
2009
  }
1834
- this.#pendingReconnections.set(name, attempt);
1835
- return attempt.finally(() => {
1836
- if (this.#pendingReconnections.get(name) === attempt) this.#pendingReconnections.delete(name);
1837
- });
2010
+ return attempt;
1838
2011
  }
1839
2012
 
1840
2013
  async #doReconnect(name: string): Promise<MCPServerConnection | null> {
@@ -1905,6 +2078,7 @@ export class MCPManager {
1905
2078
  logger.debug("MCP reconnected", { path: `mcp:${name}`, tools: connection.tools?.length ?? 0 });
1906
2079
  return connection;
1907
2080
  } catch (error) {
2081
+ const cleanupFailure = this.#retainConnectionCleanupFailure(name, error);
1908
2082
  if ((this.#disconnectEpochs.get(name) ?? 0) !== reconnectEpoch || backoffAbort.signal.aborted) {
1909
2083
  logger.debug("MCP reconnect aborted after server disconnected", {
1910
2084
  path: `mcp:${name}`,
@@ -1913,6 +2087,7 @@ export class MCPManager {
1913
2087
  });
1914
2088
  return null;
1915
2089
  }
2090
+ if (cleanupFailure) throw cleanupFailure;
1916
2091
 
1917
2092
  const msg = error instanceof Error ? error.message : String(error);
1918
2093
  if (attempt < delays.length) {