@gajae-code/coding-agent 0.15.6 → 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 (167) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/types/cli/args.d.ts +2 -0
  3. package/dist/types/cli/root-flags.d.ts +14 -0
  4. package/dist/types/cli/update-cli.d.ts +13 -0
  5. package/dist/types/cli.d.ts +7 -0
  6. package/dist/types/commands/launch.d.ts +10 -0
  7. package/dist/types/config/autorouting-tier-map.d.ts +24 -0
  8. package/dist/types/config/file-lock.d.ts +5 -0
  9. package/dist/types/config/model-registry.d.ts +6 -0
  10. package/dist/types/config/settings-schema.d.ts +10 -1
  11. package/dist/types/config/settings.d.ts +2 -0
  12. package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
  13. package/dist/types/crash/record-loader.d.ts +15 -0
  14. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  15. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  16. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
  17. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  18. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  19. package/dist/types/master-mode/context.d.ts +21 -0
  20. package/dist/types/master-mode/first-request.d.ts +34 -0
  21. package/dist/types/master-mode/snapshot.d.ts +32 -0
  22. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  23. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  24. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  25. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  26. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  27. package/dist/types/modes/print-mode.d.ts +17 -0
  28. package/dist/types/modes/ui-language.d.ts +7 -1
  29. package/dist/types/sdk/broker/broker.d.ts +77 -1
  30. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  31. package/dist/types/sdk/broker/index.d.ts +3 -0
  32. package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
  33. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  34. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  35. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  36. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  37. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  38. package/dist/types/sdk/broker/state-version.d.ts +4 -1
  39. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  40. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  41. package/dist/types/sdk/bus/index.d.ts +6 -0
  42. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  43. package/dist/types/sdk/cli/index.d.ts +1 -0
  44. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  45. package/dist/types/sdk/cli/rows.d.ts +4 -3
  46. package/dist/types/sdk/cli/session-cli.d.ts +18 -2
  47. package/dist/types/sdk/client/client.d.ts +3 -2
  48. package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
  49. package/dist/types/sdk/host/control/index.d.ts +1 -1
  50. package/dist/types/sdk/host/control/operations.d.ts +2 -0
  51. package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
  52. package/dist/types/sdk/host/host.d.ts +10 -0
  53. package/dist/types/sdk/host/session-runtime.d.ts +52 -0
  54. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  55. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  56. package/dist/types/sdk/lifecycle-session.d.ts +2 -0
  57. package/dist/types/sdk/router/session-router.d.ts +2 -0
  58. package/dist/types/sdk/session.d.ts +16 -0
  59. package/dist/types/sdk/startup-capability.d.ts +2 -1
  60. package/dist/types/session/agent-session.d.ts +5 -0
  61. package/dist/types/session/credential-store-errors.d.ts +1 -0
  62. package/dist/types/session/session-manager.d.ts +65 -0
  63. package/dist/types/session/yield-queue.d.ts +5 -4
  64. package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
  65. package/dist/types/tools/bash.d.ts +4 -0
  66. package/dist/types/tools/gh.d.ts +8 -0
  67. package/dist/types/tools/index.d.ts +4 -0
  68. package/package.json +7 -7
  69. package/scripts/generate-sdk-operation-inventory.ts +6 -0
  70. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  71. package/src/cli/accounts-cli.ts +9 -5
  72. package/src/cli/args.ts +17 -0
  73. package/src/cli/fast-help.ts +1 -0
  74. package/src/cli/root-flags.ts +5 -0
  75. package/src/cli/update-cli.ts +77 -11
  76. package/src/commands/launch.ts +86 -18
  77. package/src/commands/sdk.ts +113 -21
  78. package/src/config/autorouting-tier-map.ts +12 -0
  79. package/src/config/file-lock.ts +158 -83
  80. package/src/config/model-preset-registry.ts +1 -5
  81. package/src/config/model-profiles.ts +5 -5
  82. package/src/config/model-registry.ts +23 -0
  83. package/src/config/model-resolver.ts +5 -0
  84. package/src/config/settings-schema.ts +8 -1
  85. package/src/config/settings.ts +5 -0
  86. package/src/coordinator-mcp/projection-scan.ts +21 -0
  87. package/src/coordinator-mcp/question-state.ts +9 -11
  88. package/src/coordinator-mcp/server.ts +94 -20
  89. package/src/crash/index-store.ts +2 -0
  90. package/src/crash/record-loader.ts +10 -0
  91. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  92. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  93. package/src/gjc-runtime/session-state-lock.ts +79 -6
  94. package/src/gjc-runtime/session-state-sidecar.ts +929 -23
  95. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  96. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
  97. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  98. package/src/hooks/skill-state.ts +6 -0
  99. package/src/internal-urls/docs-index.generated.ts +7 -7
  100. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  101. package/src/main.ts +57 -2
  102. package/src/master-mode/context.ts +50 -0
  103. package/src/master-mode/first-request.ts +65 -0
  104. package/src/master-mode/snapshot.ts +205 -0
  105. package/src/modes/acp/acp-agent.ts +605 -156
  106. package/src/modes/components/footer.ts +4 -2
  107. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  108. package/src/modes/components/oauth-selector.ts +6 -1
  109. package/src/modes/components/settings-selector.ts +1 -0
  110. package/src/modes/components/status-line/model-name.ts +53 -0
  111. package/src/modes/components/status-line/priority-row.ts +107 -0
  112. package/src/modes/components/status-line/segments.ts +23 -9
  113. package/src/modes/components/tool-status-header.ts +151 -15
  114. package/src/modes/controllers/input-controller.ts +28 -0
  115. package/src/modes/controllers/selector-controller.ts +191 -96
  116. package/src/modes/print-mode.ts +64 -0
  117. package/src/modes/ui-language.ts +49 -2
  118. package/src/prompts/system/master-mode.md +28 -0
  119. package/src/sdk/broker/broker.ts +1636 -26
  120. package/src/sdk/broker/endpoint-authority.ts +86 -0
  121. package/src/sdk/broker/ensure.ts +5 -0
  122. package/src/sdk/broker/index.ts +3 -0
  123. package/src/sdk/broker/lifecycle.ts +186 -41
  124. package/src/sdk/broker/master-capability.ts +232 -0
  125. package/src/sdk/broker/session-index.ts +121 -17
  126. package/src/sdk/broker/session-scope.ts +272 -0
  127. package/src/sdk/broker/spawn-authority.ts +1031 -0
  128. package/src/sdk/broker/spawn-substrate.ts +548 -0
  129. package/src/sdk/broker/state-version.ts +26 -14
  130. package/src/sdk/broker/transport.ts +1 -0
  131. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  132. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  133. package/src/sdk/bus/chat-daemon-runtime.ts +35 -4
  134. package/src/sdk/bus/index.ts +51 -4
  135. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  136. package/src/sdk/cli/index.ts +1 -0
  137. package/src/sdk/cli/master-cli.ts +259 -0
  138. package/src/sdk/cli/rows.ts +11 -8
  139. package/src/sdk/cli/session-cli.ts +264 -5
  140. package/src/sdk/client/client.ts +14 -8
  141. package/src/sdk/host/control/dispatch.ts +99 -38
  142. package/src/sdk/host/control/index.ts +2 -0
  143. package/src/sdk/host/control/operations.ts +2 -0
  144. package/src/sdk/host/control/runtime-gate.ts +203 -11
  145. package/src/sdk/host/host.ts +27 -2
  146. package/src/sdk/host/session-runtime.test.ts +210 -6
  147. package/src/sdk/host/session-runtime.ts +291 -49
  148. package/src/sdk/lifecycle/broker-client.ts +28 -0
  149. package/src/sdk/lifecycle/service.ts +279 -20
  150. package/src/sdk/lifecycle-session.ts +10 -2
  151. package/src/sdk/protocol/operation-inventory.generated.json +51 -0
  152. package/src/sdk/protocol/operation-registry.ts +13 -3
  153. package/src/sdk/router/session-router.ts +67 -59
  154. package/src/sdk/session.ts +357 -155
  155. package/src/sdk/startup-capability.ts +1 -0
  156. package/src/session/agent-session.ts +640 -103
  157. package/src/session/blob-store.ts +15 -5
  158. package/src/session/credential-store-errors.ts +2 -0
  159. package/src/session/internal/managed-session-storage.ts +153 -78
  160. package/src/session/session-manager.ts +337 -24
  161. package/src/session/yield-queue.ts +24 -10
  162. package/src/setup/frictionless-onboarding.ts +2 -0
  163. package/src/telemetry/events.ts +1138 -12
  164. package/src/tools/bash.ts +256 -42
  165. package/src/tools/gh.ts +104 -48
  166. package/src/tools/github-cache.ts +2 -2
  167. package/src/tools/index.ts +4 -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,13 @@ 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";
73
77
  import { describeFoldReceipt } from "../session/fold-coordinator";
74
78
  import type { BashRestrictionProfile } from "../tools/bash-allowed-prefixes";
75
79
  import { SearchTool } from "../tools/search";
@@ -120,7 +124,9 @@ import { normalizePluginHook } from "../hooks/normalize";
120
124
  import { initializeLocalRoot, LocalProtocolHandler, type LocalProtocolOptions } from "../internal-urls";
121
125
  import type { LspStartupServerInfo } from "../lsp";
122
126
  import { shutdownAll as shutdownAllLspClients } from "../lsp/client";
127
+ import { createMasterPeerSnapshotContributor, MASTER_PEER_SNAPSHOT_CUSTOM_TYPE } from "../master-mode/first-request";
123
128
  import btwUserPrompt from "../prompts/system/btw-user.md" with { type: "text" };
129
+ import masterModeTemplate from "../prompts/system/master-mode.md" with { type: "text" };
124
130
  import asyncResultTemplate from "../prompts/tools/async-result.md" with { type: "text" };
125
131
  import { AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
126
132
  import { createLazyService } from "../runtime/lazy-service";
@@ -189,6 +195,7 @@ import { guardToolForUltragoalAsk } from "../tools/ultragoal-ask-guard";
189
195
  import { EventBus } from "../utils/event-bus";
190
196
  import { buildNamedToolChoice, buildNamedToolChoiceResult } from "../utils/tool-choice";
191
197
  import type { WorkspaceTree } from "../workspace-tree";
198
+ import { createSessionLifecycleService } from "./lifecycle/client";
192
199
  import {
193
200
  attachLifecycleStartupCapability,
194
201
  lifecycleMcpStartupTimeoutOption,
@@ -525,6 +532,8 @@ export interface CreateAgentSessionOptions {
525
532
  * @internal CLI-only startup optimization; SDK callers retain synchronous loading by default.
526
533
  */
527
534
  deferMcpConfigStartup?: boolean;
535
+ /** Process-local master authority. Never persist or pass to lifecycle children. */
536
+ masterModeContext?: MasterModeContext;
528
537
  /**
529
538
  * Defer memory backend startup until the caller has applied startup model profiles.
530
539
  * @internal CLI-only ordering guard; SDK callers retain immediate startup by default.
@@ -1282,6 +1291,45 @@ function findDeferredExactMcpToolNameCollisions(
1282
1291
  return [...collisions];
1283
1292
  }
1284
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
+
1285
1333
  const AUTOMATION_TOOL_NAMES = new Set<AutomationToolName>(["browser", "computer"]);
1286
1334
 
1287
1335
  function validateAutomationTools(options: CreateAgentSessionOptions): AutomationTools {
@@ -1307,6 +1355,7 @@ function validateAutomationTools(options: CreateAgentSessionOptions): Automation
1307
1355
 
1308
1356
  export async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {
1309
1357
  const automationTools = validateAutomationTools(options);
1358
+ const masterModeContext = options.masterModeContext;
1310
1359
  const lifecycleStartupCapability = (
1311
1360
  options as CreateAgentSessionOptions & { [lifecycleStartupCapabilityOption]?: SdkStartupCapability }
1312
1361
  )[lifecycleStartupCapabilityOption];
@@ -1366,6 +1415,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1366
1415
  }
1367
1416
 
1368
1417
  let agent: Agent;
1418
+ let sessionAgent: Agent | undefined;
1369
1419
  let session!: AgentSession;
1370
1420
  let sessionManager!: SessionManager;
1371
1421
  let hasSession = false;
@@ -1783,8 +1833,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1783
1833
  credentialSessionId,
1784
1834
  }),
1785
1835
  );
1836
+ const configuredDefaultPatterns = normalizeModelSelectorValue(settings.getModelRole("default"));
1786
1837
  let model = options.model;
1787
1838
  let modelFallbackMessage: string | undefined;
1839
+ if (!hasExplicitModel && configuredDefaultPatterns.length > 0 && !defaultRoleSpec.model) {
1840
+ modelFallbackMessage = `Model ${configuredDefaultPatterns.join(" -> ")} not found`;
1841
+ }
1788
1842
  const resumeModelBehavior = settings.get("session.resumeModelBehavior");
1789
1843
  const persistedDefaultChain = existingSession.configuredModelChains.default;
1790
1844
  const defaultModelEntries =
@@ -2396,174 +2450,240 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2396
2450
  );
2397
2451
  }
2398
2452
  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}`);
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}`);
2410
2458
  }
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
- );
2416
- }
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
2459
  try {
2425
- targetHandle = await SessionManager.openNoFollowDirectory(canonicalTarget);
2426
- const opened = await targetHandle.stat({ bigint: true });
2427
- 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
+ }
2428
2478
  throw new Error(`Directory does not exist or is not a directory: ${resolvedPath}`);
2429
2479
  }
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;
2480
+ if (!(await fs.stat(canonicalTarget)).isDirectory()) {
2481
+ throw new Error(`Directory does not exist or is not a directory: ${resolvedPath}`);
2436
2482
  }
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)));
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}`);
2458
2505
  }
2459
- try {
2460
- resetCapabilities();
2461
- const restoreRegistry = await resolveActiveProjectRegistryPath(canonicalFrom).catch(
2462
- () => 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}`,
2463
2519
  );
2464
- clearPluginRootsAndCaches(restoreRegistry ? [restoreRegistry] : undefined);
2465
- } catch (error) {
2466
- restoreErrors.push(error instanceof Error ? error : new Error(String(error)));
2467
2520
  }
2468
- }
2469
- try {
2470
- 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);
2471
2539
  } 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
- },
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
+ }`,
2481
2548
  );
2482
2549
  }
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);
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
+ }
2492
2580
  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);
2581
+ await rebindCwdCapturingAuthority(canonicalFrom);
2497
2582
  } catch (error) {
2498
- setProjectDir(canonicalFrom);
2499
- throw error;
2583
+ restoreErrors.push(error instanceof Error ? error : new Error(String(error)));
2500
2584
  }
2501
- }
2502
- 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
+ };
2503
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.
2504
2601
  if (ownsProcessCwd) {
2505
- resetCapabilities();
2506
- const projectRegistry = await resolveActiveProjectRegistryPath(canonicalTarget);
2507
- 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
+ });
2508
2656
  }
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
2657
  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) {
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());
2542
2671
  try {
2543
- await shutdownAllLspClients();
2672
+ await session?.refreshSshTool({ activateIfAvailable: true });
2544
2673
  } catch (error) {
2545
- 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", {
2546
2677
  error: safeErrorForLog(error),
2678
+ cwd: sessionManager.getCwd(),
2547
2679
  });
2548
2680
  }
2681
+ return { from, to: sessionManager.getCwd() };
2682
+ } finally {
2683
+ await targetHandle.close().catch(() => {});
2549
2684
  }
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
2685
  } finally {
2566
- await targetHandle.close().catch(() => {});
2686
+ await sourceHandle.close().catch(() => {});
2567
2687
  }
2568
2688
  });
2569
2689
  };
@@ -2623,6 +2743,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2623
2743
  getToolForExecution: name => session?.getToolForExecution(name),
2624
2744
  agentRegistry,
2625
2745
  getSessionSpawns: () => options.spawns ?? "*",
2746
+ getMasterBashCapability: () => masterModeContext?.getCapability(),
2747
+ getMasterOwnerSessionId: () => masterModeContext?.ownerSessionId,
2626
2748
  getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
2627
2749
  getActiveModelString,
2628
2750
  getPlanModeState: () => session?.getPlanModeState(),
@@ -3171,6 +3293,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3171
3293
  controller: notificationSessionController,
3172
3294
  spawnedByGjc,
3173
3295
  sdkHostModeSupported: options.sdkHostModeSupported,
3296
+ ...(masterModeContext
3297
+ ? {
3298
+ masterCapability: masterModeContext.getCapability(),
3299
+ masterAttestationEpoch: masterModeContext.attestationEpoch,
3300
+ masterOwnerSessionId: masterModeContext.ownerSessionId,
3301
+ }
3302
+ : {}),
3174
3303
  // INTERNAL terminal-abort seams, threaded directly from the
3175
3304
  // owning session (NOT on the public extension context).
3176
3305
  terminalAbortSeams: {
@@ -3218,9 +3347,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3218
3347
  createSdkSessionRuntimeExtension(api, {
3219
3348
  agentDir,
3220
3349
  brokerRegistrationRequired: lifecycleStartupCapability !== undefined,
3350
+ ...(lifecycleStartupCapability?.lifecycleRequestId
3351
+ ? { lifecycleRequestId: lifecycleStartupCapability.lifecycleRequestId }
3352
+ : {}),
3221
3353
  createTransport: input => createSdkWebSocketTransport(input),
3222
3354
  settings,
3223
3355
  configOverrides: new Map(),
3356
+ ...(masterModeContext
3357
+ ? {
3358
+ masterCapability: masterModeContext.getCapability(),
3359
+ masterAttestationEpoch: masterModeContext.attestationEpoch,
3360
+ masterOwnerSessionId: masterModeContext.ownerSessionId,
3361
+ }
3362
+ : {}),
3224
3363
  // INTERNAL terminal-abort seams, threaded directly from the
3225
3364
  // owning session (NOT on the public extension context).
3226
3365
  terminalAbortSeams: {
@@ -3419,11 +3558,38 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3419
3558
  // Fall back to first available model with a valid API key, honoring the
3420
3559
  // path-scoped `enabledModels` allow-list when configured. Skip when the
3421
3560
  // user explicitly requested a model via --model that wasn't found.
3422
- if (!model && !options.modelPattern && !startupCredentialModelRejected) {
3561
+ if (
3562
+ !model &&
3563
+ !options.modelPattern &&
3564
+ !startupCredentialModelRejected &&
3565
+ configuredDefaultPatterns.length === 0
3566
+ ) {
3423
3567
  // Re-resolve the allowed set: extension factories above may have
3424
3568
  // registered providers/models that weren't visible at startup.
3425
- const fallbackCandidates = await resolveAllowedModels(modelRegistry, settings, modelMatchPreferences);
3426
- 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
+ )) {
3427
3593
  if (await hasModelApiKey(candidate)) {
3428
3594
  model = candidate;
3429
3595
  break;
@@ -3775,16 +3941,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3775
3941
  if (hasSession) session.configWarnings.push(warning);
3776
3942
  logger.warn("Context file discovery warning", { warning });
3777
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;
3778
3948
  if (options.systemPrompt === undefined) {
3779
- return defaultPrompt;
3949
+ return withMasterGuidance(defaultPrompt);
3780
3950
  }
3781
3951
  if (Array.isArray(options.systemPrompt)) {
3782
- return { systemPrompt: options.systemPrompt, warnings: defaultPrompt.warnings };
3952
+ return withMasterGuidance({ systemPrompt: options.systemPrompt, warnings: defaultPrompt.warnings });
3783
3953
  }
3784
- return {
3954
+ return withMasterGuidance({
3785
3955
  systemPrompt: options.systemPrompt(defaultPrompt.systemPrompt),
3786
3956
  warnings: defaultPrompt.warnings,
3787
- };
3957
+ });
3788
3958
  };
3789
3959
 
3790
3960
  const toolNamesFromRegistry = Array.from(toolRegistry.keys());
@@ -4128,13 +4298,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4128
4298
  preferWebsockets: preferOpenAICodexWebsockets,
4129
4299
  getToolContext: tc => toolContextStore.getContext(tc),
4130
4300
  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) {
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) {
4135
4311
  throw new Error(`No API key found for provider "${provider}"`);
4136
4312
  }
4137
- return key;
4313
+ return providerKey;
4138
4314
  },
4139
4315
  getAuthCredentialType: provider => modelRegistry.getSessionCredentialType(provider, credentialSessionId),
4140
4316
  streamFn: async (streamModel, context, streamOptions) => {
@@ -4221,6 +4397,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4221
4397
  telemetry: options.telemetry,
4222
4398
  appendOnlyContext,
4223
4399
  });
4400
+ sessionAgent = agent;
4224
4401
 
4225
4402
  cursorEventEmitter = event => agent.emitExternalEvent(event);
4226
4403
 
@@ -4343,6 +4520,31 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4343
4520
  // carried by the host replay ring through the internal runtime seam above.
4344
4521
  if (autoroutingInactive) session.configWarnings.push(AUTOROUTING_INACTIVE_WARNING);
4345
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
+ }
4346
4548
  const sessionAsyncJobManager = asyncJobManager;
4347
4549
  if (sessionAsyncJobManager) {
4348
4550
  session.yieldQueue.register<AsyncResultEntry>("async-result", {