codexAgent.ts ×158

Frontier kind: Code frontier

unlabeled · c_29ba80aeb829

13 tests · 35765 LOC · 187 files · introduces 0 tests · 1640 LOC · 4 files

Introduces — evidence that enters the hierarchy at this concept

Code
176 ranges1640 lines · 4 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
3050 ranges35765 lines · 187 files · Browse complete extent
All tests (intent)
13 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

4 files ranked by introduced lines: 1640 introduced LOC across 176 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/agentHost/node/codex/codexAgent.ts 1406 introduced LOC · 158 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- codexAgent.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { spawn, type ChildProcessWithoutNullStreams } from 'child_process';
7 > import * as fs from 'fs';
8 > import * as os from 'os';
9 > import { CancellationError } from '../../../../base/common/errors.js';
10 > import { raceTimeout } from '../../../../base/common/async.js';
11 > import { fetchResourceMetadata } from '../../../../base/common/oauth.js';
12 > import { Emitter } from '../../../../base/common/event.js';
13 > import { Disposable } from '../../../../base/common/lifecycle.js';
14 > import { type IObservable, observableValue } from '../../../../base/common/observable.js';
15 > import { basename, dirname, isAbsolute, join, resolve, sep } from '../../../../base/common/path.js';
16 > import { StopWatch } from '../../../../base/common/stopwatch.js';
17 > import { URI } from '../../../../base/common/uri.js';
18 > import { generateUuid } from '../../../../base/common/uuid.js';
19 > import { IInstantiationService } from '../../../instantiation/common/instantiation.js';
20 > import { localize } from '../../../../nls.js';
21 > import { ILogService } from '../../../log/common/log.js';
22 > import { IProductService } from '../../../product/common/productService.js';
23 > import { createSchema, platformRootSchema, platformSessionSchema, schemaProperty, AgentHostMcpServersConfigKey, type ISchemaProperty, type SessionMode } from '../../common/agentHostSchema.js';
24 > import { createPricingMetaFromBilling, normalizeCAPIBilling } from '../../common/agentModelPricing.js';
25 > import { AgentHostConfigKey, agentHostCustomizationConfigSchema, type CodexUsageSource } from '../../common/agentHostCustomizationConfig.js';
26 > import { getReasoningEffortDescription, getReasoningEffortLabel } from '../../common/reasoningEffort.js';
27 > import { AgentHostCodexAgentBinaryArgsEnvVar, AgentHostCodexAgentCodexHomeEnvVar, AgentHostCodexAgentSdkRootEnvVar, AgentSession, AgentSignal, CODEX_AGENT_PROVIDER_ID, IActiveClient, IAgent, IAgentChats, IAgentCreateChatForkSource, IAgentCreateChatResult, IAgentCreateChatOptions, IAgentCreateSessionConfig, IAgentCreateSessionResult, IAgentDescriptor, IAgentMaterializeSessionEvent, IAgentModelInfo, IAgentResolveSessionConfigParams, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, IMcpNotification, type AgentProvider, type AuthenticateParams } from '../../common/agentService.js';
28 > import { SessionConfigKey } from '../../common/sessionConfigKeys.js';
29 > import { AHP_AUTH_REQUIRED, ProtocolError } from '../../common/state/sessionProtocol.js';
30 > import { ActionType, isChatAction, type SessionAction, type ChatAction } from '../../common/state/sessionActions.js';
31 > import type { ConfigSchema, ModelSelection, ProtectedResourceMetadata, ToolDefinition, AgentSelection } from '../../common/state/protocol/state.js';
32 > import type { ResolveSessionConfigResult, SessionConfigCompletionsResult } from '../../common/state/protocol/commands.js';
33 > import { AuthRequiredReason, type AuthRequiredParams } from '../../common/state/protocol/common/notifications.js';
34 > import { buildDefaultChatUri, parseChatUri, type ClientPluginCustomization, type DirectoryCustomization, type MessageAttachment, type PendingMessage, type ChatInputAnswer, ChatInputResponseKind, type PolicyState, type ToolCallResult, ToolResultContentType, type Turn, ResponsePartKind } from '../../common/state/sessionState.js';
35 > import type { IAgentServerToolHost } from '../../common/agentServerTools.js';
36 > import { ActiveClientToolSet } from '../activeClientState.js';
37 > import { McpCustomizationController } from '../shared/mcpCustomizationController.js';
38 > import { buildCodexMcpReadResult, codexMcpListToInventory, codexMcpServersFromConfig, codexMcpToolsChanged, codexStartupErrorNeedsAuth, injectCodexMcpAuthTokens, inventoryToSdkServers, normalizeCodexMcpResourceUrl, translateCodexMcpStartupState, type ICodexMcpServerConfigJson, type ICodexMcpServerEntry } from './codexMcpServers.js';
39 > import { codexHooksToContainers, codexSkillsToContainers } from './codexCustomizations.js';
40 > import { CodexClientCustomizationStore, codexMcpServersFromPlugins, codexSkillRootsFromPlugins, type ICodexClientPlugin } from './codexClientCustomizations.js';
41 > import { buildElicitationRequest, cancelledElicitationResponse, declinedElicitationResponse, elicitationResponseFromAnswers } from './codexElicitationMapper.js';
42 > import { McpAuthRequiredReason, McpServerStatus, type AhpMcpUiHostCapabilities, type Customization, type McpServerState } from '../../common/state/protocol/channels-session/state.js';
43 > import { IAgentConfigurationService } from '../agentConfigurationService.js';
44 > import { IFileService } from '../../../files/common/files.js';
45 > import { INativeEnvironmentService } from '../../../environment/common/environment.js';
46 > import { IAgentPluginManager, type ISyncedCustomization } from '../../common/agentPluginManager.js';
47 > import { parsePlugin } from '../../../agentPlugins/common/pluginParsers.js';
48 > import { IAgentHostGitHubEndpointService } from '../agentHostGitHubEndpointService.js';
49 > import { ICopilotApiService } from '../shared/copilotApiService.js';
50 > import { extractForwardedErrorInfo } from '../shared/forwardedChatError.js';
51 > import { IAgentSdkDownloader, IAgentSdkPackage } from '../agentSdkDownloader.js';
52 > import { CancellationToken } from '../../../../base/common/cancellation.js';
53 > import { PendingRequestRegistry } from '../../common/pendingRequestRegistry.js';
54 > import { CodexAppServerClient, JsonRpcError, transportFromChildProcess, type ICodexAppServerClient, type ServerRequestHandlerResult } from './codexAppServerClient.js';
55 > import { ICodexProxyService, type ICodexProxyHandle } from './codexProxyService.js';
56 > import { createCodexSessionMapState, extractUserInputText, mapAgentMessageDelta, mapCommandExecutionOutputDelta, mapFileChangeOutputDelta, mapFileChangePatchUpdated, mapItemCompleted, mapItemStarted, mapMcpToolCallProgress, mapReasoningSummaryPartAdded, mapReasoningSummaryTextDelta, mapReasoningTextDelta, mapTokenUsageUpdated, mapTurnCompleted, mapTurnStarted, resetCodexTurnMapState, type ICodexSessionMapState } from './codexMapAppServerEvents.js';
57 > import { unwrapShellInvocation } from './codexShellCommand.js';
58 > import { planForkedTurnIdMap, resolveForkBoundary } from './codexForkPlan.js';
59 > import { resolveCodexInput } from './codexPromptResolver.js';
60 > import { buildUserInputRequest, emptyUserInputResponse, userInputResponseFromAnswers } from './codexUserInputMapper.js';
61 > import { replayThreadToTurns } from './codexReplayMapper.js';
62 > import { CodexSessionMetadataStore } from './codexSessionMetadataStore.js';
63 > import { buildCodexLaunchConfig, buildCodexResumeParams, isCodexThreadProviderCompatible } from './codexLaunchConfig.js';
64 > import { codexAccountStateForUsageSource, codexAccountStateFromResponse, codexProtectedResourcesForUsageSource, resolveCodexUsageSourceAfterAccountRead, type ICodexAccountState } from './codexAccountState.js';
65 > import { CodexSessionConfigKey, CODEX_DEFAULT_PERMISSIONS_PRESET, CODEX_PERMISSIONS_PRESETS, collaborationModeKind, migrateCodexPermissionValues, narrowAdditionalDirectories, narrowBoolean, narrowPersonality, narrowReasoningEffort, narrowReasoningSummary, narrowWebSearchMode, resolveCodexPermissions, type CodexApprovalPolicy, type CodexPermissionsPreset, type ICodexResolvedPermissions } from './codexSessionConfigKeys.js';
66 > import type { ReasoningEffort } from './protocol/generated/ReasoningEffort.js';
67 > import type { ReasoningSummary } from './protocol/generated/ReasoningSummary.js';
68 > import type { Personality } from './protocol/generated/Personality.js';
69 > import type { WebSearchMode } from './protocol/generated/WebSearchMode.js';
70 > import type { SandboxMode } from './protocol/generated/v2/SandboxMode.js';
71 > import type { SandboxPolicy } from './protocol/generated/v2/SandboxPolicy.js';
72 > import type { CommandExecutionApprovalDecision } from './protocol/generated/v2/CommandExecutionApprovalDecision.js';
73 > import type { CommandExecutionRequestApprovalParams } from './protocol/generated/v2/CommandExecutionRequestApprovalParams.js';
74 > import type { CommandExecutionRequestApprovalResponse } from './protocol/generated/v2/CommandExecutionRequestApprovalResponse.js';
75 > import type { FileChangeApprovalDecision } from './protocol/generated/v2/FileChangeApprovalDecision.js';
76 > import type { FileChangeRequestApprovalParams } from './protocol/generated/v2/FileChangeRequestApprovalParams.js';
77 > import type { FileChangeRequestApprovalResponse } from './protocol/generated/v2/FileChangeRequestApprovalResponse.js';
78 > import type { PermissionsRequestApprovalParams } from './protocol/generated/v2/PermissionsRequestApprovalParams.js';
79 > import type { PermissionsRequestApprovalResponse } from './protocol/generated/v2/PermissionsRequestApprovalResponse.js';
80 > import type { DynamicToolSpec } from './protocol/generated/v2/DynamicToolSpec.js';
81 > import type { DynamicToolCallParams } from './protocol/generated/v2/DynamicToolCallParams.js';
82 > import type { DynamicToolCallResponse } from './protocol/generated/v2/DynamicToolCallResponse.js';
83 > import type { DynamicToolCallOutputContentItem } from './protocol/generated/v2/DynamicToolCallOutputContentItem.js';
84 > import type { ToolRequestUserInputParams } from './protocol/generated/v2/ToolRequestUserInputParams.js';
85 > import type { ToolRequestUserInputQuestion } from './protocol/generated/v2/ToolRequestUserInputQuestion.js';
86 > import type { ToolRequestUserInputResponse } from './protocol/generated/v2/ToolRequestUserInputResponse.js';
87 > import type { JsonValue } from './protocol/generated/serde_json/JsonValue.js';
88 > import type { GetAccountResponse } from './protocol/generated/v2/GetAccountResponse.js';
89 > import type { ModelListResponse } from './protocol/generated/v2/ModelListResponse.js';
90 > import type { Thread } from './protocol/generated/v2/Thread.js';
91 > import type { ThreadListResponse } from './protocol/generated/v2/ThreadListResponse.js';
92 > import type { ThreadReadResponse } from './protocol/generated/v2/ThreadReadResponse.js';
93 > import type { ThreadForkResponse } from './protocol/generated/v2/ThreadForkResponse.js';
94 > import type { TurnCompletedNotification } from './protocol/generated/v2/TurnCompletedNotification.js';
95 > import type { TurnStartedNotification } from './protocol/generated/v2/TurnStartedNotification.js';
96 > import type { ItemStartedNotification } from './protocol/generated/v2/ItemStartedNotification.js';
97 > import type { ItemCompletedNotification } from './protocol/generated/v2/ItemCompletedNotification.js';
98 > import type { TurnStartParams } from './protocol/generated/v2/TurnStartParams.js';
99 > import type { UserInput } from './protocol/generated/v2/UserInput.js';
100 > import type { ListMcpServerStatusResponse } from './protocol/generated/v2/ListMcpServerStatusResponse.js';
101 > import type { McpServerToolCallResponse } from './protocol/generated/v2/McpServerToolCallResponse.js';
102 > import type { McpResourceReadResponse } from './protocol/generated/v2/McpResourceReadResponse.js';
103 > import type { McpServerStartupState } from './protocol/generated/v2/McpServerStartupState.js';
104 > import type { McpServerElicitationRequestParams } from './protocol/generated/v2/McpServerElicitationRequestParams.js';
105 > import type { McpServerElicitationRequestResponse } from './protocol/generated/v2/McpServerElicitationRequestResponse.js';
106 > import type { SkillsListResponse } from './protocol/generated/v2/SkillsListResponse.js';
107 > import type { HooksListResponse } from './protocol/generated/v2/HooksListResponse.js';
108 > import type { ItemGuardianApprovalReviewCompletedNotification } from './protocol/generated/v2/ItemGuardianApprovalReviewCompletedNotification.js';
109 > import type { GuardianWarningNotification } from './protocol/generated/v2/GuardianWarningNotification.js';
110 > import type { ThreadApproveGuardianDeniedActionResponse } from './protocol/generated/v2/ThreadApproveGuardianDeniedActionResponse.js';
111 > import type { ConfigReadResponse } from './protocol/generated/v2/ConfigReadResponse.js';
112 > import type { ConfigWriteResponse } from './protocol/generated/v2/ConfigWriteResponse.js';
113 > import { formatGuardianDenialNotification, summarizeGuardianReviewAction, toGuardianAssessmentEventJson } from './codexGuardianReview.js';
114 >
115 > const CLIENT_INFO = {
116 > name: 'vscode_agent_host',
117 > title: 'VS Code Agent Host',
118 > // The codex `clientInfo.version` is informational. Hardcoded to a
119 > // non-empty placeholder; bumping it isn't required when our code
120 > // changes.
121 > version: '0.1.0',
122 > };
123 >
124 > const CODEX_THINKING_LEVEL_KEY = 'thinkingLevel';
125 >
126 > /**
127 > * User-agent prefix applied to the Codex agent's outbound CAPI calls (e.g. the
128 > * model-list fetch) so the traffic is identifiable server-side. Mirrors
129 > * `claudeAgent.ts` and the `vscode_codex` prefix used by `codexProxyService.ts`
130 > * and `oaiLanguageModelServer.ts`.
131 > */
132 > const USER_AGENT_PREFIX = 'vscode_codex';
133 >
134 > const CODEX_REASONING_EFFORTS: readonly ReasoningEffort[] = ['minimal', 'low', 'medium', 'high'];
135 >
136 > /**
137 > * MCP App capabilities advertised on every codex MCP server. Mirrors
138 > * {@link DEFAULT_MCP_APP_CAPABILITIES} but omits `sampling`: codex owns
139 > * the model connection (through the `vscode-proxy` provider) and exposes
140 > * no app-server RPC for App-initiated `sampling/createMessage`, so the
141 > * host cannot serve that capability for codex.
142 > */
143 > const CODEX_MCP_APP_CAPABILITIES: AhpMcpUiHostCapabilities = {
144 > serverTools: { listChanged: true },
145 > serverResources: {},
146 > };
147 >
148 > /**
149 > * Codex surfaces an MCP tool-call approval as a `request_user_input`
150 > * question whose id is `mcp_tool_call_approval_<callId>` (the `<callId>`
151 > * matches the `mcpToolCall` item id). The host intercepts these and renders
152 > * them on the normal tool-approval card instead of a chat-input question;
153 > * see {@link CodexAgent._handleMcpToolApprovalViaCard}.
154 > *
155 > * Codex decodes the answer string back into a decision: `Allow` accepts the
156 > * call, the synthetic `__codex_mcp_decline__` rejects it (anything else is
157 > * treated as a cancel). These mirror the constants in codex
158 > * `core/src/mcp_tool_call.rs`.
159 > */
160 > const MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX = 'mcp_tool_call_approval_';
161 > const MCP_TOOL_APPROVAL_ANSWER_ALLOW = 'Allow';
162 > const MCP_TOOL_APPROVAL_ANSWER_DECLINE = '__codex_mcp_decline__';
163 >
164 > /**
165 > * `supported_endpoints` value (on a Copilot CAPI {@link CCAModel}) that marks
166 > * a model as reachable through CAPI's OpenAI-shaped Responses endpoint. Codex
167 > * only drives models via this endpoint (the `vscode-proxy` provider uses
168 > * `wire_api="responses"`), so the model picker is filtered to models that
169 > * advertise it. Confirmed against the live CAPI catalog: gpt-5.x / gpt-5*-codex
170 > * / mai-code carry `/responses`; Anthropic models carry `/v1/messages` and
171 > * chat-only models carry `/chat/completions` (neither is usable by codex).
172 > */
173 > const CODEX_RESPONSES_ENDPOINT = '/responses';
174 >
175 > /**
176 > * Codex's Agent Mode schema, derived from the platform-generic Mode schema but
177 > * with "Autopilot" removed. Codex has only two native collaboration modes —
178 > * `plan` and `default` (see {@link ModeKind}) — so "Autopilot" would map to
179 > * `default`, identical to "Interactive", and offering it in the picker would be
180 > * a no-op duplicate. Labels and descriptions are sliced by index so they stay
181 > * in sync with the platform schema.
182 > */
183 > function createCodexModeSchema(): ISchemaProperty<SessionMode> {
184 > const base = platformSessionSchema.definition[SessionConfigKey.Mode].protocol;
185 > const kept = (base.enum ?? []).flatMap((value, index) => value === 'autopilot' ? [] : [index]);
186 > return schemaProperty<SessionMode>({
187 > ...base,
188 > enum: kept.map(index => base.enum![index]),
189 > enumLabels: base.enumLabels && kept.map(index => base.enumLabels![index]),
190 > enumDescriptions: base.enumDescriptions && kept.map(index => base.enumDescriptions![index]),
191 > });
192 > }
193 >
194 > const codexSessionConfigSchema = createSchema({
195 > [CodexSessionConfigKey.PermissionsPreset]: schemaProperty<CodexPermissionsPreset>({
196 > type: 'string',
197 > title: localize('codex.sessionConfig.permissionsPreset', "Approvals"),
198 > description: localize('codex.sessionConfig.permissionsPresetDescription', "How much Codex can do on its own before asking for approval."),
199 > enum: [...CODEX_PERMISSIONS_PRESETS],
200 > enumLabels: [
201 > localize('codex.sessionConfig.permissionsPreset.default', "Default Permissions"),
202 > localize('codex.sessionConfig.permissionsPreset.autoReview', "Auto-Review"),
203 > localize('codex.sessionConfig.permissionsPreset.fullAccess', "Full Access"),
204 > ],
205 > enumDescriptions: [
206 > localize('codex.sessionConfig.permissionsPreset.defaultDescription', "Codex can read and edit files in the workspace and run routine local commands. It asks before using the internet or going beyond the workspace."),
207 > localize('codex.sessionConfig.permissionsPreset.autoReviewDescription', "Same workspace access as Default, but approval requests are routed through the auto-reviewer instead of prompting you."),
208 > localize('codex.sessionConfig.permissionsPreset.fullAccessDescription', "Codex can edit files outside the workspace and use the internet without asking. Use only when you want full machine access."),
209 > ],
210 > default: CODEX_DEFAULT_PERMISSIONS_PRESET,
211 > sessionMutable: true,
212 > }),
213 > [CodexSessionConfigKey.ApprovalPolicy]: schemaProperty<CodexApprovalPolicy>({
214 > type: 'string',
215 > title: localize('codex.sessionConfig.approvalPolicy', "Approvals"),
216 > description: localize('codex.sessionConfig.approvalPolicyDescription', "How Codex requests approval for tool calls."),
217 > enum: ['never', 'on-request', 'on-failure', 'untrusted'],
218 > enumLabels: [
219 > localize('codex.sessionConfig.approvalPolicy.never', "No Escalations"),
220 > localize('codex.sessionConfig.approvalPolicy.onRequest', "Ask When Needed"),
221 > localize('codex.sessionConfig.approvalPolicy.onFailure', "Ask on Failure"),
222 > localize('codex.sessionConfig.approvalPolicy.untrusted', "Ask More Often"),
223 > ],
224 > enumDescriptions: [
225 > localize('codex.sessionConfig.approvalPolicy.neverDescription', "Never ask for elevated permission; commands that cannot run in the sandbox are rejected."),
226 > localize('codex.sessionConfig.approvalPolicy.onRequestDescription', "Ask only when Codex determines a command needs elevated permission."),
227 > localize('codex.sessionConfig.approvalPolicy.onFailureDescription', "Try commands in the sandbox first, then ask to retry with elevated permission if the sandbox blocks them."),
228 > localize('codex.sessionConfig.approvalPolicy.untrustedDescription', "Ask before more command categories so you can review actions more closely."),
229 > ],
230 > default: 'on-request',
231 > sessionMutable: true,
232 > }),
233 > [CodexSessionConfigKey.SandboxMode]: schemaProperty<SandboxMode>({
234 > type: 'string',
235 > title: localize('codex.sessionConfig.sandboxMode', "Sandbox"),
236 > description: localize('codex.sessionConfig.sandboxModeDescription', "Filesystem and network restrictions applied to tool calls."),
237 > enum: ['read-only', 'workspace-write', 'danger-full-access'],
238 > enumLabels: [
239 > localize('codex.sessionConfig.sandboxMode.readOnly', "Read-Only"),
240 > localize('codex.sessionConfig.sandboxMode.workspaceWrite', "Workspace Write"),
241 > localize('codex.sessionConfig.sandboxMode.dangerFullAccess', "Full Access (Dangerous)"),
242 > ],
243 > enumDescriptions: [
244 > localize('codex.sessionConfig.sandboxMode.readOnlyDescription', "Tool calls can read the workspace but cannot modify files."),
245 > localize('codex.sessionConfig.sandboxMode.workspaceWriteDescription', "Tool calls can read and write within the workspace; network is controlled separately."),
246 > localize('codex.sessionConfig.sandboxMode.dangerFullAccessDescription', "Tool calls have unrestricted disk and network access."),
247 > ],
248 > default: 'workspace-write',
249 > sessionMutable: true,
250 > }),
251 > [CodexSessionConfigKey.WebSearchMode]: schemaProperty<WebSearchMode>({
252 > type: 'string',
253 > title: localize('codex.sessionConfig.webSearchMode', "Web Search"),
254 > description: localize('codex.sessionConfig.webSearchModeDescription', "Web-search tool availability for the model."),
255 > enum: ['disabled', 'cached', 'live'],
256 > enumLabels: [
257 > localize('codex.sessionConfig.webSearchMode.disabled', "Disabled"),
258 > localize('codex.sessionConfig.webSearchMode.cached', "Cached Only"),
259 > localize('codex.sessionConfig.webSearchMode.live', "Live"),
260 > ],
261 > default: 'disabled',
262 > sessionMutable: false,
263 > }),
264 > [CodexSessionConfigKey.ModelReasoningEffort]: schemaProperty<ReasoningEffort>({
265 > type: 'string',
266 > title: localize('codex.sessionConfig.modelReasoningEffort', "Reasoning Effort"),
267 > description: localize('codex.sessionConfig.modelReasoningEffortDescription', "Controls how much reasoning effort Codex uses."),
268 > enum: [...CODEX_REASONING_EFFORTS],
269 > enumLabels: CODEX_REASONING_EFFORTS.map(getReasoningEffortLabel),
270 > enumDescriptions: CODEX_REASONING_EFFORTS.map(effort => getReasoningEffortDescription(effort) ?? ''),
271 > default: 'medium',
272 > sessionMutable: true,
273 > }),
274 > [SessionConfigKey.Mode]: createCodexModeSchema(),
275 > [CodexSessionConfigKey.Personality]: schemaProperty<Personality>({
276 > type: 'string',
277 > title: localize('codex.sessionConfig.personality', "Personality"),
278 > description: localize('codex.sessionConfig.personalityDescription', "Tone Codex uses when communicating."),
279 > enum: ['none', 'friendly', 'pragmatic'],
280 > enumLabels: [
281 > localize('codex.sessionConfig.personality.none', "Default"),
282 > localize('codex.sessionConfig.personality.friendly', "Friendly"),
283 > localize('codex.sessionConfig.personality.pragmatic', "Pragmatic"),
284 > ],
285 > enumDescriptions: [
286 > localize('codex.sessionConfig.personality.noneDescription', "Use Codex's built-in default tone."),
287 > localize('codex.sessionConfig.personality.friendlyDescription', "Warmer, more conversational tone."),
288 > localize('codex.sessionConfig.personality.pragmaticDescription', "Terse, no-nonsense tone focused on actions."),
289 > ],
290 > default: 'none',
291 > sessionMutable: true,
292 > }),
293 > [CodexSessionConfigKey.ReasoningSummary]: schemaProperty<ReasoningSummary>({
294 > type: 'string',
295 > title: localize('codex.sessionConfig.reasoningSummary', "Reasoning Summary"),
296 > description: localize('codex.sessionConfig.reasoningSummaryDescription', "How Codex summarizes its reasoning in the response stream."),
297 > enum: ['auto', 'concise', 'detailed', 'none'],
298 > enumLabels: [
299 > localize('codex.sessionConfig.reasoningSummary.auto', "Auto"),
300 > localize('codex.sessionConfig.reasoningSummary.concise', "Concise"),
301 > localize('codex.sessionConfig.reasoningSummary.detailed', "Detailed"),
302 > localize('codex.sessionConfig.reasoningSummary.none', "None"),
303 > ],
304 > default: 'auto',
305 > sessionMutable: true,
306 > }),
307 > [CodexSessionConfigKey.AdditionalDirectories]: schemaProperty<string[]>({
308 > type: 'array',
309 > title: localize('codex.sessionConfig.additionalDirectories', "Additional Writable Directories"),
310 > description: localize('codex.sessionConfig.additionalDirectoriesDescription', "Absolute paths the sandbox is allowed to write to, in addition to the workspace. Only applies when Sandbox is Workspace Write."),
311 > items: { type: 'string', title: localize('codex.sessionConfig.additionalDirectories.item', "Directory") },
312 > enumDynamic: true,
313 > default: [],
314 > sessionMutable: true,
315 > }),
316 > [CodexSessionConfigKey.NetworkAccessEnabled]: schemaProperty<boolean>({
317 > type: 'boolean',
318 > title: localize('codex.sessionConfig.networkAccessEnabled', "Network"),
319 > description: localize('codex.sessionConfig.networkAccessEnabledDescription', "Allow sandboxed tool calls to make outbound network requests. Only applies when Sandbox is Workspace Write."),
320 > default: false,
321 > sessionMutable: true,
322 > }),
323 > [SessionConfigKey.Permissions]: platformSessionSchema.definition[SessionConfigKey.Permissions],
324 > });
325 >
326 > const codexVisibleSessionConfigSchema = createSchema({
327 > [SessionConfigKey.Mode]: codexSessionConfigSchema.definition[SessionConfigKey.Mode],
328 > [CodexSessionConfigKey.PermissionsPreset]: codexSessionConfigSchema.definition[CodexSessionConfigKey.PermissionsPreset],
329 > [SessionConfigKey.Permissions]: platformSessionSchema.definition[SessionConfigKey.Permissions],
330 > });
331 >
332 > interface ICodexSessionConfigDefaults {
333 > readonly [CodexSessionConfigKey.PermissionsPreset]: CodexPermissionsPreset;
334 > readonly [CodexSessionConfigKey.ApprovalPolicy]: CodexApprovalPolicy;
335 > readonly [CodexSessionConfigKey.SandboxMode]: SandboxMode;
336 > readonly [CodexSessionConfigKey.WebSearchMode]: WebSearchMode;
337 > readonly [CodexSessionConfigKey.ModelReasoningEffort]: ReasoningEffort;
338 > readonly [CodexSessionConfigKey.AdditionalDirectories]: string[];
339 > readonly [CodexSessionConfigKey.NetworkAccessEnabled]: boolean;
340 > readonly [SessionConfigKey.Mode]: SessionMode;
341 > readonly [CodexSessionConfigKey.Personality]: Personality;
342 > readonly [CodexSessionConfigKey.ReasoningSummary]: ReasoningSummary;
343 > }
344 >
345 > const codexSessionConfigDefaults: ICodexSessionConfigDefaults = {
346 > [CodexSessionConfigKey.PermissionsPreset]: CODEX_DEFAULT_PERMISSIONS_PRESET,
347 > [CodexSessionConfigKey.ApprovalPolicy]: 'on-request',
348 > [CodexSessionConfigKey.SandboxMode]: 'workspace-write',
349 > [CodexSessionConfigKey.WebSearchMode]: 'disabled',
350 > [CodexSessionConfigKey.ModelReasoningEffort]: 'medium',
351 > [CodexSessionConfigKey.AdditionalDirectories]: [],
352 > [CodexSessionConfigKey.NetworkAccessEnabled]: false,
353 > [SessionConfigKey.Mode]: 'interactive',
354 > [CodexSessionConfigKey.Personality]: 'none',
355 > [CodexSessionConfigKey.ReasoningSummary]: 'auto',
356 > };
357 >
358 > const CodexPrewarmTtlMs = 60_000;
359 >
360 > /**
361 > * Per-session bookkeeping. The codex thread is owned by the shared
362 > * connection in {@link CodexAgent}; this struct only tracks what the
363 > * `IAgent` surface needs.
364 > */
365 > /** Resolved user-input answer captured from the client's `chat/inputCompleted`. */
366 > interface ICodexUserInputResult {
367 > readonly response: ChatInputResponseKind;
368 > readonly answers?: Record<string, ChatInputAnswer>;
369 > }
370 >
371 > interface ICodexSession {
372 > /** Caller-facing session id used in the `codex:/<id>` URI; may differ from the codex thread id. */
373 > readonly sessionId: string;
374 > /**
375 > * Codex app-server thread id used in JSON-RPC `thread/*` and `turn/*` calls.
376 > * Undefined until the session has been materialized (first `sendMessage`
377 > * triggers `thread/start`). Decoupling materialization from
378 > * `createSession` mirrors the Claude harness's provisional/materialize
379 > * split and avoids spawning an orphan codex thread when the workbench
380 > * rebinds a provisional URI after a chip-selection.
381 > */
382 > threadId: string | undefined;
383 > readonly sessionUri: URI;
384 > /**
385 > * Effective working directory. Starts as the folder the client passed to
386 > * {@link CodexAgent.createSession}; at first materialization it is replaced
387 > * with the host-resolved working directory (the isolated worktree for
388 > * worktree-isolation sessions) before `thread/start` locks the codex
389 > * subprocess `cwd`. When the client supplies none (e.g. an editor window
390 > * with no workspace folder open), a managed temp folder is lazily created
391 > * as a fallback at materialize time (tracked by
392 > * {@link managedWorkingDirectory} for cleanup). Mutable so both the
393 > * worktree swap and the lazy assignment can happen after the provisional
394 > * `createSession`.
395 > */
396 > workingDirectory: URI | undefined;
397 > /**
398 > * Set to the temp folder created for this session when no working
399 > * directory was supplied, so {@link CodexAgent.disposeSession} can remove
400 > * it. `undefined` when the client supplied a working directory.
401 > */
402 > managedWorkingDirectory: URI | undefined;
403 > readonly mapState: ICodexSessionMapState;
404 > /**
405 > * Phase 4: parked deferreds for `item/commandExecution/requestApproval`,
406 > * keyed by the host-side toolCallId. Resolved by
407 > * {@link CodexAgent.respondToPermissionRequest}.
408 > */
409 > readonly pendingCommandApprovals: PendingRequestRegistry<CommandExecutionApprovalDecision>;
410 > /**
411 > * Per-session set of "accept for session" decisions. When the user
412 > * picks Accept-for-Session in a previous approval, subsequent
413 > * approval requests on the same session resolve automatically.
414 > */
415 > readonly acceptedForSession: Set<string>;
416 > /**
417 > * Guardian (auto-review) `reviewId`s that have already been surfaced to
418 > * the user as a denied-action approval card. Guards against acting twice
419 > * on the same review if the completed notification is redelivered.
420 > */
421 > readonly handledGuardianReviews: Set<string>;
422 > /**
423 > * Host-side toolCallIds of the synthetic "Approve anyway" cards created for
424 > * guardian (auto-review) denials that are still awaiting a user decision.
425 > * Unlike codex's blocking command approvals, these cards live inside the
426 > * active turn but codex does *not* wait on them — so when the turn ends
427 > * (often via the auto-review circuit-breaker interrupt) the reducer cancels
428 > * the card. We use this set to unwind the parked deferred on turn end so the
429 > * suspended {@link CodexAgent._handleGuardianReviewCompleted} frame doesn't
430 > * leak.
431 > */
432 > readonly pendingGuardianReviewCards: Set<string>;
433 > /**
434 > * Steering messages handed to codex via `turn/steer` that are awaiting
435 > * the matching `userMessage` item echo, which promotes them into their
436 > * own visible turn. Keyed by {@link PendingMessage.id}. Drained (with a
437 > * `steering_consumed` signal) on turn completion, abort, dispose, or a
438 > * `turn/steer` rejection so the chat UI's pending bubble never sticks.
439 > */
440 > readonly pendingSteeringFlips: Map<string, PendingMessage>;
441 > /**
442 > * Client-provided tool definitions for this session, keyed by the
443 > * contributing workbench client. The merged set is registered with codex
444 > * as `dynamicTools` at `thread/start`. Empty until the first active client
445 > * sets its tools.
446 > */
447 > readonly clientToolSet: ActiveClientToolSet;
448 > /**
449 > * Parked deferreds for in-flight client-tool calls (codex
450 > * `item/tool/call`), keyed by the host-side toolCallId. Resolved by
451 > * {@link CodexAgent.onClientToolCallComplete}.
452 > */
453 > readonly pendingClientToolCalls: PendingRequestRegistry<ToolCallResult>;
454 > /**
455 > * Parked deferreds for in-flight user-input requests (codex
456 > * `item/tool/requestUserInput`, i.e. the model's `ask_user`), keyed by a
457 > * host-generated requestId. Resolved by
458 > * {@link CodexAgent.respondToUserInputRequest}.
459 > */
460 > readonly pendingUserInputs: PendingRequestRegistry<ICodexUserInputResult>;
461 > /**
462 > * Signature of the {@link clientTools} the codex thread was started
463 > * with. Codex only accepts `dynamicTools` at `thread/start`, so if the
464 > * tools change before the first turn (e.g. the prewarmed thread started
465 > * before {@link setClientTools} arrived) the thread is restarted to pick
466 > * them up. `undefined` until materialized.
467 > */
468 > materializedToolsSig: string | undefined;
469 > /**
470 > * Signature of the `mcp_servers` (root config + client plugins) the codex
471 > * thread was started with. Codex only accepts `config.mcp_servers` at
472 > * `thread/start`, so if the set changes before the first turn the thread is
473 > * restarted to pick them up. `undefined` until materialized.
474 > */
475 > materializedMcpSig: string | undefined;
476 > /** True once a turn has been started on the (materialized) thread. */
477 > firstTurnSent: boolean;
478 > model: ModelSelection | undefined;
479 > /** Workbench-facing turn id for the active turn. */
480 > currentTurnId: string | undefined;
481 > /** Local monotonic timer for the active workbench-facing turn. */
482 > turnStopWatch: StopWatch | undefined;
483 > /** Codex app-server turn id for the active turn. */
484 > currentAppTurnId: string | undefined;
485 > /** Codex app-server turn id -> workbench-facing turn id. */
486 > readonly hostTurnIdByAppTurnId: Map<string, string>;
487 > /**
488 > * Workbench-facing turn id -> codex app-server turn id, retained across
489 > * turn completion so {@link CodexAgent.truncateSession} can translate a
490 > * live host turn id to a `thread/rollback` target.
491 > */
492 > readonly codexTurnIdByHostTurnId: Map<string, string>;
493 > /** Set when this session was restored (Phase 3) and needs `thread/resume` before the first `turn/start`. */
494 > needsResume: boolean;
495 > /** Most recent user prompt sent on this session — used as fallback userMessage text in `turn/started`. */
496 > lastPromptText: string;
497 > /** True once the workbench has disposed this session. Guards background prewarm continuations. */
498 > disposed: boolean;
499 > /** In-flight background or foreground materialization, shared across callers. */
500 > materializePromise: Promise<void> | undefined;
501 > /** Whether the workbench-facing materialize event has been emitted. */
502 > materializedEventFired: boolean;
503 > /** TTL timer for a materialized-but-unused prewarmed thread. */
504 > prewarmTimer: ReturnType<typeof setTimeout> | undefined;
505 > /** True once the prewarmed session has been claimed by a user turn. */
506 > prewarmClaimed: boolean;
507 > /** True once the agent host's server tools have been advertised on this session. */
508 > serverToolsAdvertised: boolean;
509 > /**
510 > * Per-session MCP customization surface. Created lazily the first time
511 > * the session needs to surface codex's MCP servers (either via
512 > * {@link CodexAgent.getSessionCustomizations} or when the connection's
513 > * MCP inventory is applied). Disposed when the session is removed.
514 > */
515 > mcpController: McpCustomizationController | undefined;
516 > /**
517 > * Store of client-pushed ("Open Plugin") customizations synced to this
518 > * session. Their MCP servers are attached per-thread at `thread/start`
519 > * and their skills feed codex's process-global `skills/extraRoots/set`.
520 > */
521 > readonly clientCustomizations: CodexClientCustomizationStore;
522 > }
523 >
524 > /**
525 > * A live Codex collab-agent (subagent) child thread. Codex runs each spawned
526 > * subagent as its OWN app-server thread that emits a full item/turn event
527 > * stream (`turn/started`, `item/*`, `turn/completed`) under the child thread
528 > * id — it is NOT flattened onto the parent thread. We render that stream in a
529 > * read-only peer chat (the "agent team" pattern, mirroring Copilot/Claude) by
530 > * routing the child thread's notifications through the shared mappers with an
531 > * isolated {@link ICodexSession} and firing each resulting action tagged with
532 > * the parent `spawnAgent` tool call as its `parentToolCallId`, so the shared
533 > * orchestrator ({@link AgentSideEffects}) lands them in the subagent chat.
534 > */
535 > interface ICodexSubagent {
536 > /** Caller-facing sessionId of the parent session that spawned this subagent. */
537 > readonly parentSessionId: string;
538 > /** Host-side toolCallId of the parent `spawnAgent` collab tool call (routing key). */
539 > readonly toolCallId: string;
540 > /**
541 > * Isolated session used to run the shared event mappers for the child
542 > * thread. Shares the parent's `sessionUri` and `acceptedForSession` memo so
543 > * side effects target the parent's working tree and the accept-for-session
544 > * decision spans parent + subagents, but keeps its own map/turn state.
545 > */
546 > readonly session: ICodexSession;
547 > }
548 >
549 > /**
550 > * Connection state machine. The codex process is spawned lazily on first
551 > * need (Decision 6) and stays alive for the agent's lifetime.
552 > */
553 > type ConnectionState =
554 > | { readonly kind: 'idle' }
555 > | { readonly kind: 'starting'; readonly promise: Promise<IConnectionReady> }
556 > | ({ readonly kind: 'ready' } & IConnectionReady);
557 >
558 > interface IConnectionReady {
559 > readonly client: ICodexAppServerClient;
560 > readonly usageSource: CodexUsageSource;
561 > readonly proxyHandle?: ICodexProxyHandle;
562 > readonly child: ChildProcessWithoutNullStreams;
563 > }
564 >
565 > /**
566 > * `IAgent` implementation backed by `codex app-server`.
567 > *
568 > * Phase 2 surface: createSession (blocks on `thread/start`), sendMessage
569 > * (one `turn/start`, streams `agentMessage` deltas), setPendingMessages
570 > * (steering via `turn/steer`), abortSession (`turn/interrupt`),
571 > * disposeSession (`thread/unsubscribe`, no process kill).
572 > *
573 > * Decisions 3 (shared process), 6 (lazy spawn), 7 (session id == threadId),
574 > * 10 (no cwd → reject), 15 (cancel, keep streamed content), 16 (steering),
575 > * 17 (attachments), 18 (apikey auth).
576 > */
577 >
578 > /**
579 > * `@openai/codex` distribution descriptor. Lives in this file because it
580 > * encodes Codex-specific knowledge — the env-var name and the fact that
581 > * Codex's Linux binaries are statically musl-linked and ship as a single
582 > * `linux-*` SKU regardless of host libc.
583 > */
584 > export const CodexSdkPackage: IAgentSdkPackage = {
585 > id: 'codex',
586 > displayName: 'Codex',
587 > devOverrideEnvVar: AgentHostCodexAgentSdkRootEnvVar,
588 > hasSeparateMuslLinuxPackage: false,
589 > };
590 >
591 > /**
592 > * Convert a workbench {@link ToolCallResult} into the codex
593 > * {@link DynamicToolCallResponse} returned for an `item/tool/call` request.
594 > * Text content maps to `inputText`; when there is no text content the
595 > * tool's past-tense summary is used so codex never receives an empty body.
596 > */
597 function dynamicToolResponseFromResult(result: ToolCallResult): DynamicToolCallResponse {
598 const contentItems: DynamicToolCallOutputContentItem[] = [];
613 return { contentItems, success: result.success };
614 }
616 function toolsSignature(tools: readonly ToolDefinition[] | undefined): string {
617 if (!tools || tools.length === 0) {
623 .join('\u0001');
624 }
626 > /**
627 > * Stable signature of the `mcp_servers` object a thread was started with, used
628 > * to detect when the merged (root config + client plugin) MCP set changed so
629 > * the thread can be restarted before its first turn to pick up the new servers.
630 > */
631 function mcpServersSignature(servers: Record<string, ICodexMcpServerConfigJson>): string {
632 const names = Object.keys(servers).sort();
633 return names.map(name => `${name}\u0000${JSON.stringify(servers[name])}`).join('\u0001');
634 }
636 > /**
637 > * Codex active-client handle. Writes flow into the owning session's
638 > * {@link ActiveClientToolSet} (tools) and its {@link CodexClientCustomizationStore}
639 > * (customizations); the session is resolved lazily so writes that arrive before
640 > * (or after) the session exists are gracefully dropped, matching the prior
641 > * `setClientTools` early-return behavior. Assigning `customizations` caches the
642 > * inputs (so the getter echoes them) and kicks off the agent's async sync.
643 > */
644 > class CodexActiveClientHandle implements IActiveClient {
645 > private _customizations: readonly ClientPluginCustomization[] = [];
646 >
647 > constructor(
648 private readonly _getSession: () => ICodexSession | undefined,
649 readonly clientId: string,
652 private readonly _syncCustomizations: (customizations: readonly ClientPluginCustomization[]) => void,
653 ) { }
655 > get tools(): readonly ToolDefinition[] {
656 return this._getSession()?.clientToolSet.get(this.clientId) ?? [];
657 }
658 > set tools(tools: readonly ToolDefinition[]) { codexAgent.ts
659 this._getSession()?.clientToolSet.set(this.clientId, tools);
660 this._onToolsSet(tools);
661 }
663 > get customizations(): readonly ClientPluginCustomization[] {
664 return this._customizations;
665 }
666 > set customizations(customizations: readonly ClientPluginCustomization[]) { codexAgent.ts
667 this._customizations = customizations;
668 this._syncCustomizations(customizations);
669 }
670 > } codexAgent.ts
671 >
672 > /**
673 > * Map a resolved approval decision to the {@link FileChangeApprovalDecision}
674 > * subset. The host's boolean response only yields `accept`/`decline`; the
675 > * command-only amendment variants are treated as a decline for file changes.
676 > */
677 function narrowFileChangeDecision(decision: CommandExecutionApprovalDecision): FileChangeApprovalDecision {
678 switch (decision) {
686 }
687 }
689 > export class CodexAgent extends Disposable implements IAgent {
690 >
691 > readonly id: AgentProvider = CODEX_AGENT_PROVIDER_ID;
692 >
693 > private readonly _onDidSessionProgress = this._register(new Emitter<AgentSignal>());
694 > readonly onDidSessionProgress = this._onDidSessionProgress.event;
695 >
696 > private readonly _onDidMaterializeSession = this._register(new Emitter<IAgentMaterializeSessionEvent>());
697 > readonly onDidMaterializeSession = this._onDidMaterializeSession.event;
698 >
699 > private readonly _onDidRequireAuth = this._register(new Emitter<Omit<AuthRequiredParams, 'channel'>>());
700 > readonly onDidRequireAuth = this._onDidRequireAuth.event;
701 >
702 > private readonly _onMcpNotification = this._register(new Emitter<IMcpNotification>());
703 > readonly onMcpNotification = this._onMcpNotification.event;
704 >
705 > private readonly _models = observableValue<readonly IAgentModelInfo[]>(this, []);
706 > readonly models: IObservable<readonly IAgentModelInfo[]> = this._models;
707 > private _openAIAccountState: ICodexAccountState = { usageSource: 'openai', status: 'signedOut' };
708 > private _providerConfigurationValues: Record<string, unknown> = {};
709 > private _providerConfigurationWrite = Promise.resolve();
710 > private _providerConfigurationReady = false;
711 > private _providerConfigurationRefresh: Promise<void> | undefined;
712 >
713 > /** Keyed by caller-facing sessionId (the URI host). */
714 > private readonly _sessions = new Map<string, ICodexSession>();
715 > /** Inverse map: codex threadId → caller-facing sessionId, for routing codex notifications back to sessions. */
716 > private readonly _sessionIdByThreadId = new Map<string, string>();
717 > /**
718 > * Live subagent (collab-agent) child threads, keyed by the child codex
719 > * thread id. Populated when a parent session's `spawnAgent` collab tool
720 > * call completes (carrying the child `receiverThreadIds`); the child's
721 > * subsequent `turn/*` and `item/*` notifications route here instead of
722 > * {@link _sessionIdByThreadId}. Removed on the child's `turn/completed`.
723 > */
724 > private readonly _subagentsByThreadId = new Map<string, ICodexSubagent>();
725 > /**
726 > * Connection-global MCP server inventory reported by the codex
727 > * app-server (`mcpServerStatus/list` + `mcpServer/startupStatus/updated`).
728 > * Codex owns MCP servers at the process level — shared across every
729 > * thread — so the inventory lives on the agent and is mirrored onto each
730 > * session's {@link ICodexSession.mcpController}. Keyed by server name.
731 > */
732 > private readonly _mcpInventory = new Map<string, ICodexMcpServerEntry>();
733 > /**
734 > * OAuth bearer tokens acquired for auth-gated http MCP servers, keyed by
735 > * the server's {@link normalizeCodexMcpResourceUrl | normalized URL}.
736 > * Populated by {@link handleAuthenticationToken} after the workbench
737 > * completes the sign-in, then injected into the per-thread `http_headers`
738 > * by {@link _buildSessionMcpServers}. Process-global: a token for a given
739 > * server URL applies to every session/thread that uses it (codex runs one
740 > * shared app-server).
741 > */
742 > private readonly _mcpAuthTokens = new Map<string, string>();
743 > /**
744 > * Association from a normalized OAuth `resource` (what the workbench
745 > * authenticates) to the normalized MCP server URL(s) it unlocks. RFC 9728
746 > * discovery can return a `resource` that differs from the configured server
747 > * URL (e.g. root `https://host/` for a `https://host/mcp` endpoint), so the
748 > * token the workbench pushes back is keyed by the resource, not the server
749 > * URL. Recorded in {@link _surfaceMcpAuthRequired} at discovery time and
750 > * read by {@link handleAuthenticationToken} to route the token to the right
751 > * server(s).
752 > */
753 > private readonly _mcpAuthServerUrlsByResource = new Map<string, Set<string>>();
754 > private _githubToken: string | undefined;
755 > private _usageSource: CodexUsageSource;
756 > private _pendingUsageSource: CodexUsageSource | undefined;
757 > private _connection: ConnectionState = { kind: 'idle' };
758 > private _connectionGeneration = 0;
759 > private _modelsRefreshPromise: Promise<void> | undefined;
760 > private _usageSourceValidation = Promise.resolve();
761 > private readonly _metadataStore: CodexSessionMetadataStore;
762 >
763 > /**
764 > * The agent host's server-tool host (feedback "comments" today, more in the
765 > * future). Server tools execute in-process against the session's own state
766 > * — unlike client tools, which round-trip to the workbench. `undefined`
767 > * until {@link setServerToolHost} is called during registration; remains
768 > * `undefined` in test / standalone construction.
769 > */
770 > private _serverToolHost: IAgentServerToolHost | undefined;
771 >
772 > constructor(
773 @ILogService private readonly _logService: ILogService,
774 @ICopilotApiService private readonly _copilotApiService: ICopilotApiService,
800 }
801 }
803 > private async _validateOpenAIUsageSource(): Promise<void> {
804 let account: ICodexAccountState;
805 try {
827 }
828 }
830 > private _setOpenAIAccountState(state: ICodexAccountState, _publish = true): void {
831 this._openAIAccountState = state;
832 }
834 > private _resolveUsageSource(): CodexUsageSource {
835 return this._configurationService.getRootValue(agentHostCustomizationConfigSchema, AgentHostConfigKey.CodexUsageSource) ?? 'copilot';
836 }
838 > private _requestUsageSourceChange(source: CodexUsageSource): void {
839 if (this._hasActiveTurns()) {
840 this._pendingUsageSource = source;
849 this._applyUsageSourceChange(source);
850 }
852 > private _applyUsageSourceChange(source: CodexUsageSource, _publishAccount = true, refreshModels = true): void {
853 const previousSource = this._usageSource;
854 this._pendingUsageSource = undefined;
871 }
872 }
874 > private _resetSessionForUsageSourceChange(session: ICodexSession, source: CodexUsageSource, previousSource?: CodexUsageSource): void {
875 if (session.threadId === undefined) {
876 return;
886 session.codexTurnIdByHostTurnId.clear();
887 }
889 > private _hasActiveTurns(): boolean {
890 return [...this._sessions.values()].some(session => session.currentTurnId !== undefined)
891 || [...this._subagentsByThreadId.values()].some(subagent => subagent.session.currentTurnId !== undefined);
892 }
894 > private _applyPendingUsageSourceIfIdle(): void {
895 const pendingUsageSource = this._pendingUsageSource;
896 if (pendingUsageSource && !this._hasActiveTurns()) {
898 }
899 }
901 > // #region Auth
902 >
903 > getProtectedResources(): ProtectedResourceMetadata[] {
904 return codexProtectedResourcesForUsageSource(
905 this._usageSource,
908 );
909 }
911 > async authenticate(resource: string, token: string): Promise<boolean> {
912 if (resource === this._gitHubEndpointService.getRepoResource().resource) {
913 return true;
935 return true;
936 }
938 > /**
939 > * Receives a bearer token the workbench acquired for a protected resource
940 > * (the `authenticate` command is fanned out to every agent). If the
941 > * resource maps to one or more configured auth-gated http MCP servers
942 > * (via the association recorded at discovery time, or a direct URL match),
943 > * store the token per server URL (so {@link _buildSessionMcpServers} injects
944 > * it) and reconnect the affected threads so codex picks it up. This is the
945 > * codex end of the *same* OAuth mechanism the Copilot agent uses: the
946 > * workbench does the sign-in, the agent injects the resulting bearer.
947 > * Returns whether the token was consumed by an MCP server (the GitHub agent
948 > * token flows through {@link authenticate} instead).
949 > */
950 > async handleAuthenticationToken(params: AuthenticateParams): Promise<boolean> {
951 const normalizedResource = normalizeCodexMcpResourceUrl(params.resource);
952 if (normalizedResource === undefined) {
980 return true;
981 }
983 > /** Whether `normalizedUrl` is a currently-configured http MCP server (root config or any session's client plugins). */
984 > private _isConfiguredHttpServerUrl(normalizedUrl: string): boolean {
985 if (Object.values(codexMcpServersFromConfig(this._configurationService.getRootValue(platformRootSchema, AgentHostMcpServersConfigKey)))
986 .some(server => server.url !== undefined && normalizeCodexMcpResourceUrl(server.url) === normalizedUrl)) {
991 );
992 }
994 > /**
995 > * Reconnects every materialized session whose merged MCP servers include one
996 > * of `normalizedUrls` so codex re-reads `config.mcp_servers` with the
997 > * injected `Authorization` header. A thread that has not yet committed a
998 > * turn is restarted (`thread/start`, lossless); one with history is resumed
999 > * (`thread/resume` carries the same `config` field, loading history from the
1000 > * rollout) on its next turn via {@link ICodexSession.needsResume}.
1001 > */
1002 > private async _reconnectSessionsForMcpAuth(normalizedUrls: ReadonlySet<string>): Promise<void> {
1003 for (const session of this._sessions.values()) {
1004 if (session.disposed || session.threadId === undefined) {
1021 }
1022 }
1023 > codexAgent.ts
1024 > /**
1025 > * {@link IAgent.refreshModels}. Coalesces onto an in-flight refresh — from
1026 > * an account/usage-source change or an earlier tick — rather than issuing a
1027 > * second enumeration, and never rejects: {@link _refreshModels} logs and
1028 > * applies its own stale-write guards on failure.
1029 > */
1030 > refreshModels(): Promise<void> {
1031 return this._modelsRefreshPromise ?? this._queueModelRefresh();
1032 }
1033 > codexAgent.ts
1034 > private _queueModelRefresh(): Promise<void> {
1035 const refreshPromise = this._refreshModels().finally(() => {
1036 if (this._modelsRefreshPromise === refreshPromise) {
1041 return refreshPromise;
1042 }
1043 > codexAgent.ts
1044 > private _ensureAuthenticated(): string | undefined {
1045 if (this._usageSource === 'openai') {
1046 return undefined;
1056 return token;
1057 }
1058 > codexAgent.ts
1059 > private _defaultModel(): ModelSelection | undefined {
1060 const models = this._models.get();
1061 const chosen = models[0];
1062 return chosen ? { id: chosen.id } : undefined;
1063 }
1064 > codexAgent.ts
1065 > private _supportedModelOrUndefined(model: ModelSelection | undefined): ModelSelection | undefined {
1066 if (model && this._models.get().some(m => m.id === model.id)) {
1067 return model;
1072 return this._defaultModel();
1073 }
1074 > codexAgent.ts
1075 > private async _resolveModel(session: ICodexSession): Promise<ModelSelection> {
1076 // Ensure the catalog is populated before validating the selection so a
1077 // model picked before models finished loading isn't dropped.
1086 throw new Error('Codex has no available models.');
1087 }
1088 > codexAgent.ts
1089 > private _createReasoningEffortConfigSchema(): ConfigSchema {
1090 return {
1091 type: 'object',
1103 };
1104 }
1105 > codexAgent.ts
1106 > private _getReasoningEffort(session: ICodexSession): ReasoningEffort | undefined {
1107 const modelConfigEffort = narrowReasoningEffort(session.model?.config?.[CODEX_THINKING_LEVEL_KEY]);
1108 if (modelConfigEffort) {
1112 return narrowReasoningEffort(config?.[CodexSessionConfigKey.ModelReasoningEffort]) ?? codexSessionConfigDefaults[CodexSessionConfigKey.ModelReasoningEffort];
1113 }
1114 > codexAgent.ts
1115 > private _readSessionConfig(session: ICodexSession): ReturnType<typeof codexSessionConfigSchema.validateOrDefault> {
1116 return codexSessionConfigSchema.validateOrDefault(
1117 this._configurationService.getSessionConfigValues(session.sessionUri.toString()),
1119 );
1120 }
1121 > codexAgent.ts
1122 > /**
1123 > * Resolve the Codex security axes (approval policy, sandbox, reviewer) for a
1124 > * live or restored session from its RAW persisted config values.
1125 > *
1126 > * The raw values are normalized through {@link migrateCodexPermissionValues}
1127 > * (the same migration the restore path applies) before resolving, so the
1128 > * axes we send to the app-server always match the preset the "Approvals" chip
1129 > * displays. This matters for two legacy shapes:
1130 > * - a session that persisted only `sandboxMode = 'read-only'` is preserved
1131 > * verbatim, so it is NOT silently escalated back to `workspace-write` on
1132 > * resume (the chip over-promises, but the session stays more locked down);
1133 > * - a session that persisted `approvalPolicy = 'never'` + `workspace-write`
1134 > * (which the chip renders as "Default Permissions") is snapped onto the
1135 > * `default` preset's `on-request` policy so it actually prompts, instead of
1136 > * running commands unprompted while the chip claims it would ask.
1137 > */
1138 > private _resolveSessionPermissions(session: ICodexSession): ICodexResolvedPermissions {
1139 const rawValues = this._configurationService.getSessionConfigValues(session.sessionUri.toString());
1140 const defaults = {
1144 return resolveCodexPermissions(migrateCodexPermissionValues(rawValues, defaults), defaults);
1145 }
1146 > codexAgent.ts
1147 > private _sandboxPolicy(session: ICodexSession, config: ReturnType<typeof codexSessionConfigSchema.validateOrDefault>, mode: SandboxMode): SandboxPolicy {
1148 if (mode === 'danger-full-access') {
1149 return { type: 'dangerFullAccess' };
1165 };
1166 }
1167 > codexAgent.ts
1168 > private _turnStartOptions(session: ICodexSession, modelId: string): Pick<TurnStartParams, 'approvalPolicy' | 'sandboxPolicy' | 'approvalsReviewer' | 'effort' | 'runtimeWorkspaceRoots' | 'personality' | 'summary' | 'collaborationMode'> {
1169 const config = this._readSessionConfig(session);
1170 const { approvalPolicy, sandboxMode, approvalsReviewer } = this._resolveSessionPermissions(session);
1195 };
1196 }
1197 > codexAgent.ts
1198 > private async _refreshModels(): Promise<void> {
1199 const usageSource = this._usageSource;
1200 if (usageSource === 'openai') {
1250 }
1251 }
1252 > codexAgent.ts
1253 > private async _refreshOpenAIModels(): Promise<void> {
1254 try {
1255 const connection = await this._ensureConnection();
1284 }
1285 }
1286 > codexAgent.ts
1287 > // #endregion
1288 >
1289 > // #region Connection lifecycle
1290 >
1291 > /**
1292 > * Lazily spawn the codex app-server, initialize the connection,
1293 > * authenticate via apiKey, and return the ready connection. Idempotent
1294 > * — concurrent callers share the same promise.
1295 > */
1296 > private async _ensureConnection(skipUsageSourceValidation = false): Promise<IConnectionReady> {
1297 if (this._connection.kind === 'ready') {
1298 return Promise.resolve(this._connection);
1331 return promise;
1332 }
1333 > codexAgent.ts
1334 > /**
1335 > * Resolve the Codex SDK root — the directory whose
1336 > * `node_modules/@openai/codex-<target>/…` holds the native binary.
1337 > *
1338 > * Mirrors the three-tier resolution in `ClaudeAgentSdkService._loadSdk`:
1339 > * 1. dev override / product download, via the downloader, when the SDK
1340 > * `isAvailable` (env override || `product.agentSdks.codex`);
1341 > * 2. dev fallback to this repo's `node_modules`, where `@openai/codex`
1342 > * and its per-host binary package are devDependencies — this is what
1343 > * lets running-from-source (and dev smoke tests) spawn Codex without
1344 > * an env-var override.
1345 > *
1346 > * `isAvailable` is already false in dev, so it discriminates the two
1347 > * without injecting `INativeEnvironmentService`. When neither path
1348 > * resolves we defer to the downloader so callers get its actionable
1349 > * "not configured" diagnostic.
1350 > */
1351 > private async _resolveSdkRoot(): Promise<string> {
1352 if (this._agentSdkDownloader.isAvailable(CodexSdkPackage)) {
1353 return this._agentSdkDownloader.loadSdkRoot(CodexSdkPackage, CancellationToken.None);
1360 return this._agentSdkDownloader.loadSdkRoot(CodexSdkPackage, CancellationToken.None);
1361 }
1362 > codexAgent.ts
1363 > private async _startConnection(usageSource: CodexUsageSource, token: string | undefined): Promise<IConnectionReady> {
1364 // Resolve the Codex SDK root: dev override / product download via the
1365 // downloader, or this repo's `node_modules` in a source checkout (see
1545 return { client, usageSource, proxyHandle, child };
1546 }
1547 > codexAgent.ts
1548 > /**
1549 > * Builds the `mcp_servers` object for a session's `thread/start.config`:
1550 > * the workbench's root `mcpServers` config merged with the session's
1551 > * enabled client-plugin MCP servers. Passing them per-thread (rather than
1552 > * as process-global `-c` spawn overrides) means each new session picks up
1553 > * the current root config without restarting the shared app-server, and it
1554 > * merges with (leaves intact) the user's global `~/.codex/config.toml`.
1555 > * Client-plugin servers win a name collision with the root config. Any
1556 > * OAuth bearer token acquired for an auth-gated http server (see
1557 > * {@link handleAuthenticationToken}) is injected as an `Authorization`
1558 > * header so codex connects authenticated.
1559 > */
1560 > private _buildSessionMcpServers(session: ICodexSession): Record<string, ICodexMcpServerConfigJson> {
1561 const root = codexMcpServersFromConfig(this._configurationService.getRootValue(platformRootSchema, AgentHostMcpServersConfigKey));
1562 const clientPlugins = codexMcpServersFromPlugins(session.clientCustomizations.enabledPlugins());
1563 return injectCodexMcpAuthTokens({ ...root, ...clientPlugins }, this._mcpAuthTokens);
1564 }
1565 > codexAgent.ts
1566 > /**
1567 > * The normalized URLs of every configured http MCP server (root config +
1568 > * the session's client plugins), keyed by server name. Used to (a) surface
1569 > * an auth-required server's resource for the workbench sign-in and (b)
1570 > * match a workbench-acquired token back to the server(s) it unlocks.
1571 > * Computed from a token-free build so the URLs are the bare server URLs.
1572 > */
1573 > private _httpMcpServerUrls(session: ICodexSession): Map<string, string> {
1574 const root = codexMcpServersFromConfig(this._configurationService.getRootValue(platformRootSchema, AgentHostMcpServersConfigKey));
1575 const clientPlugins = codexMcpServersFromPlugins(session.clientCustomizations.enabledPlugins());
1583 return urls;
1584 }
1585 > codexAgent.ts
1586 > /** The bare (un-normalized) URL of a configured http MCP server by name, across all sessions. */
1587 > private _mcpServerUrlForName(name: string): string | undefined {
1588 const root = codexMcpServersFromConfig(this._configurationService.getRootValue(platformRootSchema, AgentHostMcpServersConfigKey));
1589 if (root[name]?.url !== undefined) {
1598 return undefined;
1599 }
1600 > codexAgent.ts
1601 > /**
1602 > * Map the session's tools into codex `dynamicTools` specs: the agent host's
1603 > * server tools (executed in-process) plus the workbench client's tools
1604 > * (round-tripped to the client). Both are registered with codex the same
1605 > * way — at `thread/start` — and dispatched apart in
1606 > * {@link _handleDynamicToolCallRpc} by name.
1607 > */
1608 > private _buildDynamicTools(session: ICodexSession): DynamicToolSpec[] | undefined {
1609 const serverTools = this._serverToolHost?.definitions ?? [];
1610 const clientTools = session.clientToolSet.merged();
1630 }));
1631 }
1632 > codexAgent.ts
1633 > private async _handleDynamicToolCallRpc(params: DynamicToolCallParams): Promise<ServerRequestHandlerResult<DynamicToolCallResponse>> {
1634 const sessionId = this._sessionIdByThreadId.get(params.threadId);
1635 const session = sessionId ? this._sessions.get(sessionId) : undefined;
1674 }
1675 }
1676 > codexAgent.ts
1677 > private _toolFailure(message: string): DynamicToolCallResponse {
1678 this._logService.warn(`[Codex] dynamic tool call failed: ${message}`);
1679 return { contentItems: [{ type: 'inputText', text: message }], success: false };
1680 }
1681 > codexAgent.ts
1682 > private async _handleUserInputRequestRpc(params: ToolRequestUserInputParams): Promise<ServerRequestHandlerResult<ToolRequestUserInputResponse>> {
1683 const sessionId = this._sessionIdByThreadId.get(params.threadId);
1684 const session = sessionId ? this._sessions.get(sessionId) : undefined;
1718 }
1719 }
1720 > codexAgent.ts
1721 > /**
1722 > * Renders an MCP tool-call approval on the normal tool-approval card
1723 > * (a pending-confirmation `ChatToolCallReady` on the originating
1724 > * `mcpToolCall` host tool call) rather than as a chat-input question.
1725 > * The user's Allow/Deny decision is mapped back to the answer string
1726 > * codex expects (`Allow` / `__codex_mcp_decline__`). Mirrors the shell
1727 > * command approval flow ({@link CodexAgent._handleCommandApprovalRequest}).
1728 > */
1729 > private async _handleMcpToolApprovalViaCard(
1730 session: ICodexSession,
1731 question: ToolRequestUserInputQuestion,
1754 return { result: { answers: { [question.id]: { answers: [answer] } } } };
1755 }
1756 > codexAgent.ts
1757 > private async _handleElicitationRequestRpc(params: McpServerElicitationRequestParams): Promise<ServerRequestHandlerResult<McpServerElicitationRequestResponse>> {
1758 const sessionId = this._sessionIdByThreadId.get(params.threadId);
1759 const session = sessionId ? this._sessions.get(sessionId) : undefined;
1782 }
1783 }
1784 > codexAgent.ts
1785 > private _hostTurnId(session: ICodexSession, appTurnId: string): string {
1786 return session.hostTurnIdByAppTurnId.get(appTurnId) ?? appTurnId;
1787 }
1788 > codexAgent.ts
1789 > private _withHostTurnId<T extends { readonly turnId: string }>(session: ICodexSession, params: T): T {
1790 const turnId = this._hostTurnId(session, params.turnId);
1791 return turnId === params.turnId ? params : { ...params, turnId };
1792 }
1793 > codexAgent.ts
1794 > private _withHostTurn<T extends { readonly turn: { readonly id: string } }>(session: ICodexSession, params: T): T {
1795 const appTurnId = params.turn.id;
1796 const hostTurnId = session.currentTurnId ?? this._hostTurnId(session, appTurnId);
1799 return hostTurnId === appTurnId ? params : { ...params, turn: { ...params.turn, id: hostTurnId } };
1800 }
1801 > codexAgent.ts
1802 > private _handleTurnStartedNotification(session: ICodexSession, params: TurnStartedNotification): (SessionAction | ChatAction)[] {
1803 // The workbench already dispatched the canonical turn start before sendMessage.
1804 // Codex's event only establishes app-server turn id correlation for later items.
1806 return [];
1807 }
1808 > codexAgent.ts
1809 > private _handleTurnCompletedNotification(session: ICodexSession, params: TurnCompletedNotification): (SessionAction | ChatAction)[] {
1810 const appTurnId = params.turn.id;
1811 const hostTurnId = this._hostTurnId(session, appTurnId);
1838 return out;
1839 }
1840 > codexAgent.ts
1841 > /**
1842 > * Dispatch a codex `item/started` notification. `userMessage` items are
1843 > * intercepted here (rather than in the pure mapper) because steering
1844 > * promotion needs the agent's per-session turn-correlation state; all
1845 > * other item kinds defer to {@link mapItemStarted}.
1846 > */
1847 > private _handleItemStarted(session: ICodexSession, params: ItemStartedNotification): (SessionAction | ChatAction)[] {
1848 if (params.item.type === 'userMessage') {
1849 return this._handleSteeredUserMessage(session, params.item.content);
1851 return mapItemStarted(session.mapState, this._withHostTurnId(session, params));
1852 }
1853 > codexAgent.ts
1854 > /**
1855 > * Codex echoes every user message — the turn opener (already shown by
1856 > * the workbench before `sendMessage`) and any steered input — as a
1857 > * `userMessage` item. Only steered input is buffered in
1858 > * {@link ICodexSession.pendingSteeringFlips}; a buffered match is
1859 > * promoted into its own visible turn and everything else is dropped.
1860 > */
1861 > private _handleSteeredUserMessage(session: ICodexSession, content: readonly UserInput[]): (SessionAction | ChatAction)[] {
1862 const text = extractUserInputText(content);
1863 const steering = this._takeMatchingPendingSteering(session, text);
1867 return this._beginSteeringTurn(session, steering);
1868 }
1869 > codexAgent.ts
1870 > /**
1871 > * Pop the buffered steering message whose text matches the echoed
1872 > * `userMessage` content. Matching by content (not FIFO) keeps the
1873 > * mapping correct when several steering messages with different texts
1874 > * are in flight.
1875 > */
1876 > private _takeMatchingPendingSteering(session: ICodexSession, text: string): PendingMessage | undefined {
1877 for (const [id, msg] of session.pendingSteeringFlips) {
1878 if (msg.message.text === text) {
1883 return undefined;
1884 }
1885 > codexAgent.ts
1886 > /**
1887 > * Promote a steered message into its own protocol turn: complete the
1888 > * in-flight turn (so its response parts settle into history) and open a
1889 > * fresh turn whose user message is the steering content. The
1890 > * `queuedMessageId` clears the corresponding pending steering bubble.
1891 > * Subsequent codex items for the same app-server turn are re-mapped to
1892 > * the new host turn id so the steering response lands there.
1893 > */
1894 > private _beginSteeringTurn(session: ICodexSession, steering: PendingMessage): (SessionAction | ChatAction)[] {
1895 const actions: (SessionAction | ChatAction)[] = [];
1896 const appTurnId = session.currentAppTurnId;
1915 return actions;
1916 }
1917 > codexAgent.ts
1918 > /**
1919 > * Clear any steering messages still buffered (never echoed by codex)
1920 > * and fire `steering_consumed` for each so the chat UI removes the
1921 > * lingering pending bubble. Called on turn completion, abort, dispose,
1922 > * and connection loss.
1923 > */
1924 > private _drainPendingSteering(session: ICodexSession): void {
1925 if (session.pendingSteeringFlips.size === 0) {
1926 return;
1932 }
1933 }
1934 > codexAgent.ts
1935 > private _fireSteeringConsumed(session: ICodexSession, id: string): void {
1936 this._onDidSessionProgress.fire({ kind: 'steering_consumed', chat: URI.parse(buildDefaultChatUri(session.sessionUri)), id });
1937 }
1938 > codexAgent.ts
1939 > private _registerIgnoredNotifications(client: ICodexAppServerClient): void {
1940 const ignored = [
1941 'thread/started', // thread/start response is authoritative for session materialization.
1953 }
1954 }
1955 > codexAgent.ts
1956 > private async _refreshAccount(client: ICodexAppServerClient, publish = true): Promise<ICodexAccountState> {
1957 try {
1958 const response = await client.request<'account/read', GetAccountResponse>('account/read', { refreshToken: false });
1969 }
1970 }
1971 > codexAgent.ts
1972 > private async _readProviderConfiguration(): Promise<Record<string, unknown>> {
1973 const connection = await this._ensureConnection();
1974 const response = await connection.client.request<'config/read', ConfigReadResponse>('config/read', { includeLayers: true });
1980 };
1981 }
1982 > codexAgent.ts
1983 > private async _writeProviderConfiguration(key: string, value: unknown): Promise<void> {
1984 const connection = await this._ensureConnection();
1985 await connection.client.request<'config/batchWrite', ConfigWriteResponse>('config/batchWrite', {
1993 });
1994 }
1995 > codexAgent.ts
1996 > private _refreshProviderConfiguration(): Promise<void> {
1997 return this._providerConfigurationRefresh ??= (async () => {
1998 try {
2007 })();
2008 }
2009 > codexAgent.ts
2010 > private _queueProviderConfigurationWrite(): void {
2011 if (!this._providerConfigurationReady) {
2012 return;
2026 }
2027 }
2028 > codexAgent.ts
2029 > private _readConfigurationValue(config: Record<string, unknown>, keyPath: string): unknown {
2030 let value: unknown = config;
2031 for (const segment of keyPath.split('.')) {
2037 return value;
2038 }
2039 > codexAgent.ts
2040 > private _dispatchByThread(threadId: string, mapFn: (s: ICodexSession) => ReturnType<typeof mapTurnStarted>): void {
2041 // Collab-agent (subagent) child threads emit their own full event
2042 // stream; route them to the isolated subagent session and fire each
2063 }
2064 }
2065 > codexAgent.ts
2066 > /**
2067 > * `item/completed` dispatch. In addition to the normal per-thread mapping,
2068 > * a parent session's completed `spawnAgent` collab tool call now carries
2069 > * the child `receiverThreadIds`, so we register each spawned subagent and
2070 > * emit a `subagent_started` signal (before mapping the completion, so the
2071 > * shared orchestrator has attached the subagent-chat block to the parent
2072 > * tool call by the time it completes).
2073 > */
2074 > private _dispatchItemCompleted(params: ItemCompletedNotification): void {
2075 const subagent = this._subagentsByThreadId.get(params.threadId);
2076 if (subagent) {
2097 }
2098 }
2099 > codexAgent.ts
2100 > /**
2101 > * `turn/completed` dispatch. For a subagent child thread, route the turn's
2102 > * flush/orphan actions to the peer chat but suppress its `ChatTurnComplete`
2103 > * — the child chat's turn is closed cleanly (without the parent's
2104 > * checkpoint/changeset/title side effects) by the `subagent_completed`
2105 > * signal, which also tears down the child-thread tracking.
2106 > */
2107 > private _dispatchTurnCompleted(params: TurnCompletedNotification): void {
2108 const subagent = this._subagentsByThreadId.get(params.threadId);
2109 if (subagent) {
2128 this._applyPendingUsageSourceIfIdle();
2129 }
2130 > codexAgent.ts
2131 > /**
2132 > * When a parent session's `spawnAgent` collab tool call completes it
2133 > * carries the child thread id(s) in `receiverThreadIds`. Register an
2134 > * isolated subagent session for each new child thread and emit a
2135 > * `subagent_started` signal so the shared orchestrator opens the read-only
2136 > * peer chat and attaches its discovery block to the parent tool call.
2137 > */
2138 > private _maybeRegisterSubagents(session: ICodexSession, params: ItemCompletedNotification): void {
2139 const item = params.item;
2140 if (item.type !== 'collabAgentToolCall' || item.tool !== 'spawnAgent') {
2171 }
2172 }
2173 > codexAgent.ts
2174 > /**
2175 > * Build an isolated {@link ICodexSession} used to run the shared event
2176 > * mappers for a subagent child thread. It shares the parent's `sessionUri`
2177 > * (so side effects target the parent's working tree and the fired actions
2178 > * resolve to the parent chat channel) and `acceptedForSession` memo (so the
2179 > * accept-for-session decision spans parent + subagents), but has its own
2180 > * fresh map/turn state and approval registry so the child's events don't
2181 > * collide with the parent's.
2182 > */
2183 > private _createSubagentSession(parent: ICodexSession, childThreadId: string): ICodexSession {
2184 const clientToolSet = new ActiveClientToolSet();
2185 return {
2219 };
2220 }
2221 > codexAgent.ts
2222 > /**
2223 > * Fire a subagent action tagged with the parent `spawnAgent` tool call.
2224 > * The `resource` is the PARENT chat channel (the key the subagent chat is
2225 > * registered under in the orchestrator); `parentToolCallId` routes the
2226 > * action into the child's read-only peer chat.
2227 > */
2228 > private _fireSubagent(subagent: ICodexSubagent, action: SessionAction | ChatAction): void {
2229 this._onDidSessionProgress.fire({
2230 kind: 'action',
2234 });
2235 }
2236 > codexAgent.ts
2237 > /**
2238 > * Phase 4: handle `item/commandExecution/requestApproval` from
2239 > * codex. Look up the host-side tool call for the item, emit a
2240 > * `ChatToolCallReady` in PendingConfirmation, park on a deferred
2241 > * keyed by toolCallId, and resolve when the user (or the
2242 > * accept-for-session memo) decides. Unknown sessions / items
2243 > * decline silently so codex stops blocking.
2244 > */
2245 > private async _handleCommandApprovalRequestRpc(params: CommandExecutionRequestApprovalParams): Promise<{ readonly result: CommandExecutionRequestApprovalResponse }> {
2246 // The request handler must return Codex's JSON-RPC result wrapper; keep
2247 // the approval method below focused on the host-side permission decision.
2249 return { result: { decision } };
2250 }
2251 > codexAgent.ts
2252 > private async _handleCommandApprovalRequest(params: {
2253 readonly threadId: string;
2254 readonly turnId: string;
2299 return decision;
2300 }
2301 > codexAgent.ts
2302 > private async _handleFileChangeApprovalRequestRpc(params: FileChangeRequestApprovalParams): Promise<{ readonly result: FileChangeRequestApprovalResponse }> {
2303 const decision = await this._requestItemApproval(params.threadId, params.itemId, params.reason ?? 'Apply file changes');
2304 return { result: { decision: narrowFileChangeDecision(decision) } };
2305 }
2306 > codexAgent.ts
2307 > private async _handlePermissionsApprovalRequestRpc(params: PermissionsRequestApprovalParams): Promise<{ readonly result: PermissionsRequestApprovalResponse }> {
2308 const decision = await this._requestItemApproval(params.threadId, params.itemId, params.reason ?? 'Grant elevated permissions');
2309 const granted = decision === 'accept' || decision === 'acceptForSession';
2318 };
2319 }
2320 > codexAgent.ts
2321 > /**
2322 > * Shared approval flow for item-scoped `requestApproval` requests that
2323 > * don't carry their own command string: look up the host tool call for
2324 > * the item, fire a pending-confirmation `ChatToolCallReady`, and resolve
2325 > * when the user (via {@link respondToPermissionRequest}) decides. Declines
2326 > * if the session or item is unknown.
2327 > */
2328 > private async _requestItemApproval(threadId: string, itemId: string, confirmationTitle: string): Promise<CommandExecutionApprovalDecision> {
2329 const target = this._resolveApprovalTarget(threadId);
2330 if (!target) {
2349 });
2350 }
2351 > codexAgent.ts
2352 > /**
2353 > * Resolve the {@link ICodexSession} that owns a codex thread for an
2354 > * approval request, plus the subagent wrapper when the thread is a
2355 > * collab-agent child. A subagent tool call's pending-confirmation
2356 > * `ChatToolCallReady` must be fired with the parent `spawnAgent` tool call
2357 > * as its `parentToolCallId` (via {@link _fireApproval}) so it lands in the
2358 > * child's read-only peer chat — where the matching `ChatToolCallStart`
2359 > * lives — instead of on the parent session.
2360 > */
2361 > private _resolveApprovalTarget(threadId: string): { readonly session: ICodexSession; readonly subagent?: ICodexSubagent } | undefined {
2362 const subagent = this._subagentsByThreadId.get(threadId);
2363 if (subagent) {
2368 return session ? { session } : undefined;
2369 }
2370 > codexAgent.ts
2371 > /** Fire an approval action to the parent session or the subagent peer chat. */
2372 > private _fireApproval(target: { readonly session: ICodexSession; readonly subagent?: ICodexSubagent }, action: SessionAction | ChatAction): void {
2373 if (target.subagent) {
2374 this._fireSubagent(target.subagent, action);
2377 }
2378 }
2379 > codexAgent.ts
2380 > private _handleGuardianWarning(session: ICodexSession, params: GuardianWarningNotification): ChatAction[] {
2381 const turnId = session.currentTurnId;
2382 if (turnId === undefined) {
2393 }];
2394 }
2395 > codexAgent.ts
2396 > private async _handleGuardianReviewCompleted(client: ICodexAppServerClient, params: ItemGuardianApprovalReviewCompletedNotification): Promise<void> {
2397 const sessionId = this._sessionIdByThreadId.get(params.threadId);
2398 const session = sessionId ? this._sessions.get(sessionId) : undefined;
2535 }
2536 }
2537 > codexAgent.ts
2538 > private _handleConnectionLost(): void {
2539 const conn = this._connection;
2540 if (conn.kind !== 'ready') {
2592 }
2593 }
2594 > codexAgent.ts
2595 > private _disposeConnection(): void {
2596 const connection = this._connection;
2597 this._connectionGeneration++;
2604 try { connection.child.kill('SIGKILL'); } catch { /* already dead */ }
2605 }
2606 > codexAgent.ts
2607 > // #endregion
2608 >
2609 > // #region IAgent methods
2610 >
2611 > getDescriptor(): IAgentDescriptor {
2612 return {
2613 provider: this.id,
2618 };
2619 }
2620 > codexAgent.ts
2621 > private _sessionUriFromChat(chat: URI): URI {
2622 const parsed = parseChatUri(chat);
2623 return parsed ? URI.parse(parsed.session) : chat;
2624 }
2625 > codexAgent.ts
2626 > // ---- Chat surface ------------------------------------------------------
2627 > //
2628 > // Chat-addressed adoption of the {@link IAgent} surface introduced
2629 > // in gate G-C1. Codex is a SINGLE-CHAT harness: a session owns exactly one
2630 > // (default) chat addressed by its default chat channel URI, so the
2631 > // chat methods simply route to the existing session-addressed
2632 > // implementations. The legacy `(session, chat?)` methods below are kept as a
2633 > // compat shim (removed centrally in gate G-C2) and both surfaces coexist.
2634 >
2635 > /**
2636 > * The chat-addressed operation surface for the chats within a session.
2637 > * Codex is single-chat: peer-chat operations
2638 > * ({@link IAgentChats.createChat}/{@link IAgentChats.fork})
2639 > * are unsupported and throw, mirroring today's behavior where Codex omits
2640 > * `createChat` (the orchestrator rejected multi-chat for Codex). The
2641 > * remaining methods address the session's single default chat, whose
2642 > * URI is the deterministic default chat channel URI.
2643 > */
2644 > readonly chats: IAgentChats = {
2645 > createChat: (_chat: URI, _options?: IAgentCreateChatOptions): Promise<IAgentCreateChatResult | void> => {
2646 throw new Error('Codex agent does not support multiple chats');
2647 },
2648 > fork: (_chat: URI, _source: IAgentCreateChatForkSource, _options?: IAgentCreateChatOptions): Promise<IAgentCreateChatResult | void> => { codexAgent.ts
2649 throw new Error('Codex agent does not support chat forking');
2650 },
2651 > disposeChat: (_chat: URI): Promise<void> => { codexAgent.ts
2652 // Codex has no additional (peer) chats to dispose; the
2653 // default chat lives and dies with its session.
2654 return Promise.resolve();
2655 },
2656 > sendMessage: (chat: URI, prompt: string, workingDirectory: URI | undefined, attachments?: readonly MessageAttachment[], turnId?: string, _senderClientId?: string): Promise<void> => { codexAgent.ts
2657 return this._sendMessage(chat, prompt, attachments, turnId, workingDirectory);
2658 },
2659 > abort: (chat: URI): Promise<void> => { codexAgent.ts
2660 return this._abort(chat);
2661 },
2662 > changeModel: (chat: URI, model: ModelSelection): Promise<void> => { codexAgent.ts
2663 return this._changeModel(chat, model);
2664 },
2665 > changeAgent: (_chat: URI, _agent: AgentSelection | undefined): Promise<void> => { codexAgent.ts
2666 // Codex does not support selecting a custom agent.
2667 return Promise.resolve();
2668 },
2669 > getMessages: (chat: URI): Promise<readonly Turn[]> => { codexAgent.ts
2670 return this.getSessionMessages(chat);
2671 },
2672 > }; codexAgent.ts
2673 >
2674 > async createSession(config: IAgentCreateSessionConfig = {}): Promise<IAgentCreateSessionResult> {
2675 this._logService.info(`[Codex DEBUG] createSession usageSource=${this._usageSource} accountStatus=${codexAccountStateForUsageSource(this._usageSource, this._openAIAccountState).status} session=${config.session?.toString() ?? '(none)'} model=${config.model?.id ?? '(none)'} cwd=${config.workingDirectory?.toString() ?? '(none)'}`);
2676 let validation = this._usageSourceValidation;
2757 };
2758 }
2759 > codexAgent.ts
2760 > /**
2761 > * Build an {@link ICodexSession} entry for a thread that already exists on
2762 > * the app-server (a restored session or a freshly forked one). Such a
2763 > * session skips materialization — its first {@link _sendMessage} issues a
2764 > * `thread/resume` (`needsResume: true`) — so the prewarm/first-turn flags
2765 > * are pre-set to their post-materialization values.
2766 > */
2767 > private _createResumedSessionEntry(sessionId: string, threadId: string, sessionUri: URI, workingDirectory: URI | undefined, model: ModelSelection | undefined): ICodexSession {
2768 const clientToolSet = new ActiveClientToolSet();
2769 return {
2803 };
2804 }
2805 > codexAgent.ts
2806 > /**
2807 > * Fork an existing codex session at a turn into a brand-new session.
2808 > *
2809 > * Codex is single-chat, so the workbench routes the "fork conversation"
2810 > * gesture here (via {@link AgentHostSessionHandler}) instead of minting a
2811 > * peer chat. We `thread/fork` the source thread — which copies its full
2812 > * history — then `thread/rollback` the trailing turns so the fork retains
2813 > * only the turns up to and including `fork.turnId`. The forked thread is
2814 > * registered as a resumable session (its first send issues a
2815 > * `thread/resume`) keyed by its new thread id, preserving the Codex
2816 > * convention that a session id equals its thread id.
2817 > */
2818 > private async _forkSession(config: IAgentCreateSessionConfig, fork: NonNullable<IAgentCreateSessionConfig['fork']>): Promise<IAgentCreateSessionResult> {
2819 const sourceRead = await this._readSession(fork.session);
2820 if (!sourceRead) {
2935 };
2936 }
2937 > codexAgent.ts
2938 > /**
2939 > * Lazily start (or resume) a codex thread for `session`. Idempotent:
2940 > * if `threadId` is already populated, just returns. Called from
2941 > * `sendMessage` before the first `turn/start`.
2942 > */
2943 > private async _materializeIfNeeded(session: ICodexSession, fireMaterializedEvent = true): Promise<void> {
2944 if (session.disposed) {
2945 return;
2966 }
2967 }
2968 > codexAgent.ts
2969 > private async _materialize(session: ICodexSession): Promise<void> {
2970 if (session.disposed) {
2971 return;
3037 void this._refreshSkillExtraRoots();
3038 }
3039 > codexAgent.ts
3040 > /**
3041 > * Tear down the current codex thread and start a fresh one so the
3042 > * session's current client tools are registered as `dynamicTools`.
3043 > * Only safe before any turn has committed history on the thread.
3044 > */
3045 > private async _restartThreadWithCurrentTools(session: ICodexSession): Promise<void> {
3046 const conn = this._connection;
3047 const oldThreadId = session.threadId;
3059 await this._materializeIfNeeded(session);
3060 }
3061 > codexAgent.ts
3062 > private _fireMaterialized(session: ICodexSession): void {
3063 if (session.disposed) {
3064 return;
3074 });
3075 }
3076 > codexAgent.ts
3077 > private _schedulePrewarm(session: ICodexSession): void {
3078 if (!session.workingDirectory) {
3079 return;
3108 });
3109 }
3110 > codexAgent.ts
3111 > private async _expirePrewarm(session: ICodexSession): Promise<void> {
3112 if (session.disposed || session.prewarmClaimed || session.threadId === undefined) {
3113 return;
3124 }
3125 }
3126 > codexAgent.ts
3127 > private _persistMaterializedSession(session: ICodexSession): void {
3128 if (session.disposed || !session.threadId) {
3129 return;
3137 });
3138 }
3139 > codexAgent.ts
3140 > private _claimPrewarm(session: ICodexSession): void {
3141 session.prewarmClaimed = true;
3142 if (session.prewarmTimer) {
3145 }
3146 }
3147 > codexAgent.ts
3148 > private _startTurnStopWatch(session: ICodexSession): StopWatch {
3149 const stopWatch = StopWatch.create(false);
3150 session.turnStopWatch = stopWatch;
3151 return stopWatch;
3152 }
3153 > codexAgent.ts
3154 > private _clearTurnStopWatch(session: ICodexSession): number {
3155 const elapsed = session.turnStopWatch?.elapsed();
3156 session.turnStopWatch = undefined;
3157 return typeof elapsed === 'number' && Number.isFinite(elapsed) ? Math.max(0, elapsed) : 0;
3158 }
3159 > codexAgent.ts
3160 > private async _sendMessage(chat: URI, prompt: string, attachments?: readonly MessageAttachment[], turnId?: string, workingDirectory?: URI): Promise<void> {
3161 const sessionUri = this._sessionUriFromChat(chat);
3162 this._logService.info(`[Codex DEBUG] sendMessage session=${sessionUri.toString()} prompt=${JSON.stringify(prompt).slice(0, 60)}`);
3292 }
3293 }
3294 > codexAgent.ts
3295 > setPendingMessages(chat: URI, steeringMessage: PendingMessage | undefined, _queuedMessages: readonly PendingMessage[]): void {
3296 // Queued messages are consumed server-side (AgentSideEffects drives a
3297 // fresh turn per `idle`); only the single steering message reaches the
3347 });
3348 }
3349 > codexAgent.ts
3350 > private async _abort(chat: URI): Promise<void> {
3351 const sessionUri = this._sessionUriFromChat(chat);
3352 const sessionId = AgentSession.id(sessionUri);
3375 }
3376 }
3377 > codexAgent.ts
3378 > async disposeSession(sessionUri: URI): Promise<void> {
3379 this._logService.info(`[Codex DEBUG] disposeSession session=${sessionUri.toString()}`);
3380 const sessionId = AgentSession.id(sessionUri);
3385 await this._teardownSessionInMemory(session, sessionId);
3386 }
3387 > codexAgent.ts
3388 > /**
3389 > * Non-destructive counterpart to {@link disposeSession}: releases the
3390 > * session's in-memory resources but keeps its codex thread resumable — the
3391 > * on-disk rollout is preserved and the shared codex process stays alive, so
3392 > * the session transparently resumes on the next access. Used by idle-session
3393 > * eviction to bound memory in long-lived host processes.
3394 > *
3395 > * No-ops for sessions that have nothing durable to resume from (provisional
3396 > * sessions whose codex thread was never started) and for sessions with a
3397 > * turn in flight — `thread/unsubscribe` mid-turn would drop live progress.
3398 > */
3399 > async releaseSession(sessionUri: URI): Promise<void> {
3400 const sessionId = AgentSession.id(sessionUri);
3401 const session = this._sessions.get(sessionId);
3417 await this._teardownSessionInMemory(session, sessionId);
3418 }
3419 > codexAgent.ts
3420 > /**
3421 > * Shared in-memory teardown for a codex session: drops the tracked entry,
3422 > * disposes its MCP controller, unparks pending approvals / client tool calls
3423 > * / user inputs, and unsubscribes the codex thread (`thread/unsubscribe`).
3424 > * Non-destructive — the codex thread's on-disk rollout is preserved, so the
3425 > * session can be resumed later. Shared by {@link disposeSession} (which the
3426 > * orchestrator pairs with durable deletion) and the non-destructive
3427 > * {@link releaseSession}.
3428 > */
3429 > private async _teardownSessionInMemory(session: ICodexSession, sessionId: string): Promise<void> {
3430 session.disposed = true;
3431 this._claimPrewarm(session);
3479 }
3480 }
3481 > codexAgent.ts
3482 > private async _changeModel(chat: URI, model: ModelSelection): Promise<void> {
3483 const sessionUri = this._sessionUriFromChat(chat);
3484 const session = this._sessions.get(AgentSession.id(sessionUri));
3490 }
3491 }
3492 > codexAgent.ts
3493 > async truncateSession(sessionUri: URI, turnId?: string): Promise<void> {
3494 // Codex rolls back by a count of trailing turns. Resolve how many turns
3495 // follow `turnId` (or all of them when omitted) from the persisted
3530 }
3531 }
3532 > codexAgent.ts
3533 > async onArchivedChanged(sessionUri: URI, isArchived: boolean): Promise<void> {
3534 const threadId = await this._resolveThreadId(sessionUri);
3535 if (threadId === undefined) {
3550 }
3551 }
3552 > codexAgent.ts
3553 > /** Resolve the codex thread id for a session: in-memory → persisted overlay. */
3554 > private async _resolveThreadId(sessionUri: URI): Promise<string | undefined> {
3555 const existing = this._sessions.get(AgentSession.id(sessionUri));
3556 if (existing?.threadId !== undefined) {
3560 return overlay.threadId;
3561 }
3562 > codexAgent.ts
3563 > respondToPermissionRequest(requestId: string, approved: boolean): void {
3564 // `requestId` is the host-side toolCallId; iterate sessions (including
3565 // live subagent child sessions, whose command approvals live on their
3581 this._logService.info(`[Codex] respondToPermissionRequest: unknown requestId=${requestId}`);
3582 }
3583 > codexAgent.ts
3584 > respondToUserInputRequest(requestId: string, response: ChatInputResponseKind, answers?: Record<string, ChatInputAnswer>): void {
3585 // `requestId` was minted per request; find the owning session and
3586 // resolve its parked deferred. Mirrors respondToPermissionRequest.
3592 this._logService.info(`[Codex] respondToUserInputRequest: unknown requestId=${requestId}`);
3593 }
3594 > codexAgent.ts
3595 > getSessionMessages(chat: URI): Promise<readonly Turn[]> {
3596 return this._readSession(this._sessionUriFromChat(chat)).then(read => read ? replayThreadToTurns(read.thread) : []);
3597 }
3598 > codexAgent.ts
3599 > async getSessionMetadata(session: URI): Promise<IAgentSessionMetadata | undefined> {
3600 const sessionId = AgentSession.id(session);
3601 const read = await this._readSession(session);
3626 return this._threadToMetadata(read.thread, session);
3627 }
3628 > codexAgent.ts
3629 > private async _readSession(session: URI): Promise<ThreadReadResponse | undefined> {
3630 // Resolve the codex thread id for this session URI. Resolution
3631 // order: in-memory session → persisted metadata overlay → URI host
3659 }
3660 }
3661 > codexAgent.ts
3662 > async listSessions(): Promise<IAgentSessionMetadata[]> {
3663 if (!this._githubToken) {
3664 return [];
3701 }
3702 }
3703 > codexAgent.ts
3704 > private _threadToMetadata(thread: Thread, sessionUri: URI): IAgentSessionMetadata {
3705 return {
3706 session: sessionUri,
3712 };
3713 }
3714 > codexAgent.ts
3715 > setServerToolHost(host: IAgentServerToolHost): void {
3716 this._serverToolHost = host;
3717 }
3718 > codexAgent.ts
3719 > getOrCreateActiveClient(session: URI, client: { readonly clientId: string; readonly displayName?: string }): IActiveClient {
3720 const sessionId = AgentSession.id(session);
3721 return new CodexActiveClientHandle(
3727 );
3728 }
3729 > codexAgent.ts
3730 > removeActiveClient(session: URI, clientId: string): void {
3731 const sessionId = AgentSession.id(session);
3732 const sess = this._sessions.get(sessionId);
3737 }
3738 }
3739 > codexAgent.ts
3740 > onClientToolCallComplete(session: URI, _chat: URI, toolCallId: string, result: ToolCallResult): void {
3741 const sessionId = AgentSession.id(session);
3742 const sess = this._sessions.get(sessionId);
3745 sess?.pendingClientToolCalls.respondOrBuffer(toolCallId, result);
3746 }
3747 > codexAgent.ts
3748 > // ---- Client-pushed plugin customizations -------------------------------
3749 >
3750 > /**
3751 > * Materialize + parse a client's pushed plugin customizations and store
3752 > * them on the session. Mirrors the Claude client-plugin path: the shared
3753 > * {@link IAgentPluginManager} copies each plugin to local disk (nonce
3754 > * cached), we parse the resulting directory into its
3755 > * {@link IParsedPlugin | components}, publish the customization surface,
3756 > * and refresh the process-global skill roots. MCP servers are attached
3757 > * per-thread at the next {@link _materialize}.
3758 > */
3759 > private async _syncClientCustomizations(sessionUri: URI, clientId: string, customizations: readonly ClientPluginCustomization[]): Promise<void> {
3760 const session = this._sessions.get(AgentSession.id(sessionUri));
3761 if (!session) {
3778 await this._refreshSkillExtraRoots();
3779 }
3780 > codexAgent.ts
3781 > /** Parse one synced plugin directory into its components (best-effort). */
3782 > private async _parseClientPlugin(session: ICodexSession, synced: ISyncedCustomization): Promise<ICodexClientPlugin> {
3783 if (!synced.pluginDir) {
3784 return { synced, parsed: undefined };
3792 }
3793 }
3794 > codexAgent.ts
3795 > /** Publish the session's client-plugin customizations as upsert actions. */
3796 > private _publishClientCustomizations(session: ICodexSession): void {
3797 for (const customization of session.clientCustomizations.toCustomizations()) {
3798 this._fire(session.sessionUri, { type: ActionType.SessionCustomizationUpdated, customization });
3799 }
3800 }
3801 > codexAgent.ts
3802 > /**
3803 > * Recompute the process-global skill roots from every live session's
3804 > * enabled client plugins and push them to codex via `skills/extraRoots/set`.
3805 > * codex's extra skill roots are a single shared list (there is no per-thread
3806 > * equivalent), so we send the union across all sessions — which matches the
3807 > * global nature of client plugin choices. No-op when the connection is not
3808 > * ready; the next {@link _materialize} re-applies.
3809 > */
3810 > private async _refreshSkillExtraRoots(): Promise<void> {
3811 if (this._connection.kind !== 'ready') {
3812 return;
3828 }
3829 }
3830 > codexAgent.ts
3831 > // ---- MCP servers -------------------------------------------------------
3832 >
3833 > /**
3834 > * Surfaces codex's MCP servers to AHP clients as per-session
3835 > * customizations. Codex has no plugin/directory customization layer, so
3836 > * every server is a bare top-level {@link McpServerCustomization}. The
3837 > * returned snapshot reflects the current connection-global inventory;
3838 > * subsequent lifecycle transitions arrive as customization actions
3839 > * emitted by the session's {@link McpCustomizationController}.
3840 > */
3841 > async getSessionCustomizations(sessionUri: URI): Promise<readonly Customization[]> {
3842 const session = this._sessions.get(AgentSession.id(sessionUri));
3843 if (!session) {
3860 ];
3861 }
3862 > codexAgent.ts
3863 > /**
3864 > * Fetches the skills and hooks codex has loaded for `session`'s working
3865 > * directory (`skills/list` + `hooks/list`, both cwd-scoped) and projects
3866 > * them into {@link DirectoryCustomization} containers. Best-effort: returns
3867 > * an empty array when no connection is ready, no working directory is known,
3868 > * or the app-server rejects the request.
3869 > */
3870 > private async _fetchSkillHookContainers(session: ICodexSession): Promise<DirectoryCustomization[]> {
3871 if (this._connection.kind !== 'ready' || !session.workingDirectory) {
3872 return [];
3882 return [...codexSkillsToContainers(skills), ...codexHooksToContainers(hooks)];
3883 }
3884 > codexAgent.ts
3885 > /**
3886 > * Re-fetches this session's skill/hook customizations and upserts each
3887 > * container into session state via {@link ActionType.SessionCustomizationUpdated}.
3888 > * Called after materialization (when the connection is ready and the cwd is
3889 > * known) so the workbench Customizations surface reflects what codex loaded
3890 > * from the working directory's `.agents`/`.codex` folders. Upserts (keyed by
3891 > * customization id) leave the MCP customizations untouched.
3892 > */
3893 > private async _refreshSkillHookCustomizations(session: ICodexSession): Promise<void> {
3894 if (session.disposed) {
3895 return;
3903 }
3904 }
3905 > codexAgent.ts
3906 > /**
3907 > * Routes an MCP request received on this session's `mcp://` side channel
3908 > * to codex. Read-only methods (`tools/list`, `resources/list`,
3909 > * `resources/templates/list`) are answered from the cached inventory;
3910 > * `tools/call` and `resources/read` round-trip to the app-server with the
3911 > * session's thread id. Unknown servers / methods reject with
3912 > * `Method not found` so the protocol server maps them to JSON-RPC
3913 > * `-32601`.
3914 > */
3915 > async handleMcpRequest(sessionUri: URI, serverName: string, method: string, params: Record<string, unknown> | undefined): Promise<unknown> {
3916 const sessionId = AgentSession.id(sessionUri);
3917 const session = this._sessions.get(sessionId);
3959 }
3960 }
3961 > codexAgent.ts
3962 > async startMcpServer(sessionUri: URI, id: string): Promise<void> {
3963 const session = this._sessions.get(AgentSession.id(sessionUri));
3964 const serverName = session ? this._resolveMcpServerName(session, id) : undefined;
3971 await this._refreshMcpInventory(conn.client);
3972 }
3973 > codexAgent.ts
3974 > async stopMcpServer(sessionUri: URI, id: string): Promise<void> {
3975 const session = this._sessions.get(AgentSession.id(sessionUri));
3976 const serverName = session ? this._resolveMcpServerName(session, id) : undefined;
3981 // TODO: Wire this when Codex exposes a typed MCP server stop request.
3982 }
3983 > codexAgent.ts
3984 > private _resolveMcpServerName(session: ICodexSession, id: string): string | undefined {
3985 const controller = this._getOrCreateMcpController(session);
3986 controller.applyAll(inventoryToSdkServers(this._mcpInventory));
3988 return controller.serverNameForCustomizationId(id);
3989 }
3990 > codexAgent.ts
3991 > /**
3992 > * Lazily create the per-session {@link McpCustomizationController}. Not
3993 > * registered on the agent (sessions come and go) — disposed explicitly
3994 > * when the session is removed.
3995 > */
3996 > private _getOrCreateMcpController(session: ICodexSession): McpCustomizationController {
3997 if (!session.mcpController) {
3998 session.mcpController = this._instantiationService.createInstance(McpCustomizationController, {
4007 return session.mcpController;
4008 }
4009 > codexAgent.ts
4010 > /** Mirrors the connection-global inventory onto every live session. */
4011 > private _applyMcpInventoryToSessions(): void {
4012 const servers = inventoryToSdkServers(this._mcpInventory);
4013 for (const session of this._sessions.values()) {
4020 }
4021 }
4022 > codexAgent.ts
4023 > /**
4024 > * Refreshes the session's mapper snapshot of server name → customization id
4025 > * (read when stamping the MCP contributor on tool calls). Plain data, owned
4026 > * here — the mapper never reaches back into the controller. Must run on every
4027 > * inventory change because MCP servers are discovered asynchronously, after a
4028 > * session (and possibly its first tool call) already exists.
4029 > */
4030 > private _refreshMcpCustomizationIds(session: ICodexSession, controller: McpCustomizationController): void {
4031 const ids = session.mapState.mcpCustomizationIds;
4032 ids.clear();
4038 }
4039 }
4040 > codexAgent.ts
4041 > /**
4042 > * Re-reads the full MCP inventory from the app-server (paginated) and
4043 > * re-publishes it to every session. Fires `notifications/tools/list_changed`
4044 > * on each ready channel whose tool set changed.
4045 > */
4046 > private async _refreshMcpInventory(client: ICodexAppServerClient): Promise<void> {
4047 let data: ListMcpServerStatusResponse['data'] = [];
4048 try {
4084 }
4085 }
4086 > codexAgent.ts
4087 > /**
4088 > * Handles a `mcpServer/startupStatus/updated` notification. `ready`
4089 > * triggers a full inventory refresh (to pull the now-loaded tools);
4090 > * other transitions update the cached state in place so the UI sees the
4091 > * server settle into starting/error/stopped promptly.
4092 > */
4093 > private _handleMcpStartupStatus(client: ICodexAppServerClient, name: string, status: McpServerStartupState, error: string | null): void {
4094 if (this._connection.kind === 'ready' && this._connection.client !== client) {
4095 return;
4121 this._setMcpServerState(name, translateCodexMcpStartupState(status, error));
4122 }
4123 > codexAgent.ts
4124 > /** Upserts a server's lifecycle state in the inventory (preserving cached tools) and republishes. */
4125 > private _setMcpServerState(name: string, state: McpServerState): void {
4126 const prev = this._mcpInventory.get(name);
4127 this._mcpInventory.set(name, {
4133 this._applyMcpInventoryToSessions();
4134 }
4135 > codexAgent.ts
4136 > /**
4137 > * Surfaces an auth-gated http MCP server as {@link McpServerStatus.AuthRequired}
4138 > * so the workbench runs the *same* OAuth sign-in it uses for the Copilot
4139 > * agent. codex's `failed` notification carries no RFC 9728 metadata, and the
4140 > * workbench's `resolveMcpServerAuthentication` needs the resource's
4141 > * `authorization_servers` to know where to sign in — so we discover the
4142 > * Protected Resource Metadata (`<url>/.well-known/oauth-protected-resource`)
4143 > * here, mirroring the discovery the Copilot SDK does internally. On
4144 > * discovery failure we still surface `AuthRequired` with bare metadata (the
4145 > * server genuinely needs auth); the one-click sign-in just can't complete
4146 > * without the authorization server, which is logged.
4147 > */
4148 > private async _surfaceMcpAuthRequired(client: ICodexAppServerClient, name: string, url: string, error: string | null): Promise<void> {
4149 let resource: ProtectedResourceMetadata = { resource: url, resource_name: name };
4150 let requiredScopes: string[] | undefined;
4184 });
4185 }
4186 > codexAgent.ts
4187 > /**
4188 > * Broadcasts `notifications/tools/list_changed` for `serverName` on every
4189 > * session whose channel for that server is currently ready. Clients
4190 > * refetch `tools/list` in response.
4191 > */
4192 > private _fireMcpToolsListChanged(serverName: string): void {
4193 for (const session of this._sessions.values()) {
4194 const channel = session.mcpController?.channelForServer(serverName);
4198 }
4199 }
4200 > codexAgent.ts
4201 > /**
4202 > * Ensures the session has a materialized codex thread and returns its id.
4203 > * MCP tool calls (`mcpServer/tool/call`) are thread-scoped, so a call
4204 > * arriving before the first turn lazily starts the thread.
4205 > */
4206 > private async _ensureThreadId(session: ICodexSession): Promise<string> {
4207 await this._materializeIfNeeded(session, false);
4208 if (session.threadId === undefined) {
4211 return session.threadId;
4212 }
4213 > codexAgent.ts
4214 > async shutdown(): Promise<void> {
4215 this._disposeConnection();
4216 for (const s of this._sessions.values()) {
4224 this._mcpInventory.clear();
4225 }
4226 > codexAgent.ts
4227 > resolveSessionConfig(params: IAgentResolveSessionConfigParams): Promise<ResolveSessionConfigResult> {
4228 const values = codexSessionConfigSchema.validateOrDefault(params.config, codexSessionConfigDefaults);
4229 const schema = codexVisibleSessionConfigSchema.toProtocol();
4253 return Promise.resolve({ values: resolvedValues, schema });
4254 }
4255 > codexAgent.ts
4256 > async sessionConfigCompletions(params: IAgentSessionConfigCompletionsParams): Promise<SessionConfigCompletionsResult> {
4257 if (params.property !== CodexSessionConfigKey.AdditionalDirectories) {
4258 return { items: [] };
4283 }
4284 }
4285 > codexAgent.ts
4286 > // #endregion
4287 >
4288 > private _fire(sessionUri: URI, action: SessionAction | ChatAction): void {
4289 this._onDidSessionProgress.fire({ kind: 'action', resource: isChatAction(action) ? URI.parse(buildDefaultChatUri(sessionUri)) : sessionUri, action });
4290 }
4291 > codexAgent.ts
4292 > override dispose(): void {
4293 this._disposeConnection();
4294 for (const s of this._sessions.values()) {
4307 super.dispose();
4308 }
4309 > } codexAgent.ts
4310 >
4311 function parseBinaryArgs(json: string | undefined): string[] {
4312 if (!json) {
4320 }
4321 }
4322 > codexAgent.ts
4323 > /**
4324 > * The suffix Codex uses for its platform `optionalDependencies` packages
4325 > * (`@openai/codex-${suffix}`). Codex's Linux binaries are statically
4326 > * musl-linked and ship under the same `linux-<arch>` package regardless of
4327 > * host libc, so this never returns a `-musl` suffix.
4328 > *
4329 > * Returns undefined for unsupported `(platform, arch)` combinations — the
4330 > * caller surfaces the error.
4331 > */
4332 > export function codexPackageSuffix(platform: NodeJS.Platform, arch: string): string | undefined {
4333 if ((platform !== 'linux' && platform !== 'darwin' && platform !== 'win32') ||
4334 (arch !== 'x64' && arch !== 'arm64')) {
4337 return `${platform}-${arch}`;
4338 }
4339 > codexAgent.ts
4340 > /**
4341 > * Mirrors the triple table inside `@openai/codex/bin/codex.js` so we can spawn
4342 > * the native binary at `vendor/<triple>/bin/codex` directly without going
4343 > * through the JS shim launcher.
4344 > */
4345 > export function codexBinaryTriple(sdkTarget: string): string | undefined {
4346 switch (sdkTarget) {
4347 case 'linux-x64': return 'x86_64-unknown-linux-musl';
4354 }
4355 }
4356 > codexAgent.ts
4357 > /**
4358 > * Locate the SDK root for the dev (running-from-source) fallback by resolving
4359 > * `@openai/codex` — a devDependency in source checkouts — out of this repo's
4360 > * `node_modules`. Returns the directory that *contains* that `node_modules`
4361 > * (i.e. the value `_startConnection` joins `node_modules/@openai/codex-<target>`
4362 > * onto), or undefined when the package can't be resolved (e.g. a built product
4363 > * where it isn't shipped). `@openai/codex` declares no `exports` map, so its
4364 > * `package.json` is resolvable.
4365 > *
4366 > * `resolvePackageJsonPath` is a seam for tests; production resolves the path
4367 > * via {@link defaultResolveCodexPackageJsonPath}.
4368 > */
4369 export async function resolveCodexDevSdkRoot(
4370 resolvePackageJsonPath: () => string | Promise<string> = defaultResolveCodexPackageJsonPath,
4378 }
4379 }
4380 > codexAgent.ts
4381 async function defaultResolveCodexPackageJsonPath(): Promise<string> {
4382 // Dynamic import of `node:module` (not a static top-level import): the
src/vs/platform/agentHost/node/codex/codexSessionConfigKeys.ts 86 introduced LOC · 11 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- codexSessionConfigKeys.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import type { ReasoningEffort } from './protocol/generated/ReasoningEffort.js';
7 > import type { ReasoningSummary } from './protocol/generated/ReasoningSummary.js';
8 > import type { Personality } from './protocol/generated/Personality.js';
9 > import type { WebSearchMode } from './protocol/generated/WebSearchMode.js';
10 > import type { ModeKind } from './protocol/generated/ModeKind.js';
11 > import type { SandboxMode } from './protocol/generated/v2/SandboxMode.js';
12 > import { CodexSessionConfigKey, CODEX_DEFAULT_PERMISSIONS_PRESET, narrowCodexPermissionsPreset, presetForResolvedPermissions, resolveCodexPermissionsPreset, type CodexApprovalPolicy, type ICodexResolvedPermissions } from '../../common/codexSessionConfigKeys.js';
13 >
14 > // Re-export the shared, protocol-free config-key surface so node callers can
15 > // keep importing everything from this module.
16 > export { CodexSessionConfigKey, resolveCodexPermissionsPreset, presetForResolvedPermissions, narrowCodexPermissionsPreset, CODEX_PERMISSIONS_PRESETS, CODEX_DEFAULT_PERMISSIONS_PRESET } from '../../common/codexSessionConfigKeys.js';
17 > export type { CodexApprovalPolicy, CodexPermissionsPreset, CodexSandboxMode, CodexApprovalsReviewer, ICodexResolvedPermissions } from '../../common/codexSessionConfigKeys.js';
18 >
19 > export function narrowApprovalPolicy(value: unknown): CodexApprovalPolicy | undefined {
20 switch (value) {
21 case 'never':
28 }
29 }
31 > export function narrowSandboxMode(value: unknown): SandboxMode | undefined {
32 switch (value) {
33 case 'read-only':
39 }
40 }
42 > /**
43 > * Resolve the Codex security axes (approval policy, sandbox, approvals
44 > * reviewer) for a session's stored config values.
45 > *
46 > * The user-facing {@link CodexSessionConfigKey.PermissionsPreset} is the source
47 > * of truth; when present it expands into all three axes. For backward
48 > * compatibility (older sessions / programmatic config) we fall back to the
49 > * individual {@link CodexSessionConfigKey.ApprovalPolicy} /
50 > * {@link CodexSessionConfigKey.SandboxMode} keys with a `user` reviewer.
51 > */
52 > export function resolveCodexPermissions(
53 values: Record<string, unknown> | undefined,
54 defaults: { approvalPolicy: CodexApprovalPolicy; sandboxMode: SandboxMode },
64 };
65 }
67 > /**
68 > * Decide how a restored session's three permission keys (`permissionsPreset`,
69 > * `approvalPolicy`, `sandboxMode`) should be represented, given its raw
70 > * persisted config values.
71 > *
72 > * This exists to prevent a silent privilege escalation on restore: a legacy
73 > * session that persisted only the individual axes (for example
74 > * `sandboxMode = 'read-only'`) and never chose a preset must not have a
75 > * materialized `permissionsPreset = 'default'` inserted on top of it, because
76 > * {@link resolveCodexPermissions} checks the preset first and would resume the
77 > * session as `workspace-write`.
78 > *
79 > * The returned object contains ONLY the permission keys that should be present
80 > * afterwards, so callers should drop all three permission keys before applying
81 > * it:
82 > * - an explicitly chosen preset is kept as-is;
83 > * - legacy axes that map exactly onto a preset are migrated to that preset
84 > * (single source of truth) and the raw axes dropped;
85 > * - legacy axes with a `workspace-write` or `danger-full-access` sandbox that
86 > * do NOT map exactly onto a preset are snapped to the preset whose sandbox
87 > * matches (`default` / `full-access`). This keeps the resolved axes in sync
88 > * with the preset the "Approvals" chip displays, so a legacy
89 > * `approvalPolicy = 'never'` + `workspace-write` session resolves to the
90 > * `default` preset's `on-request` policy (and actually prompts) instead of
91 > * silently running without approval while the chip claims "Default
92 > * Permissions". Snapping never grants more sandbox access than the legacy
93 > * value already had;
94 > * - legacy axes with a `read-only` sandbox (which no preset expands to, and
95 > * which is more locked-down than any preset) are preserved verbatim and no
96 > * preset is surfaced, so restore never silently escalates them to
97 > * `workspace-write`.
98 > */
99 > export function migrateCodexPermissionValues(
100 config: Record<string, unknown> | undefined,
101 defaults: { approvalPolicy: CodexApprovalPolicy; sandboxMode: SandboxMode },
128 };
129 }
131 > export function narrowAdditionalDirectories(value: unknown): readonly string[] | undefined {
132 if (!Array.isArray(value)) {
133 return undefined;
135 return value.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0);
136 }
138 > export function narrowBoolean(value: unknown): boolean | undefined {
139 return typeof value === 'boolean' ? value : undefined;
140 }
142 > export function narrowWebSearchMode(value: unknown): WebSearchMode | undefined {
143 switch (value) {
144 case 'disabled':
150 }
151 }
153 > export function narrowReasoningEffort(value: unknown): ReasoningEffort | undefined {
154 switch (value) {
155 case 'none':
164 }
165 }
167 > export function narrowPersonality(value: unknown): Personality | undefined {
168 switch (value) {
169 case 'none':
175 }
176 }
178 > export function narrowReasoningSummary(value: unknown): ReasoningSummary | undefined {
179 switch (value) {
180 case 'auto':
187 }
188 }
190 > /**
191 > * Map the platform-generic {@link SessionMode} (Agent Mode) to codex's native
192 > * collaboration {@link ModeKind}: VS Code "Plan" → codex `plan`, "Interactive"
193 > * → codex `default`.
194 > */
195 > export function collaborationModeKind(value: unknown): ModeKind {
196 return value === 'plan' ? 'plan' : 'default';
197 }
src/vs/platform/agentHost/common/codexSessionConfigKeys.ts 85 introduced LOC · 3 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- codexSessionConfigKeys.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > /**
7 > * Well-known session-config keys advertised by the agent-host Codex provider
8 > * in its `resolveSessionConfig` schema.
9 > *
10 > * This file is intentionally protocol-free (no imports from the generated
11 > * `node/protocol` types) so it can be shared with the browser pickers, which
12 > * cannot import from the `node` layer. The string-literal unions below are
13 > * declared to match — and are structurally assignable to — the corresponding
14 > * generated Codex app-server types (`AskForApproval`, `SandboxMode`,
15 > * `ApprovalsReviewer`). Protocol-typed narrowing helpers live alongside the
16 > * node agent in `node/codex/codexSessionConfigKeys.ts`.
17 > */
18 > export const enum CodexSessionConfigKey {
19 > PermissionsPreset = 'codex.permissionsPreset',
20 > ApprovalPolicy = 'codex.approvalPolicy',
21 > SandboxMode = 'codex.sandboxMode',
22 > AdditionalDirectories = 'codex.additionalDirectories',
23 > NetworkAccessEnabled = 'codex.networkAccessEnabled',
24 > WebSearchMode = 'codex.webSearchMode',
25 > ModelReasoningEffort = 'codex.modelReasoningEffort',
26 > Personality = 'codex.personality',
27 > ReasoningSummary = 'codex.reasoningSummary',
28 > }
29 >
30 > /** Subset of the generated `AskForApproval` union that VS Code exposes. */
31 > export type CodexApprovalPolicy = 'never' | 'on-request' | 'on-failure' | 'untrusted';
32 >
33 > /** Mirrors the generated `SandboxMode` union. */
34 > export type CodexSandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access';
35 >
36 > /** Mirrors the generated `ApprovalsReviewer` union. */
37 > export type CodexApprovalsReviewer = 'user' | 'auto_review' | 'guardian_subagent';
38 >
39 > /**
40 > * Codex collapses its three security axes (sandbox × approval policy ×
41 > * approvals reviewer) into a single user-facing "permissions" preset, matching
42 > * the selector in the Codex app and IDE extension.
43 > *
44 > * @see https://developers.openai.com/codex/concepts/sandboxing#how-you-control-it
45 > */
46 > export type CodexPermissionsPreset = 'default' | 'auto-review' | 'full-access';
47 >
48 > /** Ordered preset list advertised in the Codex session-config schema. */
49 > export const CODEX_PERMISSIONS_PRESETS: readonly CodexPermissionsPreset[] = ['default', 'auto-review', 'full-access'];
50 >
51 > /** Default preset applied to new Codex sessions. */
52 > export const CODEX_DEFAULT_PERMISSIONS_PRESET: CodexPermissionsPreset = 'default';
53 >
54 > /**
55 > * Single source of truth for narrowing an arbitrary runtime value to the
56 > * closed {@link CodexPermissionsPreset} union. Returns `undefined` for
57 > * non-strings or unmatched strings; callers apply their own fallback.
58 > */
59 > export function narrowCodexPermissionsPreset(raw: unknown): CodexPermissionsPreset | undefined {
60 switch (raw) {
61 case 'default':
67 }
68 }
70 > export interface ICodexResolvedPermissions {
71 > readonly approvalPolicy: CodexApprovalPolicy;
72 > readonly sandboxMode: CodexSandboxMode;
73 > readonly approvalsReviewer: CodexApprovalsReviewer;
74 > }
75 >
76 > /**
77 > * Expand a {@link CodexPermissionsPreset} into the three underlying Codex
78 > * security axes sent to the app-server (`approvalPolicy`, `sandbox`,
79 > * `approvalsReviewer`).
80 > */
81 > export function resolveCodexPermissionsPreset(preset: CodexPermissionsPreset): ICodexResolvedPermissions {
82 switch (preset) {
83 case 'auto-review':
92 }
93 }
95 > /**
96 > * Inverse of {@link resolveCodexPermissionsPreset}: find the preset whose
97 > * expanded axes exactly match the given resolved permissions, or `undefined`
98 > * when no preset can represent them (e.g. a `read-only` sandbox, which no
99 > * preset expands to).
100 > *
101 > * Used when restoring a legacy session that persisted the individual security
102 > * axes but no preset: if the axes map cleanly onto a preset we can migrate them
103 > * to the modern single-preset representation; otherwise the raw axes must be
104 > * preserved so they are not silently escalated.
105 > */
106 > export function presetForResolvedPermissions(resolved: ICodexResolvedPermissions): CodexPermissionsPreset | undefined {
107 for (const preset of CODEX_PERMISSIONS_PRESETS) {
108 const axes = resolveCodexPermissionsPreset(preset);
src/vs/platform/agentHost/node/codex/codexSessionMetadataStore.ts 63 introduced LOC · 4 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- codexSessionMetadataStore.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { URI } from '../../../../base/common/uri.js';
7 > import { ILogService } from '../../../log/common/log.js';
8 > import { ISessionDataService } from '../../common/sessionDataService.js';
9 >
10 > /**
11 > * Per-session bookkeeping codex needs to persist across agent host
12 > * restarts. The fundamental tension this store resolves: codex's
13 > * `thread/start` mints the canonical thread id server-side, but the
14 > * workbench owns the chat session URI and refuses to accept a different
15 > * one back from `createSession`. We therefore keep a stable mapping
16 > * `workbench session URI ↔ codex thread id` here so restored sessions
17 > * can be resumed without leaking duplicate sidebar entries.
18 > *
19 > * Layout (per-session SQLite DB, opened via {@link ISessionDataService}):
20 > * `codex.threadId` — the codex app-server thread id assigned at
21 > * materialize time.
22 > * `codex.cwd` — absolute path to the working directory the
23 > * session was created against (URI string).
24 > * `codex.model` — serialized {@link ModelSelection.id} string,
25 > * remembered for restore so resumed sessions reuse
26 > * the model picked during the prior process.
27 > */
28 >
29 > export interface ICodexSessionOverlay {
30 > readonly threadId?: string;
31 > readonly cwd?: URI;
32 > readonly modelId?: string;
33 > }
34 >
35 > export interface ICodexSessionOverlayUpdate {
36 > readonly threadId?: string;
37 > readonly cwd?: URI;
38 > readonly modelId?: string;
39 > }
40 >
41 > export class CodexSessionMetadataStore {
42 >
43 > private static readonly KEY_THREAD_ID = 'codex.threadId';
44 > private static readonly KEY_CWD = 'codex.cwd';
45 > private static readonly KEY_MODEL = 'codex.model';
46 >
47 > constructor(
48 @ISessionDataService private readonly _sessionDataService: ISessionDataService,
49 @ILogService private readonly _logService: ILogService,
50 ) { }
52 > /**
53 > * Persist the supplied overlay fields. Only-write-on-defined.
54 > * Best-effort: failures are logged and swallowed because the caller
55 > * has already committed in-memory state and a corrupt DB shouldn't
56 > * abort the current turn.
57 > */
58 > async write(session: URI, fields: ICodexSessionOverlayUpdate): Promise<void> {
59 try {
60 const ref = this._sessionDataService.openDatabase(session);
79 }
80 }
82 > /**
83 > * Read overlay fields for `session`. Returns `{}` when no DB has
84 > * been created yet (fresh session, or external codex CLI thread the
85 > * workbench has never touched).
86 > */
87 > async read(session: URI): Promise<ICodexSessionOverlay> {
88 try {
89 const ref = await this._sessionDataService.tryOpenDatabase(session);