@gajae-code/coding-agent 0.15.6 → 0.16.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 (270) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/dist/types/async/job-manager.d.ts +32 -1
  3. package/dist/types/browser-backend.d.ts +12 -0
  4. package/dist/types/cli/args.d.ts +2 -0
  5. package/dist/types/cli/root-flags.d.ts +14 -0
  6. package/dist/types/cli/update-cli.d.ts +14 -0
  7. package/dist/types/cli.d.ts +7 -0
  8. package/dist/types/commands/launch.d.ts +10 -0
  9. package/dist/types/commands/sdk.d.ts +2 -0
  10. package/dist/types/config/autorouting-tier-map.d.ts +24 -0
  11. package/dist/types/config/billing-path.d.ts +25 -0
  12. package/dist/types/config/config-schema-version.d.ts +13 -1
  13. package/dist/types/config/file-lock.d.ts +5 -0
  14. package/dist/types/config/model-registry.d.ts +22 -0
  15. package/dist/types/config/settings-schema.d.ts +27 -8
  16. package/dist/types/config/settings.d.ts +2 -0
  17. package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
  18. package/dist/types/crash/record-loader.d.ts +15 -0
  19. package/dist/types/extensibility/extensions/types.d.ts +8 -3
  20. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  21. package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
  22. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  23. package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
  24. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
  25. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  26. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  27. package/dist/types/main.d.ts +1 -0
  28. package/dist/types/master-mode/context.d.ts +21 -0
  29. package/dist/types/master-mode/first-request.d.ts +34 -0
  30. package/dist/types/master-mode/snapshot.d.ts +32 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +21 -21
  32. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  33. package/dist/types/modes/components/model-selector.d.ts +4 -0
  34. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  35. package/dist/types/modes/components/session-selector.d.ts +1 -1
  36. package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
  37. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  38. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  39. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  40. package/dist/types/modes/interactive-mode.d.ts +16 -2
  41. package/dist/types/modes/print-mode.d.ts +17 -0
  42. package/dist/types/modes/theme/theme.d.ts +2 -1
  43. package/dist/types/modes/types.d.ts +2 -0
  44. package/dist/types/modes/ui-language.d.ts +7 -1
  45. package/dist/types/sdk/broker/broker.d.ts +79 -2
  46. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  47. package/dist/types/sdk/broker/ensure.d.ts +3 -0
  48. package/dist/types/sdk/broker/index.d.ts +3 -0
  49. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
  50. package/dist/types/sdk/broker/lifecycle.d.ts +40 -0
  51. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  52. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  53. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  54. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  55. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  56. package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
  57. package/dist/types/sdk/broker/state-version.d.ts +21 -1
  58. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  59. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  60. package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
  61. package/dist/types/sdk/bus/index.d.ts +9 -1
  62. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
  63. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
  64. package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
  65. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  66. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  67. package/dist/types/sdk/cli/index.d.ts +1 -0
  68. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  69. package/dist/types/sdk/cli/rows.d.ts +12 -5
  70. package/dist/types/sdk/cli/session-cli.d.ts +20 -3
  71. package/dist/types/sdk/client/client.d.ts +3 -2
  72. package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
  73. package/dist/types/sdk/host/control/index.d.ts +1 -1
  74. package/dist/types/sdk/host/control/operations.d.ts +2 -0
  75. package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
  76. package/dist/types/sdk/host/events.d.ts +1 -0
  77. package/dist/types/sdk/host/host.d.ts +10 -0
  78. package/dist/types/sdk/host/query/cursor.d.ts +4 -2
  79. package/dist/types/sdk/host/query/handlers.d.ts +1 -0
  80. package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
  81. package/dist/types/sdk/host/session-runtime.d.ts +70 -17
  82. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  83. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  84. package/dist/types/sdk/lifecycle-session.d.ts +3 -1
  85. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
  86. package/dist/types/sdk/receipt-state.d.ts +2 -0
  87. package/dist/types/sdk/router/session-router.d.ts +6 -0
  88. package/dist/types/sdk/session.d.ts +16 -0
  89. package/dist/types/sdk/startup-capability.d.ts +2 -1
  90. package/dist/types/sdk/turn-result.d.ts +6 -0
  91. package/dist/types/session/agent-session.d.ts +52 -7
  92. package/dist/types/session/credential-store-errors.d.ts +1 -0
  93. package/dist/types/session/fold-coordinator.d.ts +29 -2
  94. package/dist/types/session/session-manager.d.ts +79 -2
  95. package/dist/types/session/yield-queue.d.ts +5 -4
  96. package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
  97. package/dist/types/tools/ask.d.ts +1 -0
  98. package/dist/types/tools/bash.d.ts +16 -0
  99. package/dist/types/tools/browser/launch.d.ts +10 -0
  100. package/dist/types/tools/browser/registry.d.ts +8 -0
  101. package/dist/types/tools/gh.d.ts +8 -0
  102. package/dist/types/tools/index.d.ts +10 -10
  103. package/dist/types/tools/job.d.ts +3 -1
  104. package/dist/types/utils/external-editor.d.ts +7 -0
  105. package/examples/extensions/README.md +1 -1
  106. package/examples/extensions/pirate.ts +7 -4
  107. package/package.json +7 -7
  108. package/scripts/generate-sdk-operation-inventory.ts +13 -1
  109. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  110. package/src/async/job-manager.ts +107 -9
  111. package/src/browser-backend.ts +29 -0
  112. package/src/cli/accounts-cli.ts +9 -5
  113. package/src/cli/args.ts +17 -0
  114. package/src/cli/fast-help.ts +1 -0
  115. package/src/cli/plugin-cli.ts +29 -0
  116. package/src/cli/root-flags.ts +5 -0
  117. package/src/cli/update-cli.ts +140 -23
  118. package/src/commands/launch.ts +86 -18
  119. package/src/commands/sdk.ts +135 -28
  120. package/src/config/autorouting-tier-map.ts +12 -0
  121. package/src/config/billing-path.ts +42 -0
  122. package/src/config/config-schema-version.ts +18 -1
  123. package/src/config/file-lock.ts +158 -83
  124. package/src/config/model-preset-registry.ts +178 -23
  125. package/src/config/model-profiles.ts +5 -5
  126. package/src/config/model-registry.ts +291 -42
  127. package/src/config/model-resolver.ts +5 -0
  128. package/src/config/settings-schema.ts +27 -8
  129. package/src/config/settings.ts +51 -15
  130. package/src/coordinator-mcp/projection-scan.ts +27 -6
  131. package/src/coordinator-mcp/question-state.ts +9 -11
  132. package/src/coordinator-mcp/server.ts +115 -21
  133. package/src/crash/index-store.ts +2 -0
  134. package/src/crash/record-loader.ts +10 -0
  135. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  136. package/src/extensibility/extensions/runner.ts +27 -0
  137. package/src/extensibility/extensions/types.ts +8 -3
  138. package/src/extensibility/gjc-plugins/installer.ts +51 -11
  139. package/src/extensibility/runtime-skill-discovery.ts +110 -3
  140. package/src/extensibility/skills.ts +22 -12
  141. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  142. package/src/gjc-runtime/launch-worktree.ts +285 -4
  143. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  144. package/src/gjc-runtime/session-state-lock.ts +213 -14
  145. package/src/gjc-runtime/session-state-sidecar.ts +938 -39
  146. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  147. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -3
  148. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  149. package/src/hooks/skill-state.ts +6 -0
  150. package/src/internal-urls/docs-index.generated.ts +15 -15
  151. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  152. package/src/main.ts +61 -3
  153. package/src/master-mode/context.ts +50 -0
  154. package/src/master-mode/first-request.ts +65 -0
  155. package/src/master-mode/snapshot.ts +205 -0
  156. package/src/modes/acp/acp-agent.ts +610 -161
  157. package/src/modes/components/footer.ts +4 -2
  158. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  159. package/src/modes/components/model-selector.ts +76 -8
  160. package/src/modes/components/oauth-selector.ts +6 -1
  161. package/src/modes/components/session-selector.ts +23 -15
  162. package/src/modes/components/sessions-dashboard.ts +8 -5
  163. package/src/modes/components/settings-selector.ts +1 -0
  164. package/src/modes/components/status-line/model-name.ts +53 -0
  165. package/src/modes/components/status-line/priority-row.ts +107 -0
  166. package/src/modes/components/status-line/segments.ts +23 -9
  167. package/src/modes/components/tool-status-header.ts +151 -15
  168. package/src/modes/controllers/command-controller.ts +1 -1
  169. package/src/modes/controllers/event-controller.ts +30 -4
  170. package/src/modes/controllers/extension-ui-controller.ts +13 -13
  171. package/src/modes/controllers/input-controller.ts +30 -2
  172. package/src/modes/controllers/selector-controller.ts +200 -98
  173. package/src/modes/interactive-mode.ts +36 -12
  174. package/src/modes/print-mode.ts +64 -0
  175. package/src/modes/prompt-action-autocomplete.ts +30 -4
  176. package/src/modes/runtime-init.ts +12 -9
  177. package/src/modes/theme/theme.ts +5 -0
  178. package/src/modes/types.ts +2 -0
  179. package/src/modes/ui-language.ts +49 -2
  180. package/src/modes/utils/ui-helpers.ts +22 -3
  181. package/src/prompts/agent-fragments/browser-aside.md +22 -0
  182. package/src/prompts/system/master-mode.md +28 -0
  183. package/src/prompts/tools/ask.md +1 -1
  184. package/src/prompts/tools/bash.md +1 -0
  185. package/src/prompts/tools/job.md +1 -1
  186. package/src/sdk/acp/adapter.ts +5 -0
  187. package/src/sdk/broker/broker.ts +1646 -27
  188. package/src/sdk/broker/endpoint-authority.ts +86 -0
  189. package/src/sdk/broker/ensure.ts +81 -3
  190. package/src/sdk/broker/index.ts +3 -0
  191. package/src/sdk/broker/lifecycle-ledger.ts +6 -0
  192. package/src/sdk/broker/lifecycle.ts +440 -75
  193. package/src/sdk/broker/master-capability.ts +232 -0
  194. package/src/sdk/broker/session-index.ts +121 -17
  195. package/src/sdk/broker/session-scope.ts +272 -0
  196. package/src/sdk/broker/spawn-authority.ts +1031 -0
  197. package/src/sdk/broker/spawn-substrate.ts +591 -0
  198. package/src/sdk/broker/startup-budget.ts +144 -2
  199. package/src/sdk/broker/state-version.ts +43 -14
  200. package/src/sdk/broker/transport.ts +1 -0
  201. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  202. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  203. package/src/sdk/bus/chat-daemon-runtime.ts +136 -18
  204. package/src/sdk/bus/discord-daemon.ts +50 -10
  205. package/src/sdk/bus/index.ts +84 -7
  206. package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
  207. package/src/sdk/bus/lifecycle-commands.ts +4 -0
  208. package/src/sdk/bus/notification-service.ts +22 -5
  209. package/src/sdk/bus/prompt-reconciliation.ts +7 -2
  210. package/src/sdk/bus/reconciliation-store.ts +55 -31
  211. package/src/sdk/bus/slack-conversation.ts +1 -1
  212. package/src/sdk/bus/slack-daemon.ts +72 -38
  213. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  214. package/src/sdk/cli/index.ts +1 -0
  215. package/src/sdk/cli/master-cli.ts +259 -0
  216. package/src/sdk/cli/rows.ts +41 -15
  217. package/src/sdk/cli/session-cli.ts +297 -26
  218. package/src/sdk/client/client.ts +14 -8
  219. package/src/sdk/host/control/dispatch.ts +101 -40
  220. package/src/sdk/host/control/index.ts +2 -0
  221. package/src/sdk/host/control/operations.ts +2 -0
  222. package/src/sdk/host/control/runtime-gate.ts +203 -11
  223. package/src/sdk/host/events.ts +1 -0
  224. package/src/sdk/host/host.ts +29 -2
  225. package/src/sdk/host/query/cursor.ts +52 -7
  226. package/src/sdk/host/query/handlers.ts +33 -12
  227. package/src/sdk/host/query/revision-store.ts +12 -0
  228. package/src/sdk/host/session-runtime.test.ts +673 -14
  229. package/src/sdk/host/session-runtime.ts +674 -103
  230. package/src/sdk/host/surface-policy.ts +3 -3
  231. package/src/sdk/lifecycle/broker-client.ts +28 -0
  232. package/src/sdk/lifecycle/service.ts +279 -20
  233. package/src/sdk/lifecycle-session.ts +17 -4
  234. package/src/sdk/prompt-deadline-manager.ts +2 -1
  235. package/src/sdk/protocol/operation-inventory.generated.json +105 -3
  236. package/src/sdk/protocol/operation-registry.ts +14 -4
  237. package/src/sdk/receipt-state.ts +10 -0
  238. package/src/sdk/router/session-router.ts +219 -118
  239. package/src/sdk/session.ts +416 -195
  240. package/src/sdk/startup-capability.ts +1 -0
  241. package/src/sdk/turn-result.ts +22 -0
  242. package/src/session/agent-session.ts +1628 -318
  243. package/src/session/blob-store.ts +15 -5
  244. package/src/session/credential-store-errors.ts +2 -0
  245. package/src/session/fold-coordinator.ts +40 -3
  246. package/src/session/internal/managed-session-storage.ts +153 -78
  247. package/src/session/session-manager.ts +850 -97
  248. package/src/session/yield-queue.ts +24 -10
  249. package/src/setup/frictionless-onboarding.ts +2 -0
  250. package/src/slash-commands/builtin-registry.ts +41 -0
  251. package/src/task/executor.ts +9 -4
  252. package/src/telemetry/events.ts +1138 -12
  253. package/src/tools/ask-contract.test.ts +64 -0
  254. package/src/tools/ask-contract.ts +36 -1
  255. package/src/tools/ask.ts +10 -8
  256. package/src/tools/bash-interactive.ts +11 -2
  257. package/src/tools/bash.ts +412 -75
  258. package/src/tools/browser/launch.ts +37 -0
  259. package/src/tools/browser/registry.ts +99 -9
  260. package/src/tools/cron.ts +6 -4
  261. package/src/tools/descriptors.test.ts +15 -0
  262. package/src/tools/descriptors.ts +5 -1
  263. package/src/tools/gh.ts +104 -48
  264. package/src/tools/github-cache.ts +2 -2
  265. package/src/tools/index.ts +10 -6
  266. package/src/tools/job.ts +4 -1
  267. package/src/tools/monitor.ts +12 -9
  268. package/src/tools/resolve.ts +30 -10
  269. package/src/tools/tool-catalog.generated.ts +2 -2
  270. package/src/utils/external-editor.ts +72 -1
@@ -17,6 +17,8 @@ import {
17
17
  type AuthCredentialSelector,
18
18
  type CredentialDisabledEvent,
19
19
  codexToolWireName,
20
+ DEFAULT_MODEL_PER_PROVIDER,
21
+ type KnownProvider,
20
22
  type Message,
21
23
  type Model,
22
24
  type ProviderSessionState,
@@ -49,6 +51,7 @@ import {
49
51
  isBackgroundJobSupportEnabled,
50
52
  jobElapsedMs,
51
53
  } from "../async";
54
+ import { resolveBrowserBackend } from "../browser-backend";
52
55
  import { loadCapability, reset as resetCapabilities } from "../capability";
53
56
  import { type Rule, ruleCapability, setActiveRules } from "../capability/rule";
54
57
  import type { SourceMeta } from "../capability/types";
@@ -65,11 +68,13 @@ import {
65
68
  resolveModelRoleValue,
66
69
  type ScopedModelSelection,
67
70
  } from "../config/model-resolver";
71
+ import { normalizeModelSelectorValue } from "../config/model-selector-value";
68
72
  import { loadPromptTemplates as loadPromptTemplatesInternal, type PromptTemplate } from "../config/prompt-templates";
69
73
  import { Settings, type SkillsSettings } from "../config/settings";
70
74
  import { resolveEagerTaskDelegation } from "../config/task-delegation";
71
75
  import { CursorExecHandlers } from "../cursor";
72
76
  import { EditTool } from "../edit";
77
+ import type { MasterModeContext } from "../master-mode/context";
73
78
  import { describeFoldReceipt } from "../session/fold-coordinator";
74
79
  import type { BashRestrictionProfile } from "../tools/bash-allowed-prefixes";
75
80
  import { SearchTool } from "../tools/search";
@@ -120,7 +125,9 @@ import { normalizePluginHook } from "../hooks/normalize";
120
125
  import { initializeLocalRoot, LocalProtocolHandler, type LocalProtocolOptions } from "../internal-urls";
121
126
  import type { LspStartupServerInfo } from "../lsp";
122
127
  import { shutdownAll as shutdownAllLspClients } from "../lsp/client";
128
+ import { createMasterPeerSnapshotContributor, MASTER_PEER_SNAPSHOT_CUSTOM_TYPE } from "../master-mode/first-request";
123
129
  import btwUserPrompt from "../prompts/system/btw-user.md" with { type: "text" };
130
+ import masterModeTemplate from "../prompts/system/master-mode.md" with { type: "text" };
124
131
  import asyncResultTemplate from "../prompts/tools/async-result.md" with { type: "text" };
125
132
  import { AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
126
133
  import { createLazyService } from "../runtime/lazy-service";
@@ -144,7 +151,7 @@ import { markAutoroutingInactive } from "../sdk/host/internal-autorouting-state"
144
151
  import { createSdkSessionRuntimeExtension, registerSdkOnlyNotificationCommand } from "../sdk/host/session-runtime";
145
152
  import { createSdkWebSocketTransport } from "../sdk/host/websocket-transport";
146
153
  import type { SecretObfuscator } from "../secrets";
147
- import { AgentSession, type ForkContextSeed } from "../session/agent-session";
154
+ import { AgentSession, type ForkContextSeed, isSessionDisposalIncompleteError } from "../session/agent-session";
148
155
  import { AuthBrokerClient, AuthStorage, RemoteAuthCredentialStore } from "../session/auth-storage";
149
156
  import { type CustomMessage, convertToLlm } from "../session/messages";
150
157
  import { createReadonlySessionManager, SessionManager } from "../session/session-manager";
@@ -189,6 +196,7 @@ import { guardToolForUltragoalAsk } from "../tools/ultragoal-ask-guard";
189
196
  import { EventBus } from "../utils/event-bus";
190
197
  import { buildNamedToolChoice, buildNamedToolChoiceResult } from "../utils/tool-choice";
191
198
  import type { WorkspaceTree } from "../workspace-tree";
199
+ import { createSessionLifecycleService } from "./lifecycle/client";
192
200
  import {
193
201
  attachLifecycleStartupCapability,
194
202
  lifecycleMcpStartupTimeoutOption,
@@ -525,6 +533,8 @@ export interface CreateAgentSessionOptions {
525
533
  * @internal CLI-only startup optimization; SDK callers retain synchronous loading by default.
526
534
  */
527
535
  deferMcpConfigStartup?: boolean;
536
+ /** Process-local master authority. Never persist or pass to lifecycle children. */
537
+ masterModeContext?: MasterModeContext;
528
538
  /**
529
539
  * Defer memory backend startup until the caller has applied startup model profiles.
530
540
  * @internal CLI-only ordering guard; SDK callers retain immediate startup by default.
@@ -1095,12 +1105,16 @@ function buildMCPPromptCommands(manager: MCPManager): LoadedCustomCommand[] {
1095
1105
  */
1096
1106
 
1097
1107
  function withEmbeddedDefaultGjcSkills(skills: Skill[]): Skill[] {
1098
- const byName = new Map(skills.map(skill => [skill.name, skill]));
1108
+ const byName = new Map<string, Skill>();
1109
+ for (const skill of skills) {
1110
+ const key = skill.name.toLowerCase();
1111
+ if (!byName.has(key)) byName.set(key, skill);
1112
+ }
1099
1113
  // The four public GJC workflow skills are a product invariant: even if a
1100
1114
  // caller-supplied or filesystem skill shares a name, the bundled definition
1101
1115
  // wins so workflow routing can never be silently hijacked.
1102
1116
  for (const defaultSkill of getEmbeddedDefaultGjcSkills()) {
1103
- byName.set(defaultSkill.name, defaultSkill);
1117
+ byName.set(defaultSkill.name.toLowerCase(), defaultSkill);
1104
1118
  }
1105
1119
  return [...byName.values()];
1106
1120
  }
@@ -1282,6 +1296,45 @@ function findDeferredExactMcpToolNameCollisions(
1282
1296
  return [...collisions];
1283
1297
  }
1284
1298
 
1299
+ /**
1300
+ * Order candidates so each known provider's curated default model is tried
1301
+ * before the rest of the catalog.
1302
+ *
1303
+ * Provider catalogs are not ranked by fitness — a withdrawn model whose ID
1304
+ * carries an older date suffix sorts ahead of its current replacement — so
1305
+ * picking the first credentialed entry can start an unconfigured install on a
1306
+ * model the provider no longer serves. `DEFAULT_MODEL_PER_PROVIDER` is the
1307
+ * curated table `findInitialModel` already sweeps for the same purpose;
1308
+ * reusing it keeps both unconfigured paths on one source of truth. Candidates
1309
+ * that are not a provider default keep their original relative order.
1310
+ */
1311
+ export function orderByProviderDefaultFirst(
1312
+ candidates: readonly Model[],
1313
+ providerOrder: readonly string[] = Object.keys(DEFAULT_MODEL_PER_PROVIDER),
1314
+ ): Model[] {
1315
+ const preferred: Model[] = [];
1316
+ const rest: Model[] = [];
1317
+ const preferredCandidates = new Set<Model>();
1318
+ const seenProviders = new Set<string>();
1319
+ for (const rawProvider of [...providerOrder, ...Object.keys(DEFAULT_MODEL_PER_PROVIDER)]) {
1320
+ const provider = rawProvider.trim().toLowerCase();
1321
+ if (!provider || seenProviders.has(provider) || !(provider in DEFAULT_MODEL_PER_PROVIDER)) continue;
1322
+ seenProviders.add(provider);
1323
+ const defaultId = DEFAULT_MODEL_PER_PROVIDER[provider as KnownProvider];
1324
+ for (const candidate of candidates) {
1325
+ if (candidate.provider.trim().toLowerCase() === provider && candidate.id === defaultId) {
1326
+ preferred.push(candidate);
1327
+ preferredCandidates.add(candidate);
1328
+ }
1329
+ }
1330
+ }
1331
+ if (preferred.length === 0) return [...candidates];
1332
+ for (const candidate of candidates) {
1333
+ if (!preferredCandidates.has(candidate)) rest.push(candidate);
1334
+ }
1335
+ return [...preferred, ...rest];
1336
+ }
1337
+
1285
1338
  const AUTOMATION_TOOL_NAMES = new Set<AutomationToolName>(["browser", "computer"]);
1286
1339
 
1287
1340
  function validateAutomationTools(options: CreateAgentSessionOptions): AutomationTools {
@@ -1307,6 +1360,7 @@ function validateAutomationTools(options: CreateAgentSessionOptions): Automation
1307
1360
 
1308
1361
  export async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {
1309
1362
  const automationTools = validateAutomationTools(options);
1363
+ const masterModeContext = options.masterModeContext;
1310
1364
  const lifecycleStartupCapability = (
1311
1365
  options as CreateAgentSessionOptions & { [lifecycleStartupCapabilityOption]?: SdkStartupCapability }
1312
1366
  )[lifecycleStartupCapabilityOption];
@@ -1366,6 +1420,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1366
1420
  }
1367
1421
 
1368
1422
  let agent: Agent;
1423
+ let sessionAgent: Agent | undefined;
1369
1424
  let session!: AgentSession;
1370
1425
  let sessionManager!: SessionManager;
1371
1426
  let hasSession = false;
@@ -1783,8 +1838,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1783
1838
  credentialSessionId,
1784
1839
  }),
1785
1840
  );
1841
+ const configuredDefaultPatterns = normalizeModelSelectorValue(settings.getModelRole("default"));
1786
1842
  let model = options.model;
1787
1843
  let modelFallbackMessage: string | undefined;
1844
+ if (!hasExplicitModel && configuredDefaultPatterns.length > 0 && !defaultRoleSpec.model) {
1845
+ modelFallbackMessage = `Model ${configuredDefaultPatterns.join(" -> ")} not found`;
1846
+ }
1788
1847
  const resumeModelBehavior = settings.get("session.resumeModelBehavior");
1789
1848
  const persistedDefaultChain = existingSession.configuredModelChains.default;
1790
1849
  const defaultModelEntries =
@@ -2384,7 +2443,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2384
2443
  "Cannot rescope a session with caller-owned MCP authority; recreate the session at the target cwd.",
2385
2444
  );
2386
2445
  }
2387
- return sessionManager.runExclusiveCwdTransition(async () => {
2446
+ return sessionManager.runExclusiveCwdMoveTransition(async () => {
2388
2447
  if (moveConsumed) {
2389
2448
  throw new Error(
2390
2449
  "This session has already been rescoped; only one agent-invoked move is allowed per session.",
@@ -2396,174 +2455,240 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2396
2455
  );
2397
2456
  }
2398
2457
  const from = sessionManager.getCwd();
2399
- const resolvedPath = path.resolve(from, target);
2400
- let canonicalFrom: string;
2401
- let canonicalTarget: string;
2402
- try {
2403
- canonicalFrom = await fs.realpath(from);
2404
- canonicalTarget = await fs.realpath(resolvedPath);
2405
- } catch {
2406
- throw new Error(`Directory does not exist or is not a directory: ${resolvedPath}`);
2407
- }
2408
- if (!(await fs.stat(canonicalTarget)).isDirectory()) {
2409
- throw new Error(`Directory does not exist or is not a directory: ${resolvedPath}`);
2410
- }
2411
- const relative = path.relative(canonicalFrom, canonicalTarget);
2412
- if (relative === "") {
2413
- throw new Error(
2414
- `Target ${canonicalTarget} is the current session directory; nothing to move.`,
2415
- );
2458
+ const sourceHandle = await SessionManager.openNoFollowDirectory(from);
2459
+ const sourceOpened = await sourceHandle.stat({ bigint: true });
2460
+ if (!sourceOpened.isDirectory()) {
2461
+ await sourceHandle.close().catch(() => {});
2462
+ throw new Error(`Current session directory is no longer a directory: ${from}`);
2416
2463
  }
2417
- if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
2418
- throw new Error(
2419
- `Refusing to rescope outside the current session directory: ${canonicalTarget} is not within ${canonicalFrom}. move_session only narrows the session scope; ask the user to restart or /move for a broader relocation.`,
2420
- );
2421
- }
2422
- let targetHandle: nodeFs.promises.FileHandle | undefined;
2423
- let expectedIdentity: { dev: bigint; ino: bigint };
2424
2464
  try {
2425
- targetHandle = await SessionManager.openNoFollowDirectory(canonicalTarget);
2426
- const opened = await targetHandle.stat({ bigint: true });
2427
- if (!opened.isDirectory()) {
2465
+ const resolvedPath = path.resolve(from, target);
2466
+ let canonicalFrom: string;
2467
+ let canonicalTarget: string;
2468
+ try {
2469
+ canonicalFrom = await fs.realpath(from);
2470
+ canonicalTarget = await fs.realpath(resolvedPath);
2471
+ } catch (error) {
2472
+ // macOS `realpath(3)` reports EACCES on an unreadable
2473
+ // directory where Linux succeeds and fails later at the
2474
+ // `access` probe; keep both platforms on the access message.
2475
+ const code = (error as NodeJS.ErrnoException)?.code;
2476
+ if (code === "EACCES" || code === "EPERM") {
2477
+ throw new Error(
2478
+ `Directory identity or access unavailable: ${resolvedPath}${
2479
+ error instanceof Error ? ` (${error.message})` : ""
2480
+ }`,
2481
+ );
2482
+ }
2428
2483
  throw new Error(`Directory does not exist or is not a directory: ${resolvedPath}`);
2429
2484
  }
2430
- expectedIdentity = { dev: opened.dev, ino: opened.ino };
2431
- await fs.access(canonicalTarget, nodeFs.constants.R_OK | nodeFs.constants.X_OK);
2432
- } catch (error) {
2433
- await targetHandle?.close().catch(() => {});
2434
- if (error instanceof Error && error.message.startsWith("Directory does not exist")) {
2435
- throw error;
2485
+ if (!(await fs.stat(canonicalTarget)).isDirectory()) {
2486
+ throw new Error(`Directory does not exist or is not a directory: ${resolvedPath}`);
2436
2487
  }
2437
- throw new Error(
2438
- `Directory identity or access unavailable: ${canonicalTarget}${
2439
- error instanceof Error ? ` (${error.message})` : ""
2440
- }`,
2441
- );
2442
- }
2443
- // Process-cwd authority is an explicit claim, never inferred from
2444
- // `process.cwd() === from`: sibling sessions launched at the same
2445
- // root both satisfy that, so acting on it would chdir the process
2446
- // and clear process-global caches underneath the sibling.
2447
- const ownsProcessCwd = SessionManager.isProcessCwdOwner(sessionManager);
2448
- const restoreLaunchRoot = async (failure: unknown): Promise<never> => {
2449
- const restoreErrors: Error[] = [];
2450
- if (ownsProcessCwd) {
2451
- try {
2452
- setProjectDir(canonicalFrom);
2453
- if (path.resolve(process.cwd()) !== path.resolve(canonicalFrom)) {
2454
- throw new Error("Process cwd did not restore to the launch root.");
2455
- }
2456
- } catch (error) {
2457
- restoreErrors.push(error instanceof Error ? error : new Error(String(error)));
2488
+ const relative = path.relative(canonicalFrom, canonicalTarget);
2489
+ if (relative === "") {
2490
+ throw new Error(
2491
+ `Target ${canonicalTarget} is the current session directory; nothing to move.`,
2492
+ );
2493
+ }
2494
+ if (
2495
+ relative === ".." ||
2496
+ relative.startsWith(`..${path.sep}`) ||
2497
+ path.isAbsolute(relative)
2498
+ ) {
2499
+ throw new Error(
2500
+ `Refusing to rescope outside the current session directory: ${canonicalTarget} is not within ${canonicalFrom}. move_session only narrows the session scope; ask the user to restart or /move for a broader relocation.`,
2501
+ );
2502
+ }
2503
+ let targetHandle: nodeFs.promises.FileHandle | undefined;
2504
+ let expectedIdentity: { dev: bigint; ino: bigint };
2505
+ try {
2506
+ targetHandle = await SessionManager.openNoFollowDirectory(canonicalTarget);
2507
+ const opened = await targetHandle.stat({ bigint: true });
2508
+ if (!opened.isDirectory()) {
2509
+ throw new Error(`Directory does not exist or is not a directory: ${resolvedPath}`);
2458
2510
  }
2459
- try {
2460
- resetCapabilities();
2461
- const restoreRegistry = await resolveActiveProjectRegistryPath(canonicalFrom).catch(
2462
- () => undefined,
2511
+ const revalidatedFrom = await fs.realpath(from);
2512
+ const revalidatedTarget = await fs.realpath(resolvedPath);
2513
+ const revalidatedRelative = path.relative(revalidatedFrom, revalidatedTarget);
2514
+ if (
2515
+ revalidatedFrom !== canonicalFrom ||
2516
+ revalidatedTarget !== canonicalTarget ||
2517
+ revalidatedRelative === "" ||
2518
+ revalidatedRelative === ".." ||
2519
+ revalidatedRelative.startsWith(`..${path.sep}`) ||
2520
+ path.isAbsolute(revalidatedRelative)
2521
+ ) {
2522
+ throw new Error(
2523
+ `Directory identity changed during confinement validation: ${resolvedPath}`,
2463
2524
  );
2464
- clearPluginRootsAndCaches(restoreRegistry ? [restoreRegistry] : undefined);
2465
- } catch (error) {
2466
- restoreErrors.push(error instanceof Error ? error : new Error(String(error)));
2467
2525
  }
2468
- }
2469
- try {
2470
- await rebindCwdCapturingAuthority(canonicalFrom);
2526
+ const named = await fs.lstat(revalidatedTarget, { bigint: true });
2527
+ const namedSource = await fs.lstat(revalidatedFrom, { bigint: true });
2528
+ if (
2529
+ !namedSource.isDirectory() ||
2530
+ namedSource.isSymbolicLink() ||
2531
+ namedSource.dev !== sourceOpened.dev ||
2532
+ namedSource.ino !== sourceOpened.ino ||
2533
+ !named.isDirectory() ||
2534
+ named.isSymbolicLink() ||
2535
+ named.dev !== opened.dev ||
2536
+ named.ino !== opened.ino
2537
+ ) {
2538
+ throw new Error(
2539
+ `Directory identity changed during confinement validation: ${resolvedPath}`,
2540
+ );
2541
+ }
2542
+ expectedIdentity = { dev: opened.dev, ino: opened.ino };
2543
+ await fs.access(canonicalTarget, nodeFs.constants.R_OK | nodeFs.constants.X_OK);
2471
2544
  } catch (error) {
2472
- restoreErrors.push(error instanceof Error ? error : new Error(String(error)));
2473
- }
2474
- if (restoreErrors.length > 0) {
2475
- throw new AggregateError(
2476
- restoreErrors,
2477
- "Failed to restore launch-root rescope authority.",
2478
- {
2479
- cause: failure,
2480
- },
2545
+ await targetHandle?.close().catch(() => {});
2546
+ if (error instanceof Error && error.message.startsWith("Directory does not exist")) {
2547
+ throw error;
2548
+ }
2549
+ throw new Error(
2550
+ `Directory identity or access unavailable: ${canonicalTarget}${
2551
+ error instanceof Error ? ` (${error.message})` : ""
2552
+ }`,
2481
2553
  );
2482
2554
  }
2483
- throw failure;
2484
- };
2485
- try {
2486
- // Every fallible step that the moved session depends on runs
2487
- // BEFORE the session-file commit, so a failure here leaves the
2488
- // session exactly where it was and the tool call is a clean
2489
- // rejection rather than a half-moved session.
2490
- if (ownsProcessCwd) {
2491
- setProjectDir(canonicalTarget);
2555
+ // Process-cwd authority is an explicit claim, never inferred from
2556
+ // `process.cwd() === from`: sibling sessions launched at the same
2557
+ // root both satisfy that, so acting on it would chdir the process
2558
+ // and clear process-global caches underneath the sibling.
2559
+ const ownsProcessCwd = SessionManager.isProcessCwdOwner(sessionManager);
2560
+ const restoreLaunchRoot = async (failure: unknown): Promise<never> => {
2561
+ const restoreErrors: Error[] = [];
2562
+ if (ownsProcessCwd) {
2563
+ try {
2564
+ setProjectDir(canonicalFrom);
2565
+ if (path.resolve(process.cwd()) !== path.resolve(canonicalFrom)) {
2566
+ throw new Error("Process cwd did not restore to the launch root.");
2567
+ }
2568
+ await SessionManager.assertProcessCwdIdentity({
2569
+ dev: sourceOpened.dev,
2570
+ ino: sourceOpened.ino,
2571
+ });
2572
+ } catch (error) {
2573
+ restoreErrors.push(error instanceof Error ? error : new Error(String(error)));
2574
+ }
2575
+ try {
2576
+ resetCapabilities();
2577
+ const restoreRegistry = await resolveActiveProjectRegistryPath(
2578
+ canonicalFrom,
2579
+ ).catch(() => undefined);
2580
+ clearPluginRootsAndCaches(restoreRegistry ? [restoreRegistry] : undefined);
2581
+ } catch (error) {
2582
+ restoreErrors.push(error instanceof Error ? error : new Error(String(error)));
2583
+ }
2584
+ }
2492
2585
  try {
2493
- // `setProjectDir` chdirs a NAME. Confirm the process actually
2494
- // landed on the pinned directory, so a path replaced after
2495
- // the name checks cannot escape the validated descendant.
2496
- await SessionManager.assertProcessCwdIdentity(expectedIdentity);
2586
+ await rebindCwdCapturingAuthority(canonicalFrom);
2497
2587
  } catch (error) {
2498
- setProjectDir(canonicalFrom);
2499
- throw error;
2588
+ restoreErrors.push(error instanceof Error ? error : new Error(String(error)));
2500
2589
  }
2501
- }
2502
- let rescopeFailure: unknown;
2590
+ if (restoreErrors.length > 0) {
2591
+ throw new AggregateError(
2592
+ restoreErrors,
2593
+ "Failed to restore launch-root rescope authority.",
2594
+ {
2595
+ cause: failure,
2596
+ },
2597
+ );
2598
+ }
2599
+ throw failure;
2600
+ };
2503
2601
  try {
2602
+ // Every fallible step that the moved session depends on runs
2603
+ // BEFORE the session-file commit, so a failure here leaves the
2604
+ // session exactly where it was and the tool call is a clean
2605
+ // rejection rather than a half-moved session.
2504
2606
  if (ownsProcessCwd) {
2505
- resetCapabilities();
2506
- const projectRegistry = await resolveActiveProjectRegistryPath(canonicalTarget);
2507
- clearPluginRootsAndCaches(projectRegistry ? [projectRegistry] : undefined);
2607
+ setProjectDir(canonicalTarget);
2608
+ try {
2609
+ // `setProjectDir` chdirs a NAME. Confirm the process actually
2610
+ // landed on the pinned directory, so a path replaced after
2611
+ // the name checks cannot escape the validated descendant.
2612
+ await SessionManager.assertProcessCwdIdentity(expectedIdentity);
2613
+ } catch (error) {
2614
+ setProjectDir(canonicalFrom);
2615
+ await SessionManager.assertProcessCwdIdentity({
2616
+ dev: sourceOpened.dev,
2617
+ ino: sourceOpened.ino,
2618
+ });
2619
+ throw error;
2620
+ }
2621
+ }
2622
+ let rescopeFailure: unknown;
2623
+ try {
2624
+ if (ownsProcessCwd) {
2625
+ resetCapabilities();
2626
+ const projectRegistry = await resolveActiveProjectRegistryPath(canonicalTarget);
2627
+ clearPluginRootsAndCaches(projectRegistry ? [projectRegistry] : undefined);
2628
+ }
2629
+ // Plugin/MCP/Python authority must be rebound successfully
2630
+ // before committing; swallowing a failure here is what leaves
2631
+ // a moved session holding launch-root tool authority.
2632
+ await rebindCwdCapturingAuthority(canonicalTarget);
2633
+ } catch (error) {
2634
+ rescopeFailure = error;
2635
+ }
2636
+ if (rescopeFailure !== undefined) {
2637
+ await restoreLaunchRoot(rescopeFailure);
2638
+ }
2639
+ try {
2640
+ await sessionManager.flush();
2641
+ // Commit last: `moveTo` re-validates the pinned identity through
2642
+ // the still-open handle at the state-changing boundary.
2643
+ await sessionManager.moveTo(canonicalTarget, {
2644
+ expectedIdentity,
2645
+ targetHandle,
2646
+ expectedSourceIdentity: { dev: sourceOpened.dev, ino: sourceOpened.ino },
2647
+ sourceHandle,
2648
+ });
2649
+ } catch (error) {
2650
+ const committedCwd = sessionManager.getCwd();
2651
+ const stayedAtLaunchRoot = path.resolve(committedCwd) === path.resolve(from);
2652
+ if (stayedAtLaunchRoot) await restoreLaunchRoot(error);
2653
+ // SessionManager can publish the durable move before a later metadata
2654
+ // write fails. Treat that state as committed rather than reporting a
2655
+ // rejection after the session has moved.
2656
+ moveConsumed = true;
2657
+ logger.warn("Session rescope committed before finalization failed", {
2658
+ error: safeErrorForLog(error),
2659
+ cwd: committedCwd,
2660
+ });
2508
2661
  }
2509
- // Plugin/MCP/Python authority must be rebound successfully
2510
- // before committing; swallowing a failure here is what leaves
2511
- // a moved session holding launch-root tool authority.
2512
- await rebindCwdCapturingAuthority(canonicalTarget);
2513
- } catch (error) {
2514
- rescopeFailure = error;
2515
- }
2516
- if (rescopeFailure !== undefined) {
2517
- await restoreLaunchRoot(rescopeFailure);
2518
- }
2519
- try {
2520
- await sessionManager.flush();
2521
- // Commit last: `moveTo` re-validates the pinned identity through
2522
- // the still-open handle at the state-changing boundary.
2523
- await sessionManager.moveTo(canonicalTarget, {
2524
- expectedIdentity,
2525
- targetHandle,
2526
- });
2527
- } catch (error) {
2528
- const committedCwd = sessionManager.getCwd();
2529
- const stayedAtLaunchRoot = path.resolve(committedCwd) === path.resolve(from);
2530
- if (stayedAtLaunchRoot) await restoreLaunchRoot(error);
2531
- // SessionManager can publish the durable move before a later metadata
2532
- // write fails. Treat that state as committed rather than reporting a
2533
- // rejection after the session has moved.
2534
2662
  moveConsumed = true;
2535
- logger.warn("Session rescope committed before finalization failed", {
2536
- error: safeErrorForLog(error),
2537
- cwd: committedCwd,
2538
- });
2539
- }
2540
- moveConsumed = true;
2541
- if (ownsProcessCwd) {
2663
+ if (ownsProcessCwd) {
2664
+ try {
2665
+ await shutdownAllLspClients();
2666
+ } catch (error) {
2667
+ logger.warn("Failed to reset launch-root LSP clients after session rescope", {
2668
+ error: safeErrorForLog(error),
2669
+ });
2670
+ }
2671
+ }
2672
+ // Cwd-derived read-only state the prompt and subagents consume.
2673
+ // Best-effort by design: the move is committed, and a failed
2674
+ // re-discovery must not present a committed move as a failure.
2675
+ await applyRescopedReadState(sessionManager.getCwd());
2542
2676
  try {
2543
- await shutdownAllLspClients();
2677
+ await session?.refreshSshTool({ activateIfAvailable: true });
2544
2678
  } catch (error) {
2545
- logger.warn("Failed to reset launch-root LSP clients after session rescope", {
2679
+ // Non-fatal: the session has moved; the SSH tool refreshes
2680
+ // on its next activation attempt.
2681
+ logger.warn("Committed session rescope could not refresh the SSH tool", {
2546
2682
  error: safeErrorForLog(error),
2683
+ cwd: sessionManager.getCwd(),
2547
2684
  });
2548
2685
  }
2686
+ return { from, to: sessionManager.getCwd() };
2687
+ } finally {
2688
+ await targetHandle.close().catch(() => {});
2549
2689
  }
2550
- // Cwd-derived read-only state the prompt and subagents consume.
2551
- // Best-effort by design: the move is committed, and a failed
2552
- // re-discovery must not present a committed move as a failure.
2553
- await applyRescopedReadState(sessionManager.getCwd());
2554
- try {
2555
- await session?.refreshSshTool({ activateIfAvailable: true });
2556
- } catch (error) {
2557
- // Non-fatal: the session has moved; the SSH tool refreshes
2558
- // on its next activation attempt.
2559
- logger.warn("Committed session rescope could not refresh the SSH tool", {
2560
- error: safeErrorForLog(error),
2561
- cwd: sessionManager.getCwd(),
2562
- });
2563
- }
2564
- return { from, to: sessionManager.getCwd() };
2565
2690
  } finally {
2566
- await targetHandle.close().catch(() => {});
2691
+ await sourceHandle.close().catch(() => {});
2567
2692
  }
2568
2693
  });
2569
2694
  };
@@ -2593,7 +2718,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2593
2718
  registerForegroundFoldParticipant: adapter =>
2594
2719
  session?.registerForegroundFoldParticipant(adapter) ?? (() => {}),
2595
2720
  hasForegroundBashBackgroundRequestHandler: () => session?.hasForegroundBashBackgroundRequestHandler() ?? false,
2596
- requestForegroundBashBackground: () => Promise.resolve(session?.requestForegroundBashBackground() ?? false),
2721
+ requestForegroundBashBackground: (reason, adapter) =>
2722
+ Promise.resolve(session?.requestForegroundBashBackground(reason, adapter) ?? false),
2723
+ getToolInterruptPolicy: () => agent?.getToolInterruptPolicy() ?? "abort_tools",
2597
2724
 
2598
2725
  getCredentialSessionId: () => session?.credentialSessionId ?? credentialSessionId,
2599
2726
  getMcpManager: () => mcpManager ?? options.inheritedMcpManager,
@@ -2623,6 +2750,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2623
2750
  getToolForExecution: name => session?.getToolForExecution(name),
2624
2751
  agentRegistry,
2625
2752
  getSessionSpawns: () => options.spawns ?? "*",
2753
+ getMasterBashCapability: () => masterModeContext?.getCapability(),
2754
+ getMasterOwnerSessionId: () => masterModeContext?.ownerSessionId,
2626
2755
  getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
2627
2756
  getActiveModelString,
2628
2757
  getPlanModeState: () => session?.getPlanModeState(),
@@ -2648,16 +2777,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2648
2777
  return m ? buildNamedToolChoice(name, m) : undefined;
2649
2778
  },
2650
2779
  buildToolChoiceResult: name => buildNamedToolChoiceResult(name, session.model),
2651
- steer: msg =>
2652
- session.agent.steer({
2653
- role: "custom",
2654
- customType: msg.customType,
2655
- content: msg.content,
2656
- display: false,
2657
- details: msg.details,
2658
- attribution: "agent",
2659
- timestamp: Date.now(),
2660
- }),
2661
2780
  sendCustomMessage: (msg, opts) => session.sendCustomMessage(msg, opts),
2662
2781
  purgeQueuedCustomMessages: predicate => session.purgeQueuedCustomMessages(predicate),
2663
2782
  peekQueueInvoker: () => session.peekQueueInvoker(),
@@ -3171,6 +3290,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3171
3290
  controller: notificationSessionController,
3172
3291
  spawnedByGjc,
3173
3292
  sdkHostModeSupported: options.sdkHostModeSupported,
3293
+ ...(masterModeContext
3294
+ ? {
3295
+ masterCapability: masterModeContext.getCapability(),
3296
+ masterAttestationEpoch: masterModeContext.attestationEpoch,
3297
+ masterOwnerSessionId: masterModeContext.ownerSessionId,
3298
+ }
3299
+ : {}),
3174
3300
  // INTERNAL terminal-abort seams, threaded directly from the
3175
3301
  // owning session (NOT on the public extension context).
3176
3302
  terminalAbortSeams: {
@@ -3218,9 +3344,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3218
3344
  createSdkSessionRuntimeExtension(api, {
3219
3345
  agentDir,
3220
3346
  brokerRegistrationRequired: lifecycleStartupCapability !== undefined,
3347
+ ...(lifecycleStartupCapability?.lifecycleRequestId
3348
+ ? { lifecycleRequestId: lifecycleStartupCapability.lifecycleRequestId }
3349
+ : {}),
3221
3350
  createTransport: input => createSdkWebSocketTransport(input),
3222
3351
  settings,
3223
3352
  configOverrides: new Map(),
3353
+ ...(masterModeContext
3354
+ ? {
3355
+ masterCapability: masterModeContext.getCapability(),
3356
+ masterAttestationEpoch: masterModeContext.attestationEpoch,
3357
+ masterOwnerSessionId: masterModeContext.ownerSessionId,
3358
+ }
3359
+ : {}),
3224
3360
  // INTERNAL terminal-abort seams, threaded directly from the
3225
3361
  // owning session (NOT on the public extension context).
3226
3362
  terminalAbortSeams: {
@@ -3419,11 +3555,38 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3419
3555
  // Fall back to first available model with a valid API key, honoring the
3420
3556
  // path-scoped `enabledModels` allow-list when configured. Skip when the
3421
3557
  // user explicitly requested a model via --model that wasn't found.
3422
- if (!model && !options.modelPattern && !startupCredentialModelRejected) {
3558
+ if (
3559
+ !model &&
3560
+ !options.modelPattern &&
3561
+ !startupCredentialModelRejected &&
3562
+ (configuredDefaultPatterns.length === 0 || preferredCredentialProvider !== undefined)
3563
+ ) {
3423
3564
  // Re-resolve the allowed set: extension factories above may have
3424
3565
  // registered providers/models that weren't visible at startup.
3425
- const fallbackCandidates = await resolveAllowedModels(modelRegistry, settings, modelMatchPreferences);
3426
- for (const candidate of fallbackCandidates) {
3566
+ const allowedFallbackCandidates = await resolveAllowedModels(modelRegistry, settings, modelMatchPreferences);
3567
+ // A fresh provider discovery can disprove a bundled model while the
3568
+ // general available catalog retains it for offline/profile compatibility.
3569
+ // Exclude only those positively disproved bundled entries from the
3570
+ // unconfigured startup path; explicit model/profile resolution above keeps
3571
+ // its existing precedence and semantics.
3572
+ const profileAvailableKeys = new Set(
3573
+ modelRegistry
3574
+ .getAvailableForProfileActivation()
3575
+ .map(candidate => `${candidate.provider}\u0000${candidate.id}`),
3576
+ );
3577
+ const fallbackCandidates = allowedFallbackCandidates.filter(candidate =>
3578
+ profileAvailableKeys.has(`${candidate.provider}\u0000${candidate.id}`),
3579
+ );
3580
+ // Candidate order is not a quality signal: catalogs sort retired models
3581
+ // ahead of current ones whenever their IDs carry older date suffixes, so
3582
+ // an unconfigured install would otherwise start on a model its provider
3583
+ // has already withdrawn. Sweep each known provider's curated default
3584
+ // first — the same table `findInitialModel` consults — and only then fall
3585
+ // back to catalog order.
3586
+ for (const candidate of orderByProviderDefaultFirst(
3587
+ fallbackCandidates,
3588
+ modelRegistry.automaticProviderOrder(credentialSessionId),
3589
+ )) {
3427
3590
  if (await hasModelApiKey(candidate)) {
3428
3591
  model = candidate;
3429
3592
  break;
@@ -3730,7 +3893,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3730
3893
  ? await memoryBackend.buildDeveloperInstructions(agentDir, settings, session)
3731
3894
  : undefined;
3732
3895
 
3733
- const appendPrompt: string | undefined = memoryInstructions ?? undefined;
3896
+ const browserBackend = resolveBrowserBackend(settings);
3897
+ const appendPrompt =
3898
+ [memoryInstructions, browserBackend.developerInstructions].filter(Boolean).join("\n\n") || undefined;
3734
3899
  let pluginSystemAppendices = "";
3735
3900
  try {
3736
3901
  pluginSystemAppendices = await renderAlwaysOnSystemAppendices({ cwd: getLiveCwd() });
@@ -3775,16 +3940,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3775
3940
  if (hasSession) session.configWarnings.push(warning);
3776
3941
  logger.warn("Context file discovery warning", { warning });
3777
3942
  }
3943
+ // The master doctrine block is appended exactly once, AFTER any custom
3944
+ // system-prompt transformation, and only for the master session itself.
3945
+ const withMasterGuidance = (built: BuildSystemPromptResult): BuildSystemPromptResult =>
3946
+ masterModeContext ? { ...built, systemPrompt: [...built.systemPrompt, masterModeTemplate.trim()] } : built;
3778
3947
  if (options.systemPrompt === undefined) {
3779
- return defaultPrompt;
3948
+ return withMasterGuidance(defaultPrompt);
3780
3949
  }
3781
3950
  if (Array.isArray(options.systemPrompt)) {
3782
- return { systemPrompt: options.systemPrompt, warnings: defaultPrompt.warnings };
3951
+ return withMasterGuidance({ systemPrompt: options.systemPrompt, warnings: defaultPrompt.warnings });
3783
3952
  }
3784
- return {
3953
+ return withMasterGuidance({
3785
3954
  systemPrompt: options.systemPrompt(defaultPrompt.systemPrompt),
3786
3955
  warnings: defaultPrompt.warnings,
3787
- };
3956
+ });
3788
3957
  };
3789
3958
 
3790
3959
  const toolNamesFromRegistry = Array.from(toolRegistry.keys());
@@ -4107,7 +4276,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4107
4276
  transformContext,
4108
4277
  steeringMode: settings.get("steeringMode") ?? "one-at-a-time",
4109
4278
  followUpMode: settings.get("followUpMode") ?? "one-at-a-time",
4110
- interruptMode: settings.get("interruptMode") ?? "immediate",
4279
+ toolInterruptPolicy: settings.get("toolInterruptPolicy") ?? "abort_tools",
4111
4280
  thinkingBudgets: settings.getGroup("thinkingBudgets"),
4112
4281
  temperature: settings.get("temperature") >= 0 ? settings.get("temperature") : undefined,
4113
4282
  topP: settings.get("topP") >= 0 ? settings.get("topP") : undefined,
@@ -4128,13 +4297,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4128
4297
  preferWebsockets: preferOpenAICodexWebsockets,
4129
4298
  getToolContext: tc => toolContextStore.getContext(tc),
4130
4299
  getApiKey: async provider => {
4131
- // Read agent.sessionId at call time so credential selection stays aligned
4132
- // with metadataResolver after /new, fork, resume, or branch switches.
4133
- const key = await modelRegistry.getApiKeyForProvider(provider, credentialSessionId);
4134
- if (!key) {
4300
+ // AgentLoop asks by provider, but the active model carries the
4301
+ // model-scoped credential selector. Read it at call time so model
4302
+ // changes are honored after /new, fork, resume, or branch switches.
4303
+ const liveModel = sessionAgent?.state.model ?? model;
4304
+ const key =
4305
+ liveModel?.provider === provider
4306
+ ? await modelRegistry.getApiKey(liveModel, credentialSessionId)
4307
+ : undefined;
4308
+ const providerKey = key ?? (await modelRegistry.getApiKeyForProvider(provider, credentialSessionId));
4309
+ if (!providerKey) {
4135
4310
  throw new Error(`No API key found for provider "${provider}"`);
4136
4311
  }
4137
- return key;
4312
+ return providerKey;
4138
4313
  },
4139
4314
  getAuthCredentialType: provider => modelRegistry.getSessionCredentialType(provider, credentialSessionId),
4140
4315
  streamFn: async (streamModel, context, streamOptions) => {
@@ -4221,6 +4396,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4221
4396
  telemetry: options.telemetry,
4222
4397
  appendOnlyContext,
4223
4398
  });
4399
+ sessionAgent = agent;
4224
4400
 
4225
4401
  cursorEventEmitter = event => agent.emitExternalEvent(event);
4226
4402
 
@@ -4343,6 +4519,31 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4343
4519
  // carried by the host replay ring through the internal runtime seam above.
4344
4520
  if (autoroutingInactive) session.configWarnings.push(AUTOROUTING_INACTIVE_WARNING);
4345
4521
  hasSession = true;
4522
+ if (masterModeContext) {
4523
+ // One scoped, no-probe peer snapshot immediately before the FIRST accepted
4524
+ // provider request; see createMasterPeerSnapshotContributor for the
4525
+ // discard/reuse/no-duplicate semantics.
4526
+ const capturedSession = session;
4527
+ session.registerBeforeAgentStartContributor(
4528
+ createMasterPeerSnapshotContributor({
4529
+ lifecycle: createSessionLifecycleService(agentDir),
4530
+ ownerSessionId: masterModeContext.ownerSessionId,
4531
+ getSessionId: () => sessionManager.getSessionId(),
4532
+ scope: masterModeContext.scope,
4533
+ getCwd: getLiveCwd,
4534
+ hasPersistedInjection: () =>
4535
+ capturedSession.sessionManager
4536
+ .getBranch()
4537
+ .some(
4538
+ entry =>
4539
+ entry.type === "message" &&
4540
+ (entry.message as { customType?: string }).customType === MASTER_PEER_SNAPSHOT_CUSTOM_TYPE,
4541
+ ),
4542
+ onError: error =>
4543
+ logger.debug("master peer snapshot injection failed", { error: safeErrorForLog(error) }),
4544
+ }),
4545
+ );
4546
+ }
4346
4547
  const sessionAsyncJobManager = asyncJobManager;
4347
4548
  if (sessionAsyncJobManager) {
4348
4549
  session.yieldQueue.register<AsyncResultEntry>("async-result", {
@@ -4373,38 +4574,49 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4373
4574
 
4374
4575
  // Attach the live session to the pre-registered ref so peers can route IRC
4375
4576
  // messages here. Refresh sessionFile in case it was unavailable at pre-register
4376
- // time. The dispose wrapper below unregisters on teardown.
4577
+ // time. The registered terminal cleanup below unregisters on teardown.
4377
4578
  agentRegistry.attachSession(resolvedAgentId, session, sessionManager.getSessionFile() ?? null);
4378
4579
  {
4379
- const originalDispose = session.dispose.bind(session);
4380
- session.dispose = async () => {
4381
- try {
4382
- await originalDispose();
4383
- } finally {
4580
+ let cleanupPromise: Promise<void> | undefined;
4581
+ const finishCleanup = async (): Promise<void> => {
4582
+ if (cleanupPromise) return cleanupPromise;
4583
+ const cleanup = (async () => {
4584
+ const failures: unknown[] = [];
4384
4585
  try {
4385
4586
  agentRegistry.unregister(resolvedAgentId);
4386
4587
  releaseCredentialDisabledSubscription();
4387
4588
  releaseLocalProtocolOverride();
4589
+ } catch (error) {
4590
+ failures.push(error);
4388
4591
  } finally {
4389
- // The endpoint is gone: its owned registrations can never
4390
- // reach a delivery settlement boundary, and foreign-endpoint
4391
- // tuples are deliberately never classified terminal by other
4392
- // managers — retire them before unregistering the manager so
4393
- // repeated session churn cannot saturate the registry
4394
- // (review thread P2). The endpoint is the manager's live
4395
- // registration key, which survives newSession/switchSession
4396
- // rekeying and may differ from the persisted logical session id.
4397
- if (!options.parentTaskPrefix) {
4398
- retireOwnedRegistrationsForEndpoint(
4399
- AsyncJobManager.endpointIdOf(asyncJobManager) ?? asyncJobEndpointId,
4400
- );
4401
- AsyncJobManager.unregisterManager(asyncJobManager);
4592
+ try {
4593
+ if (!options.parentTaskPrefix) {
4594
+ retireOwnedRegistrationsForEndpoint(
4595
+ AsyncJobManager.endpointIdOf(asyncJobManager) ?? asyncJobEndpointId,
4596
+ );
4597
+ AsyncJobManager.unregisterManager(asyncJobManager);
4598
+ }
4599
+ } catch (error) {
4600
+ failures.push(error);
4601
+ }
4602
+ try {
4603
+ await closeOwnedAuthStorage();
4604
+ } catch (error) {
4605
+ failures.push(error);
4606
+ }
4607
+ try {
4608
+ await closeOwnedSettings();
4609
+ } catch (error) {
4610
+ failures.push(error);
4402
4611
  }
4403
- await closeOwnedAuthStorage();
4404
- await closeOwnedSettings();
4405
4612
  }
4406
- }
4613
+ if (failures.length > 0) throw new AggregateError(failures, "SDK disposal cleanup failed.");
4614
+ })();
4615
+ cleanupPromise = cleanup;
4616
+ void cleanup.catch(() => {});
4617
+ return cleanup;
4407
4618
  };
4619
+ session.registerToolSessionCleanup(finishCleanup);
4408
4620
  }
4409
4621
 
4410
4622
  if (model?.api === "openai-codex-responses") {
@@ -4632,7 +4844,16 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4632
4844
  let cleanupDiagnostic: unknown;
4633
4845
  try {
4634
4846
  if (hasSession) {
4635
- await session.dispose();
4847
+ try {
4848
+ await session.dispose();
4849
+ } catch (disposeError) {
4850
+ if (!isSessionDisposalIncompleteError(disposeError)) {
4851
+ throw disposeError;
4852
+ }
4853
+ // The first call is intentionally bounded for startup callers. Join the
4854
+ // retained teardown before releasing the resources it still owns.
4855
+ await session.awaitDisposeCompletion();
4856
+ }
4636
4857
  } else {
4637
4858
  if (hasRegistered) agentRegistry.unregister(resolvedAgentId);
4638
4859
  // Admission happens before session construction. Any later startup