@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.
- package/CHANGELOG.md +59 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +13 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/config/autorouting-tier-map.d.ts +24 -0
- package/dist/types/config/file-lock.d.ts +5 -0
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +10 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
- package/dist/types/master-mode/context.d.ts +21 -0
- package/dist/types/master-mode/first-request.d.ts +34 -0
- package/dist/types/master-mode/snapshot.d.ts +32 -0
- package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
- package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
- package/dist/types/modes/components/status-line/segments.d.ts +10 -0
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +77 -1
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
- package/dist/types/sdk/broker/master-capability.d.ts +10 -0
- package/dist/types/sdk/broker/session-index.d.ts +40 -11
- package/dist/types/sdk/broker/session-scope.d.ts +72 -0
- package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
- package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
- package/dist/types/sdk/broker/state-version.d.ts +4 -1
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
- package/dist/types/sdk/bus/index.d.ts +6 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/index.d.ts +1 -0
- package/dist/types/sdk/cli/master-cli.d.ts +63 -0
- package/dist/types/sdk/cli/rows.d.ts +4 -3
- package/dist/types/sdk/cli/session-cli.d.ts +18 -2
- package/dist/types/sdk/client/client.d.ts +3 -2
- package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +2 -0
- package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
- package/dist/types/sdk/host/host.d.ts +10 -0
- package/dist/types/sdk/host/session-runtime.d.ts +52 -0
- package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
- package/dist/types/sdk/lifecycle/service.d.ts +15 -3
- package/dist/types/sdk/lifecycle-session.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +5 -0
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +65 -0
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +4 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +6 -0
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +77 -11
- package/src/commands/launch.ts +86 -18
- package/src/commands/sdk.ts +113 -21
- package/src/config/autorouting-tier-map.ts +12 -0
- package/src/config/file-lock.ts +158 -83
- package/src/config/model-preset-registry.ts +1 -5
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +23 -0
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +8 -1
- package/src/config/settings.ts +5 -0
- package/src/coordinator-mcp/projection-scan.ts +21 -0
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +94 -20
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +79 -6
- package/src/gjc-runtime/session-state-sidecar.ts +929 -23
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/main.ts +57 -2
- package/src/master-mode/context.ts +50 -0
- package/src/master-mode/first-request.ts +65 -0
- package/src/master-mode/snapshot.ts +205 -0
- package/src/modes/acp/acp-agent.ts +605 -156
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/oauth-selector.ts +6 -1
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/model-name.ts +53 -0
- package/src/modes/components/status-line/priority-row.ts +107 -0
- package/src/modes/components/status-line/segments.ts +23 -9
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/input-controller.ts +28 -0
- package/src/modes/controllers/selector-controller.ts +191 -96
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/ui-language.ts +49 -2
- package/src/prompts/system/master-mode.md +28 -0
- package/src/sdk/broker/broker.ts +1636 -26
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +5 -0
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle.ts +186 -41
- package/src/sdk/broker/master-capability.ts +232 -0
- package/src/sdk/broker/session-index.ts +121 -17
- package/src/sdk/broker/session-scope.ts +272 -0
- package/src/sdk/broker/spawn-authority.ts +1031 -0
- package/src/sdk/broker/spawn-substrate.ts +548 -0
- package/src/sdk/broker/state-version.ts +26 -14
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/broker/worktree-occupancy.ts +90 -0
- package/src/sdk/bus/chat-daemon-control.ts +5 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +35 -4
- package/src/sdk/bus/index.ts +51 -4
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/master-cli.ts +259 -0
- package/src/sdk/cli/rows.ts +11 -8
- package/src/sdk/cli/session-cli.ts +264 -5
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/host/control/dispatch.ts +99 -38
- package/src/sdk/host/control/index.ts +2 -0
- package/src/sdk/host/control/operations.ts +2 -0
- package/src/sdk/host/control/runtime-gate.ts +203 -11
- package/src/sdk/host/host.ts +27 -2
- package/src/sdk/host/session-runtime.test.ts +210 -6
- package/src/sdk/host/session-runtime.ts +291 -49
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +10 -2
- package/src/sdk/protocol/operation-inventory.generated.json +51 -0
- package/src/sdk/protocol/operation-registry.ts +13 -3
- package/src/sdk/router/session-router.ts +67 -59
- package/src/sdk/session.ts +357 -155
- package/src/sdk/startup-capability.ts +1 -0
- package/src/session/agent-session.ts +640 -103
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/session-manager.ts +337 -24
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/telemetry/events.ts +1138 -12
- package/src/tools/bash.ts +256 -42
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +4 -0
package/src/sdk/session.ts
CHANGED
|
@@ -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
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
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
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
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
|
-
|
|
2431
|
-
|
|
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
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
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
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
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
|
-
|
|
2470
|
-
|
|
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
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
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
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
//
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2499
|
-
throw error;
|
|
2583
|
+
restoreErrors.push(error instanceof Error ? error : new Error(String(error)));
|
|
2500
2584
|
}
|
|
2501
|
-
|
|
2502
|
-
|
|
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
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
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
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
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
|
|
2672
|
+
await session?.refreshSshTool({ activateIfAvailable: true });
|
|
2544
2673
|
} catch (error) {
|
|
2545
|
-
|
|
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
|
|
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 (
|
|
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
|
|
3426
|
-
|
|
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
|
-
//
|
|
4132
|
-
//
|
|
4133
|
-
|
|
4134
|
-
|
|
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
|
|
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", {
|