@meistrari/agent-core 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +24 -3
  2. package/bin/supervisor.ts +116 -0
  3. package/package.json +42 -3
  4. package/scripts/build-supervisor-executable.ts +32 -0
  5. package/src/agents/agent-error-serializer.ts +23 -0
  6. package/src/agents/agent-event-stream.ts +81 -0
  7. package/src/agents/agent-id.ts +17 -0
  8. package/src/agents/agent-operation.ts +11 -0
  9. package/src/agents/agent-provider.ts +37 -0
  10. package/src/agents/agent-run.ts +32 -0
  11. package/src/agents/agent-runtime-error.ts +161 -0
  12. package/src/agents/agent-session-events.ts +32 -0
  13. package/src/agents/agent-tool-runner.ts +83 -0
  14. package/src/agents/agent-tool.ts +111 -0
  15. package/src/agents/author-context.ts +36 -0
  16. package/src/agents/claude/claude-command-mapper.ts +234 -0
  17. package/src/agents/claude/claude-event-mapper.ts +736 -0
  18. package/src/agents/claude/claude-provider.ts +191 -0
  19. package/src/agents/claude/claude-run.ts +464 -0
  20. package/src/agents/claude/claude-tool-mapper.ts +186 -0
  21. package/src/agents/claude/index.ts +1 -0
  22. package/src/agents/codex/codex-auth.ts +34 -0
  23. package/src/agents/codex/codex-command-mapper.ts +78 -0
  24. package/src/agents/codex/codex-event-mapper.ts +708 -0
  25. package/src/agents/codex/codex-json-rpc-client.ts +326 -0
  26. package/src/agents/codex/codex-protocol.ts +36 -0
  27. package/src/agents/codex/codex-provider.ts +1050 -0
  28. package/src/agents/codex/codex-run.ts +404 -0
  29. package/src/agents/codex/codex-skill-catalog.ts +158 -0
  30. package/src/agents/codex/codex-skill-roots.ts +19 -0
  31. package/src/agents/codex/codex-tool-mapper.ts +55 -0
  32. package/src/agents/codex/codex.errors.ts +68 -0
  33. package/src/agents/codex/generated/meta.gen.ts +606 -0
  34. package/src/agents/codex/generated/namespaces.gen.ts +311 -0
  35. package/src/agents/codex/generated/schema.gen.ts +34883 -0
  36. package/src/agents/codex/index.ts +2 -0
  37. package/src/agents/index.ts +14 -0
  38. package/src/agents/input-attachment-preparation.ts +86 -0
  39. package/src/agents/input-attachment.errors.ts +16 -0
  40. package/src/agents/instructions.ts +8 -0
  41. package/src/agents/materialized-input-attachment.ts +26 -0
  42. package/src/agents/message-id.ts +23 -0
  43. package/src/agents/normalize.ts +8 -0
  44. package/src/agents/sandbox-environment.ts +1 -0
  45. package/src/agents/tools/ping.tool.ts +13 -0
  46. package/src/agents/user-input-request.ts +470 -0
  47. package/src/provenance.gen.ts +3 -3
  48. package/src/supervisor/agent-provider-factory.ts +189 -0
  49. package/src/supervisor/bootstrap-binder.ts +125 -0
  50. package/src/supervisor/config.ts +49 -0
  51. package/src/supervisor/control-authority-verifier.ts +135 -0
  52. package/src/supervisor/create-supervisor-runtime.ts +25 -0
  53. package/src/supervisor/errors.ts +24 -0
  54. package/src/supervisor/index.ts +34 -0
  55. package/src/supervisor/persistence/json.ts +21 -0
  56. package/src/supervisor/persistence/state-discovery.ts +56 -0
  57. package/src/supervisor/persistence/supervisor-store.ts +364 -0
  58. package/src/supervisor/ports/index.ts +109 -0
  59. package/src/supervisor/provider-factory.ts +37 -0
  60. package/src/supervisor/resident.ts +143 -0
  61. package/src/supervisor/rpc-client.ts +120 -0
  62. package/src/supervisor/runtime-handler.ts +309 -0
  63. package/src/supervisor/websocket-server.ts +434 -0
  64. package/src/supervisor-protocol/bootstrap.ts +1 -1
  65. package/src/template-onboarding.ts +47 -0
  66. package/src/testing/es256-test-keys.ts +73 -0
  67. package/src/testing/in-memory-runtime-control-plane.ts +205 -0
  68. package/src/testing/index.ts +6 -0
  69. package/src/testing/loopback-supervisor-connection.ts +71 -0
  70. package/src/testing/scripted-provider.ts +64 -0
  71. package/src/worker-runtime-client/command-pump.ts +132 -0
  72. package/src/worker-runtime-client/connection-attempt.ts +340 -0
  73. package/src/worker-runtime-client/control-authority-signer.ts +100 -0
  74. package/src/worker-runtime-client/e2b-supervisor-connection.ts +102 -0
  75. package/src/worker-runtime-client/frame-processor.ts +178 -0
  76. package/src/worker-runtime-client/index.ts +27 -0
  77. package/src/worker-runtime-client/lease-reconciler.ts +14 -0
  78. package/src/worker-runtime-client/ports.ts +137 -0
  79. package/src/worker-runtime-client/postgres-notification-listener.ts +91 -0
  80. package/src/worker-runtime-client/rpc-dispatcher.ts +27 -0
  81. package/src/worker-runtime-client/rpc-request-manager.ts +141 -0
  82. package/src/worker-runtime-client/sandbox-connection-runtime.ts +300 -0
  83. package/src/worker-runtime-client/token-crypto.ts +46 -0
@@ -0,0 +1,1050 @@
1
+ import type { AgentReasoningEffort, AgentToolDefinition, CodexModelId } from '../../protocol'
2
+ import type { AgentOperationOptions } from '../agent-operation'
3
+ import type { AgentProvider, AgentSessionOpenInput, AgentSessionResumeInput } from '../agent-provider'
4
+ import type { AgentTool } from '../agent-tool'
5
+ import type { InstructionComposer } from '../instructions'
6
+ import type { CodexAuthentication, CodexAuthProvider, CodexAuthRefreshInput, CodexAuthTokens } from './codex-auth'
7
+ import type { CodexEventMapperState, CodexNotification } from './codex-event-mapper'
8
+ import type { CodexJsonRpcMessage, CodexServerRequest } from './codex-json-rpc-client'
9
+ import type { ClientRequestResponsesByMethod, CodexChatGPTAuthTokensRefreshParams } from './codex-protocol'
10
+ import { randomUUID } from 'node:crypto'
11
+ import { lstat, open, readFile, realpath, rename, unlink } from 'node:fs/promises'
12
+ import { basename, dirname, join, resolve } from 'node:path'
13
+ import { codexModelIdSchema } from '../../protocol'
14
+ import { serializeAgentError } from '../agent-error-serializer'
15
+ import { AgentNotAcceptedError, AgentProviderMetadataError, AgentProviderProtocolError, AgentProviderRuntimeError } from '../agent-runtime-error'
16
+ import { sessionConfiguredEvent } from '../agent-session-events'
17
+ import { selectAgentTools, toolByName } from '../agent-tool'
18
+ import { emptyInstructionComposer } from '../instructions'
19
+ import { UserInputRequestStore, userInputRequestStoreRootFromTranscriptPath } from '../user-input-request'
20
+ import { cacheCodexThreadMetadata, codexNotificationFromMessage, createCodexEventMapperState, mapCodexNotification } from './codex-event-mapper'
21
+ import { CodexJsonRpcClient, isCodexServerRequest } from './codex-json-rpc-client'
22
+ import { CodexRun } from './codex-run'
23
+ import { catalogRepositorySkillRoots, stableRepositoryRoots, workspaceSkillRoots } from './codex-skill-roots'
24
+ import { runCodexDynamicTool, toCodexDynamicTools } from './codex-tool-mapper'
25
+
26
+ export interface CodexStartupStageFact {
27
+ event: 'sandbox.startup-stage'
28
+ stage: 'codex-initialize' | 'auth-acquisition-login' | 'thread-start' | 'thread-resume'
29
+ outcome: 'success' | 'failure'
30
+ duration: number
31
+ sessionId: string
32
+ }
33
+
34
+ export interface CodexProviderConfig {
35
+ auth: CodexAuthProvider | CodexAuthentication
36
+ tools?: readonly AgentTool[]
37
+ executable?: string
38
+ environment?: Record<string, string | undefined>
39
+ onStartupStage?: (fact: CodexStartupStageFact) => void
40
+ instructions?: InstructionComposer
41
+ }
42
+
43
+ interface CodexRunHolder {
44
+ toolsByName: Map<string, AgentTool>
45
+ run?: CodexRun
46
+ authTokens?: CodexAuthTokens
47
+ startupFailure?: Error
48
+ }
49
+ type CodexAuthMaterial
50
+ = | { kind: 'chatgpt', tokens: CodexAuthTokens }
51
+ | { kind: 'api-key', apiKey: string }
52
+ interface CodexConnection {
53
+ client: CodexJsonRpcClient
54
+ abortController: AbortController
55
+ state: CodexEventMapperState
56
+ }
57
+ interface StartupTaskResult<Source extends 'connection' | 'auth', Value> {
58
+ source: Source
59
+ result: PromiseSettledResult<Value>
60
+ }
61
+ type CodexThreadBootstrapResult
62
+ = | ClientRequestResponsesByMethod['thread/start']
63
+ | ClientRequestResponsesByMethod['thread/resume']
64
+ interface CodexSessionBootstrapConfig {
65
+ sessionId: string
66
+ cwd: string
67
+ workspaceRepositoryRoots: readonly string[]
68
+ model?: string
69
+ reasoningEffort?: AgentReasoningEffort
70
+ tools?: AgentToolDefinition[]
71
+ }
72
+
73
+ export class CodexProvider implements AgentProvider<'codex'> {
74
+ readonly id = 'codex' as const
75
+ private readonly tools: readonly AgentTool[]
76
+ private readonly executable: string
77
+ private readonly environment: Record<string, string | undefined>
78
+ private readonly auth: CodexAuthentication
79
+ private readonly onStartupStage: ((fact: CodexStartupStageFact) => void) | undefined
80
+ private readonly instructions: InstructionComposer
81
+
82
+ private constructor(config: CodexProviderConfig) {
83
+ this.auth = normalizeCodexAuthentication(config.auth)
84
+ this.tools = config.tools ?? []
85
+ this.executable = config.executable ?? 'codex'
86
+ this.environment = config.environment ?? {}
87
+ this.onStartupStage = config.onStartupStage
88
+ this.instructions = config.instructions ?? emptyInstructionComposer
89
+ }
90
+
91
+ static create(config: CodexProviderConfig): CodexProvider {
92
+ return new CodexProvider(config)
93
+ }
94
+
95
+ async openSession(input: AgentSessionOpenInput<'codex'>, options: AgentOperationOptions = {}): Promise<CodexRun> {
96
+ return await this.startSession({
97
+ config: input,
98
+ options,
99
+ stage: 'thread-start',
100
+ requestThread: async ({ client, tools }): Promise<CodexThreadBootstrapResult> => await client.request('thread/start', {
101
+ cwd: input.cwd,
102
+ model: input.model,
103
+ modelProvider: this.auth.kind === 'api-key' ? this.auth.modelProvider : undefined,
104
+ ephemeral: false,
105
+ approvalPolicy: 'never',
106
+ sandbox: 'danger-full-access',
107
+ developerInstructions: this.instructions({ provider: 'codex', tools }),
108
+ dynamicTools: toCodexDynamicTools(tools),
109
+ }),
110
+ })
111
+ }
112
+
113
+ async resumeSession(input: AgentSessionResumeInput<'codex'>, options: AgentOperationOptions = {}): Promise<CodexRun> {
114
+ return await this.startSession({
115
+ config: input,
116
+ options,
117
+ stage: 'thread-resume',
118
+ requestThread: async ({ client, tools }): Promise<CodexThreadBootstrapResult> => await client.request('thread/resume', {
119
+ threadId: input.providerSessionId,
120
+ cwd: input.cwd,
121
+ approvalPolicy: 'never',
122
+ sandbox: 'danger-full-access',
123
+ model: input.model,
124
+ modelProvider: this.auth.kind === 'api-key' ? this.auth.modelProvider : undefined,
125
+ developerInstructions: this.instructions({ provider: 'codex', tools }),
126
+ }),
127
+ validateThread: (result) => {
128
+ // Resume must rebind the same provider session; adopting a different thread would silently
129
+ // divorce the durable binding from the transcript the user expects.
130
+ if (result.thread?.id && result.thread.id !== input.providerSessionId) {
131
+ throw new AgentProviderProtocolError({
132
+ message: 'Codex thread/resume returned a different thread id than requested.',
133
+ details: { expected: input.providerSessionId, actual: result.thread.id },
134
+ })
135
+ }
136
+ },
137
+ })
138
+ }
139
+
140
+ private async startSession(input: {
141
+ config: CodexSessionBootstrapConfig
142
+ options: AgentOperationOptions
143
+ stage: 'thread-start' | 'thread-resume'
144
+ requestThread: (context: {
145
+ client: CodexJsonRpcClient
146
+ tools: readonly AgentTool[]
147
+ }) => Promise<CodexThreadBootstrapResult>
148
+ validateThread?: (result: CodexThreadBootstrapResult) => void
149
+ }): Promise<CodexRun> {
150
+ input.options.signal?.throwIfAborted()
151
+ const tools = selectAgentTools(this.tools, input.config.tools)
152
+ const holder: CodexRunHolder = { toolsByName: toolByName(tools) }
153
+ const authStartedAt = performance.now()
154
+ let authObserved = false
155
+ const instructionBridge = this.prepareSessionBootstrap({
156
+ cwd: input.config.cwd,
157
+ workspaceRepositoryRoots: input.config.workspaceRepositoryRoots,
158
+ startupSignal: input.options.signal,
159
+ })
160
+ void instructionBridge.catch(() => undefined)
161
+ const { client, abortController, state, authMaterial } = await this.openConnectionWithAuth({
162
+ sessionId: input.config.sessionId,
163
+ cwd: input.config.cwd,
164
+ options: input.options,
165
+ holder,
166
+ }).catch(async (error: unknown) => {
167
+ await instructionBridge.catch(() => undefined)
168
+ this.observeStartupStage({
169
+ stage: 'auth-acquisition-login',
170
+ outcome: 'failure',
171
+ startedAt: authStartedAt,
172
+ sessionId: input.config.sessionId,
173
+ })
174
+ authObserved = true
175
+ throw error
176
+ })
177
+ const removeBootstrapAbort = this.bindBootstrapAbort({
178
+ signal: input.options.signal,
179
+ client,
180
+ abortController,
181
+ })
182
+ try {
183
+ input.options.signal?.throwIfAborted()
184
+ await this.login({ client, holder, authMaterial })
185
+ this.observeStartupStage({
186
+ stage: 'auth-acquisition-login',
187
+ outcome: 'success',
188
+ startedAt: authStartedAt,
189
+ sessionId: input.config.sessionId,
190
+ })
191
+ authObserved = true
192
+ const instructionBridgePath = await instructionBridge
193
+ const result = await this.runStartupStage({
194
+ stage: input.stage,
195
+ sessionId: input.config.sessionId,
196
+ operation: async () => await input.requestThread({ client, tools }),
197
+ })
198
+ input.options.signal?.throwIfAborted()
199
+ input.validateThread?.(result)
200
+
201
+ return await this.bootstrapRun({
202
+ config: input.config,
203
+ result,
204
+ client,
205
+ abortController,
206
+ state,
207
+ holder,
208
+ instructionBridgePath,
209
+ startupSignal: input.options.signal,
210
+ })
211
+ }
212
+ catch (error) {
213
+ if (!authObserved) {
214
+ this.observeStartupStage({
215
+ stage: 'auth-acquisition-login',
216
+ outcome: 'failure',
217
+ startedAt: authStartedAt,
218
+ sessionId: input.config.sessionId,
219
+ })
220
+ }
221
+ await stopCodexClientBestEffort(client)
222
+ input.options.signal?.throwIfAborted()
223
+ throw holder.startupFailure ?? error
224
+ }
225
+ finally {
226
+ removeBootstrapAbort()
227
+ }
228
+ }
229
+
230
+ private async prepareSessionBootstrap(input: {
231
+ cwd: string
232
+ workspaceRepositoryRoots: readonly string[]
233
+ startupSignal?: AbortSignal
234
+ }): Promise<string> {
235
+ input.startupSignal?.throwIfAborted()
236
+ const instructionBridgePath = await reconcileSelectedRepositoryInstructionBridge({
237
+ cwd: input.cwd,
238
+ repositoryRoots: input.workspaceRepositoryRoots,
239
+ })
240
+ input.startupSignal?.throwIfAborted()
241
+ return instructionBridgePath
242
+ }
243
+
244
+ private async attestSessionBootstrap(input: {
245
+ holder: CodexRunHolder
246
+ instructionBridgePath: string
247
+ instructionSources?: readonly string[]
248
+ workspaceRepositoryRoots: readonly string[]
249
+ startupSignal?: AbortSignal
250
+ }): Promise<void> {
251
+ throwIfStartupFailed(input.holder)
252
+ await requireSelectedRepositoryInstructionSource({
253
+ instructionBridgePath: input.instructionBridgePath,
254
+ instructionSources: input.instructionSources,
255
+ repositoryRoots: input.workspaceRepositoryRoots,
256
+ })
257
+ input.startupSignal?.throwIfAborted()
258
+ }
259
+
260
+ private async openConnectionWithAuth(input: {
261
+ sessionId: string
262
+ cwd: string
263
+ options: AgentOperationOptions
264
+ holder: CodexRunHolder
265
+ }): Promise<CodexConnection & { authMaterial: CodexAuthMaterial }> {
266
+ const authAbortController = new AbortController()
267
+ const connectionAbortController = new AbortController()
268
+ const abortStartup = () => {
269
+ authAbortController.abort(input.options.signal?.reason)
270
+ connectionAbortController.abort(input.options.signal?.reason)
271
+ }
272
+ if (input.options.signal?.aborted)
273
+ abortStartup()
274
+ else
275
+ input.options.signal?.addEventListener('abort', abortStartup, { once: true })
276
+
277
+ const connectionTask = this.openConnection({
278
+ ...input,
279
+ startupSignal: connectionAbortController.signal,
280
+ }).then(
281
+ (value): StartupTaskResult<'connection', CodexConnection> => ({ source: 'connection', result: { status: 'fulfilled', value } }),
282
+ (reason): StartupTaskResult<'connection', CodexConnection> => ({ source: 'connection', result: { status: 'rejected', reason } }),
283
+ )
284
+ const authTask = this.getAuthMaterial(authAbortController.signal).then(
285
+ (value): StartupTaskResult<'auth', CodexAuthMaterial> => ({ source: 'auth', result: { status: 'fulfilled', value } }),
286
+ (reason): StartupTaskResult<'auth', CodexAuthMaterial> => ({ source: 'auth', result: { status: 'rejected', reason } }),
287
+ )
288
+
289
+ try {
290
+ const first = await Promise.race([connectionTask, authTask])
291
+ if (first.source === 'connection') {
292
+ if (first.result.status === 'rejected') {
293
+ authAbortController.abort(first.result.reason)
294
+ throw first.result.reason
295
+ }
296
+ const auth = await authTask
297
+ if (auth.result.status === 'rejected') {
298
+ await stopCodexClientBestEffort(first.result.value.client)
299
+ throw auth.result.reason
300
+ }
301
+ return { ...first.result.value, authMaterial: auth.result.value }
302
+ }
303
+
304
+ if (first.result.status === 'fulfilled') {
305
+ const connection = await connectionTask
306
+ if (connection.result.status === 'rejected')
307
+ throw connection.result.reason
308
+ return { ...connection.result.value, authMaterial: first.result.value }
309
+ }
310
+
311
+ connectionAbortController.abort(first.result.reason)
312
+ const interruptedConnection = await connectionTask
313
+ if (interruptedConnection.result.status === 'fulfilled')
314
+ await stopCodexClientBestEffort(interruptedConnection.result.value.client)
315
+ throw first.result.reason
316
+ }
317
+ finally {
318
+ input.options.signal?.removeEventListener('abort', abortStartup)
319
+ }
320
+ }
321
+
322
+ private async getAuthMaterial(signal: AbortSignal): Promise<CodexAuthMaterial> {
323
+ if (this.auth.kind === 'api-key') {
324
+ const credentials = await this.auth.provider.getApiKey({ signal })
325
+ return { kind: 'api-key', apiKey: credentials.apiKey }
326
+ }
327
+ const tokens = await this.auth.provider.getAuthTokens({ signal })
328
+ return { kind: 'chatgpt', tokens }
329
+ }
330
+
331
+ private async openConnection(input: {
332
+ sessionId: string
333
+ cwd: string
334
+ options: AgentOperationOptions
335
+ holder: CodexRunHolder
336
+ startupSignal: AbortSignal
337
+ }): Promise<CodexConnection> {
338
+ const { cwd, holder, startupSignal } = input
339
+ const state = createCodexEventMapperState()
340
+ const abortController = new AbortController()
341
+ const initializeStartedAt = performance.now()
342
+ let initializeObserved = false
343
+ let messageQueue = Promise.resolve()
344
+ let client: CodexJsonRpcClient
345
+
346
+ try {
347
+ client = new CodexJsonRpcClient(this.executable, {
348
+ cwd,
349
+ environment: this.environment,
350
+ onMessage: (message) => {
351
+ messageQueue = this.enqueueMessageHandling(messageQueue, { message, holder, state, client, signal: abortController.signal })
352
+ },
353
+ onInvalidLine: (line, error) => holder.run?.pushEvent({ type: 'error', payload: { message: `Invalid Codex JSON-RPC line: ${line.slice(0, 200)}`, fatal: false, source: 'transport', error: serializeAgentError(error) } }),
354
+ onStderr: line => holder.run?.pushEvent({ type: 'error', payload: { message: line, fatal: false, source: 'provider' } }),
355
+ })
356
+ }
357
+ catch (error) {
358
+ this.observeStartupStage({
359
+ stage: 'codex-initialize',
360
+ outcome: 'failure',
361
+ startedAt: initializeStartedAt,
362
+ sessionId: input.sessionId,
363
+ })
364
+ throw error
365
+ }
366
+ const stopForStartupAbort = () => {
367
+ abortController.abort(startupSignal.reason)
368
+ void stopCodexClientBestEffort(client)
369
+ }
370
+ if (startupSignal.aborted)
371
+ stopForStartupAbort()
372
+ else
373
+ startupSignal.addEventListener('abort', stopForStartupAbort, { once: true })
374
+
375
+ try {
376
+ await client.request('initialize', {
377
+ clientInfo: { name: 'coding-agent', title: 'Coding Agent', version: '0.1.0' },
378
+ capabilities: { experimentalApi: true, requestAttestation: false },
379
+ })
380
+ client.notify('initialized', undefined)
381
+ await yieldToPendingServerRequests()
382
+ await messageQueue
383
+ throwIfStartupFailed(holder)
384
+ this.observeStartupStage({
385
+ stage: 'codex-initialize',
386
+ outcome: 'success',
387
+ startedAt: initializeStartedAt,
388
+ sessionId: input.sessionId,
389
+ })
390
+ initializeObserved = true
391
+
392
+ return { client, abortController, state }
393
+ }
394
+ catch (error) {
395
+ if (!initializeObserved) {
396
+ this.observeStartupStage({
397
+ stage: 'codex-initialize',
398
+ outcome: 'failure',
399
+ startedAt: initializeStartedAt,
400
+ sessionId: input.sessionId,
401
+ })
402
+ }
403
+ await stopCodexClientBestEffort(client)
404
+ startupSignal.throwIfAborted()
405
+ throw error
406
+ }
407
+ finally {
408
+ startupSignal.removeEventListener('abort', stopForStartupAbort)
409
+ }
410
+ }
411
+
412
+ private async runStartupStage<T>(input: {
413
+ stage: CodexStartupStageFact['stage']
414
+ sessionId: string
415
+ operation: () => Promise<T>
416
+ }): Promise<T> {
417
+ const startedAt = performance.now()
418
+ try {
419
+ const result = await input.operation()
420
+ this.observeStartupStage({ ...input, outcome: 'success', startedAt })
421
+ return result
422
+ }
423
+ catch (error) {
424
+ this.observeStartupStage({ ...input, outcome: 'failure', startedAt })
425
+ throw error
426
+ }
427
+ }
428
+
429
+ private observeStartupStage(input: {
430
+ stage: CodexStartupStageFact['stage']
431
+ outcome: CodexStartupStageFact['outcome']
432
+ startedAt: number
433
+ sessionId: string
434
+ }): void {
435
+ try {
436
+ this.onStartupStage?.({
437
+ event: 'sandbox.startup-stage',
438
+ stage: input.stage,
439
+ outcome: input.outcome,
440
+ duration: Math.max(0, Math.round(performance.now() - input.startedAt)),
441
+ sessionId: input.sessionId,
442
+ })
443
+ }
444
+ catch {
445
+ // Startup telemetry is best effort and cannot alter provider behavior.
446
+ }
447
+ }
448
+
449
+ private bindBootstrapAbort(input: {
450
+ signal?: AbortSignal
451
+ client: CodexJsonRpcClient
452
+ abortController: AbortController
453
+ }): () => void {
454
+ const abortBootstrap = () => {
455
+ input.abortController.abort(input.signal?.reason)
456
+ void stopCodexClientBestEffort(input.client)
457
+ }
458
+ if (input.signal?.aborted)
459
+ abortBootstrap()
460
+ else
461
+ input.signal?.addEventListener('abort', abortBootstrap, { once: true })
462
+
463
+ return () => input.signal?.removeEventListener('abort', abortBootstrap)
464
+ }
465
+
466
+ private async bootstrapRun(input: {
467
+ config: CodexSessionBootstrapConfig
468
+ result: CodexThreadBootstrapResult
469
+ client: CodexJsonRpcClient
470
+ abortController: AbortController
471
+ state: CodexEventMapperState
472
+ holder: CodexRunHolder
473
+ instructionBridgePath: string
474
+ startupSignal?: AbortSignal
475
+ }): Promise<CodexRun> {
476
+ const { config, result, client, abortController, state, holder, instructionBridgePath, startupSignal } = input
477
+ const thread = result.thread
478
+ const effectiveModel = result.model ?? config.model
479
+ if (!thread?.id || !thread.path || !effectiveModel) {
480
+ throw new AgentProviderMetadataError({
481
+ message: 'Codex did not return required persistent thread metadata.',
482
+ details: { provider: 'codex' },
483
+ })
484
+ }
485
+
486
+ const metadata = {
487
+ provider: 'codex' as const,
488
+ sessionId: config.sessionId,
489
+ providerSessionId: thread.id,
490
+ cwd: thread.cwd ?? config.cwd,
491
+ model: firstCodexModelId(effectiveModel),
492
+ transcriptPath: thread.path,
493
+ }
494
+ throwIfStartupFailed(holder)
495
+ const [, userInputRequests] = await Promise.all([
496
+ this.attestSessionBootstrap({
497
+ holder,
498
+ instructionBridgePath,
499
+ instructionSources: result.instructionSources,
500
+ workspaceRepositoryRoots: config.workspaceRepositoryRoots,
501
+ startupSignal,
502
+ }),
503
+ UserInputRequestStore.open({
504
+ sessionId: metadata.sessionId,
505
+ rootDir: userInputRequestStoreRootFromTranscriptPath(metadata.transcriptPath),
506
+ }),
507
+ ])
508
+ startupSignal?.throwIfAborted()
509
+ const run = new CodexRun({
510
+ metadata,
511
+ reasoningEffort: config.reasoningEffort,
512
+ client,
513
+ requestedCwd: config.cwd,
514
+ workspaceRepositoryRoots: config.workspaceRepositoryRoots,
515
+ registeredSkillRoots: workspaceSkillRoots(config.workspaceRepositoryRoots),
516
+ repositorySkillRoots: catalogRepositorySkillRoots({
517
+ repositoryRoots: config.workspaceRepositoryRoots,
518
+ requestedCwd: config.cwd,
519
+ runtimeCwd: metadata.cwd,
520
+ }),
521
+ reconcileInstructionBridge: async (repositoryRoots) => {
522
+ await reconcileSelectedRepositoryInstructionBridge({ cwd: config.cwd, repositoryRoots })
523
+ },
524
+ userInputRequests,
525
+ eventMapperState: state,
526
+ abortSignal: abortController.signal,
527
+ onStop: () => abortController.abort(),
528
+ })
529
+ holder.run = run
530
+
531
+ run.pushEvent({ type: 'session.started', payload: run.metadata })
532
+ run.pushEvent(sessionConfiguredEvent({ model: effectiveModel, cwd: run.metadata.cwd, reasoningEffort: config.reasoningEffort, tools: config.tools, environment: this.environment }))
533
+ run.pushEvent({ type: 'session.state.changed', payload: { state: 'ready' } })
534
+ return run
535
+ }
536
+
537
+ private async enqueueMessageHandling(queue: Promise<void>, input: {
538
+ message: CodexJsonRpcMessage
539
+ holder: CodexRunHolder
540
+ state: CodexEventMapperState
541
+ client: CodexJsonRpcClient
542
+ signal: AbortSignal
543
+ }): Promise<void> {
544
+ return await queue
545
+ .then(async () => await this.handleMessage(input))
546
+ .catch((error: unknown) => {
547
+ input.holder.run?.pushEvent({ type: 'error', payload: { message: 'Codex app-server message handling failed.', fatal: false, source: 'runtime', error: serializeAgentError(error) } })
548
+ })
549
+ }
550
+
551
+ private async handleMessage(input: {
552
+ message: CodexJsonRpcMessage
553
+ holder: CodexRunHolder
554
+ state: CodexEventMapperState
555
+ client: CodexJsonRpcClient
556
+ signal: AbortSignal
557
+ }): Promise<void> {
558
+ const { message, holder, state, client, signal } = input
559
+ const run = holder.run
560
+ if (isCodexServerRequest(message)) {
561
+ const handling = this.handleServerRequest(message, holder, client, signal)
562
+ // Startup requests must flush their response before start/resume surfaces the failure.
563
+ if (!run)
564
+ await handling
565
+ else
566
+ void handling
567
+ return
568
+ }
569
+ if (!run)
570
+ return
571
+ const notification = codexNotificationFromMessage(message)
572
+ if (!notification)
573
+ return
574
+ if (notification.method === 'skills/changed') {
575
+ run.requestSkillsRefresh()
576
+ return
577
+ }
578
+ // Child-thread notifications must reach the mapper, which buffers them until spawn correlation.
579
+ await this.cacheSpawnedSubagentThreadMetadata({ notification, state, client })
580
+ const drafts = mapCodexNotification(notification, state)
581
+ for (const draft of drafts) {
582
+ run.pushEvent(draft)
583
+ }
584
+ if (notification.method === 'turn/started' && notification.params.threadId === run.metadata.providerSessionId) {
585
+ run.setActiveTurn(notification.params.turn.id)
586
+ run.pushEvent({ type: 'session.state.changed', payload: { state: 'running' } })
587
+ }
588
+ if (notification.method === 'turn/completed' && notification.params.threadId === run.metadata.providerSessionId) {
589
+ run.clearActiveTurn()
590
+ run.pushEvent({ type: 'session.state.changed', payload: { state: 'idle' } })
591
+ }
592
+ }
593
+
594
+ private async cacheSpawnedSubagentThreadMetadata(input: { notification: CodexNotification, state: CodexEventMapperState, client: CodexJsonRpcClient }): Promise<void> {
595
+ const threadId = spawnedSubagentThreadId(input.notification)
596
+ if (!threadId)
597
+ return
598
+
599
+ let thread: Parameters<typeof cacheCodexThreadMetadata>[0]
600
+ try {
601
+ const result = await input.client.request('thread/read', { threadId, includeTurns: false })
602
+ thread = result.thread
603
+ }
604
+ catch {
605
+ // Metadata improves display labels only; lifecycle mapping can proceed without it.
606
+ return
607
+ }
608
+ cacheCodexThreadMetadata(thread, input.state)
609
+ }
610
+
611
+ private async login(input: {
612
+ client: CodexJsonRpcClient
613
+ holder: CodexRunHolder
614
+ authMaterial: CodexAuthMaterial
615
+ }): Promise<void> {
616
+ const { client, holder, authMaterial } = input
617
+ throwIfStartupFailed(holder)
618
+ if (authMaterial.kind === 'api-key') {
619
+ await client.request('account/login/start', { type: 'apiKey', apiKey: authMaterial.apiKey })
620
+ throwIfStartupFailed(holder)
621
+ return
622
+ }
623
+ await client.request('account/login/start', toCodexLoginParams(authMaterial.tokens))
624
+ throwIfStartupFailed(holder)
625
+ holder.authTokens = authMaterial.tokens
626
+ }
627
+
628
+ private async handleAuthRefreshRequest(
629
+ request: Extract<CodexServerRequest, { method: 'account/chatgptAuthTokens/refresh' }>,
630
+ holder: CodexRunHolder,
631
+ client: CodexJsonRpcClient,
632
+ ): Promise<void> {
633
+ const current = holder.authTokens
634
+ if (!current) {
635
+ const error = new AgentProviderProtocolError({
636
+ message: 'Codex requested ChatGPT auth refresh before external auth tokens were available.',
637
+ details: { method: request.method },
638
+ })
639
+ holder.startupFailure = error
640
+ await client.respondErrorAndFlush(request.id, { code: -32000, message: 'Codex auth tokens are not available for refresh.' })
641
+ await client.closeInputAndStop()
642
+ return
643
+ }
644
+ try {
645
+ if (this.auth.kind !== 'chatgpt')
646
+ throw new AgentProviderProtocolError({ message: 'Codex requested ChatGPT token refresh while using API-key authentication.' })
647
+ const refreshed = await this.auth.provider.refreshAuthTokens(toAuthRefreshInput(request.params, current))
648
+ holder.authTokens = refreshed
649
+ client.respond(request.id, toCodexRefreshResponse(refreshed))
650
+ }
651
+ catch (error) {
652
+ const serializedError = redactSerializedError(error, current.accessToken)
653
+ const run = holder.run
654
+ if (!run) {
655
+ holder.startupFailure = new AgentProviderProtocolError({
656
+ message: 'Codex ChatGPT auth refresh failed during startup.',
657
+ details: { method: request.method },
658
+ cause: serializedError,
659
+ })
660
+ try {
661
+ await client.respondErrorAndFlush(request.id, { code: -32001, message: 'Codex auth refresh failed.' })
662
+ }
663
+ finally {
664
+ await client.closeInputAndStop()
665
+ }
666
+ return
667
+ }
668
+ try {
669
+ await client.respondErrorAndFlush(request.id, { code: -32001, message: 'Codex auth refresh failed.' })
670
+ }
671
+ finally {
672
+ await run.failActiveTurnAndEndRun({ reason: 'codex_auth_refresh_failed', error: serializedError })
673
+ }
674
+ }
675
+ }
676
+
677
+ private async handleServerRequest(
678
+ request: CodexServerRequest,
679
+ holder: CodexRunHolder,
680
+ client: CodexJsonRpcClient,
681
+ signal: AbortSignal,
682
+ ): Promise<void> {
683
+ const run = holder.run
684
+ if (request.method === 'account/chatgptAuthTokens/refresh') {
685
+ await this.handleAuthRefreshRequest(request, holder, client)
686
+ return
687
+ }
688
+ if (request.method !== 'item/tool/call') {
689
+ client.respond(request.id, {})
690
+ return
691
+ }
692
+ if (!run) {
693
+ client.respondError(request.id, { code: -32000, message: 'Codex run is not ready for server requests.' })
694
+ return
695
+ }
696
+
697
+ const toolParams = request.params
698
+ const runtimeTool = holder.toolsByName.get(toolParams.tool)
699
+ if (!runtimeTool) {
700
+ client.respond(request.id, { success: false, contentItems: [{ type: 'inputText', text: `Unknown tool: ${toolParams.tool}` }] })
701
+ return
702
+ }
703
+
704
+ const toolCallId = toolParams.callId ?? String(request.id)
705
+ try {
706
+ const result = await runCodexDynamicTool({
707
+ tool: runtimeTool,
708
+ params: toolParams,
709
+ context: {
710
+ provider: 'codex',
711
+ sessionId: run.metadata.sessionId,
712
+ providerSessionId: run.metadata.providerSessionId,
713
+ turnId: toolParams.turnId,
714
+ cwd: run.metadata.cwd,
715
+ toolCallId,
716
+ signal,
717
+ requestUserInput: async request => await run.requestUserInput({
718
+ ...request,
719
+ turnId: toolParams.turnId,
720
+ toolCallId,
721
+ }),
722
+ },
723
+ })
724
+ client.respond(request.id, result)
725
+ }
726
+ catch (error) {
727
+ const message = error instanceof Error ? error.message : String(error)
728
+ client.respond(request.id, { success: false, contentItems: [{ type: 'inputText', text: message }] })
729
+ }
730
+ }
731
+ }
732
+
733
+ function throwIfStartupFailed(holder: CodexRunHolder): void {
734
+ if (holder.startupFailure)
735
+ throw holder.startupFailure
736
+ }
737
+
738
+ const SELECTED_REPOSITORY_INSTRUCTION_BRIDGE_FILENAME = 'AGENTS.override.md'
739
+ const SELECTED_REPOSITORY_INSTRUCTION_BRIDGE_SENTINEL = '<!-- coding-agent-managed-selected-repository-instructions -->'
740
+
741
+ type InstructionBridgeOwnership = 'missing' | 'managed' | 'unowned'
742
+
743
+ async function reconcileSelectedRepositoryInstructionBridge(input: { cwd: string, repositoryRoots: readonly string[] }): Promise<string> {
744
+ const instructionBridgePath = resolve(input.cwd, SELECTED_REPOSITORY_INSTRUCTION_BRIDGE_FILENAME)
745
+ let ownership: InstructionBridgeOwnership
746
+ try {
747
+ ownership = await readInstructionBridgeOwnership(instructionBridgePath)
748
+ }
749
+ catch (error) {
750
+ if (!(error instanceof AgentProviderRuntimeError))
751
+ throw error
752
+ throw repositoryInstructionsUnavailable({ recoveryKind: 'retry', cause: error })
753
+ }
754
+ const repositoryRoots = stableRepositoryRoots(input.repositoryRoots)
755
+
756
+ if (repositoryRoots.length === 0) {
757
+ if (ownership === 'managed') {
758
+ try {
759
+ await removeManagedInstructionBridge(instructionBridgePath)
760
+ }
761
+ catch (error) {
762
+ if (!(error instanceof AgentProviderRuntimeError))
763
+ throw error
764
+ throw repositoryInstructionsUnavailable({ recoveryKind: 'retry', cause: error })
765
+ }
766
+ }
767
+ return instructionBridgePath
768
+ }
769
+
770
+ if (ownership === 'unowned') {
771
+ const cause = new AgentProviderRuntimeError({
772
+ message: 'Refusing to overwrite an AGENTS.override.md not owned by the Coding Agent runtime.',
773
+ details: { instructionBridgePath },
774
+ retryable: false,
775
+ })
776
+ throw repositoryInstructionsUnavailable({ recoveryKind: 'terminal', cause })
777
+ }
778
+
779
+ try {
780
+ await writeInstructionBridgeAtomically({
781
+ instructionBridgePath,
782
+ content: selectedRepositoryInstructionBridge(repositoryRoots),
783
+ })
784
+ }
785
+ catch (error) {
786
+ if (!(error instanceof AgentProviderRuntimeError))
787
+ throw error
788
+ throw repositoryInstructionsUnavailable({ recoveryKind: 'retry', cause: error })
789
+ }
790
+ return instructionBridgePath
791
+ }
792
+
793
+ async function readInstructionBridgeOwnership(instructionBridgePath: string): Promise<InstructionBridgeOwnership> {
794
+ let entry
795
+ try {
796
+ entry = await lstat(instructionBridgePath)
797
+ }
798
+ catch (error) {
799
+ if (isNodeError(error) && error.code === 'ENOENT')
800
+ return 'missing'
801
+ throw new AgentProviderRuntimeError({
802
+ message: 'Failed to inspect the selected-repository instruction bridge.',
803
+ details: { instructionBridgePath },
804
+ retryable: true,
805
+ cause: error,
806
+ })
807
+ }
808
+
809
+ if (!entry.isFile() || entry.isSymbolicLink())
810
+ return 'unowned'
811
+
812
+ let content: string
813
+ try {
814
+ content = await readFile(instructionBridgePath, 'utf8')
815
+ }
816
+ catch (error) {
817
+ throw new AgentProviderRuntimeError({
818
+ message: 'Failed to inspect the selected-repository instruction bridge.',
819
+ details: { instructionBridgePath },
820
+ retryable: true,
821
+ cause: error,
822
+ })
823
+ }
824
+
825
+ return firstLine(content) === SELECTED_REPOSITORY_INSTRUCTION_BRIDGE_SENTINEL ? 'managed' : 'unowned'
826
+ }
827
+
828
+ async function writeInstructionBridgeAtomically(input: { instructionBridgePath: string, content: string }): Promise<void> {
829
+ const tempPath = join(
830
+ dirname(input.instructionBridgePath),
831
+ `.${basename(input.instructionBridgePath)}.${process.pid}.${randomUUID()}.tmp`,
832
+ )
833
+ let tempFile: Awaited<ReturnType<typeof open>> | undefined
834
+ try {
835
+ tempFile = await open(tempPath, 'wx', 0o640)
836
+ await tempFile.chmod(0o640)
837
+ await tempFile.writeFile(input.content, 'utf8')
838
+ await tempFile.close()
839
+ tempFile = undefined
840
+ await rename(tempPath, input.instructionBridgePath)
841
+ }
842
+ catch (error) {
843
+ await tempFile?.close().catch(() => undefined)
844
+ await unlink(tempPath).catch(() => undefined)
845
+ throw new AgentProviderRuntimeError({
846
+ message: 'Failed to publish the selected-repository instruction bridge.',
847
+ details: { instructionBridgePath: input.instructionBridgePath },
848
+ retryable: true,
849
+ cause: error,
850
+ })
851
+ }
852
+ }
853
+
854
+ async function removeManagedInstructionBridge(instructionBridgePath: string): Promise<void> {
855
+ try {
856
+ await unlink(instructionBridgePath)
857
+ }
858
+ catch (error) {
859
+ if (isNodeError(error) && error.code === 'ENOENT')
860
+ return
861
+ throw new AgentProviderRuntimeError({
862
+ message: 'Failed to remove the stale selected-repository instruction bridge.',
863
+ details: { instructionBridgePath },
864
+ retryable: true,
865
+ cause: error,
866
+ })
867
+ }
868
+ }
869
+
870
+ async function requireSelectedRepositoryInstructionSource(input: {
871
+ instructionBridgePath: string
872
+ instructionSources?: readonly string[]
873
+ repositoryRoots: readonly string[]
874
+ }): Promise<void> {
875
+ if (input.repositoryRoots.length === 0)
876
+ return
877
+
878
+ const instructionSources = input.instructionSources ?? []
879
+ let expectedInstructionSource: string
880
+ let attestedInstructionSources: string[]
881
+ try {
882
+ expectedInstructionSource = await realpath(input.instructionBridgePath)
883
+ attestedInstructionSources = await Promise.all(instructionSources.map(async source => await realpath(source)))
884
+ }
885
+ catch (error) {
886
+ if (!isFileSystemErrno(error))
887
+ throw error
888
+
889
+ const details = {
890
+ expectedInstructionSource: input.instructionBridgePath,
891
+ instructionSources,
892
+ }
893
+ if (isDeterministicRealpathFailure(error)) {
894
+ const cause = new AgentProviderProtocolError({
895
+ message: 'Failed to attest Codex selected-repository instruction sources.',
896
+ details,
897
+ cause: error,
898
+ })
899
+ throw repositoryInstructionsUnavailable({ recoveryKind: 'terminal', cause })
900
+ }
901
+
902
+ const cause = new AgentProviderRuntimeError({
903
+ message: 'Failed to resolve Codex selected-repository instruction sources.',
904
+ details,
905
+ retryable: true,
906
+ cause: error,
907
+ })
908
+ throw repositoryInstructionsUnavailable({ recoveryKind: 'retry', cause })
909
+ }
910
+
911
+ if (attestedInstructionSources.includes(expectedInstructionSource))
912
+ return
913
+
914
+ const cause = new AgentProviderProtocolError({
915
+ message: 'Codex did not load the selected-repository instruction bridge.',
916
+ details: {
917
+ expectedInstructionSource,
918
+ instructionSources: attestedInstructionSources,
919
+ },
920
+ })
921
+ throw repositoryInstructionsUnavailable({ recoveryKind: 'terminal', cause })
922
+ }
923
+
924
+ function repositoryInstructionsUnavailable(input: {
925
+ recoveryKind: 'retry' | 'terminal'
926
+ cause: AgentProviderRuntimeError
927
+ }): AgentNotAcceptedError {
928
+ return new AgentNotAcceptedError({
929
+ message: 'Selected repository instructions are unavailable.',
930
+ recovery: { kind: input.recoveryKind, reason: 'repository_instructions_unavailable' },
931
+ cause: input.cause,
932
+ })
933
+ }
934
+
935
+ function selectedRepositoryInstructionBridge(repositoryRoots: readonly string[]): string {
936
+ return [
937
+ SELECTED_REPOSITORY_INSTRUCTION_BRIDGE_SENTINEL,
938
+ '# Selected repository instructions',
939
+ '',
940
+ 'Selected repository roots:',
941
+ ...repositoryRoots.map(root => `- ${JSON.stringify(root)}`),
942
+ '',
943
+ 'For each repository root, you must:',
944
+ '- Inspect `AGENTS.override.md` first; if it does not exist, inspect `AGENTS.md`.',
945
+ '- Follow every documentation file referenced by the applicable AGENTS file.',
946
+ '- Before editing nested files, re-check for more-local `AGENTS.override.md` or `AGENTS.md` files.',
947
+ '',
948
+ 'Runtime and developer instructions take precedence over these repository instructions.',
949
+ 'Repository instructions apply only within their own repository.',
950
+ 'If selected repositories contain irreconcilable instructions, report the conflict instead of resolving it by repository input order.',
951
+ '',
952
+ ].join('\n')
953
+ }
954
+
955
+ function firstLine(content: string): string {
956
+ const lineEnd = content.indexOf('\n')
957
+ return lineEnd === -1 ? content : content.slice(0, lineEnd)
958
+ }
959
+
960
+ function isNodeError(error: unknown): error is NodeJS.ErrnoException {
961
+ return error instanceof Error && 'code' in error
962
+ }
963
+
964
+ function isFileSystemErrno(error: unknown): error is NodeJS.ErrnoException & { code: string, syscall: string } {
965
+ return isNodeError(error)
966
+ && typeof error.code === 'string'
967
+ && /^E[A-Z0-9]+$/.test(error.code)
968
+ && typeof error.syscall === 'string'
969
+ }
970
+
971
+ function isDeterministicRealpathFailure(error: NodeJS.ErrnoException): boolean {
972
+ return error.code === 'ENOENT' || error.code === 'ENOTDIR' || error.code === 'ELOOP'
973
+ }
974
+
975
+ async function yieldToPendingServerRequests(): Promise<void> {
976
+ await new Promise(resolve => setTimeout(resolve, 0))
977
+ }
978
+
979
+ async function stopCodexClientBestEffort(client: CodexJsonRpcClient): Promise<void> {
980
+ await client.stop().catch(() => undefined)
981
+ }
982
+
983
+ function toCodexLoginParams(tokens: CodexAuthTokens): {
984
+ type: 'chatgptAuthTokens'
985
+ accessToken: string
986
+ chatgptAccountId: string
987
+ chatgptPlanType?: string | null
988
+ } {
989
+ return {
990
+ type: 'chatgptAuthTokens',
991
+ accessToken: tokens.accessToken,
992
+ chatgptAccountId: tokens.chatgptAccountId,
993
+ chatgptPlanType: tokens.chatgptPlanType ?? null,
994
+ }
995
+ }
996
+
997
+ function normalizeCodexAuthentication(auth: CodexAuthProvider | CodexAuthentication): CodexAuthentication {
998
+ return 'kind' in auth ? auth : { kind: 'chatgpt', provider: auth }
999
+ }
1000
+
1001
+ function toAuthRefreshInput(params: CodexChatGPTAuthTokensRefreshParams, tokens: CodexAuthTokens): CodexAuthRefreshInput {
1002
+ return {
1003
+ reason: params.reason,
1004
+ rejectedTokenGeneration: tokens.accessTokenGeneration,
1005
+ rejectedTokenFingerprint: tokens.accessTokenFingerprint,
1006
+ }
1007
+ }
1008
+
1009
+ function toCodexRefreshResponse(tokens: CodexAuthTokens): { accessToken: string, chatgptAccountId: string, chatgptPlanType?: string | null } {
1010
+ return {
1011
+ accessToken: tokens.accessToken,
1012
+ chatgptAccountId: tokens.chatgptAccountId,
1013
+ chatgptPlanType: tokens.chatgptPlanType ?? null,
1014
+ }
1015
+ }
1016
+
1017
+ function redactSerializedError(error: unknown, accessToken: string): { message: string, code?: string, name?: string, stack?: string } {
1018
+ return redactAgentError(serializeAgentError(error), accessToken)
1019
+ }
1020
+
1021
+ function redactAgentError<T extends { message: string, stack?: string, cause?: T }>(error: T, secret: string): T {
1022
+ const redacted = '<redacted>'
1023
+ const replaceSecret = (value: string | undefined) => value?.split(secret).join(redacted)
1024
+ return {
1025
+ ...error,
1026
+ message: replaceSecret(error.message) ?? error.message,
1027
+ ...(error.stack === undefined ? {} : { stack: replaceSecret(error.stack) }),
1028
+ ...(error.cause === undefined ? {} : { cause: redactAgentError(error.cause, secret) }),
1029
+ }
1030
+ }
1031
+
1032
+ function firstCodexModelId(...values: Array<string | null | undefined>): CodexModelId | undefined {
1033
+ for (const value of values) {
1034
+ const parsed = codexModelIdSchema.safeParse(value)
1035
+ if (parsed.success)
1036
+ return parsed.data
1037
+ }
1038
+ return undefined
1039
+ }
1040
+
1041
+ function spawnedSubagentThreadId(notification: CodexNotification): string | undefined {
1042
+ if (notification.method !== 'item/completed')
1043
+ return undefined
1044
+
1045
+ const item = notification.params.item
1046
+ if (item.type !== 'collabAgentToolCall' || item.tool !== 'spawnAgent' || item.status === 'failed')
1047
+ return undefined
1048
+
1049
+ return item.receiverThreadIds.at(0)
1050
+ }