@gajae-code/coding-agent 0.15.5 → 0.16.0

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 (272) hide show
  1. package/CHANGELOG.md +85 -1
  2. package/dist/types/async/job-manager.d.ts +83 -0
  3. package/dist/types/cli/args.d.ts +2 -0
  4. package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
  5. package/dist/types/cli/model-presets-cli.d.ts +9 -0
  6. package/dist/types/cli/root-flags.d.ts +14 -0
  7. package/dist/types/cli/update-cli.d.ts +15 -0
  8. package/dist/types/cli.d.ts +7 -0
  9. package/dist/types/commands/auth-gateway.d.ts +3 -0
  10. package/dist/types/commands/gc.d.ts +13 -0
  11. package/dist/types/commands/launch.d.ts +10 -0
  12. package/dist/types/commands/model-presets.d.ts +21 -0
  13. package/dist/types/config/autorouting-tier-map.d.ts +120 -0
  14. package/dist/types/config/file-lock.d.ts +54 -2
  15. package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
  16. package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
  17. package/dist/types/config/keybindings.d.ts +3 -2
  18. package/dist/types/config/model-preset-registry.d.ts +368 -0
  19. package/dist/types/config/model-profile-activation.d.ts +39 -0
  20. package/dist/types/config/model-profiles.d.ts +5 -3
  21. package/dist/types/config/model-registry.d.ts +17 -5
  22. package/dist/types/config/settings-schema.d.ts +19 -1
  23. package/dist/types/config/settings.d.ts +2 -0
  24. package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
  25. package/dist/types/coordinator-mcp/projection-scan.d.ts +54 -0
  26. package/dist/types/crash/record-loader.d.ts +15 -0
  27. package/dist/types/extensibility/extensions/types.d.ts +2 -5
  28. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
  29. package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
  30. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  31. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  32. package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
  33. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +72 -1
  34. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  35. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  36. package/dist/types/index.d.ts +1 -0
  37. package/dist/types/main.d.ts +3 -1
  38. package/dist/types/master-mode/context.d.ts +21 -0
  39. package/dist/types/master-mode/first-request.d.ts +34 -0
  40. package/dist/types/master-mode/snapshot.d.ts +32 -0
  41. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  42. package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
  43. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  44. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  45. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  46. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  47. package/dist/types/modes/jobs-observer.d.ts +36 -0
  48. package/dist/types/modes/print-mode.d.ts +17 -0
  49. package/dist/types/modes/ui-language.d.ts +7 -1
  50. package/dist/types/sdk/broker/broker.d.ts +80 -1
  51. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  52. package/dist/types/sdk/broker/index.d.ts +3 -0
  53. package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
  54. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  55. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  56. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  57. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  58. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  59. package/dist/types/sdk/broker/state-version.d.ts +4 -1
  60. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  61. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  62. package/dist/types/sdk/bus/index.d.ts +6 -0
  63. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
  64. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
  65. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  66. package/dist/types/sdk/cli/index.d.ts +1 -0
  67. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  68. package/dist/types/sdk/cli/rows.d.ts +4 -3
  69. package/dist/types/sdk/cli/session-cli.d.ts +18 -2
  70. package/dist/types/sdk/client/client.d.ts +3 -2
  71. package/dist/types/sdk/client/discovery.d.ts +1 -0
  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/host.d.ts +10 -0
  77. package/dist/types/sdk/host/model-pin.d.ts +7 -2
  78. package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
  79. package/dist/types/sdk/host/session-runtime.d.ts +67 -1
  80. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  81. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  82. package/dist/types/sdk/lifecycle-session.d.ts +2 -0
  83. package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
  84. package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
  85. package/dist/types/sdk/router/session-router.d.ts +11 -0
  86. package/dist/types/sdk/session.d.ts +16 -0
  87. package/dist/types/sdk/startup-capability.d.ts +2 -1
  88. package/dist/types/session/account-inventory.d.ts +1 -0
  89. package/dist/types/session/agent-session.d.ts +24 -7
  90. package/dist/types/session/credential-store-errors.d.ts +1 -0
  91. package/dist/types/session/fold-coordinator.d.ts +201 -0
  92. package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
  93. package/dist/types/session/sdk-run-capability.d.ts +2 -0
  94. package/dist/types/session/session-manager.d.ts +65 -0
  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/telemetry/control.d.ts +11 -0
  98. package/dist/types/telemetry/events.d.ts +20 -0
  99. package/dist/types/telemetry/index.d.ts +3 -0
  100. package/dist/types/telemetry/transport.d.ts +22 -0
  101. package/dist/types/tools/bash-interactive.d.ts +23 -0
  102. package/dist/types/tools/bash.d.ts +4 -0
  103. package/dist/types/tools/gh.d.ts +8 -0
  104. package/dist/types/tools/index.d.ts +10 -5
  105. package/dist/types/web/search/providers/base.d.ts +9 -4
  106. package/dist/types/web/search/providers/xai.d.ts +2 -1
  107. package/dist/types/web/search/types.d.ts +14 -0
  108. package/package.json +10 -7
  109. package/scripts/generate-hotkeys-docs.ts +4 -0
  110. package/scripts/generate-sdk-operation-inventory.ts +9 -3
  111. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  112. package/src/async/job-manager.ts +671 -39
  113. package/src/cli/accounts-cli.ts +9 -5
  114. package/src/cli/args.ts +17 -0
  115. package/src/cli/auth-broker-cli.ts +40 -12
  116. package/src/cli/auth-gateway-cli.ts +273 -97
  117. package/src/cli/config-cli.ts +2 -2
  118. package/src/cli/fast-help.ts +4 -0
  119. package/src/cli/model-presets-cli.ts +100 -0
  120. package/src/cli/root-flags.ts +5 -0
  121. package/src/cli/update-cli.ts +117 -16
  122. package/src/cli.ts +6 -2
  123. package/src/commands/auth-gateway.ts +12 -8
  124. package/src/commands/gc.ts +19 -0
  125. package/src/commands/launch.ts +86 -18
  126. package/src/commands/model-presets.ts +24 -0
  127. package/src/commands/sdk.ts +113 -21
  128. package/src/config/autorouting-tier-map.ts +44 -0
  129. package/src/config/file-lock-gc.ts +9 -4
  130. package/src/config/file-lock.ts +890 -99
  131. package/src/config/internal/model-preset-registry-test-state.ts +28 -0
  132. package/src/config/internal/model-preset-registry-test-support.ts +11 -0
  133. package/src/config/keybindings.ts +6 -2
  134. package/src/config/model-preset-registry.ts +2795 -0
  135. package/src/config/model-profile-activation.ts +115 -28
  136. package/src/config/model-profile-contract.ts +4 -1
  137. package/src/config/model-profiles.ts +56 -14
  138. package/src/config/model-registry.ts +1044 -122
  139. package/src/config/model-resolver.ts +5 -0
  140. package/src/config/settings-schema.ts +19 -1
  141. package/src/config/settings.ts +11 -1
  142. package/src/coordinator-mcp/model-preset.ts +32 -17
  143. package/src/coordinator-mcp/projection-scan.ts +592 -0
  144. package/src/coordinator-mcp/question-state.ts +9 -11
  145. package/src/coordinator-mcp/server.ts +111 -42
  146. package/src/coordinator-mcp/session-reaper.ts +9 -3
  147. package/src/crash/index-store.ts +2 -0
  148. package/src/crash/record-loader.ts +10 -0
  149. package/src/extensibility/extensions/loader.ts +0 -1
  150. package/src/extensibility/extensions/types.ts +2 -3
  151. package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
  152. package/src/gjc-runtime/empty-delete-gc.ts +301 -0
  153. package/src/gjc-runtime/gc-render.ts +29 -0
  154. package/src/gjc-runtime/gc-runtime.ts +174 -11
  155. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  156. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  157. package/src/gjc-runtime/session-state-lock.ts +1321 -147
  158. package/src/gjc-runtime/session-state-sidecar.ts +1004 -25
  159. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  160. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
  161. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  162. package/src/hooks/skill-state.ts +6 -0
  163. package/src/index.ts +1 -0
  164. package/src/internal-urls/docs-index.generated.ts +14 -13
  165. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  166. package/src/lsp/config.ts +4 -3
  167. package/src/lsp/path-trust.ts +12 -6
  168. package/src/main.ts +66 -4
  169. package/src/master-mode/context.ts +50 -0
  170. package/src/master-mode/first-request.ts +65 -0
  171. package/src/master-mode/snapshot.ts +205 -0
  172. package/src/modes/acp/acp-agent.ts +616 -161
  173. package/src/modes/components/footer.ts +4 -2
  174. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  175. package/src/modes/components/jobs-overlay-model.ts +99 -16
  176. package/src/modes/components/model-selector.ts +158 -18
  177. package/src/modes/components/oauth-selector.ts +6 -1
  178. package/src/modes/components/settings-selector.ts +1 -0
  179. package/src/modes/components/status-line/model-name.ts +53 -0
  180. package/src/modes/components/status-line/priority-row.ts +107 -0
  181. package/src/modes/components/status-line/segments.ts +29 -10
  182. package/src/modes/components/tool-status-header.ts +151 -15
  183. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  184. package/src/modes/controllers/input-controller.ts +51 -6
  185. package/src/modes/controllers/selector-controller.ts +191 -96
  186. package/src/modes/interactive-mode.ts +14 -1
  187. package/src/modes/jobs-observer.ts +149 -16
  188. package/src/modes/print-mode.ts +64 -0
  189. package/src/modes/runtime-init.ts +1 -1
  190. package/src/modes/ui-language.ts +49 -2
  191. package/src/modes/utils/injected-user-submission.ts +15 -1
  192. package/src/prompts/system/master-mode.md +28 -0
  193. package/src/prompts/tools/bash.md +2 -2
  194. package/src/prompts/tools/fold-receipt.md +3 -0
  195. package/src/prompts/tools/job.md +1 -1
  196. package/src/sdk/acp/adapter.ts +17 -2
  197. package/src/sdk/broker/broker.ts +1671 -34
  198. package/src/sdk/broker/endpoint-authority.ts +86 -0
  199. package/src/sdk/broker/ensure.ts +5 -0
  200. package/src/sdk/broker/index.ts +3 -0
  201. package/src/sdk/broker/lifecycle.ts +217 -46
  202. package/src/sdk/broker/master-capability.ts +232 -0
  203. package/src/sdk/broker/session-index.ts +121 -17
  204. package/src/sdk/broker/session-scope.ts +272 -0
  205. package/src/sdk/broker/spawn-authority.ts +1031 -0
  206. package/src/sdk/broker/spawn-substrate.ts +548 -0
  207. package/src/sdk/broker/state-version.ts +26 -14
  208. package/src/sdk/broker/transport.ts +1 -0
  209. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  210. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  211. package/src/sdk/bus/chat-daemon-runtime.ts +46 -5
  212. package/src/sdk/bus/index.ts +51 -4
  213. package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
  214. package/src/sdk/bus/prompt-reconciliation.ts +28 -4
  215. package/src/sdk/bus/slack-conversation.ts +5 -1
  216. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  217. package/src/sdk/cli/index.ts +1 -0
  218. package/src/sdk/cli/master-cli.ts +259 -0
  219. package/src/sdk/cli/rows.ts +11 -8
  220. package/src/sdk/cli/session-cli.ts +276 -6
  221. package/src/sdk/client/client.ts +14 -8
  222. package/src/sdk/client/discovery.ts +3 -1
  223. package/src/sdk/host/control/dispatch.ts +101 -37
  224. package/src/sdk/host/control/index.ts +2 -0
  225. package/src/sdk/host/control/operations.ts +2 -0
  226. package/src/sdk/host/control/runtime-gate.ts +203 -11
  227. package/src/sdk/host/host.ts +27 -2
  228. package/src/sdk/host/model-pin.ts +185 -25
  229. package/src/sdk/host/sdk-run-capability.ts +1 -0
  230. package/src/sdk/host/session-runtime.test.ts +581 -43
  231. package/src/sdk/host/session-runtime.ts +655 -101
  232. package/src/sdk/lifecycle/broker-client.ts +28 -0
  233. package/src/sdk/lifecycle/service.ts +279 -20
  234. package/src/sdk/lifecycle-session.ts +10 -2
  235. package/src/sdk/prompt-deadline-manager.ts +16 -1
  236. package/src/sdk/protocol/adapter-validation.ts +27 -0
  237. package/src/sdk/protocol/operation-inventory.generated.json +53 -2
  238. package/src/sdk/protocol/operation-registry.ts +17 -4
  239. package/src/sdk/router/session-router.ts +294 -154
  240. package/src/sdk/session.ts +440 -167
  241. package/src/sdk/startup-capability.ts +1 -0
  242. package/src/session/account-inventory.ts +27 -11
  243. package/src/session/agent-session.ts +1292 -180
  244. package/src/session/blob-store.ts +15 -5
  245. package/src/session/credential-store-errors.ts +2 -0
  246. package/src/session/fold-coordinator.ts +433 -0
  247. package/src/session/internal/managed-session-storage.ts +153 -78
  248. package/src/session/sdk-run-capability-internal.ts +15 -0
  249. package/src/session/sdk-run-capability.ts +2 -0
  250. package/src/session/session-manager.ts +342 -28
  251. package/src/session/yield-queue.ts +24 -10
  252. package/src/setup/frictionless-onboarding.ts +2 -0
  253. package/src/setup/provider-presets.json +1 -1
  254. package/src/slash-commands/helpers/credential-switch.ts +6 -1
  255. package/src/task/executor.ts +19 -3
  256. package/src/telemetry/control.ts +26 -0
  257. package/src/telemetry/events.ts +1268 -0
  258. package/src/telemetry/index.ts +3 -0
  259. package/src/telemetry/transport.ts +87 -0
  260. package/src/tools/bash-interactive.ts +182 -85
  261. package/src/tools/bash.ts +1028 -109
  262. package/src/tools/gh.ts +104 -48
  263. package/src/tools/github-cache.ts +2 -2
  264. package/src/tools/index.ts +11 -5
  265. package/src/tools/tool-catalog.generated.ts +1 -1
  266. package/src/web/search/provider.ts +120 -11
  267. package/src/web/search/providers/anthropic.ts +89 -17
  268. package/src/web/search/providers/base.ts +9 -4
  269. package/src/web/search/providers/gemini.ts +87 -15
  270. package/src/web/search/providers/openai-compatible.ts +23 -8
  271. package/src/web/search/providers/xai.ts +98 -12
  272. package/src/web/search/types.ts +16 -0
@@ -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,
@@ -65,11 +67,14 @@ import {
65
67
  resolveModelRoleValue,
66
68
  type ScopedModelSelection,
67
69
  } from "../config/model-resolver";
70
+ import { normalizeModelSelectorValue } from "../config/model-selector-value";
68
71
  import { loadPromptTemplates as loadPromptTemplatesInternal, type PromptTemplate } from "../config/prompt-templates";
69
72
  import { Settings, type SkillsSettings } from "../config/settings";
70
73
  import { resolveEagerTaskDelegation } from "../config/task-delegation";
71
74
  import { CursorExecHandlers } from "../cursor";
72
75
  import { EditTool } from "../edit";
76
+ import type { MasterModeContext } from "../master-mode/context";
77
+ import { describeFoldReceipt } from "../session/fold-coordinator";
73
78
  import type { BashRestrictionProfile } from "../tools/bash-allowed-prefixes";
74
79
  import { SearchTool } from "../tools/search";
75
80
  import "../discovery";
@@ -119,7 +124,9 @@ import { normalizePluginHook } from "../hooks/normalize";
119
124
  import { initializeLocalRoot, LocalProtocolHandler, type LocalProtocolOptions } from "../internal-urls";
120
125
  import type { LspStartupServerInfo } from "../lsp";
121
126
  import { shutdownAll as shutdownAllLspClients } from "../lsp/client";
127
+ import { createMasterPeerSnapshotContributor, MASTER_PEER_SNAPSHOT_CUSTOM_TYPE } from "../master-mode/first-request";
122
128
  import btwUserPrompt from "../prompts/system/btw-user.md" with { type: "text" };
129
+ import masterModeTemplate from "../prompts/system/master-mode.md" with { type: "text" };
123
130
  import asyncResultTemplate from "../prompts/tools/async-result.md" with { type: "text" };
124
131
  import { AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
125
132
  import { createLazyService } from "../runtime/lazy-service";
@@ -188,6 +195,7 @@ import { guardToolForUltragoalAsk } from "../tools/ultragoal-ask-guard";
188
195
  import { EventBus } from "../utils/event-bus";
189
196
  import { buildNamedToolChoice, buildNamedToolChoiceResult } from "../utils/tool-choice";
190
197
  import type { WorkspaceTree } from "../workspace-tree";
198
+ import { createSessionLifecycleService } from "./lifecycle/client";
191
199
  import {
192
200
  attachLifecycleStartupCapability,
193
201
  lifecycleMcpStartupTimeoutOption,
@@ -524,6 +532,8 @@ export interface CreateAgentSessionOptions {
524
532
  * @internal CLI-only startup optimization; SDK callers retain synchronous loading by default.
525
533
  */
526
534
  deferMcpConfigStartup?: boolean;
535
+ /** Process-local master authority. Never persist or pass to lifecycle children. */
536
+ masterModeContext?: MasterModeContext;
527
537
  /**
528
538
  * Defer memory backend startup until the caller has applied startup model profiles.
529
539
  * @internal CLI-only ordering guard; SDK callers retain immediate startup by default.
@@ -1281,6 +1291,45 @@ function findDeferredExactMcpToolNameCollisions(
1281
1291
  return [...collisions];
1282
1292
  }
1283
1293
 
1294
+ /**
1295
+ * Order candidates so each known provider's curated default model is tried
1296
+ * before the rest of the catalog.
1297
+ *
1298
+ * Provider catalogs are not ranked by fitness — a withdrawn model whose ID
1299
+ * carries an older date suffix sorts ahead of its current replacement — so
1300
+ * picking the first credentialed entry can start an unconfigured install on a
1301
+ * model the provider no longer serves. `DEFAULT_MODEL_PER_PROVIDER` is the
1302
+ * curated table `findInitialModel` already sweeps for the same purpose;
1303
+ * reusing it keeps both unconfigured paths on one source of truth. Candidates
1304
+ * that are not a provider default keep their original relative order.
1305
+ */
1306
+ export function orderByProviderDefaultFirst(
1307
+ candidates: readonly Model[],
1308
+ providerOrder: readonly string[] = Object.keys(DEFAULT_MODEL_PER_PROVIDER),
1309
+ ): Model[] {
1310
+ const preferred: Model[] = [];
1311
+ const rest: Model[] = [];
1312
+ const preferredCandidates = new Set<Model>();
1313
+ const seenProviders = new Set<string>();
1314
+ for (const rawProvider of [...providerOrder, ...Object.keys(DEFAULT_MODEL_PER_PROVIDER)]) {
1315
+ const provider = rawProvider.trim().toLowerCase();
1316
+ if (!provider || seenProviders.has(provider) || !(provider in DEFAULT_MODEL_PER_PROVIDER)) continue;
1317
+ seenProviders.add(provider);
1318
+ const defaultId = DEFAULT_MODEL_PER_PROVIDER[provider as KnownProvider];
1319
+ for (const candidate of candidates) {
1320
+ if (candidate.provider.trim().toLowerCase() === provider && candidate.id === defaultId) {
1321
+ preferred.push(candidate);
1322
+ preferredCandidates.add(candidate);
1323
+ }
1324
+ }
1325
+ }
1326
+ if (preferred.length === 0) return [...candidates];
1327
+ for (const candidate of candidates) {
1328
+ if (!preferredCandidates.has(candidate)) rest.push(candidate);
1329
+ }
1330
+ return [...preferred, ...rest];
1331
+ }
1332
+
1284
1333
  const AUTOMATION_TOOL_NAMES = new Set<AutomationToolName>(["browser", "computer"]);
1285
1334
 
1286
1335
  function validateAutomationTools(options: CreateAgentSessionOptions): AutomationTools {
@@ -1306,6 +1355,7 @@ function validateAutomationTools(options: CreateAgentSessionOptions): Automation
1306
1355
 
1307
1356
  export async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {
1308
1357
  const automationTools = validateAutomationTools(options);
1358
+ const masterModeContext = options.masterModeContext;
1309
1359
  const lifecycleStartupCapability = (
1310
1360
  options as CreateAgentSessionOptions & { [lifecycleStartupCapabilityOption]?: SdkStartupCapability }
1311
1361
  )[lifecycleStartupCapabilityOption];
@@ -1345,14 +1395,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1345
1395
  const startupAuthConfig = hasInjectedAuth
1346
1396
  ? undefined
1347
1397
  : (options.startupAuthConfig ?? (await resolveStartupAuthConfig(agentDir)));
1398
+ const ownsModelRegistry = options.modelRegistry === undefined;
1348
1399
  const ownsAuthStorage = options.modelRegistry === undefined && options.authStorage === undefined;
1349
1400
  const modelRegistry =
1350
1401
  options.modelRegistry ??
1351
1402
  new ModelRegistry(
1352
1403
  options.authStorage ??
1353
1404
  (await logger.time("discoverModels", () => discoverAuthStorage(agentDir, startupAuthConfig))),
1405
+ path.join(agentDir, "models.yml"),
1406
+ undefined,
1407
+ { agentDir },
1354
1408
  );
1355
1409
  const authStorage = modelRegistry.authStorage;
1410
+ const authStorageOwner = modelRegistry.getAuthStorageOwner();
1356
1411
  if (options.authStorage && options.authStorage !== authStorage) {
1357
1412
  throw new Error(
1358
1413
  "options.authStorage and options.modelRegistry.authStorage must be the same instance when both are provided",
@@ -1360,6 +1415,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1360
1415
  }
1361
1416
 
1362
1417
  let agent: Agent;
1418
+ let sessionAgent: Agent | undefined;
1363
1419
  let session!: AgentSession;
1364
1420
  let sessionManager!: SessionManager;
1365
1421
  let hasSession = false;
@@ -1390,7 +1446,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1390
1446
  let credentialScopeId: string | undefined;
1391
1447
  let credentialScopeLeased = false;
1392
1448
  let closeOwnedSettings: () => Promise<void> = async () => {};
1393
- const closeOwnedAuthStorage = (): void => {
1449
+ const closeOwnedAuthStorage = async (): Promise<void> => {
1450
+ if (ownsModelRegistry) await modelRegistry.dispose();
1394
1451
  if (!hasSession && credentialScopeLeased && credentialScopeId) {
1395
1452
  authStorage.releaseCredentialScope(credentialScopeId);
1396
1453
  credentialScopeLeased = false;
@@ -1422,10 +1479,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1422
1479
  }
1423
1480
  });
1424
1481
  const applyCredentialSelector = (scopeId: string, provider: string, selector: AuthCredentialSelector): void => {
1425
- authStorage.setSessionCredentialSelector(scopeId, provider, selector);
1482
+ authStorage.setSessionCredentialSelector(scopeId, provider, selector, authStorageOwner);
1426
1483
  };
1427
1484
  const ownsScopedSettings = options.settings === undefined;
1428
1485
  const settings = options.settings ?? (await logger.time("settings", Settings.loadForScope, { cwd, agentDir }));
1486
+ if (ownsModelRegistry) modelRegistry.setScopedSettings(settings);
1429
1487
  const autoroutingInactive =
1430
1488
  settings.get("task.autorouting.enabled") === true && !settings.getEffectiveAutorouting().active;
1431
1489
  closeOwnedSettings = async (): Promise<void> => {
@@ -1695,10 +1753,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1695
1753
  ) {
1696
1754
  throw new Error("Durable numeric credential pin authority changed");
1697
1755
  }
1698
- authStorage.setSessionCredentialSelector(credentialSessionId, record.provider, {
1699
- kind: pin.kind,
1700
- value: pin.value,
1701
- });
1756
+ authStorage.setSessionCredentialSelector(
1757
+ credentialSessionId,
1758
+ record.provider,
1759
+ {
1760
+ kind: pin.kind,
1761
+ value: pin.value,
1762
+ },
1763
+ authStorageOwner,
1764
+ );
1702
1765
  staleDurableCredentialPins.delete(resolveOAuthStorageProvider(record.provider));
1703
1766
  }
1704
1767
  } catch {
@@ -1770,8 +1833,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1770
1833
  credentialSessionId,
1771
1834
  }),
1772
1835
  );
1836
+ const configuredDefaultPatterns = normalizeModelSelectorValue(settings.getModelRole("default"));
1773
1837
  let model = options.model;
1774
1838
  let modelFallbackMessage: string | undefined;
1839
+ if (!hasExplicitModel && configuredDefaultPatterns.length > 0 && !defaultRoleSpec.model) {
1840
+ modelFallbackMessage = `Model ${configuredDefaultPatterns.join(" -> ")} not found`;
1841
+ }
1775
1842
  const resumeModelBehavior = settings.get("session.resumeModelBehavior");
1776
1843
  const persistedDefaultChain = existingSession.configuredModelChains.default;
1777
1844
  const defaultModelEntries =
@@ -2066,13 +2133,49 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2066
2133
  if (asyncJobManager!.isDeliverySuppressed(jobId, job?.generation)) return;
2067
2134
  const deniedOwnedDelivery =
2068
2135
  ownedCompletion !== undefined && !isOwnedCompletionEnvelopeAllowed(ownedCompletion);
2136
+ // Fold disposition is decided by the coordinator's durable slot state.
2137
+ // `parked` means the fold transaction has not finished capturing its
2138
+ // receipt yet; it replays this completion itself, so enqueuing here
2139
+ // would double-deliver.
2140
+ const foldDisposition = job
2141
+ ? session.foldCoordinator.onDelivery(job, result)
2142
+ : ({ kind: "ordinary" } as const);
2143
+ if (foldDisposition.kind === "parked") {
2144
+ if (job) asyncJobManager?.retainParkedDelivery(job, result);
2145
+ return;
2146
+ }
2069
2147
  const formattedResult = await formatAsyncResultForFollowUp(result, !deniedOwnedDelivery);
2148
+ if (
2149
+ foldDisposition.kind === "receipt" &&
2150
+ job &&
2151
+ !session.foldCoordinator.claimCompletionDelivery(job)
2152
+ )
2153
+ return;
2154
+ // A folded job's result must arrive with its receipt so the wake turn
2155
+ // completes the original task rather than merely reporting output.
2156
+ const deliveredResult =
2157
+ foldDisposition.kind === "receipt"
2158
+ ? `${formattedResult}\n\n${describeFoldReceipt(foldDisposition.receipt)}`
2159
+ : formattedResult;
2160
+ // Exactly one transcript notice per completion: a retried delivery
2161
+ // reuses the same job object, so the claim guards against repeats.
2162
+ if (
2163
+ foldDisposition.kind === "receipt" &&
2164
+ job &&
2165
+ session.foldCoordinator.claimCompletionNotice(job)
2166
+ ) {
2167
+ session.emitNotice(
2168
+ "info",
2169
+ `Folded job ${foldDisposition.receipt.jobId} (${foldDisposition.receipt.label}) finished.`,
2170
+ "fold",
2171
+ );
2172
+ }
2070
2173
 
2071
2174
  const durationMs = job ? jobElapsedMs(job) : undefined;
2072
2175
  session.yieldQueue.enqueue<AsyncResultEntry>("async-result", {
2073
2176
  jobId,
2074
2177
  generation: job?.generation ?? "",
2075
- result: formattedResult,
2178
+ result: deliveredResult,
2076
2179
  job,
2077
2180
  durationMs,
2078
2181
  ...(ownedCompletion
@@ -2085,6 +2188,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2085
2188
  }
2086
2189
  : {}),
2087
2190
  });
2191
+ if (job) asyncJobManager?.retainDeliveryClaim(job);
2088
2192
  },
2089
2193
  })
2090
2194
  : options.inheritedAsyncJobManager;
@@ -2346,174 +2450,240 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2346
2450
  );
2347
2451
  }
2348
2452
  const from = sessionManager.getCwd();
2349
- const resolvedPath = path.resolve(from, target);
2350
- let canonicalFrom: string;
2351
- let canonicalTarget: string;
2352
- try {
2353
- canonicalFrom = await fs.realpath(from);
2354
- canonicalTarget = await fs.realpath(resolvedPath);
2355
- } catch {
2356
- throw new Error(`Directory does not exist or is not a directory: ${resolvedPath}`);
2357
- }
2358
- if (!(await fs.stat(canonicalTarget)).isDirectory()) {
2359
- throw new Error(`Directory does not exist or is not a directory: ${resolvedPath}`);
2360
- }
2361
- const relative = path.relative(canonicalFrom, canonicalTarget);
2362
- if (relative === "") {
2363
- throw new Error(
2364
- `Target ${canonicalTarget} is the current session directory; nothing to move.`,
2365
- );
2453
+ const sourceHandle = await SessionManager.openNoFollowDirectory(from);
2454
+ const sourceOpened = await sourceHandle.stat({ bigint: true });
2455
+ if (!sourceOpened.isDirectory()) {
2456
+ await sourceHandle.close().catch(() => {});
2457
+ throw new Error(`Current session directory is no longer a directory: ${from}`);
2366
2458
  }
2367
- if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
2368
- throw new Error(
2369
- `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.`,
2370
- );
2371
- }
2372
- let targetHandle: nodeFs.promises.FileHandle | undefined;
2373
- let expectedIdentity: { dev: bigint; ino: bigint };
2374
2459
  try {
2375
- targetHandle = await SessionManager.openNoFollowDirectory(canonicalTarget);
2376
- const opened = await targetHandle.stat({ bigint: true });
2377
- if (!opened.isDirectory()) {
2460
+ const resolvedPath = path.resolve(from, target);
2461
+ let canonicalFrom: string;
2462
+ let canonicalTarget: string;
2463
+ try {
2464
+ canonicalFrom = await fs.realpath(from);
2465
+ canonicalTarget = await fs.realpath(resolvedPath);
2466
+ } catch (error) {
2467
+ // macOS `realpath(3)` reports EACCES on an unreadable
2468
+ // directory where Linux succeeds and fails later at the
2469
+ // `access` probe; keep both platforms on the access message.
2470
+ const code = (error as NodeJS.ErrnoException)?.code;
2471
+ if (code === "EACCES" || code === "EPERM") {
2472
+ throw new Error(
2473
+ `Directory identity or access unavailable: ${resolvedPath}${
2474
+ error instanceof Error ? ` (${error.message})` : ""
2475
+ }`,
2476
+ );
2477
+ }
2378
2478
  throw new Error(`Directory does not exist or is not a directory: ${resolvedPath}`);
2379
2479
  }
2380
- expectedIdentity = { dev: opened.dev, ino: opened.ino };
2381
- await fs.access(canonicalTarget, nodeFs.constants.R_OK | nodeFs.constants.X_OK);
2382
- } catch (error) {
2383
- await targetHandle?.close().catch(() => {});
2384
- if (error instanceof Error && error.message.startsWith("Directory does not exist")) {
2385
- throw error;
2480
+ if (!(await fs.stat(canonicalTarget)).isDirectory()) {
2481
+ throw new Error(`Directory does not exist or is not a directory: ${resolvedPath}`);
2386
2482
  }
2387
- throw new Error(
2388
- `Directory identity or access unavailable: ${canonicalTarget}${
2389
- error instanceof Error ? ` (${error.message})` : ""
2390
- }`,
2391
- );
2392
- }
2393
- // Process-cwd authority is an explicit claim, never inferred from
2394
- // `process.cwd() === from`: sibling sessions launched at the same
2395
- // root both satisfy that, so acting on it would chdir the process
2396
- // and clear process-global caches underneath the sibling.
2397
- const ownsProcessCwd = SessionManager.isProcessCwdOwner(sessionManager);
2398
- const restoreLaunchRoot = async (failure: unknown): Promise<never> => {
2399
- const restoreErrors: Error[] = [];
2400
- if (ownsProcessCwd) {
2401
- try {
2402
- setProjectDir(canonicalFrom);
2403
- if (path.resolve(process.cwd()) !== path.resolve(canonicalFrom)) {
2404
- throw new Error("Process cwd did not restore to the launch root.");
2405
- }
2406
- } catch (error) {
2407
- restoreErrors.push(error instanceof Error ? error : new Error(String(error)));
2483
+ const relative = path.relative(canonicalFrom, canonicalTarget);
2484
+ if (relative === "") {
2485
+ throw new Error(
2486
+ `Target ${canonicalTarget} is the current session directory; nothing to move.`,
2487
+ );
2488
+ }
2489
+ if (
2490
+ relative === ".." ||
2491
+ relative.startsWith(`..${path.sep}`) ||
2492
+ path.isAbsolute(relative)
2493
+ ) {
2494
+ throw new Error(
2495
+ `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.`,
2496
+ );
2497
+ }
2498
+ let targetHandle: nodeFs.promises.FileHandle | undefined;
2499
+ let expectedIdentity: { dev: bigint; ino: bigint };
2500
+ try {
2501
+ targetHandle = await SessionManager.openNoFollowDirectory(canonicalTarget);
2502
+ const opened = await targetHandle.stat({ bigint: true });
2503
+ if (!opened.isDirectory()) {
2504
+ throw new Error(`Directory does not exist or is not a directory: ${resolvedPath}`);
2408
2505
  }
2409
- try {
2410
- resetCapabilities();
2411
- const restoreRegistry = await resolveActiveProjectRegistryPath(canonicalFrom).catch(
2412
- () => undefined,
2506
+ const revalidatedFrom = await fs.realpath(from);
2507
+ const revalidatedTarget = await fs.realpath(resolvedPath);
2508
+ const revalidatedRelative = path.relative(revalidatedFrom, revalidatedTarget);
2509
+ if (
2510
+ revalidatedFrom !== canonicalFrom ||
2511
+ revalidatedTarget !== canonicalTarget ||
2512
+ revalidatedRelative === "" ||
2513
+ revalidatedRelative === ".." ||
2514
+ revalidatedRelative.startsWith(`..${path.sep}`) ||
2515
+ path.isAbsolute(revalidatedRelative)
2516
+ ) {
2517
+ throw new Error(
2518
+ `Directory identity changed during confinement validation: ${resolvedPath}`,
2413
2519
  );
2414
- clearPluginRootsAndCaches(restoreRegistry ? [restoreRegistry] : undefined);
2415
- } catch (error) {
2416
- restoreErrors.push(error instanceof Error ? error : new Error(String(error)));
2417
2520
  }
2418
- }
2419
- try {
2420
- await rebindCwdCapturingAuthority(canonicalFrom);
2521
+ const named = await fs.lstat(revalidatedTarget, { bigint: true });
2522
+ const namedSource = await fs.lstat(revalidatedFrom, { bigint: true });
2523
+ if (
2524
+ !namedSource.isDirectory() ||
2525
+ namedSource.isSymbolicLink() ||
2526
+ namedSource.dev !== sourceOpened.dev ||
2527
+ namedSource.ino !== sourceOpened.ino ||
2528
+ !named.isDirectory() ||
2529
+ named.isSymbolicLink() ||
2530
+ named.dev !== opened.dev ||
2531
+ named.ino !== opened.ino
2532
+ ) {
2533
+ throw new Error(
2534
+ `Directory identity changed during confinement validation: ${resolvedPath}`,
2535
+ );
2536
+ }
2537
+ expectedIdentity = { dev: opened.dev, ino: opened.ino };
2538
+ await fs.access(canonicalTarget, nodeFs.constants.R_OK | nodeFs.constants.X_OK);
2421
2539
  } catch (error) {
2422
- restoreErrors.push(error instanceof Error ? error : new Error(String(error)));
2423
- }
2424
- if (restoreErrors.length > 0) {
2425
- throw new AggregateError(
2426
- restoreErrors,
2427
- "Failed to restore launch-root rescope authority.",
2428
- {
2429
- cause: failure,
2430
- },
2540
+ await targetHandle?.close().catch(() => {});
2541
+ if (error instanceof Error && error.message.startsWith("Directory does not exist")) {
2542
+ throw error;
2543
+ }
2544
+ throw new Error(
2545
+ `Directory identity or access unavailable: ${canonicalTarget}${
2546
+ error instanceof Error ? ` (${error.message})` : ""
2547
+ }`,
2431
2548
  );
2432
2549
  }
2433
- throw failure;
2434
- };
2435
- try {
2436
- // Every fallible step that the moved session depends on runs
2437
- // BEFORE the session-file commit, so a failure here leaves the
2438
- // session exactly where it was and the tool call is a clean
2439
- // rejection rather than a half-moved session.
2440
- if (ownsProcessCwd) {
2441
- setProjectDir(canonicalTarget);
2550
+ // Process-cwd authority is an explicit claim, never inferred from
2551
+ // `process.cwd() === from`: sibling sessions launched at the same
2552
+ // root both satisfy that, so acting on it would chdir the process
2553
+ // and clear process-global caches underneath the sibling.
2554
+ const ownsProcessCwd = SessionManager.isProcessCwdOwner(sessionManager);
2555
+ const restoreLaunchRoot = async (failure: unknown): Promise<never> => {
2556
+ const restoreErrors: Error[] = [];
2557
+ if (ownsProcessCwd) {
2558
+ try {
2559
+ setProjectDir(canonicalFrom);
2560
+ if (path.resolve(process.cwd()) !== path.resolve(canonicalFrom)) {
2561
+ throw new Error("Process cwd did not restore to the launch root.");
2562
+ }
2563
+ await SessionManager.assertProcessCwdIdentity({
2564
+ dev: sourceOpened.dev,
2565
+ ino: sourceOpened.ino,
2566
+ });
2567
+ } catch (error) {
2568
+ restoreErrors.push(error instanceof Error ? error : new Error(String(error)));
2569
+ }
2570
+ try {
2571
+ resetCapabilities();
2572
+ const restoreRegistry = await resolveActiveProjectRegistryPath(
2573
+ canonicalFrom,
2574
+ ).catch(() => undefined);
2575
+ clearPluginRootsAndCaches(restoreRegistry ? [restoreRegistry] : undefined);
2576
+ } catch (error) {
2577
+ restoreErrors.push(error instanceof Error ? error : new Error(String(error)));
2578
+ }
2579
+ }
2442
2580
  try {
2443
- // `setProjectDir` chdirs a NAME. Confirm the process actually
2444
- // landed on the pinned directory, so a path replaced after
2445
- // the name checks cannot escape the validated descendant.
2446
- await SessionManager.assertProcessCwdIdentity(expectedIdentity);
2581
+ await rebindCwdCapturingAuthority(canonicalFrom);
2447
2582
  } catch (error) {
2448
- setProjectDir(canonicalFrom);
2449
- throw error;
2583
+ restoreErrors.push(error instanceof Error ? error : new Error(String(error)));
2450
2584
  }
2451
- }
2452
- let rescopeFailure: unknown;
2585
+ if (restoreErrors.length > 0) {
2586
+ throw new AggregateError(
2587
+ restoreErrors,
2588
+ "Failed to restore launch-root rescope authority.",
2589
+ {
2590
+ cause: failure,
2591
+ },
2592
+ );
2593
+ }
2594
+ throw failure;
2595
+ };
2453
2596
  try {
2597
+ // Every fallible step that the moved session depends on runs
2598
+ // BEFORE the session-file commit, so a failure here leaves the
2599
+ // session exactly where it was and the tool call is a clean
2600
+ // rejection rather than a half-moved session.
2454
2601
  if (ownsProcessCwd) {
2455
- resetCapabilities();
2456
- const projectRegistry = await resolveActiveProjectRegistryPath(canonicalTarget);
2457
- clearPluginRootsAndCaches(projectRegistry ? [projectRegistry] : undefined);
2602
+ setProjectDir(canonicalTarget);
2603
+ try {
2604
+ // `setProjectDir` chdirs a NAME. Confirm the process actually
2605
+ // landed on the pinned directory, so a path replaced after
2606
+ // the name checks cannot escape the validated descendant.
2607
+ await SessionManager.assertProcessCwdIdentity(expectedIdentity);
2608
+ } catch (error) {
2609
+ setProjectDir(canonicalFrom);
2610
+ await SessionManager.assertProcessCwdIdentity({
2611
+ dev: sourceOpened.dev,
2612
+ ino: sourceOpened.ino,
2613
+ });
2614
+ throw error;
2615
+ }
2616
+ }
2617
+ let rescopeFailure: unknown;
2618
+ try {
2619
+ if (ownsProcessCwd) {
2620
+ resetCapabilities();
2621
+ const projectRegistry = await resolveActiveProjectRegistryPath(canonicalTarget);
2622
+ clearPluginRootsAndCaches(projectRegistry ? [projectRegistry] : undefined);
2623
+ }
2624
+ // Plugin/MCP/Python authority must be rebound successfully
2625
+ // before committing; swallowing a failure here is what leaves
2626
+ // a moved session holding launch-root tool authority.
2627
+ await rebindCwdCapturingAuthority(canonicalTarget);
2628
+ } catch (error) {
2629
+ rescopeFailure = error;
2630
+ }
2631
+ if (rescopeFailure !== undefined) {
2632
+ await restoreLaunchRoot(rescopeFailure);
2633
+ }
2634
+ try {
2635
+ await sessionManager.flush();
2636
+ // Commit last: `moveTo` re-validates the pinned identity through
2637
+ // the still-open handle at the state-changing boundary.
2638
+ await sessionManager.moveTo(canonicalTarget, {
2639
+ expectedIdentity,
2640
+ targetHandle,
2641
+ expectedSourceIdentity: { dev: sourceOpened.dev, ino: sourceOpened.ino },
2642
+ sourceHandle,
2643
+ });
2644
+ } catch (error) {
2645
+ const committedCwd = sessionManager.getCwd();
2646
+ const stayedAtLaunchRoot = path.resolve(committedCwd) === path.resolve(from);
2647
+ if (stayedAtLaunchRoot) await restoreLaunchRoot(error);
2648
+ // SessionManager can publish the durable move before a later metadata
2649
+ // write fails. Treat that state as committed rather than reporting a
2650
+ // rejection after the session has moved.
2651
+ moveConsumed = true;
2652
+ logger.warn("Session rescope committed before finalization failed", {
2653
+ error: safeErrorForLog(error),
2654
+ cwd: committedCwd,
2655
+ });
2458
2656
  }
2459
- // Plugin/MCP/Python authority must be rebound successfully
2460
- // before committing; swallowing a failure here is what leaves
2461
- // a moved session holding launch-root tool authority.
2462
- await rebindCwdCapturingAuthority(canonicalTarget);
2463
- } catch (error) {
2464
- rescopeFailure = error;
2465
- }
2466
- if (rescopeFailure !== undefined) {
2467
- await restoreLaunchRoot(rescopeFailure);
2468
- }
2469
- try {
2470
- await sessionManager.flush();
2471
- // Commit last: `moveTo` re-validates the pinned identity through
2472
- // the still-open handle at the state-changing boundary.
2473
- await sessionManager.moveTo(canonicalTarget, {
2474
- expectedIdentity,
2475
- targetHandle,
2476
- });
2477
- } catch (error) {
2478
- const committedCwd = sessionManager.getCwd();
2479
- const stayedAtLaunchRoot = path.resolve(committedCwd) === path.resolve(from);
2480
- if (stayedAtLaunchRoot) await restoreLaunchRoot(error);
2481
- // SessionManager can publish the durable move before a later metadata
2482
- // write fails. Treat that state as committed rather than reporting a
2483
- // rejection after the session has moved.
2484
2657
  moveConsumed = true;
2485
- logger.warn("Session rescope committed before finalization failed", {
2486
- error: safeErrorForLog(error),
2487
- cwd: committedCwd,
2488
- });
2489
- }
2490
- moveConsumed = true;
2491
- if (ownsProcessCwd) {
2658
+ if (ownsProcessCwd) {
2659
+ try {
2660
+ await shutdownAllLspClients();
2661
+ } catch (error) {
2662
+ logger.warn("Failed to reset launch-root LSP clients after session rescope", {
2663
+ error: safeErrorForLog(error),
2664
+ });
2665
+ }
2666
+ }
2667
+ // Cwd-derived read-only state the prompt and subagents consume.
2668
+ // Best-effort by design: the move is committed, and a failed
2669
+ // re-discovery must not present a committed move as a failure.
2670
+ await applyRescopedReadState(sessionManager.getCwd());
2492
2671
  try {
2493
- await shutdownAllLspClients();
2672
+ await session?.refreshSshTool({ activateIfAvailable: true });
2494
2673
  } catch (error) {
2495
- logger.warn("Failed to reset launch-root LSP clients after session rescope", {
2674
+ // Non-fatal: the session has moved; the SSH tool refreshes
2675
+ // on its next activation attempt.
2676
+ logger.warn("Committed session rescope could not refresh the SSH tool", {
2496
2677
  error: safeErrorForLog(error),
2678
+ cwd: sessionManager.getCwd(),
2497
2679
  });
2498
2680
  }
2681
+ return { from, to: sessionManager.getCwd() };
2682
+ } finally {
2683
+ await targetHandle.close().catch(() => {});
2499
2684
  }
2500
- // Cwd-derived read-only state the prompt and subagents consume.
2501
- // Best-effort by design: the move is committed, and a failed
2502
- // re-discovery must not present a committed move as a failure.
2503
- await applyRescopedReadState(sessionManager.getCwd());
2504
- try {
2505
- await session?.refreshSshTool({ activateIfAvailable: true });
2506
- } catch (error) {
2507
- // Non-fatal: the session has moved; the SSH tool refreshes
2508
- // on its next activation attempt.
2509
- logger.warn("Committed session rescope could not refresh the SSH tool", {
2510
- error: safeErrorForLog(error),
2511
- cwd: sessionManager.getCwd(),
2512
- });
2513
- }
2514
- return { from, to: sessionManager.getCwd() };
2515
2685
  } finally {
2516
- await targetHandle.close().catch(() => {});
2686
+ await sourceHandle.close().catch(() => {});
2517
2687
  }
2518
2688
  });
2519
2689
  };
@@ -2540,6 +2710,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2540
2710
  // (review thread P1). For a top-level session this equals the
2541
2711
  // session id.
2542
2712
  getSessionId: () => AsyncJobManager.endpointIdOf(asyncJobManager) ?? asyncJobEndpointId,
2713
+ registerForegroundFoldParticipant: adapter =>
2714
+ session?.registerForegroundFoldParticipant(adapter) ?? (() => {}),
2715
+ hasForegroundBashBackgroundRequestHandler: () => session?.hasForegroundBashBackgroundRequestHandler() ?? false,
2716
+ requestForegroundBashBackground: () => Promise.resolve(session?.requestForegroundBashBackground() ?? false),
2717
+
2543
2718
  getCredentialSessionId: () => session?.credentialSessionId ?? credentialSessionId,
2544
2719
  getMcpManager: () => mcpManager ?? options.inheritedMcpManager,
2545
2720
  isManagedSessionDestination: () => sessionManager.isManagedDestination(),
@@ -2568,6 +2743,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2568
2743
  getToolForExecution: name => session?.getToolForExecution(name),
2569
2744
  agentRegistry,
2570
2745
  getSessionSpawns: () => options.spawns ?? "*",
2746
+ getMasterBashCapability: () => masterModeContext?.getCapability(),
2747
+ getMasterOwnerSessionId: () => masterModeContext?.ownerSessionId,
2571
2748
  getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
2572
2749
  getActiveModelString,
2573
2750
  getPlanModeState: () => session?.getPlanModeState(),
@@ -3116,6 +3293,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3116
3293
  controller: notificationSessionController,
3117
3294
  spawnedByGjc,
3118
3295
  sdkHostModeSupported: options.sdkHostModeSupported,
3296
+ ...(masterModeContext
3297
+ ? {
3298
+ masterCapability: masterModeContext.getCapability(),
3299
+ masterAttestationEpoch: masterModeContext.attestationEpoch,
3300
+ masterOwnerSessionId: masterModeContext.ownerSessionId,
3301
+ }
3302
+ : {}),
3119
3303
  // INTERNAL terminal-abort seams, threaded directly from the
3120
3304
  // owning session (NOT on the public extension context).
3121
3305
  terminalAbortSeams: {
@@ -3163,9 +3347,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3163
3347
  createSdkSessionRuntimeExtension(api, {
3164
3348
  agentDir,
3165
3349
  brokerRegistrationRequired: lifecycleStartupCapability !== undefined,
3350
+ ...(lifecycleStartupCapability?.lifecycleRequestId
3351
+ ? { lifecycleRequestId: lifecycleStartupCapability.lifecycleRequestId }
3352
+ : {}),
3166
3353
  createTransport: input => createSdkWebSocketTransport(input),
3167
3354
  settings,
3168
3355
  configOverrides: new Map(),
3356
+ ...(masterModeContext
3357
+ ? {
3358
+ masterCapability: masterModeContext.getCapability(),
3359
+ masterAttestationEpoch: masterModeContext.attestationEpoch,
3360
+ masterOwnerSessionId: masterModeContext.ownerSessionId,
3361
+ }
3362
+ : {}),
3169
3363
  // INTERNAL terminal-abort seams, threaded directly from the
3170
3364
  // owning session (NOT on the public extension context).
3171
3365
  terminalAbortSeams: {
@@ -3364,11 +3558,38 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3364
3558
  // Fall back to first available model with a valid API key, honoring the
3365
3559
  // path-scoped `enabledModels` allow-list when configured. Skip when the
3366
3560
  // user explicitly requested a model via --model that wasn't found.
3367
- if (!model && !options.modelPattern && !startupCredentialModelRejected) {
3561
+ if (
3562
+ !model &&
3563
+ !options.modelPattern &&
3564
+ !startupCredentialModelRejected &&
3565
+ configuredDefaultPatterns.length === 0
3566
+ ) {
3368
3567
  // Re-resolve the allowed set: extension factories above may have
3369
3568
  // registered providers/models that weren't visible at startup.
3370
- const fallbackCandidates = await resolveAllowedModels(modelRegistry, settings, modelMatchPreferences);
3371
- for (const candidate of fallbackCandidates) {
3569
+ const allowedFallbackCandidates = await resolveAllowedModels(modelRegistry, settings, modelMatchPreferences);
3570
+ // A fresh provider discovery can disprove a bundled model while the
3571
+ // general available catalog retains it for offline/profile compatibility.
3572
+ // Exclude only those positively disproved bundled entries from the
3573
+ // unconfigured startup path; explicit model/profile resolution above keeps
3574
+ // its existing precedence and semantics.
3575
+ const profileAvailableKeys = new Set(
3576
+ modelRegistry
3577
+ .getAvailableForProfileActivation()
3578
+ .map(candidate => `${candidate.provider}\u0000${candidate.id}`),
3579
+ );
3580
+ const fallbackCandidates = allowedFallbackCandidates.filter(candidate =>
3581
+ profileAvailableKeys.has(`${candidate.provider}\u0000${candidate.id}`),
3582
+ );
3583
+ // Candidate order is not a quality signal: catalogs sort retired models
3584
+ // ahead of current ones whenever their IDs carry older date suffixes, so
3585
+ // an unconfigured install would otherwise start on a model its provider
3586
+ // has already withdrawn. Sweep each known provider's curated default
3587
+ // first — the same table `findInitialModel` consults — and only then fall
3588
+ // back to catalog order.
3589
+ for (const candidate of orderByProviderDefaultFirst(
3590
+ fallbackCandidates,
3591
+ modelRegistry.automaticProviderOrder(credentialSessionId),
3592
+ )) {
3372
3593
  if (await hasModelApiKey(candidate)) {
3373
3594
  model = candidate;
3374
3595
  break;
@@ -3720,16 +3941,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3720
3941
  if (hasSession) session.configWarnings.push(warning);
3721
3942
  logger.warn("Context file discovery warning", { warning });
3722
3943
  }
3944
+ // The master doctrine block is appended exactly once, AFTER any custom
3945
+ // system-prompt transformation, and only for the master session itself.
3946
+ const withMasterGuidance = (built: BuildSystemPromptResult): BuildSystemPromptResult =>
3947
+ masterModeContext ? { ...built, systemPrompt: [...built.systemPrompt, masterModeTemplate.trim()] } : built;
3723
3948
  if (options.systemPrompt === undefined) {
3724
- return defaultPrompt;
3949
+ return withMasterGuidance(defaultPrompt);
3725
3950
  }
3726
3951
  if (Array.isArray(options.systemPrompt)) {
3727
- return { systemPrompt: options.systemPrompt, warnings: defaultPrompt.warnings };
3952
+ return withMasterGuidance({ systemPrompt: options.systemPrompt, warnings: defaultPrompt.warnings });
3728
3953
  }
3729
- return {
3954
+ return withMasterGuidance({
3730
3955
  systemPrompt: options.systemPrompt(defaultPrompt.systemPrompt),
3731
3956
  warnings: defaultPrompt.warnings,
3732
- };
3957
+ });
3733
3958
  };
3734
3959
 
3735
3960
  const toolNamesFromRegistry = Array.from(toolRegistry.keys());
@@ -4073,31 +4298,43 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4073
4298
  preferWebsockets: preferOpenAICodexWebsockets,
4074
4299
  getToolContext: tc => toolContextStore.getContext(tc),
4075
4300
  getApiKey: async provider => {
4076
- // Read agent.sessionId at call time so credential selection stays aligned
4077
- // with metadataResolver after /new, fork, resume, or branch switches.
4078
- const key = await modelRegistry.getApiKeyForProvider(provider, credentialSessionId);
4079
- if (!key) {
4301
+ // AgentLoop asks by provider, but the active model carries the
4302
+ // model-scoped credential selector. Read it at call time so model
4303
+ // changes are honored after /new, fork, resume, or branch switches.
4304
+ const liveModel = sessionAgent?.state.model ?? model;
4305
+ const key =
4306
+ liveModel?.provider === provider
4307
+ ? await modelRegistry.getApiKey(liveModel, credentialSessionId)
4308
+ : undefined;
4309
+ const providerKey = key ?? (await modelRegistry.getApiKeyForProvider(provider, credentialSessionId));
4310
+ if (!providerKey) {
4080
4311
  throw new Error(`No API key found for provider "${provider}"`);
4081
4312
  }
4082
- return key;
4313
+ return providerKey;
4083
4314
  },
4084
4315
  getAuthCredentialType: provider => modelRegistry.getSessionCredentialType(provider, credentialSessionId),
4085
4316
  streamFn: async (streamModel, context, streamOptions) => {
4086
4317
  const requestStartedAt = performance.now();
4087
4318
  let stream: Awaited<ReturnType<typeof streamSimple>>;
4088
4319
  try {
4320
+ const effectiveApiKey = await modelRegistry.getApiKey(streamModel, credentialSessionId);
4089
4321
  stream = await streamSimple(streamModel, context, {
4090
4322
  ...streamOptions,
4323
+ apiKey: effectiveApiKey,
4324
+ authCredentialType: modelRegistry.getSessionCredentialType(streamModel.provider, credentialSessionId),
4091
4325
  onAuthError: async (provider, oldKey, error) => {
4092
4326
  await modelRegistry.authStorage.invalidateCredentialMatching(provider, oldKey, {
4093
4327
  signal: streamOptions?.signal,
4094
4328
  sessionId: credentialSessionId,
4329
+ owner: modelRegistry.getAuthStorageOwner(),
4095
4330
  });
4096
4331
  logger.debug("Retrying provider request after credential invalidation", {
4097
4332
  provider,
4098
4333
  error: error instanceof Error ? error.message : String(error),
4099
4334
  });
4100
- return modelRegistry.getApiKeyForProvider(provider, credentialSessionId);
4335
+ return modelRegistry.getApiKey(streamModel, credentialSessionId, {
4336
+ signal: streamOptions?.signal,
4337
+ });
4101
4338
  },
4102
4339
  });
4103
4340
  } catch (error) {
@@ -4160,6 +4397,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4160
4397
  telemetry: options.telemetry,
4161
4398
  appendOnlyContext,
4162
4399
  });
4400
+ sessionAgent = agent;
4163
4401
 
4164
4402
  cursorEventEmitter = event => agent.emitExternalEvent(event);
4165
4403
 
@@ -4282,10 +4520,39 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4282
4520
  // carried by the host replay ring through the internal runtime seam above.
4283
4521
  if (autoroutingInactive) session.configWarnings.push(AUTOROUTING_INACTIVE_WARNING);
4284
4522
  hasSession = true;
4523
+ if (masterModeContext) {
4524
+ // One scoped, no-probe peer snapshot immediately before the FIRST accepted
4525
+ // provider request; see createMasterPeerSnapshotContributor for the
4526
+ // discard/reuse/no-duplicate semantics.
4527
+ const capturedSession = session;
4528
+ session.registerBeforeAgentStartContributor(
4529
+ createMasterPeerSnapshotContributor({
4530
+ lifecycle: createSessionLifecycleService(agentDir),
4531
+ ownerSessionId: masterModeContext.ownerSessionId,
4532
+ getSessionId: () => sessionManager.getSessionId(),
4533
+ scope: masterModeContext.scope,
4534
+ getCwd: getLiveCwd,
4535
+ hasPersistedInjection: () =>
4536
+ capturedSession.sessionManager
4537
+ .getBranch()
4538
+ .some(
4539
+ entry =>
4540
+ entry.type === "message" &&
4541
+ (entry.message as { customType?: string }).customType === MASTER_PEER_SNAPSHOT_CUSTOM_TYPE,
4542
+ ),
4543
+ onError: error =>
4544
+ logger.debug("master peer snapshot injection failed", { error: safeErrorForLog(error) }),
4545
+ }),
4546
+ );
4547
+ }
4285
4548
  const sessionAsyncJobManager = asyncJobManager;
4286
4549
  if (sessionAsyncJobManager) {
4287
4550
  session.yieldQueue.register<AsyncResultEntry>("async-result", {
4288
- isStale: entry => sessionAsyncJobManager.isDeliverySuppressed(entry.jobId, entry.generation),
4551
+ isStale: entry => {
4552
+ const stale = sessionAsyncJobManager.isDeliverySuppressed(entry.jobId, entry.generation);
4553
+ if (stale) sessionAsyncJobManager.releaseDeliveryClaim(entry.generation);
4554
+ return stale;
4555
+ },
4289
4556
  // Build one message per ownership origin so an owned-scope drop of
4290
4557
  // one turn's message never suppresses other turns'/ordinary
4291
4558
  // completions batched in the same flush (review thread P2).
@@ -4293,7 +4560,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4293
4560
  entry.ownedCompletion
4294
4561
  ? `${entry.ownedCompletion.lineageIdHash}\u0000${entry.ownedCompletion.promptAttemptEpoch}`
4295
4562
  : "ordinary",
4296
- build: buildAsyncResultBatchMessage,
4563
+ build: entries => {
4564
+ try {
4565
+ return buildAsyncResultBatchMessage(entries);
4566
+ } finally {
4567
+ for (const entry of entries) sessionAsyncJobManager.releaseDeliveryClaim(entry.generation);
4568
+ }
4569
+ },
4297
4570
  });
4298
4571
  }
4299
4572
  session.yieldQueue.register<McpNotificationEntry>("mcp-notification", {
@@ -4329,7 +4602,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4329
4602
  );
4330
4603
  AsyncJobManager.unregisterManager(asyncJobManager);
4331
4604
  }
4332
- closeOwnedAuthStorage();
4605
+ await closeOwnedAuthStorage();
4333
4606
  await closeOwnedSettings();
4334
4607
  }
4335
4608
  }
@@ -4599,7 +4872,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4599
4872
  }
4600
4873
  releaseLocalProtocolOverride();
4601
4874
  try {
4602
- closeOwnedAuthStorage();
4875
+ await closeOwnedAuthStorage();
4603
4876
  } catch (authCleanupError) {
4604
4877
  logger.warn("Failed to close owned auth storage after startup error", { error: authCleanupError });
4605
4878
  }