@gajae-code/coding-agent 0.16.7 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (321) hide show
  1. package/CHANGELOG.md +180 -1
  2. package/dist/types/capability/fs.d.ts +37 -4
  3. package/dist/types/capability/index.d.ts +19 -0
  4. package/dist/types/capability/types.d.ts +9 -0
  5. package/dist/types/cli/doctor/args.d.ts +36 -0
  6. package/dist/types/cli/doctor/checks.d.ts +18 -0
  7. package/dist/types/cli/doctor/config-repairs.d.ts +38 -0
  8. package/dist/types/cli/doctor/context.d.ts +89 -0
  9. package/dist/types/cli/doctor/files.d.ts +27 -0
  10. package/dist/types/cli/doctor/ids.d.ts +20 -0
  11. package/dist/types/cli/doctor/install-repairs.d.ts +94 -0
  12. package/dist/types/cli/doctor/journal.d.ts +43 -0
  13. package/dist/types/cli/doctor/managed-link.d.ts +39 -0
  14. package/dist/types/cli/doctor/native-probe.d.ts +2 -0
  15. package/dist/types/cli/doctor/permission-repairs.d.ts +25 -0
  16. package/dist/types/cli/doctor/plugin-quarantine.d.ts +77 -0
  17. package/dist/types/cli/doctor/plugin-restore.d.ts +40 -0
  18. package/dist/types/cli/doctor/plugin-targets.d.ts +11 -0
  19. package/dist/types/cli/doctor/probe-client.d.ts +9 -0
  20. package/dist/types/cli/doctor/probe-types.d.ts +18 -0
  21. package/dist/types/cli/doctor/probe.d.ts +3 -0
  22. package/dist/types/cli/doctor/projection-probe.d.ts +2 -0
  23. package/dist/types/cli/doctor/repairs.d.ts +28 -0
  24. package/dist/types/cli/doctor/report.d.ts +27 -0
  25. package/dist/types/cli/doctor/runner.d.ts +4 -0
  26. package/dist/types/cli/doctor/safety.d.ts +10 -0
  27. package/dist/types/cli/doctor/service-repairs.d.ts +8 -0
  28. package/dist/types/cli/doctor/service-targets.d.ts +41 -0
  29. package/dist/types/cli/doctor/types.d.ts +145 -0
  30. package/dist/types/cli/doctor-cli.d.ts +7 -0
  31. package/dist/types/cli/doctor-supervisor.d.ts +121 -0
  32. package/dist/types/cli/doctor-worker.d.ts +29 -0
  33. package/dist/types/cli/install-activation.d.ts +158 -0
  34. package/dist/types/cli/macos-community-app.d.ts +41 -0
  35. package/dist/types/cli/mcp-cli.d.ts +0 -11
  36. package/dist/types/cli/update-cli.d.ts +41 -4
  37. package/dist/types/cli-main.d.ts +174 -0
  38. package/dist/types/cli.d.ts +1 -171
  39. package/dist/types/commands/doctor.d.ts +7 -0
  40. package/dist/types/commands/sdk.d.ts +17 -8
  41. package/dist/types/config/autorouting-tier-map.d.ts +19 -0
  42. package/dist/types/config/file-lock.d.ts +27 -3
  43. package/dist/types/config/model-preset-registry.d.ts +1 -0
  44. package/dist/types/config/model-registry.d.ts +12 -5
  45. package/dist/types/config/model-resolver.d.ts +16 -3
  46. package/dist/types/config/models-config-schema.d.ts +2 -0
  47. package/dist/types/config/settings-schema.d.ts +1 -1
  48. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +1 -1
  49. package/dist/types/coordinator-mcp/session-reaper.d.ts +15 -5
  50. package/dist/types/cursor.d.ts +9 -9
  51. package/dist/types/discovery/agents-md.d.ts +6 -1
  52. package/dist/types/discovery/helpers.d.ts +36 -7
  53. package/dist/types/discovery/index.d.ts +1 -1
  54. package/dist/types/discovery/mcp-json.d.ts +2 -0
  55. package/dist/types/extensibility/extensions/loader.d.ts +22 -1
  56. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +14 -1
  57. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -1
  58. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  59. package/dist/types/extensibility/plugins/manager.d.ts +33 -4
  60. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +6 -0
  61. package/dist/types/extensibility/plugins/marketplace/manager.d.ts +186 -0
  62. package/dist/types/extensibility/plugins/marketplace/registry.d.ts +27 -0
  63. package/dist/types/extensibility/plugins/marketplace/source-resolver.d.ts +16 -1
  64. package/dist/types/extensibility/runtime-skill-discovery.d.ts +16 -3
  65. package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +6 -0
  66. package/dist/types/gjc-runtime/launch-worktree.d.ts +53 -0
  67. package/dist/types/gjc-runtime/session-layout.d.ts +8 -0
  68. package/dist/types/gjc-runtime/session-state-lock.d.ts +15 -0
  69. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
  70. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +6 -0
  71. package/dist/types/main.d.ts +19 -1
  72. package/dist/types/modes/acp/acp-agent.d.ts +40 -0
  73. package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
  74. package/dist/types/modes/components/model-selector.d.ts +2 -0
  75. package/dist/types/modes/interactive-mode.d.ts +1 -1
  76. package/dist/types/runtime-mcp/config-writer.d.ts +47 -0
  77. package/dist/types/runtime-mcp/policy-json-edit.d.ts +60 -0
  78. package/dist/types/runtime-mcp/startup-policy.d.ts +4 -0
  79. package/dist/types/runtime-mcp/types.d.ts +2 -2
  80. package/dist/types/sdk/acp/adapter.d.ts +5 -1
  81. package/dist/types/sdk/broker/broker.d.ts +51 -0
  82. package/dist/types/sdk/broker/daemon-entry.d.ts +39 -0
  83. package/dist/types/sdk/broker/discovery.d.ts +38 -0
  84. package/dist/types/sdk/broker/doctor-restart.d.ts +79 -0
  85. package/dist/types/sdk/broker/endpoint-authority.d.ts +7 -0
  86. package/dist/types/sdk/broker/ensure.d.ts +14 -0
  87. package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
  88. package/dist/types/sdk/broker/process-incarnation.d.ts +50 -0
  89. package/dist/types/sdk/broker/runtime.d.ts +26 -0
  90. package/dist/types/sdk/broker/session-index.d.ts +13 -4
  91. package/dist/types/sdk/broker/worktree-occupancy.d.ts +14 -0
  92. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +16 -0
  93. package/dist/types/sdk/bus/chat-daemon-control.d.ts +101 -6
  94. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +16 -0
  95. package/dist/types/sdk/bus/daemon-startup-exclusion.d.ts +43 -0
  96. package/dist/types/sdk/bus/doctor-daemon-restart-client.d.ts +51 -0
  97. package/dist/types/sdk/bus/doctor-daemon-restart.d.ts +39 -0
  98. package/dist/types/sdk/bus/index.d.ts +2 -0
  99. package/dist/types/sdk/bus/notification-service.d.ts +3 -0
  100. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +11 -0
  101. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  102. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -0
  103. package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +1 -0
  104. package/dist/types/sdk/bus/telegram-daemon.d.ts +36 -5
  105. package/dist/types/sdk/cli/session-cli.d.ts +1 -0
  106. package/dist/types/sdk/host/session-runtime.d.ts +4 -0
  107. package/dist/types/sdk/host/surface-policy.d.ts +3 -1
  108. package/dist/types/sdk/lifecycle/service.d.ts +44 -10
  109. package/dist/types/sdk/prompt-deadline-lease.d.ts +6 -3
  110. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -2
  111. package/dist/types/sdk/prompt-failure.d.ts +47 -0
  112. package/dist/types/sdk/prompt-status.d.ts +19 -0
  113. package/dist/types/sdk/router/session-router.d.ts +3 -4
  114. package/dist/types/sdk/service-artifact-paths.d.ts +16 -0
  115. package/dist/types/sdk/session-authority.d.ts +9 -0
  116. package/dist/types/sdk/session.d.ts +3 -1
  117. package/dist/types/session/agent-session.d.ts +1 -1
  118. package/dist/types/session/blob-store.d.ts +1 -1
  119. package/dist/types/session/fallback-chain-controller.d.ts +3 -6
  120. package/dist/types/session/internal/managed-session-scope.d.ts +44 -0
  121. package/dist/types/session/primary-control-surface.d.ts +4 -0
  122. package/dist/types/session/session-manager.d.ts +6 -0
  123. package/dist/types/system-prompt.d.ts +6 -0
  124. package/dist/types/tools/ask.d.ts +25 -0
  125. package/dist/types/tools/image-gen.d.ts +0 -4
  126. package/dist/types/tools/skill-discovery.d.ts +7 -5
  127. package/dist/types/tools/ultragoal-ask-guard.d.ts +7 -4
  128. package/dist/types/utils/changelog.d.ts +0 -9
  129. package/dist/types/utils/mupdf-wasm.d.ts +1 -0
  130. package/package.json +9 -8
  131. package/scripts/compile-args.ts +8 -4
  132. package/src/capability/fs.ts +395 -17
  133. package/src/capability/index.ts +273 -10
  134. package/src/capability/types.ts +9 -0
  135. package/src/cli/customize-doctor.ts +18 -12
  136. package/src/cli/doctor/args.ts +236 -0
  137. package/src/cli/doctor/checks.ts +431 -0
  138. package/src/cli/doctor/config-repairs.ts +517 -0
  139. package/src/cli/doctor/context.ts +149 -0
  140. package/src/cli/doctor/files.ts +149 -0
  141. package/src/cli/doctor/ids.ts +115 -0
  142. package/src/cli/doctor/install-repairs.ts +335 -0
  143. package/src/cli/doctor/journal.ts +423 -0
  144. package/src/cli/doctor/managed-link.ts +514 -0
  145. package/src/cli/doctor/native-probe.ts +13 -0
  146. package/src/cli/doctor/permission-repairs.ts +242 -0
  147. package/src/cli/doctor/plugin-quarantine.ts +377 -0
  148. package/src/cli/doctor/plugin-restore.ts +59 -0
  149. package/src/cli/doctor/plugin-targets.ts +373 -0
  150. package/src/cli/doctor/probe-client.ts +195 -0
  151. package/src/cli/doctor/probe-types.ts +21 -0
  152. package/src/cli/doctor/probe.ts +42 -0
  153. package/src/cli/doctor/projection-probe.ts +249 -0
  154. package/src/cli/doctor/repairs.ts +1157 -0
  155. package/src/cli/doctor/report.ts +176 -0
  156. package/src/cli/doctor/runner.ts +342 -0
  157. package/src/cli/doctor/safety.ts +40 -0
  158. package/src/cli/doctor/service-repairs.ts +452 -0
  159. package/src/cli/doctor/service-targets.ts +274 -0
  160. package/src/cli/doctor/types.ts +164 -0
  161. package/src/cli/doctor-cli.ts +62 -0
  162. package/src/cli/doctor-supervisor.ts +571 -0
  163. package/src/cli/doctor-worker.ts +181 -0
  164. package/src/cli/install-activation.ts +666 -0
  165. package/src/cli/macos-community-app.ts +1449 -0
  166. package/src/cli/mcp-cli.ts +11 -21
  167. package/src/cli/nofile-limit.ts +5 -1
  168. package/src/cli/plugin-cli.ts +6 -1
  169. package/src/cli/update-cli.ts +362 -264
  170. package/src/cli-main.ts +517 -0
  171. package/src/cli.ts +64 -433
  172. package/src/commands/autoresearch.ts +11 -5
  173. package/src/commands/completion.ts +1 -1
  174. package/src/commands/doctor.ts +13 -0
  175. package/src/commands/sdk.ts +58 -10
  176. package/src/commands/ultragoal.ts +11 -5
  177. package/src/config/autorouting-tier-map.ts +7 -0
  178. package/src/config/file-lock.ts +736 -71
  179. package/src/config/mcp-schema.json +3 -3
  180. package/src/config/model-preset-registry.ts +1 -0
  181. package/src/config/model-registry.ts +80 -27
  182. package/src/config/model-resolver.ts +33 -4
  183. package/src/config/models-config-schema.ts +2 -0
  184. package/src/config/settings-schema.ts +6 -2
  185. package/src/coordinator-mcp/question-gate-codec.ts +1 -0
  186. package/src/coordinator-mcp/server.ts +534 -51
  187. package/src/coordinator-mcp/session-reaper.ts +64 -8
  188. package/src/cursor.ts +223 -53
  189. package/src/discovery/agents-md.ts +74 -8
  190. package/src/discovery/agents.ts +32 -7
  191. package/src/discovery/builtin.ts +184 -75
  192. package/src/discovery/claude-plugins.ts +85 -17
  193. package/src/discovery/cline.ts +38 -10
  194. package/src/discovery/cursor.ts +19 -6
  195. package/src/discovery/gemini.ts +62 -22
  196. package/src/discovery/github.ts +13 -3
  197. package/src/discovery/helpers.ts +483 -89
  198. package/src/discovery/index.ts +1 -0
  199. package/src/discovery/mcp-json.ts +36 -10
  200. package/src/discovery/opencode.ts +36 -19
  201. package/src/discovery/ssh.ts +11 -8
  202. package/src/discovery/windsurf.ts +12 -3
  203. package/src/exec/bash-executor.ts +10 -3
  204. package/src/exec/isolated-shell.ts +15 -2
  205. package/src/extensibility/extensions/loader.ts +88 -26
  206. package/src/extensibility/gjc-plugins/lifecycle.ts +189 -0
  207. package/src/extensibility/gjc-plugins/paths.ts +4 -1
  208. package/src/extensibility/gjc-plugins/types.ts +27 -0
  209. package/src/extensibility/plugins/manager.ts +200 -22
  210. package/src/extensibility/plugins/marketplace/cache.ts +55 -2
  211. package/src/extensibility/plugins/marketplace/manager.ts +1262 -162
  212. package/src/extensibility/plugins/marketplace/registry.ts +153 -0
  213. package/src/extensibility/plugins/marketplace/source-resolver.ts +116 -10
  214. package/src/extensibility/runtime-skill-discovery.ts +61 -9
  215. package/src/extensibility/skill-management.ts +7 -1
  216. package/src/gjc-runtime/autoresearch-runtime.ts +10 -1
  217. package/src/gjc-runtime/launch-worktree.test.ts +248 -0
  218. package/src/gjc-runtime/launch-worktree.ts +275 -38
  219. package/src/gjc-runtime/repository-binding.ts +26 -1
  220. package/src/gjc-runtime/session-layout.ts +10 -0
  221. package/src/gjc-runtime/session-state-lock.ts +330 -23
  222. package/src/gjc-runtime/session-state-sidecar.ts +328 -80
  223. package/src/gjc-runtime/ultragoal-runtime.ts +10 -1
  224. package/src/harness-control-plane/session-lease.ts +2 -2
  225. package/src/internal-urls/docs-index.generated.ts +28 -26
  226. package/src/main.ts +150 -29
  227. package/src/memories/index.ts +24 -2
  228. package/src/modes/acp/acp-agent.ts +522 -68
  229. package/src/modes/components/agent-dashboard.ts +3 -1
  230. package/src/modes/components/model-selector.ts +28 -19
  231. package/src/modes/components/plugin-settings.ts +2 -1
  232. package/src/modes/controllers/selector-controller.ts +15 -9
  233. package/src/modes/interactive-mode.ts +21 -3
  234. package/src/prompts/system/system-prompt.md +5 -1
  235. package/src/prompts/tools/browser.md +1 -1
  236. package/src/prompts/tools/move-session.md +4 -0
  237. package/src/prompts/tools/read.md +2 -1
  238. package/src/prompts/tools/skill-discovery.md +6 -5
  239. package/src/prompts/tools/task.md +1 -0
  240. package/src/runtime-mcp/config-writer.ts +228 -0
  241. package/src/runtime-mcp/oauth-discovery.ts +114 -14
  242. package/src/runtime-mcp/policy-json-edit.ts +129 -0
  243. package/src/runtime-mcp/startup-policy.ts +14 -0
  244. package/src/runtime-mcp/types.ts +2 -2
  245. package/src/sdk/acp/adapter.ts +27 -12
  246. package/src/sdk/broker/broker.ts +273 -44
  247. package/src/sdk/broker/daemon-entry.ts +119 -0
  248. package/src/sdk/broker/discovery.ts +173 -20
  249. package/src/sdk/broker/doctor-restart.ts +206 -0
  250. package/src/sdk/broker/endpoint-authority.ts +36 -0
  251. package/src/sdk/broker/ensure.ts +49 -14
  252. package/src/sdk/broker/lifecycle.ts +174 -42
  253. package/src/sdk/broker/process-incarnation.ts +86 -2
  254. package/src/sdk/broker/runtime.ts +145 -21
  255. package/src/sdk/broker/session-index.ts +136 -22
  256. package/src/sdk/broker/spawn-substrate.ts +4 -1
  257. package/src/sdk/broker/transport.ts +5 -0
  258. package/src/sdk/broker/worktree-occupancy.ts +21 -1
  259. package/src/sdk/bus/chat-daemon-cli.ts +63 -0
  260. package/src/sdk/bus/chat-daemon-control.ts +260 -64
  261. package/src/sdk/bus/chat-daemon-runtime.ts +141 -25
  262. package/src/sdk/bus/daemon-startup-exclusion.ts +119 -0
  263. package/src/sdk/bus/doctor-daemon-restart-client.ts +291 -0
  264. package/src/sdk/bus/doctor-daemon-restart.ts +92 -0
  265. package/src/sdk/bus/index.ts +252 -51
  266. package/src/sdk/bus/kind-aware-reconciliation.ts +76 -41
  267. package/src/sdk/bus/notification-service.ts +21 -3
  268. package/src/sdk/bus/reconciliation-store.ts +17 -11
  269. package/src/sdk/bus/telegram-daemon-cli.ts +91 -5
  270. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  271. package/src/sdk/bus/telegram-daemon-control.ts +47 -0
  272. package/src/sdk/bus/telegram-daemon-owner-registry.ts +1 -1
  273. package/src/sdk/bus/telegram-daemon.ts +178 -13
  274. package/src/sdk/cli/session-cli.ts +100 -12
  275. package/src/sdk/host/host.ts +2 -7
  276. package/src/sdk/host/session-runtime.test.ts +329 -8
  277. package/src/sdk/host/session-runtime.ts +202 -50
  278. package/src/sdk/host/surface-policy.ts +8 -1
  279. package/src/sdk/lifecycle/service.ts +199 -22
  280. package/src/sdk/mcp/server.ts +1 -1
  281. package/src/sdk/prompt-deadline-lease.ts +12 -4
  282. package/src/sdk/prompt-deadline-manager.ts +5 -7
  283. package/src/sdk/prompt-failure.ts +148 -0
  284. package/src/sdk/prompt-status.ts +30 -1
  285. package/src/sdk/prompt-watchdog.ts +11 -3
  286. package/src/sdk/protocol/operation-inventory.generated.json +18 -0
  287. package/src/sdk/protocol/operation-registry.ts +20 -8
  288. package/src/sdk/router/session-router.ts +8 -3
  289. package/src/sdk/service-artifact-paths.ts +26 -0
  290. package/src/sdk/session-authority.ts +10 -0
  291. package/src/sdk/session.ts +361 -194
  292. package/src/session/agent-session.ts +398 -93
  293. package/src/session/blob-store.ts +9 -9
  294. package/src/session/fallback-chain-controller.ts +3 -7
  295. package/src/session/internal/managed-session-scope.ts +445 -29
  296. package/src/session/internal/managed-session-storage.ts +38 -6
  297. package/src/session/primary-control-surface.ts +12 -0
  298. package/src/session/session-manager.ts +11 -3
  299. package/src/session/session-storage.ts +1 -1
  300. package/src/session-import/codex.ts +6 -1
  301. package/src/session-import/redact.ts +7 -0
  302. package/src/skill-state/workflow-mutation-guard.ts +645 -39
  303. package/src/slash-commands/builtin-registry.ts +7 -3
  304. package/src/system-prompt.ts +8 -0
  305. package/src/task/executor.ts +36 -3
  306. package/src/task/types.ts +27 -5
  307. package/src/tools/ask.ts +114 -34
  308. package/src/tools/browser/actions.ts +2 -2
  309. package/src/tools/browser/tab-worker.ts +11 -8
  310. package/src/tools/descriptors.ts +6 -1
  311. package/src/tools/fetch.ts +7 -2
  312. package/src/tools/image-gen.ts +33 -34
  313. package/src/tools/read.ts +78 -32
  314. package/src/tools/skill-discovery.ts +17 -7
  315. package/src/tools/tool-catalog.generated.ts +8 -8
  316. package/src/tools/ultragoal-ask-guard.ts +16 -26
  317. package/src/utils/changelog.ts +7 -1
  318. package/src/utils/git.ts +3 -1
  319. package/src/utils/markit.ts +24 -2
  320. package/src/utils/mupdf-wasm.ts +26 -0
  321. package/src/web/search/providers/openai-compatible.ts +191 -14
@@ -39,6 +39,7 @@ import {
39
39
  postmortem,
40
40
  prompt,
41
41
  Snowflake,
42
+ safeErrorDescription,
42
43
  setProjectDir,
43
44
  } from "@gajae-code/utils";
44
45
  import {
@@ -92,6 +93,7 @@ import type { CustomTool, CustomToolContext, CustomToolSessionEvent } from "../e
92
93
  import { CustomToolAdapter } from "../extensibility/custom-tools/wrapper";
93
94
  import {
94
95
  createCustomToolSettings,
96
+ discoverAndLoadExtensions,
95
97
  type ExtensionContext,
96
98
  type ExtensionEvent,
97
99
  type ExtensionFactory,
@@ -103,6 +105,7 @@ import {
103
105
  type FunctionHookResult,
104
106
  type LoadExtensionsResult,
105
107
  loadExtensionFromFactory,
108
+ loadExtensions,
106
109
  type ToolDefinition,
107
110
  wrapRegisteredTools,
108
111
  } from "../extensibility/extensions";
@@ -142,6 +145,7 @@ import {
142
145
  type OptionalRuntimeServicesOverrides,
143
146
  } from "../runtime/optional-runtime-services";
144
147
  import { loadAllMCPConfigs, MCPManager } from "../runtime-mcp";
148
+ import type { MCPLoadResult } from "../runtime-mcp/manager";
145
149
  import type { MCPServerConfig } from "../runtime-mcp/types";
146
150
  import {
147
151
  getNotificationConfig,
@@ -160,6 +164,7 @@ import type { SecretObfuscator } from "../secrets";
160
164
  import { AgentSession, type ForkContextSeed, isSessionDisposalIncompleteError } from "../session/agent-session";
161
165
  import { AuthBrokerClient, AuthStorage, RemoteAuthCredentialStore } from "../session/auth-storage";
162
166
  import { type CustomMessage, convertToLlm } from "../session/messages";
167
+ import { primaryControlSurfaceFor } from "../session/primary-control-surface";
163
168
  import { createReadonlySessionManager, SessionManager } from "../session/session-manager";
164
169
  import {
165
170
  parsePersistedCredentialSelector,
@@ -505,7 +510,9 @@ export interface CreateAgentSessionOptions {
505
510
  /** Disable extension discovery (explicit paths still load). */
506
511
  disableExtensionDiscovery?: boolean;
507
512
  /**
508
- * Pre-loaded extensions (skips file discovery).
513
+ * Pre-loaded extensions (skips file discovery). A COMPLETE result is required:
514
+ * the session reuses its live `runtime`, so a partial result dereferences a
515
+ * missing runtime instead of degrading.
509
516
  * @internal Used by CLI when extensions are loaded early to parse custom flags.
510
517
  */
511
518
  preloadedExtensions?: LoadExtensionsResult;
@@ -1271,28 +1278,6 @@ class McpManagerCleanupDiagnosticError extends Error {
1271
1278
  }
1272
1279
  }
1273
1280
 
1274
- function safeErrorDescription(value: unknown): string {
1275
- let isError = false;
1276
- try {
1277
- isError = value instanceof Error;
1278
- } catch {
1279
- // Hostile proxies can throw from getPrototypeOf during instanceof.
1280
- }
1281
- if (isError) {
1282
- try {
1283
- const message = (value as { message?: unknown }).message;
1284
- if (typeof message === "string") return message;
1285
- } catch {
1286
- // Hostile error getters must not replace the primary failure.
1287
- }
1288
- }
1289
- try {
1290
- return String(value);
1291
- } catch {
1292
- return "<unprintable error>";
1293
- }
1294
- }
1295
-
1296
1281
  function safeIsInstanceOf<T extends object>(value: unknown, ctor: abstract new (...args: any[]) => T): boolean {
1297
1282
  try {
1298
1283
  return value instanceof ctor;
@@ -1454,6 +1439,7 @@ function validateAutomationTools(options: CreateAgentSessionOptions): Automation
1454
1439
 
1455
1440
  export async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {
1456
1441
  const automationTools = validateAutomationTools(options);
1442
+ const primaryControlSurface = primaryControlSurfaceFor(options);
1457
1443
  const masterModeContext = options.masterModeContext;
1458
1444
  const lifecycleStartupCapability = (
1459
1445
  options as CreateAgentSessionOptions & { [lifecycleStartupCapabilityOption]?: SdkStartupCapability }
@@ -1496,10 +1482,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1496
1482
  // Pin authStorage to modelRegistry.authStorage: ModelRegistry.getApiKey() routes refresh
1497
1483
  // failures through that instance, so any divergent storage handed to the bridge / mcpManager
1498
1484
  // / session would silently miss credential_disabled events.
1499
- // Injected auth is already the caller's authority; do not discover global
1500
- // startup auth or apply persistent pins while constructing this session.
1485
+ // Injected auth is the caller's authority. Only the CLI root can hand off its
1486
+ // explicit startup snapshot; never discover global config for injected SDK auth.
1501
1487
  const startupAuthConfig = hasInjectedAuth
1502
- ? undefined
1488
+ ? options.modelRegistryStartupMutation?.owner === "cli-root"
1489
+ ? options.startupAuthConfig
1490
+ : undefined
1503
1491
  : (options.startupAuthConfig ?? (await resolveStartupAuthConfig(agentDir)));
1504
1492
  const ownsModelRegistry = options.modelRegistry === undefined;
1505
1493
  const ownsAuthStorage = options.modelRegistry === undefined && options.authStorage === undefined;
@@ -1647,9 +1635,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1647
1635
  );
1648
1636
  }
1649
1637
  }
1650
- if (!options.modelRegistry && !attemptedStartupCacheAdmission) {
1651
- modelRegistry.refreshInBackground();
1652
- }
1653
1638
  // Resolve the workspace tree through its runtime service. The compatibility
1654
1639
  // default starts the native scan at the legacy startup trigger; lazy mode
1655
1640
  // leaves the service idle until the first-turn prompt barrier. The native scan
@@ -1731,7 +1716,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1731
1716
  const scopeAlreadyLeased = authStorage.hasCredentialScopeLease(credentialSessionId);
1732
1717
  authStorage.acquireCredentialScope(credentialSessionId);
1733
1718
  credentialScopeLeased = true;
1734
- const configuredPins = hasInjectedAuth ? {} : (startupAuthConfig?.credentialPins ?? {});
1719
+ const configuredPins = startupAuthConfig?.credentialPins ?? {};
1735
1720
  const persistedPinStoreMatches =
1736
1721
  startupAuthConfig?.credentialPinStoreIdentity === startupAuthConfig?.credentialStoreIdentity;
1737
1722
  // AuthStorage has no scope-level "pinned but unavailable" marker. Keep the
@@ -1871,8 +1856,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1871
1856
  staleDurableCredentialPins.delete(resolveOAuthStorageProvider(record.provider));
1872
1857
  }
1873
1858
  } catch {
1859
+ const unavailableSelector: AuthCredentialSelector | undefined =
1860
+ pin &&
1861
+ (pin.kind === "id" || pin.kind === "email" || pin.kind === "account" || pin.kind === "project") &&
1862
+ typeof pin.value === "string"
1863
+ ? { kind: pin.kind, value: pin.value }
1864
+ : undefined;
1874
1865
  // A newer stale pin must not leave an earlier replayed selector active.
1875
1866
  authStorage.clearSessionCredentialSelector(record.provider, credentialSessionId);
1867
+ if (unavailableSelector) {
1868
+ authStorage.markSessionCredentialUnavailable(credentialSessionId, record.provider, unavailableSelector);
1869
+ }
1876
1870
  staleDurableCredentialPins.add(resolveOAuthStorageProvider(record.provider));
1877
1871
  }
1878
1872
  }
@@ -1907,6 +1901,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1907
1901
  options.credentialSelector.selector,
1908
1902
  );
1909
1903
  }
1904
+ if (!options.modelRegistry && !attemptedStartupCacheAdmission) {
1905
+ modelRegistry.refreshInBackground("online-if-uncached", credentialSessionId);
1906
+ }
1910
1907
 
1911
1908
  const hasExplicitModel = options.model !== undefined || options.modelPattern !== undefined;
1912
1909
  const modelMatchPreferences = {
@@ -2984,6 +2981,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2984
2981
  const pluginMcpToolNames: string[] = [];
2985
2982
  const conventionalMcpToolNames: string[] = [];
2986
2983
  let deferredExactMcpConfig: { manager: MCPManager; configPath: string } | undefined;
2984
+ let deferredConventionalMcp:
2985
+ | {
2986
+ manager: MCPManager;
2987
+ configs: Record<string, MCPServerConfig>;
2988
+ sources: Record<string, SourceMeta>;
2989
+ conventionalConfigs: Record<string, MCPServerConfig>;
2990
+ }
2991
+ | undefined;
2987
2992
 
2988
2993
  // Add image tools when an image role model is configured.
2989
2994
  const { getImageGenTools } = await import("../tools/image-gen");
@@ -3089,6 +3094,88 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3089
3094
  }
3090
3095
 
3091
3096
  const preExactCustomToolNames = customTools.map(tool => tool.name);
3097
+ // Connect the conventional-autoload manager and run its registration
3098
+ // bookkeeping. Shared by the eager startup path and the deferred starter so
3099
+ // a deferred session registers exactly what an eager one would.
3100
+ const connectOwnedConventionalMcp = async (
3101
+ owned: MCPManager,
3102
+ mergedConfigs: Record<string, MCPServerConfig>,
3103
+ mergedSources: Record<string, SourceMeta>,
3104
+ conventionalConfigs: Record<string, MCPServerConfig>,
3105
+ pluginNames: ReadonlySet<string>,
3106
+ ): Promise<MCPLoadResult> => {
3107
+ let result: MCPLoadResult;
3108
+ try {
3109
+ result = await owned.connectServers(mergedConfigs, mergedSources as never);
3110
+ } catch (error) {
3111
+ if (safeIsInstanceOf(error, McpManagerCleanupError)) throw error;
3112
+ // Avoid leaking partially-started server processes on failure.
3113
+ let cleanupError: unknown;
3114
+ try {
3115
+ await owned.disconnectAll();
3116
+ } catch (disconnectError) {
3117
+ cleanupError = disconnectError;
3118
+ } finally {
3119
+ cleanupOwnedMcpManager = undefined;
3120
+ }
3121
+ if (cleanupError !== undefined) throw attachMcpCleanupDiagnostic(error, cleanupError);
3122
+ throw error;
3123
+ }
3124
+ for (const [server, err] of result.errors) {
3125
+ // A server that failed to connect leaves this generation incomplete: its
3126
+ // surfaces produced no evidence, so publishing would present a partial
3127
+ // pass as a clear one. On the deferred path this flag is already
3128
+ // consumed (publication happens before the starter can run), which is
3129
+ // acceptable only because deferral never carries plugin-bundle servers.
3130
+ gjcProducersComplete = false;
3131
+ logger.warn("GJC plugin MCP connect failed", {
3132
+ path: `mcp:${server}`,
3133
+ error: safeErrorForLog(err),
3134
+ });
3135
+ }
3136
+ const connectedPluginNames = new Set(result.connectedServers.filter(name => pluginNames.has(name)));
3137
+ // Retain while any conventional server is still live: "connecting"
3138
+ // covers the declared-timeout window, and a server that landed in
3139
+ // "connected" inside the microtask between connectServers() resolving
3140
+ // and this synchronous check must not be torn down either.
3141
+ const unsettledConventionalNames = Object.keys(conventionalConfigs).filter(
3142
+ name => owned.getConnectionStatus(name) !== "disconnected",
3143
+ );
3144
+ const retainOwnedManager = result.connectedServers.length > 0 || unsettledConventionalNames.length > 0;
3145
+ if (retainOwnedManager) {
3146
+ mcpManager = owned;
3147
+ ownsMcpManager = true;
3148
+ customTools.push(...(result.tools as CustomTool[]));
3149
+ cwdCapturingToolNames.push(...result.tools.map(tool => tool.name));
3150
+ for (const name of Object.keys(conventionalConfigs)) ownedConventionalMcpServerNames.add(name);
3151
+ pluginMcpManagerServers.set(owned, connectedPluginNames);
3152
+ conventionalMcpManagerServers.set(owned, new Set(Object.keys(conventionalConfigs)));
3153
+ for (const tool of result.tools) {
3154
+ const serverName = tool.mcpServerName;
3155
+ if (serverName === undefined) continue;
3156
+ if (connectedPluginNames.has(serverName)) pluginMcpToolNames.push(tool.name);
3157
+ else {
3158
+ conventionalMcpToolNames.push(tool.name);
3159
+ ownedConventionalMcpToolNames.push(tool.name);
3160
+ }
3161
+ }
3162
+ publishOwnedConventionalMcpTools = ownedConventionalMcpServerNames.size > 0;
3163
+ ownedPluginServersConnected = connectedPluginNames.size > 0;
3164
+ // Plugin-bundle connections are fixed for the session lifetime only
3165
+ // when no conventional server is still connecting in the same manager;
3166
+ // otherwise the seal is re-applied once they settle (below).
3167
+ if (connectedPluginNames.size > 0 && unsettledConventionalNames.length === 0) owned.sealConnectionSet();
3168
+ } else {
3169
+ try {
3170
+ await owned.disconnectAll();
3171
+ cleanupOwnedMcpManager = undefined;
3172
+ } catch (cleanupError) {
3173
+ cleanupOwnedMcpManager = undefined;
3174
+ throw new McpManagerCleanupError(cleanupError);
3175
+ }
3176
+ }
3177
+ return result;
3178
+ };
3092
3179
  if (explicitMcpConfigPath !== undefined) {
3093
3180
  const owned = new MCPManager(cwd, null, {
3094
3181
  toolsOnly: true,
@@ -3191,78 +3278,39 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3191
3278
  ]),
3192
3279
  ),
3193
3280
  };
3281
+ // Plugin-source entries omit SourceMeta.path (see connectOwnedConventionalMcp's
3282
+ // own bypass of the same mismatch at the manager boundary).
3283
+ const mergedSourceMetas = mergedSources as Record<string, SourceMeta>;
3194
3284
  if (Object.keys(mergedConfigs).length > 0) {
3195
3285
  const owned = new MCPManager(cwd, null, { sharedPoolIdleMs: settings.get("mcp.sharedPoolIdleMs") });
3196
3286
  owned.setAuthStorage(authStorage);
3197
3287
  cleanupOwnedMcpManager = () => owned.disconnectAll();
3198
- try {
3199
- const result = await owned.connectServers(mergedConfigs, mergedSources as never);
3200
- for (const [server, err] of result.errors) {
3201
- // A server that failed to connect leaves this generation
3202
- // incomplete: its surfaces produced no evidence, so publishing
3203
- // would present a partial pass as a clear one.
3204
- gjcProducersComplete = false;
3205
- logger.warn("GJC plugin MCP connect failed", {
3206
- path: `mcp:${server}`,
3207
- error: safeErrorForLog(err),
3208
- });
3209
- }
3210
- const connectedPluginNames = new Set(result.connectedServers.filter(name => pluginNames.has(name)));
3211
- // Retain while any conventional server is still live: "connecting"
3212
- // covers the declared-timeout window, and a server that landed in
3213
- // "connected" inside the microtask between connectServers() resolving
3214
- // and this synchronous check must not be torn down either.
3215
- const unsettledConventionalNames = Object.keys(conventionalConfigs).filter(
3216
- name => owned.getConnectionStatus(name) !== "disconnected",
3288
+ // Interactive launches may paint before MCP connects; the deferred
3289
+ // starter reconnects this manager after first paint and a startup
3290
+ // turn barrier keeps the first prompt from racing tool registration.
3291
+ // Plugin-bundle servers never defer: their connect evidence feeds the
3292
+ // GJC runtime snapshot published during creation, and a late failure
3293
+ // would present a partial pass as a clear one.
3294
+ if (options.deferMcpConfigStartup && pluginNames.size === 0) {
3295
+ deferredConventionalMcp = {
3296
+ manager: owned,
3297
+ configs: mergedConfigs,
3298
+ sources: mergedSourceMetas,
3299
+ conventionalConfigs,
3300
+ };
3301
+ // Publish the manager immediately (empty until the starter connects)
3302
+ // so /mcp status, reconnect, and resource inspection resolve it once
3303
+ // the deferred tools land instead of reporting no manager.
3304
+ mcpManager = owned;
3305
+ ownsMcpManager = true;
3306
+ } else {
3307
+ await connectOwnedConventionalMcp(
3308
+ owned,
3309
+ mergedConfigs,
3310
+ mergedSourceMetas,
3311
+ conventionalConfigs,
3312
+ pluginNames,
3217
3313
  );
3218
- const retainOwnedManager =
3219
- result.connectedServers.length > 0 || unsettledConventionalNames.length > 0;
3220
- if (retainOwnedManager) {
3221
- mcpManager = owned;
3222
- ownsMcpManager = true;
3223
- customTools.push(...(result.tools as CustomTool[]));
3224
- cwdCapturingToolNames.push(...result.tools.map(tool => tool.name));
3225
- for (const name of Object.keys(conventionalConfigs)) ownedConventionalMcpServerNames.add(name);
3226
- pluginMcpManagerServers.set(owned, connectedPluginNames);
3227
- conventionalMcpManagerServers.set(owned, new Set(Object.keys(conventionalConfigs)));
3228
- for (const tool of result.tools) {
3229
- const serverName = tool.mcpServerName;
3230
- if (serverName === undefined) continue;
3231
- if (connectedPluginNames.has(serverName)) pluginMcpToolNames.push(tool.name);
3232
- else {
3233
- conventionalMcpToolNames.push(tool.name);
3234
- ownedConventionalMcpToolNames.push(tool.name);
3235
- }
3236
- }
3237
- publishOwnedConventionalMcpTools = ownedConventionalMcpServerNames.size > 0;
3238
- ownedPluginServersConnected = connectedPluginNames.size > 0;
3239
- // Plugin-bundle connections are fixed for the session lifetime only
3240
- // when no conventional server is still connecting in the same manager;
3241
- // otherwise the seal is re-applied once they settle (below).
3242
- if (connectedPluginNames.size > 0 && unsettledConventionalNames.length === 0)
3243
- owned.sealConnectionSet();
3244
- } else {
3245
- try {
3246
- await owned.disconnectAll();
3247
- cleanupOwnedMcpManager = undefined;
3248
- } catch (cleanupError) {
3249
- cleanupOwnedMcpManager = undefined;
3250
- throw new McpManagerCleanupError(cleanupError);
3251
- }
3252
- }
3253
- } catch (error) {
3254
- if (safeIsInstanceOf(error, McpManagerCleanupError)) throw error;
3255
- // Avoid leaking partially-started server processes on failure.
3256
- let cleanupError: unknown;
3257
- try {
3258
- await owned.disconnectAll();
3259
- } catch (disconnectError) {
3260
- cleanupError = disconnectError;
3261
- } finally {
3262
- cleanupOwnedMcpManager = undefined;
3263
- }
3264
- if (cleanupError !== undefined) throw attachMcpCleanupDiagnostic(error, cleanupError);
3265
- throw error;
3266
3314
  }
3267
3315
  }
3268
3316
  } catch (error) {
@@ -3299,9 +3347,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3299
3347
  }
3300
3348
  // MCP routing is scope-held; no process-global manager registration.
3301
3349
 
3302
- // General extension discovery is quarantined from the public SDK surface.
3303
- // Recognized hook conventions are the bounded exception: their descriptors
3350
+ // Recognized hook conventions are a bounded additional source: their descriptors
3304
3351
  // normalize before import and then adapt into the authoritative ExtensionRunner.
3352
+ // General extension-module discovery runs further below, before that runner is built.
3305
3353
  const inlineExtensions: ExtensionFactory[] = [...(options.extensions ?? [])];
3306
3354
  const discoveredHookExtensions: Array<{ factory: ExtensionFactory; name: string }> = [];
3307
3355
  if (customTools.length > 0) {
@@ -3406,6 +3454,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3406
3454
  const createNotificationsExtension = await notificationAdapterService.get("session-extension");
3407
3455
  createNotificationsExtension(api, {
3408
3456
  settings,
3457
+ primaryControlSurface,
3409
3458
 
3410
3459
  controller: notificationSessionController,
3411
3460
  spawnedByGjc,
@@ -3470,6 +3519,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3470
3519
  createTransport: input => createSdkWebSocketTransport(input),
3471
3520
  settings,
3472
3521
  configOverrides: new Map(),
3522
+ primaryControlSurface,
3473
3523
  ...(masterModeContext
3474
3524
  ? {
3475
3525
  masterCapability: masterModeContext.getCapability(),
@@ -3545,15 +3595,60 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3545
3595
  });
3546
3596
  }
3547
3597
 
3548
- // Extension/module discovery is quarantined; retain only the private
3549
- // runtime needed for bundled product extensions, explicitly supplied SDK
3550
- // extension factories, and custom tools. Filesystem extension paths remain
3551
- // ignored here even when options.additionalExtensionPaths is supplied.
3552
- const extensionsResult: LoadExtensionsResult = options.preloadedExtensions ?? {
3553
- extensions: [],
3554
- errors: [],
3555
- runtime: new ExtensionRuntime(),
3598
+ // Extension modules are discovered at session startup from the canonical
3599
+ // native locations (`<agentDir>/extensions`, `<cwd>/.gjc/extensions`),
3600
+ // installed plugin bundles, the `extensions` setting, and explicitly
3601
+ // supplied `additionalExtensionPaths`. `disableExtensionDiscovery` keeps its
3602
+ // documented opt-out semantics: explicit paths still load, matching
3603
+ // `cli/list-models.ts`. A caller-supplied `preloadedExtensions` result
3604
+ // suppresses discovery entirely.
3605
+ const explicitExtensionPaths = options.additionalExtensionPaths ?? [];
3606
+ // The `extensions` setting is schema-typed as an array of paths, but a
3607
+ // hand-edited config can hold an invalid shape; discovery must degrade to
3608
+ // no extra paths instead of aborting startup for every session.
3609
+ const extensionsSettingPaths = Array.isArray(settings.get("extensions")) ? settings.get("extensions") : [];
3610
+ // Discovery must never block session creation: a filesystem or plugin-registry
3611
+ // failure degrades to the explicit paths and then to no extensions at all, and
3612
+ // the failure stays observable in `extensionsResult.errors`.
3613
+ const loadStartupExtensions = async (): Promise<LoadExtensionsResult> => {
3614
+ try {
3615
+ return options.disableExtensionDiscovery
3616
+ ? await loadExtensions(explicitExtensionPaths, cwd, eventBus)
3617
+ : await discoverAndLoadExtensions(
3618
+ [...explicitExtensionPaths, ...extensionsSettingPaths],
3619
+ cwd,
3620
+ eventBus,
3621
+ settings.get("disabledExtensions"),
3622
+ { agentDir, profileAuthority, settings },
3623
+ );
3624
+ } catch (error) {
3625
+ logger.warn(
3626
+ options.disableExtensionDiscovery
3627
+ ? "Failed to load extension modules"
3628
+ : "Failed to discover extension modules",
3629
+ { error: safeErrorForLog(error) },
3630
+ );
3631
+ const failure = { path: "<extension-discovery>", error: safeErrorDescription(error) };
3632
+ if (options.disableExtensionDiscovery) {
3633
+ return { extensions: [], errors: [failure], runtime: new ExtensionRuntime() };
3634
+ }
3635
+ try {
3636
+ const fallback = await loadExtensions(explicitExtensionPaths, cwd, eventBus);
3637
+ return { ...fallback, errors: [failure, ...fallback.errors] };
3638
+ } catch (fallbackError) {
3639
+ // Last resort: `loadExtensions` does not throw for a bad module, so this
3640
+ // rung guards its contract rather than a known input.
3641
+ logger.warn("Failed to load explicit extension paths", { error: safeErrorForLog(fallbackError) });
3642
+ return { extensions: [], errors: [failure], runtime: new ExtensionRuntime() };
3643
+ }
3644
+ }
3556
3645
  };
3646
+ const extensionsResult: LoadExtensionsResult = options.preloadedExtensions ?? (await loadStartupExtensions());
3647
+ if (options.preloadedExtensions === undefined) {
3648
+ for (const { path: extPath, error } of extensionsResult.errors) {
3649
+ logger.warn("Failed to load extension", { path: extPath, error });
3650
+ }
3651
+ }
3557
3652
 
3558
3653
  if (!extensionsResult.extensions.some(extension => extension.path === BUNDLED_GROK_BUILD_EXTENSION_ID)) {
3559
3654
  const bundledGrokExtension = await loadExtensionFromFactory(
@@ -4055,6 +4150,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4055
4150
  toolDiscoveryActive: effectiveDiscoveryMode === "all" || mcpDiscoveryEnabled,
4056
4151
  eagerTasks: resolveEagerTasks(),
4057
4152
  secretsEnabled,
4153
+ taskIsolationEnabled: settings.get("task.isolation.mode") !== "none",
4058
4154
  workspaceTree: workspaceTreePromise,
4059
4155
  subagent: options.parentTaskPrefix !== undefined,
4060
4156
  });
@@ -4141,13 +4237,18 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4141
4237
  );
4142
4238
  let initialToolNames = [...initialRequestedActiveToolNames];
4143
4239
  if (mcpDiscoveryEnabled) {
4144
- const restoredSelectedMCPToolNames = deferredExactMcpConfig
4240
+ // A pending deferred MCP connect (exact or conventional) registers its
4241
+ // tools after construction, so persisted selections must be preserved
4242
+ // verbatim instead of being trimmed to names the registry currently has.
4243
+ const deferredMcpConnectPending =
4244
+ deferredExactMcpConfig !== undefined || deferredConventionalMcp !== undefined;
4245
+ const restoredSelectedMCPToolNames = deferredMcpConnectPending
4145
4246
  ? existingSession.selectedMCPToolNames.filter(name => !mandatoryMCPToolNameSet.has(name))
4146
4247
  : existingSession.selectedMCPToolNames.filter(
4147
4248
  name => toolRegistry.has(name) && !mandatoryMCPToolNameSet.has(name),
4148
4249
  );
4149
4250
  if (
4150
- !deferredExactMcpConfig &&
4251
+ !deferredMcpConnectPending &&
4151
4252
  existingSession.hasPersistedMCPToolSelection &&
4152
4253
  restoredSelectedMCPToolNames.length !== existingSession.selectedMCPToolNames.length
4153
4254
  ) {
@@ -4342,10 +4443,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4342
4443
  guardToolForUltragoalAsk(
4343
4444
  tool,
4344
4445
  () => sessionManager.getCwd(),
4345
- () => ({
4346
- activeSkillState: session?.getActiveSkillState(),
4347
- sessionId: sessionManager.getSessionId?.() ?? null,
4348
- }),
4446
+ () => ({ sessionId: sessionManager.getSessionId?.() ?? null }),
4349
4447
  () => session?.getSessionAgentDir(),
4350
4448
  ),
4351
4449
  );
@@ -4575,7 +4673,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4575
4673
  }
4576
4674
  }
4577
4675
 
4578
- const deferredMcpTurnReady = deferredExactMcpConfig ? Promise.withResolvers<void>() : undefined;
4676
+ const deferredMcpTurnReady =
4677
+ deferredExactMcpConfig || deferredConventionalMcp ? Promise.withResolvers<void>() : undefined;
4579
4678
  if (deferredMcpTurnReady) void deferredMcpTurnReady.promise.catch(() => {});
4580
4679
 
4581
4680
  session = new AgentSession({
@@ -4660,7 +4759,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4660
4759
  mcpDiscoveryEnabled,
4661
4760
  discoveryMode: effectiveDiscoveryMode,
4662
4761
  initialSelectedMCPToolNames,
4663
- preserveUnavailableInitialMCPToolSelection: deferredExactMcpConfig !== undefined,
4762
+ // Both deferral shapes register their tools only in the deferred starter,
4763
+ // so the registry is empty at construction and persisted selections must be
4764
+ // preserved verbatim until the starter publishes the real catalog.
4765
+ preserveUnavailableInitialMCPToolSelection:
4766
+ deferredExactMcpConfig !== undefined || deferredConventionalMcp !== undefined,
4664
4767
  initialMCPToolSelectionIsExplicit: hasExplicitMCPToolSelection,
4665
4768
  initialDiscoveredBuiltinToolSelectionIsExplicit: hasExplicitDiscoveredBuiltinToolSelection,
4666
4769
  initialSelectedDiscoveredBuiltinToolNames,
@@ -4872,68 +4975,82 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4872
4975
  // createAgentSession rather than surfacing as an unhandled rejection.
4873
4976
  if (!options.deferMemoryBackendStartup) await startMemoryBackend();
4874
4977
 
4978
+ // Wire the owned manager's late tool publications into the live session.
4979
+ // Shared by the eager startup path below and by the deferred conventional
4980
+ // starter once its connect lands. No-op when there is no owned manager or
4981
+ // nothing conventional to publish.
4982
+ const wireOwnedConventionalToolSync = (): void => {
4983
+ if (!mcpManager || !publishOwnedConventionalMcpTools) return;
4984
+ // Late conventional connections can publish near-simultaneously.
4985
+ // Serialize the swaps so an older snapshot cannot interleave with a
4986
+ // newer one inside replaceNamedCustomTools and leave a stale list.
4987
+ // Each link swallows (and logs) its own failure so one bad
4988
+ // publication cannot kill the chain for every later one.
4989
+ let conventionalToolsSync: Promise<void> = Promise.resolve();
4990
+ const syncConventionalTools = (tools: CustomTool[]): Promise<void> => {
4991
+ conventionalToolsSync = conventionalToolsSync
4992
+ .then(async () => {
4993
+ if (session.isDisposed) return;
4994
+ const nextTools = tools.filter(tool =>
4995
+ tool.mcpServerName ? ownedConventionalMcpServerNames.has(tool.mcpServerName) : false,
4996
+ );
4997
+ const previousNames = ownedConventionalMcpToolNames;
4998
+ ownedConventionalMcpToolNames = nextTools.map(tool => tool.name);
4999
+ const previousSet = new Set(previousNames);
5000
+ cwdCapturingToolNames.splice(
5001
+ 0,
5002
+ cwdCapturingToolNames.length,
5003
+ ...cwdCapturingToolNames.filter(name => !previousSet.has(name)),
5004
+ ...ownedConventionalMcpToolNames,
5005
+ );
5006
+ await session.replaceNamedCustomTools(previousNames, nextTools);
5007
+ // Mixed plugin + conventional sessions deferred the seal while
5008
+ // a conventional server was still connecting; restore the
5009
+ // fixed-connection plugin contract once every conventional
5010
+ // server has reached a terminal state.
5011
+ if (
5012
+ ownedPluginServersConnected &&
5013
+ mcpManager !== undefined &&
5014
+ !mcpManager.isConnectionSetSealed() &&
5015
+ ![...ownedConventionalMcpServerNames].some(
5016
+ name => mcpManager?.getConnectionStatus(name) === "connecting",
5017
+ )
5018
+ ) {
5019
+ mcpManager.sealConnectionSet();
5020
+ }
5021
+ })
5022
+ .catch(error => {
5023
+ logger.warn("Failed to publish conventional MCP tools", { error: safeErrorForLog(error) });
5024
+ });
5025
+ return conventionalToolsSync;
5026
+ };
5027
+ mcpManager.setOnToolsChanged(tools => {
5028
+ void syncConventionalTools(tools as CustomTool[]);
5029
+ });
5030
+ void syncConventionalTools(mcpManager.getTools() as CustomTool[]);
5031
+ };
5032
+ const wireOwnedMcpManagerLifecycle = (): void => {
5033
+ if (!mcpManager) return;
5034
+ const clearDebounceTimers = () => {
5035
+ for (const timer of notificationDebounceTimers.values()) clearTimeout(timer);
5036
+ notificationDebounceTimers.clear();
5037
+ };
5038
+ postmortem.register("mcp-notification-cleanup", clearDebounceTimers);
5039
+ wireMcpManagerCallbacks(mcpManager);
5040
+ };
4875
5041
  // Exact-config managers do not receive reactive callbacks; their tools are
4876
- // registered once in the session-owned catalog.
4877
- if (mcpManager && !options.mcpManager && explicitMcpConfigPath === undefined) {
5042
+ // registered once in the session-owned catalog. A pending conventional
5043
+ // deferral wires its own callbacks from the starter after it connects, so
5044
+ // nothing is wired here yet.
5045
+ if (mcpManager && !options.mcpManager && explicitMcpConfigPath === undefined && !deferredConventionalMcp) {
4878
5046
  if (publishOwnedConventionalMcpTools) {
4879
- // Late conventional connections can publish near-simultaneously.
4880
- // Serialize the swaps so an older snapshot cannot interleave with a
4881
- // newer one inside replaceNamedCustomTools and leave a stale list.
4882
- // Each link swallows (and logs) its own failure so one bad
4883
- // publication cannot kill the chain for every later one.
4884
- let conventionalToolsSync: Promise<void> = Promise.resolve();
4885
- const syncConventionalTools = (tools: CustomTool[]): Promise<void> => {
4886
- conventionalToolsSync = conventionalToolsSync
4887
- .then(async () => {
4888
- if (session.isDisposed) return;
4889
- const nextTools = tools.filter(tool =>
4890
- tool.mcpServerName ? ownedConventionalMcpServerNames.has(tool.mcpServerName) : false,
4891
- );
4892
- const previousNames = ownedConventionalMcpToolNames;
4893
- ownedConventionalMcpToolNames = nextTools.map(tool => tool.name);
4894
- const previousSet = new Set(previousNames);
4895
- cwdCapturingToolNames.splice(
4896
- 0,
4897
- cwdCapturingToolNames.length,
4898
- ...cwdCapturingToolNames.filter(name => !previousSet.has(name)),
4899
- ...ownedConventionalMcpToolNames,
4900
- );
4901
- await session.replaceNamedCustomTools(previousNames, nextTools);
4902
- // Mixed plugin + conventional sessions deferred the seal while
4903
- // a conventional server was still connecting; restore the
4904
- // fixed-connection plugin contract once every conventional
4905
- // server has reached a terminal state.
4906
- if (
4907
- ownedPluginServersConnected &&
4908
- mcpManager !== undefined &&
4909
- !mcpManager.isConnectionSetSealed() &&
4910
- ![...ownedConventionalMcpServerNames].some(
4911
- name => mcpManager?.getConnectionStatus(name) === "connecting",
4912
- )
4913
- ) {
4914
- mcpManager.sealConnectionSet();
4915
- }
4916
- })
4917
- .catch(error => {
4918
- logger.warn("Failed to publish conventional MCP tools", { error: safeErrorForLog(error) });
4919
- });
4920
- return conventionalToolsSync;
4921
- };
4922
- mcpManager.setOnToolsChanged(tools => {
4923
- void syncConventionalTools(tools as CustomTool[]);
4924
- });
4925
- void syncConventionalTools(mcpManager.getTools() as CustomTool[]);
5047
+ wireOwnedConventionalToolSync();
4926
5048
  } else if (!ownsMcpManager) {
4927
5049
  mcpManager.setOnToolsChanged(tools => {
4928
5050
  void session.refreshMCPTools(tools);
4929
5051
  });
4930
5052
  }
4931
- const clearDebounceTimers = () => {
4932
- for (const timer of notificationDebounceTimers.values()) clearTimeout(timer);
4933
- notificationDebounceTimers.clear();
4934
- };
4935
- postmortem.register("mcp-notification-cleanup", clearDebounceTimers);
4936
- wireMcpManagerCallbacks(mcpManager);
5053
+ wireOwnedMcpManagerLifecycle();
4937
5054
  }
4938
5055
 
4939
5056
  // Constructor-time workflow-gate tool restoration is deferred by one
@@ -4941,15 +5058,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4941
5058
  // so a resumed canonical workflow session returns with `ask` resident.
4942
5059
  await session.workflowGateToolRestoration;
4943
5060
  let startDeferredMcpConfig: (() => Promise<DeferredMcpConfigStartupResult>) | undefined;
4944
- if (deferredExactMcpConfig) {
4945
- const { manager, configPath } = deferredExactMcpConfig;
5061
+ if (deferredExactMcpConfig || deferredConventionalMcp) {
4946
5062
  let cancelled = false;
4947
5063
  let startupPromise: Promise<DeferredMcpConfigStartupResult> | undefined;
4948
5064
  session.registerToolSessionCleanup(async () => {
4949
5065
  cancelled = true;
4950
5066
  deferredMcpTurnReady?.resolve();
4951
5067
  if (startupPromise) {
4952
- await manager.disconnectAll().catch(() => {});
5068
+ await deferredExactMcpConfig?.manager.disconnectAll().catch(() => {});
5069
+ await deferredConventionalMcp?.manager.disconnectAll().catch(() => {});
4953
5070
  await startupPromise.catch(() => {});
4954
5071
  }
4955
5072
  });
@@ -4959,33 +5076,83 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4959
5076
  }
4960
5077
  startupPromise ??= (async () => {
4961
5078
  try {
4962
- const result = await manager.discoverAndConnect({ configPath });
4963
- const resultTools = result.tools as CustomTool[];
4964
- const toolNames = resultTools.map(tool => tool.name);
4965
- const collidingToolNames = findDeferredExactMcpToolNameCollisions(
4966
- toolNames,
4967
- exactMcpCatalogToolNames,
4968
- );
4969
- if (collidingToolNames.length > 0) {
4970
- throw new ExactMcpToolNameCollisionError(collidingToolNames);
4971
- }
4972
- if (!cancelled && !session.isDisposed) {
4973
- await session.refreshMCPTools(resultTools);
4974
- if (
4975
- !session.isDisposed &&
4976
- (!mcpDiscoveryEnabled || !existingSession.hasPersistedMCPToolSelection)
4977
- ) {
4978
- await session.activateDiscoveredTools(toolNames);
5079
+ const exact = deferredExactMcpConfig;
5080
+ const conventional = deferredConventionalMcp;
5081
+ let loadedToolCount = 0;
5082
+ let hasErrors = false;
5083
+ if (exact) {
5084
+ const result = await exact.manager.discoverAndConnect({ configPath: exact.configPath });
5085
+ const resultTools = result.tools as CustomTool[];
5086
+ const toolNames = resultTools.map(tool => tool.name);
5087
+ const collidingToolNames = findDeferredExactMcpToolNameCollisions(
5088
+ toolNames,
5089
+ exactMcpCatalogToolNames,
5090
+ );
5091
+ if (collidingToolNames.length > 0) {
5092
+ throw new ExactMcpToolNameCollisionError(collidingToolNames);
4979
5093
  }
5094
+ if (!cancelled && !session.isDisposed) {
5095
+ await session.refreshMCPTools(resultTools);
5096
+ if (
5097
+ !session.isDisposed &&
5098
+ (!mcpDiscoveryEnabled || !existingSession.hasPersistedMCPToolSelection)
5099
+ ) {
5100
+ await session.activateDiscoveredTools(toolNames);
5101
+ }
5102
+ }
5103
+ loadedToolCount = resultTools.length;
5104
+ hasErrors = result.errors.size > 0 || resultTools.length === 0;
5105
+ } else if (conventional) {
5106
+ // Deferral never carries plugin-bundle servers, so this merge is
5107
+ // purely conventional and the GJC evidence snapshot published
5108
+ // during creation has no deferred producer to wait for.
5109
+ const result = await connectOwnedConventionalMcp(
5110
+ conventional.manager,
5111
+ conventional.configs,
5112
+ conventional.sources,
5113
+ conventional.conventionalConfigs,
5114
+ new Set<string>(),
5115
+ );
5116
+ const resultTools = result.tools as CustomTool[];
5117
+ if (!cancelled && !session.isDisposed) {
5118
+ // Register the deferred tools before wiring the reactive conventional-tool
5119
+ // sync. The sync's initial publication calls replaceNamedCustomTools, which
5120
+ // only re-activates a swapped tool that was already active; running it
5121
+ // before this registration would therefore drop a persisted conventional
5122
+ // MCP selection and persist the empty set.
5123
+ await session.refreshMCPTools(resultTools);
5124
+ wireOwnedConventionalToolSync();
5125
+ wireOwnedMcpManagerLifecycle();
5126
+ if (
5127
+ !session.isDisposed &&
5128
+ (!mcpDiscoveryEnabled || !existingSession.hasPersistedMCPToolSelection)
5129
+ ) {
5130
+ await session.activateDiscoveredTools(resultTools.map(tool => tool.name));
5131
+ }
5132
+ }
5133
+ loadedToolCount = resultTools.length;
5134
+ // A server whose declared timeout outlived the startup budget is
5135
+ // still connecting and publishes reactively (same steady state as
5136
+ // the eager path): that is not a failure, so do not warn or count
5137
+ // it as one while its handshake window is open.
5138
+ const stillConnectingServers = Object.keys(conventional.conventionalConfigs).filter(
5139
+ name => conventional.manager.getConnectionStatus(name) === "connecting",
5140
+ );
5141
+ hasErrors =
5142
+ result.errors.size > 0 || (resultTools.length === 0 && stillConnectingServers.length === 0);
4980
5143
  }
4981
5144
  deferredMcpTurnReady?.resolve();
4982
- const hasErrors = result.errors.size > 0 || result.tools.length === 0;
4983
5145
  if (hasErrors) logger.warn(DEFERRED_MCP_CONFIG_STARTUP_ERROR);
4984
- return { loadedToolCount: cancelled || session.isDisposed ? 0 : resultTools.length, hasErrors };
5146
+ return { loadedToolCount: cancelled || session.isDisposed ? 0 : loadedToolCount, hasErrors };
4985
5147
  } catch {
4986
5148
  const startupError = new Error(DEFERRED_MCP_CONFIG_STARTUP_ERROR);
4987
- deferredMcpTurnReady?.reject(startupError);
4988
- await manager.disconnectAll().catch(() => {});
5149
+ // Report the failure through this promise (the caller surfaces the MCP
5150
+ // error), but release the startup turn barrier: rejecting it would leave
5151
+ // it installed forever, so every later prompt would await a rejected
5152
+ // barrier and the interactive session would be unusable.
5153
+ deferredMcpTurnReady?.resolve();
5154
+ await deferredExactMcpConfig?.manager.disconnectAll().catch(() => {});
5155
+ await deferredConventionalMcp?.manager.disconnectAll().catch(() => {});
4989
5156
  throw startupError;
4990
5157
  }
4991
5158
  })();