@meistrari/agent-core 0.2.0-experimental.0 → 0.2.0-experimental.2

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 (90) hide show
  1. package/README.md +82 -0
  2. package/docs/consumer-capability-matrix.md +374 -0
  3. package/package.json +17 -10
  4. package/scripts/generate-codex-protocol.ts +711 -0
  5. package/src/agents/agent-operation.ts +8 -0
  6. package/src/agents/agent-provider.ts +3 -0
  7. package/src/agents/agent-run.ts +14 -2
  8. package/src/agents/agent-runtime-error.ts +28 -0
  9. package/src/agents/agent-session-events.ts +33 -0
  10. package/src/agents/agent-tool-runner.ts +12 -11
  11. package/src/agents/agent-tool.ts +12 -4
  12. package/src/agents/author-context.ts +1 -1
  13. package/src/agents/claude/claude-auth.ts +40 -0
  14. package/src/agents/claude/claude-browser-turn-metadata.ts +70 -0
  15. package/src/agents/claude/claude-code-process.ts +98 -0
  16. package/src/agents/claude/claude-command-mapper.ts +63 -3
  17. package/src/agents/claude/claude-desktop.ts +55 -0
  18. package/src/agents/claude/claude-event-mapper.ts +876 -124
  19. package/src/agents/claude/claude-file-change.ts +128 -0
  20. package/src/agents/claude/claude-provider.ts +344 -29
  21. package/src/agents/claude/claude-run.ts +922 -91
  22. package/src/agents/claude/claude-selected-repository-instructions.ts +91 -0
  23. package/src/agents/claude/claude-skill-catalog.ts +75 -0
  24. package/src/agents/claude/claude-steered-notifications.ts +180 -0
  25. package/src/agents/claude/claude-tool-mapper.ts +81 -2
  26. package/src/agents/claude/claude-workspace-skills.ts +87 -0
  27. package/src/agents/claude/index.ts +2 -0
  28. package/src/agents/codex/codex-auth-mapper.ts +27 -0
  29. package/src/agents/codex/codex-auth.ts +19 -0
  30. package/src/agents/codex/codex-browser-auth-broker.ts +320 -0
  31. package/src/agents/codex/codex-browser-auth-status.ts +55 -0
  32. package/src/agents/codex/codex-command-mapper.ts +38 -3
  33. package/src/agents/codex/codex-event-mapper.ts +375 -64
  34. package/src/agents/codex/codex-json-rpc-client.ts +66 -21
  35. package/src/agents/codex/codex-provider.ts +118 -236
  36. package/src/agents/codex/codex-run-auth.ts +113 -0
  37. package/src/agents/codex/codex-run.ts +428 -48
  38. package/src/agents/codex/codex-tool-mapper.ts +2 -2
  39. package/src/agents/codex/codex.errors.ts +8 -0
  40. package/src/agents/codex/generated/meta.gen.ts +144 -6
  41. package/src/agents/codex/generated/namespaces.gen.ts +92 -3
  42. package/src/agents/codex/generated/schema.gen.ts +24698 -15638
  43. package/src/agents/codex/lazy-codex-run-auth.ts +47 -0
  44. package/src/agents/index.ts +5 -0
  45. package/src/agents/input-attachment-preparation.ts +13 -0
  46. package/src/agents/mcp-tool-content.ts +82 -0
  47. package/src/agents/model-rebinding-run.ts +14 -3
  48. package/src/agents/selected-repository-instructions.ts +202 -0
  49. package/src/agents/session-context.ts +41 -0
  50. package/src/agents/subscription-token-refresh.ts +73 -0
  51. package/src/agents/user-input-request.ts +19 -20
  52. package/src/errors/index.ts +1 -1
  53. package/src/errors/validation-error.ts +6 -0
  54. package/src/protocol/agent-catalog.ts +159 -0
  55. package/src/protocol/agent-command.ts +54 -7
  56. package/src/protocol/agent-content.ts +36 -16
  57. package/src/protocol/agent-event.ts +210 -73
  58. package/src/protocol/agent-identity.ts +29 -0
  59. package/src/protocol/agent-json.ts +122 -8
  60. package/src/protocol/agent-metadata.ts +2 -2
  61. package/src/protocol/agent-model.ts +13 -1
  62. package/src/protocol/agent-notification.ts +110 -0
  63. package/src/protocol/agent-presentation.ts +71 -20
  64. package/src/protocol/agent-tool.ts +66 -10
  65. package/src/protocol/agent-usage.ts +4 -1
  66. package/src/protocol/agent-user-input.ts +4 -4
  67. package/src/protocol/agent-work.ts +12 -10
  68. package/src/protocol/index.ts +2 -0
  69. package/src/protocol/rejected-keys.ts +11 -0
  70. package/src/provenance.gen.ts +3 -3
  71. package/src/steps/contract.ts +4 -1
  72. package/src/steps/projector.ts +17 -1
  73. package/src/supervisor/agent-provider-factory.ts +60 -5
  74. package/src/supervisor/bootstrap-binder.ts +3 -2
  75. package/src/supervisor/index.ts +1 -1
  76. package/src/supervisor/persistence/supervisor-store.ts +22 -8
  77. package/src/supervisor/runtime-handler.ts +30 -2
  78. package/src/supervisor-protocol/agent-event-wrapper.ts +30 -0
  79. package/src/supervisor-protocol/bootstrap.ts +4 -1
  80. package/src/supervisor-protocol/command-body.ts +4 -1
  81. package/src/supervisor-protocol/envelopes/supervisor-to-control-plane.ts +1 -0
  82. package/src/testing/in-memory-runtime-control-plane.ts +118 -12
  83. package/src/testing/index.ts +1 -1
  84. package/src/worker-runtime-client/command-pump.ts +85 -10
  85. package/src/worker-runtime-client/connection-attempt.ts +95 -20
  86. package/src/worker-runtime-client/frame-processor.ts +242 -58
  87. package/src/worker-runtime-client/index.ts +8 -2
  88. package/src/worker-runtime-client/ports.ts +34 -2
  89. package/src/worker-runtime-client/rpc-request-manager.ts +152 -107
  90. package/src/worker-runtime-client/sandbox-connection-runtime.ts +72 -35
@@ -1,48 +1,85 @@
1
1
  import type {
2
2
  AgentAuthor,
3
+ AgentContextCompactionRequestResult,
4
+ AgentNotifyCommand,
3
5
  AgentPrompt,
4
6
  AgentReasoningEffort,
5
7
  AgentRunMetadata,
8
+ AgentSettingsChange,
9
+ CompactContextAgentCommand,
6
10
  InterruptAgentCommand,
7
11
  RespondUserInputAgentCommand,
8
12
  SendPromptCommand,
9
13
  StopAgentCommand,
10
14
  } from '../../protocol'
11
15
  import type { AgentEventDraft } from '../agent-event-stream'
12
- import type { AgentOperationOptions } from '../agent-operation'
13
- import type { AgentPromptAcceptance, AgentRun, AgentUserInputSubmission } from '../agent-run'
16
+ import type { AgentOperationOptions, AgentPromptOperationOptions } from '../agent-operation'
17
+ import type { AgentNotificationAcceptance, AgentPromptAcceptance, AgentRun, AgentUserInputSubmission } from '../agent-run'
18
+ import type { AgentSettingsPlacement } from '../agent-session-events'
14
19
  import type { UserInputRequestInput, UserInputRequestStore } from '../user-input-request'
15
20
  import type { CodexEventMapperState } from './codex-event-mapper'
16
21
  import type { CodexJsonRpcClient } from './codex-json-rpc-client'
17
- import type { ClientRequestResponsesByMethod, CodexTurnStartParams, CodexUserInput } from './codex-protocol'
22
+ import type { ClientRequestResponsesByMethod, CodexUserInput } from './codex-protocol'
23
+ import { serializeAgentNotificationToolOutput } from '../../protocol'
18
24
  import { AgentEventStream } from '../agent-event-stream'
19
25
  import { AgentDeliveryUnknownError, AgentNotAcceptedError, AgentPromptDeferredError, AgentRunStateError } from '../agent-runtime-error'
26
+ import { sessionSettingsChangedEvent } from '../agent-session-events'
20
27
  import { deriveMessageId } from '../message-id'
21
- import { codexEffort, prepareCodexUserInput, toCodexUserInput } from './codex-command-mapper'
28
+ import { codexEffort, codexReasoningSummary, prepareCodexUserInput, toCodexUserInput } from './codex-command-mapper'
22
29
  import { CodexJsonRpcResponseError } from './codex-json-rpc-client'
23
30
  import { CodexSkillCatalog } from './codex-skill-catalog'
24
31
  import { catalogRepositorySkillRoots, workspaceSkillRoots } from './codex-skill-roots'
25
32
  import { CodexJsonRpcTransportClosedError } from './codex.errors'
26
33
 
27
- const codexReasoningSummary: CodexTurnStartParams['summary'] = 'auto'
34
+ const codexNotificationTurnTrigger = 'application_notification'
35
+ const codexNotificationToolNamespace = 'coding_agent'
36
+ const codexNotificationToolName = 'session_notifications'
28
37
 
29
38
  interface CodexRunAuthFailureInput {
30
39
  reason: 'codex_auth_refresh_failed'
31
40
  error: { message: string, name?: string, stack?: string }
32
41
  }
33
42
 
34
- interface PreparedCodexTurn {
43
+ interface PreparedCodexPromptTurn {
35
44
  input: CodexUserInput[]
36
45
  commandId: string
37
46
  origin: 'user' | 'supervisor'
38
- model?: string
47
+ sessionContext: string | undefined
48
+ }
49
+
50
+ // The model and effort the run sends with every turn/start. Codex applies turn/start settings to that
51
+ // turn and every later one, so each turn must carry the current values or it silently reverts a change.
52
+ interface CodexTurnSettings {
53
+ model: string
54
+ effort: AgentReasoningEffort | undefined
55
+ }
56
+
57
+ interface PreparedCodexTurnStart {
58
+ commandId: string
59
+ settings: CodexTurnSettings
60
+ content:
61
+ | { kind: 'prompt', input: CodexUserInput[], origin: 'user' | 'supervisor' }
62
+ | { kind: 'notification', output: string }
63
+ }
64
+
65
+ interface CodexTurnStartReceipt {
66
+ kind: 'turn'
67
+ turnId: string
68
+ }
69
+
70
+ interface CodexTurnStartResult {
71
+ receipt: CodexTurnStartReceipt
72
+ placement: 'started' | 'steered'
39
73
  }
40
74
 
41
75
  interface CodexRunInput {
42
76
  beforePrompt?: (signal?: AbortSignal) => Promise<void>
43
77
  resolveInferenceModel?: (model: string) => string
44
78
  metadata: AgentRunMetadata
45
- reasoningEffort?: AgentReasoningEffort
79
+ // Delivered once, with the session's first prompt, then cleared (see session-context.ts).
80
+ sessionContext: string | undefined
81
+ // The resolved settings the thread was opened or resumed with.
82
+ settings: CodexTurnSettings
46
83
  client: CodexJsonRpcClient
47
84
  requestedCwd: string
48
85
  workspaceRepositoryRoots: readonly string[]
@@ -52,7 +89,7 @@ interface CodexRunInput {
52
89
  userInputRequests: UserInputRequestStore
53
90
  eventMapperState: CodexEventMapperState
54
91
  abortSignal: AbortSignal
55
- onStop: () => void
92
+ onStop: () => void | Promise<void>
56
93
  }
57
94
 
58
95
  export class CodexRun implements AgentRun {
@@ -61,8 +98,8 @@ export class CodexRun implements AgentRun {
61
98
  private readonly client: CodexJsonRpcClient
62
99
  private readonly beforePrompt: ((signal?: AbortSignal) => Promise<void>) | undefined
63
100
  private readonly resolveInferenceModel: (model: string) => string
64
- private readonly reasoningEffort: AgentReasoningEffort | undefined
65
- private readonly onStop: () => void
101
+ private current: CodexTurnSettings
102
+ private readonly onStop: () => void | Promise<void>
66
103
  private readonly userInputRequests: UserInputRequestStore
67
104
  private readonly eventMapperState: CodexEventMapperState
68
105
  private readonly skillCatalog: CodexSkillCatalog
@@ -74,14 +111,16 @@ export class CodexRun implements AgentRun {
74
111
  private stopped = false
75
112
  private stoppingTask: Promise<void> | undefined
76
113
  private activeTurnId: string | undefined
114
+ private sessionContext: string | undefined
77
115
  private readonly interruptingTurnIds = new Set<string>()
78
116
 
79
117
  constructor(input: CodexRunInput) {
80
118
  this.metadata = input.metadata
119
+ this.sessionContext = input.sessionContext
81
120
  this.client = input.client
82
121
  this.beforePrompt = input.beforePrompt
83
122
  this.resolveInferenceModel = input.resolveInferenceModel ?? (model => model)
84
- this.reasoningEffort = input.reasoningEffort
123
+ this.current = { ...input.settings }
85
124
  this.onStop = input.onStop
86
125
  this.userInputRequests = input.userInputRequests
87
126
  this.eventMapperState = input.eventMapperState
@@ -204,6 +243,10 @@ export class CodexRun implements AgentRun {
204
243
  }
205
244
 
206
245
  async failActiveTurnAndEndRun(input: CodexRunAuthFailureInput): Promise<void> {
246
+ if (this.stoppingTask) {
247
+ await this.stoppingTask
248
+ return
249
+ }
207
250
  if (this.stopped)
208
251
  return
209
252
  this.pushEvent({ type: 'error', payload: { message: 'Codex auth refresh failed.', fatal: false, source: 'provider', error: input.error } })
@@ -219,23 +262,38 @@ export class CodexRun implements AgentRun {
219
262
  }
220
263
  this.pushEvent({ type: 'session.state.changed', payload: { state: 'failed' } })
221
264
  this.stopped = true
222
- // Pinned Codex forwards preceding stdin lines before EOF, preserving the refresh error ordering.
223
- await this.client.closeInputAndStop()
224
- this.events.push({ type: 'session.ended', payload: { reason: 'failed', error: input.error } })
225
- this.events.end()
265
+ // The refresh error was flushed before this path. Revoke run capabilities while
266
+ // native EOF/signal escalation proceeds, not after a potentially slow child exit.
267
+ const stopping = Promise.withResolvers<void>()
268
+ this.stoppingTask = stopping.promise
269
+ this.startStopping(() => {
270
+ this.events.push({ type: 'session.ended', payload: { reason: 'failed', error: input.error } })
271
+ this.events.end()
272
+ }, stopping)
273
+ await stopping.promise
226
274
  }
227
275
 
228
- async sendPrompt(command: SendPromptCommand, options: AgentOperationOptions = {}): Promise<AgentPromptAcceptance> {
276
+ async sendPrompt(command: SendPromptCommand, options: AgentPromptOperationOptions = {}): Promise<AgentPromptAcceptance> {
229
277
  this.assertCommandTargetsRun(command)
230
278
  this.assertRunning()
279
+ options.signal?.throwIfAborted()
280
+ this.assertSubmissionOrder(command.mode, options.submissionOrder)
231
281
  // Released 0.145.0 accepts turn/start during an active turn by silently steering the input
232
- // and answering a phantom turn id, so the next-vs-active race must be refused from adapter
233
- // state before any request — nothing is prepared or retained.
234
- this.deferNextWhileTurnActive(command.mode)
235
- if (this.beforePrompt)
282
+ // and answering a phantom turn id, so in-order next-vs-active races are refused before any
283
+ // request. Bypass submission instead requires an active turn before preparation.
284
+ const submissionOrder = options.submissionOrder ?? 'in-order'
285
+ if (submissionOrder === 'in-order')
286
+ this.deferNextWhileTurnActive(command.mode)
287
+ else
288
+ this.deferBypassWithoutTurn()
289
+ if (this.beforePrompt) {
236
290
  await this.beforePrompt(options.signal)
237
- this.assertRunning()
238
- this.deferNextWhileTurnActive(command.mode)
291
+ this.assertRunning()
292
+ if (submissionOrder === 'in-order')
293
+ this.deferNextWhileTurnActive(command.mode)
294
+ else
295
+ this.deferBypassWithoutTurn()
296
+ }
239
297
  const prepared = await this.prepareTurn({
240
298
  prompt: command.prompt,
241
299
  commandId: command.commandId,
@@ -243,19 +301,49 @@ export class CodexRun implements AgentRun {
243
301
  author: command.author,
244
302
  options,
245
303
  })
246
- prepared.model = command.model
247
- // A turn/started fact may have reduced during attachment preparation.
248
- this.deferNextWhileTurnActive(command.mode)
304
+ // Provider events and stop can reduce while attachment preparation is awaited. Recheck the
305
+ // run and use the execution-time active turn before creating correlation or provider input.
306
+ this.assertRunning()
307
+ options.signal?.throwIfAborted()
308
+ if (submissionOrder === 'in-order')
309
+ this.deferNextWhileTurnActive(command.mode)
310
+ else
311
+ this.deferBypassWithoutTurn()
249
312
 
313
+ if (command.turnId && (command.mode !== 'steer' || command.turnId !== this.activeTurnId))
314
+ throw new AgentNotAcceptedError({ message: 'The targeted codex turn is no longer active.', recovery: { kind: 'terminal', reason: 'target_turn_not_active' } })
250
315
  const activeTurnId = this.activeTurnId
251
316
  if (activeTurnId && command.model && command.model !== this.metadata.model)
252
317
  throw new AgentPromptDeferredError({ message: 'Codex model changes require an idle turn boundary.' })
253
- if (command.mode === 'next' || !activeTurnId)
254
- return await this.startPreparedTurn(prepared)
318
+ if (activeTurnId && command.reasoningEffort && command.reasoningEffort !== this.current.effort)
319
+ throw new AgentPromptDeferredError({ message: 'Codex reasoning effort changes require an idle turn boundary.' })
320
+ if (command.mode === 'next' || !activeTurnId) {
321
+ const settings = withSettingsChange(this.current, command.settings ?? { model: command.model, reasoningEffort: command.reasoningEffort })
322
+ const { receipt } = await this.startTurn({
323
+ commandId: prepared.commandId,
324
+ settings,
325
+ content: { kind: 'prompt', input: prepared.input, origin: prepared.origin },
326
+ }, options)
327
+ this.sessionContextDelivered(prepared.sessionContext)
328
+ // The turn this prompt started runs on the merged settings from its first request.
329
+ if (command.settings)
330
+ this.settingsAccepted({ settings, commandId: command.commandId, turnId: receipt.turnId, placement: { appliesFrom: 'current_turn' } })
331
+ return { ...receipt, placement: 'started' }
332
+ }
255
333
 
256
- const clientUserMessageId = this.rememberPromptCommand(prepared.commandId, prepared.origin)
334
+ // Settings must reach the provider before the input. Keep the committed settings until
335
+ // acceptance and compensate a proven rejection before returning the prompt to the caller.
336
+ const previousSettings = this.current
337
+ const settings = command.settings && withSettingsChange(previousSettings, command.settings)
338
+ const placement = settings && await this.applySteerSettings({ turnId: activeTurnId, settings })
339
+ const clientUserMessageId = deriveMessageId(prepared.commandId)
340
+ let steerRequested = false
257
341
  let result: ClientRequestResponsesByMethod['turn/steer']
258
342
  try {
343
+ this.assertRunning()
344
+ options.signal?.throwIfAborted()
345
+ this.rememberPromptCommand(prepared.commandId, prepared.origin)
346
+ steerRequested = true
259
347
  result = await this.client.request('turn/steer', {
260
348
  threadId: this.metadata.providerSessionId,
261
349
  expectedTurnId: activeTurnId,
@@ -264,13 +352,32 @@ export class CodexRun implements AgentRun {
264
352
  })
265
353
  }
266
354
  catch (error) {
355
+ const mapped = mapTurnTransportLoss(error)
356
+ // A lost response may represent accepted input; leave its settings intact and retain
357
+ // delivery-unknown recovery. Cancellation before writing and explicit refusal are safe
358
+ // to compensate, without using the cancelled operation's signal.
359
+ if (settings && placement && (!steerRequested || isSteerNotAcceptedResponse(error) || mapped instanceof AgentNotAcceptedError))
360
+ await this.restoreSteerSettings({ turnId: activeTurnId, previousSettings, settings, placement })
267
361
  if (isSteerNotAcceptedResponse(error)) {
362
+ if (submissionOrder === 'bypass')
363
+ this.eventMapperState.promptCommandByClientUserMessageId.delete(clientUserMessageId)
268
364
  throw new AgentPromptDeferredError({
269
365
  message: 'Codex rejected the steer at a turn boundary race.',
270
366
  cause: error,
271
367
  })
272
368
  }
273
- throw mapTurnTransportLoss(error)
369
+ if (submissionOrder === 'bypass' && mapped instanceof AgentNotAcceptedError)
370
+ this.eventMapperState.promptCommandByClientUserMessageId.delete(clientUserMessageId)
371
+ throw mapped
372
+ }
373
+ this.sessionContextDelivered(prepared.sessionContext)
374
+ if (settings && placement) {
375
+ this.settingsAccepted({
376
+ settings,
377
+ commandId: command.commandId,
378
+ turnId: placement.appliesFrom === 'current_turn' ? activeTurnId : undefined,
379
+ placement,
380
+ })
274
381
  }
275
382
  return { kind: 'turn', turnId: result.turnId, placement: 'steered' }
276
383
  }
@@ -279,6 +386,97 @@ export class CodexRun implements AgentRun {
279
386
  return this.activeTurnId !== undefined
280
387
  }
281
388
 
389
+ // Changes the running turn (only honoured with `features.step_model_switching`) and the thread's
390
+ // later turns. Sends only the fields that differ from `current`; with none, no provider call and the
391
+ // running turn already runs on those values.
392
+ private async applySteerSettings({ turnId, settings }: { turnId: string, settings: CodexTurnSettings }): Promise<AgentSettingsPlacement> {
393
+ const changes = {
394
+ ...(settings.model === this.current.model ? {} : { model: this.resolveInferenceModel(settings.model) }),
395
+ ...(settings.effort === this.current.effort ? {} : { effort: codexEffort(settings.effort) }),
396
+ }
397
+ if (Object.keys(changes).length === 0)
398
+ return { appliesFrom: 'current_turn' }
399
+
400
+ const threadId = this.metadata.providerSessionId
401
+ // Least durable effect first: thread defaults only reach future turns, and every turn/start
402
+ // re-sends the committed `current`, so a failure after this call leaves nothing that outlives
403
+ // the next turn. The running turn changes last, so a failed thread update never leaves the
404
+ // active turn on settings the steer was not accepted for.
405
+ try {
406
+ await this.client.request('thread/settings/update', { threadId, ...changes })
407
+ }
408
+ catch (error) {
409
+ throw mapSettingsTransportLoss(error)
410
+ }
411
+ try {
412
+ const result = await this.client.request('turn/settings/update', { threadId, turnId, ...changes })
413
+ // `targetUnavailable` (the turn ended or was replaced first) carries no provider reason.
414
+ return result.status === 'applied' ? { appliesFrom: 'current_turn' } : { appliesFrom: 'next_turn' }
415
+ }
416
+ catch (error) {
417
+ if (!isTurnSettingsUnavailableResponse(error))
418
+ throw mapSettingsTransportLoss(error)
419
+ return { appliesFrom: 'next_turn', currentTurnUnavailableReason: error.responseErrorMessage || undefined }
420
+ }
421
+ }
422
+
423
+ private async restoreSteerSettings(input: { turnId: string, previousSettings: CodexTurnSettings, settings: CodexTurnSettings, placement: AgentSettingsPlacement }): Promise<void> {
424
+ const { turnId, previousSettings, settings, placement } = input
425
+ const modelChanged = settings.model !== previousSettings.model
426
+ const effortChanged = settings.effort !== previousSettings.effort
427
+ if (!modelChanged && !effortChanged)
428
+ return
429
+ const threadId = this.metadata.providerSessionId
430
+ try {
431
+ // The SDK treats null/omitted effort as unchanged. An implicit provider default cannot
432
+ // be reconstructed here, so retire this run instead of claiming it was restored.
433
+ if (effortChanged && previousSettings.effort === undefined)
434
+ throw new AgentRunStateError({ message: 'Cannot restore an implicit Codex reasoning effort.' })
435
+ const changes = {
436
+ ...(modelChanged ? { model: this.resolveInferenceModel(previousSettings.model) } : {}),
437
+ ...(effortChanged ? { effort: codexEffort(previousSettings.effort) } : {}),
438
+ }
439
+ // Restore the active turn first. targetUnavailable is safe: that turn can no longer
440
+ // execute with the tentative settings. A refusal/transport failure is not restoration.
441
+ if (placement.appliesFrom === 'current_turn')
442
+ await this.client.request('turn/settings/update', { threadId, turnId, ...changes })
443
+ await this.client.request('thread/settings/update', { threadId, ...changes })
444
+ }
445
+ catch (error) {
446
+ // Do not defer while an active provider can continue on unaccepted settings.
447
+ await this.stop({ type: 'agent.stop', provider: 'codex', sessionId: this.metadata.sessionId, providerSessionId: threadId, reason: 'settings-rollback-failed' })
448
+ throw new AgentNotAcceptedError({
449
+ message: 'Codex settings could not be restored after a rejected steer; the run was stopped.',
450
+ recovery: { kind: 'retry', reason: 'provider_connection_lost' },
451
+ cause: error,
452
+ })
453
+ }
454
+ }
455
+
456
+ private settingsAccepted(input: { settings: CodexTurnSettings, commandId: string, turnId: string | undefined, placement: AgentSettingsPlacement }): void {
457
+ this.current = input.settings
458
+ this.pushEvent(sessionSettingsChangedEvent({
459
+ model: input.settings.model,
460
+ reasoningEffort: input.settings.effort,
461
+ commandId: input.commandId,
462
+ turnId: input.turnId,
463
+ ...input.placement,
464
+ }))
465
+ }
466
+
467
+ private sessionContextDelivered(sessionContext: string | undefined): void {
468
+ if (sessionContext !== undefined && this.sessionContext === sessionContext)
469
+ this.sessionContext = undefined
470
+ }
471
+
472
+ private assertSubmissionOrder(mode: SendPromptCommand['mode'], submissionOrder: AgentPromptOperationOptions['submissionOrder']): void {
473
+ if (submissionOrder === 'bypass' && mode === 'next') {
474
+ throw new AgentRunStateError({
475
+ message: 'Bypass submission requires a steer prompt.',
476
+ })
477
+ }
478
+ }
479
+
282
480
  private deferNextWhileTurnActive(mode: SendPromptCommand['mode']): void {
283
481
  if (mode !== 'next' || !this.activeTurnId)
284
482
  return
@@ -287,6 +485,93 @@ export class CodexRun implements AgentRun {
287
485
  })
288
486
  }
289
487
 
488
+ private deferBypassWithoutTurn(): void {
489
+ if (this.activeTurnId)
490
+ return
491
+ throw new AgentPromptDeferredError({
492
+ message: 'Codex has no active turn for bypass prompt submission.',
493
+ })
494
+ }
495
+
496
+ async notify(command: AgentNotifyCommand, options: AgentOperationOptions = {}): Promise<AgentNotificationAcceptance> {
497
+ this.assertCommandTargetsRun(command)
498
+ this.assertRunning()
499
+ options.signal?.throwIfAborted()
500
+ this.deferNotificationForUnresolvedUserInput()
501
+
502
+ const { receipt, placement } = await this.startTurn({
503
+ commandId: command.commandId,
504
+ settings: this.current,
505
+ content: {
506
+ kind: 'notification',
507
+ output: serializeAgentNotificationToolOutput({
508
+ commandId: command.commandId,
509
+ instruction: command.instruction,
510
+ notifications: command.notifications,
511
+ }),
512
+ },
513
+ }, options)
514
+ return { ...receipt, placement }
515
+ }
516
+
517
+ private deferNotificationForUnresolvedUserInput(): void {
518
+ if (this.userInputRequests.hasUnresolvedUserInput()) {
519
+ throw new AgentPromptDeferredError({
520
+ message: 'Codex is waiting for unresolved user input; a notification cannot be submitted.',
521
+ })
522
+ }
523
+ }
524
+
525
+ async compactContext(command: CompactContextAgentCommand, options: AgentOperationOptions = {}): Promise<AgentContextCompactionRequestResult> {
526
+ this.assertCommandTargetsRun(command)
527
+ this.assertRunning()
528
+ options.signal?.throwIfAborted()
529
+ if (this.activeTurnId) {
530
+ throw new AgentPromptDeferredError({
531
+ message: 'Codex turn is active; context compaction must wait for an idle main thread.',
532
+ })
533
+ }
534
+
535
+ let result: AgentContextCompactionRequestResult
536
+ try {
537
+ await this.client.request('thread/compact/start', { threadId: this.metadata.providerSessionId })
538
+ // The pinned RPC response is an empty acknowledgement. Native item/turn lifecycle has
539
+ // no request identity or manual/auto discriminator, so this control result stays unknown.
540
+ result = { status: 'unknown' }
541
+ }
542
+ catch (error) {
543
+ if (error instanceof CodexJsonRpcTransportClosedError) {
544
+ if (error.writeState === 'possible_write') {
545
+ result = { status: 'unknown' }
546
+ }
547
+ else {
548
+ throw new AgentNotAcceptedError({
549
+ message: 'Codex app-server connection closed before the compact request was written.',
550
+ recovery: { kind: 'retry', reason: 'provider_connection_lost' },
551
+ cause: error,
552
+ })
553
+ }
554
+ }
555
+ else if (error instanceof CodexJsonRpcResponseError) {
556
+ result = {
557
+ status: 'failed',
558
+ error: {
559
+ message: error.responseErrorMessage || 'Codex rejected context compaction.',
560
+ ...(error.responseErrorCode === undefined ? {} : { code: String(error.responseErrorCode) }),
561
+ },
562
+ }
563
+ }
564
+ else {
565
+ throw error
566
+ }
567
+ }
568
+ this.pushEvent({
569
+ type: 'context.compaction.request.completed',
570
+ payload: { commandId: command.commandId, result },
571
+ })
572
+ return result
573
+ }
574
+
290
575
  async respondUserInput(command: RespondUserInputAgentCommand, options: AgentOperationOptions = {}): Promise<AgentUserInputSubmission> {
291
576
  this.assertCommandTargetsRun(command)
292
577
  this.assertRunning()
@@ -297,7 +582,12 @@ export class CodexRun implements AgentRun {
297
582
  if (resolved.event)
298
583
  this.pushEvent(resolved.event)
299
584
  const prepared = await this.prepareTurn({ prompt: resolved.prompt, commandId: command.commandId, options })
300
- await this.startPreparedTurn(prepared)
585
+ await this.startTurn({
586
+ commandId: prepared.commandId,
587
+ settings: this.current,
588
+ content: { kind: 'prompt', input: prepared.input, origin: prepared.origin },
589
+ }, options)
590
+ this.sessionContextDelivered(prepared.sessionContext)
301
591
  this.userInputRequests.markDelivered(command.requestId)
302
592
  return 'submitted'
303
593
  }
@@ -306,6 +596,8 @@ export class CodexRun implements AgentRun {
306
596
  this.assertCommandTargetsRun(command)
307
597
  this.assertRunning()
308
598
  options.signal?.throwIfAborted()
599
+ if (command.turnId && command.turnId !== this.activeTurnId)
600
+ return
309
601
  const turnId = command.turnId ?? this.activeTurnId
310
602
  if (!turnId)
311
603
  return
@@ -333,51 +625,100 @@ export class CodexRun implements AgentRun {
333
625
  options.signal?.throwIfAborted()
334
626
  this.stopped = true
335
627
  this.events.push({ type: 'session.state.changed', payload: { state: 'stopping' } })
336
- this.onStop()
337
- this.stoppingTask = (async () => {
338
- await this.client.stop()
628
+ const stopping = Promise.withResolvers<void>()
629
+ this.stoppingTask = stopping.promise
630
+ this.startStopping(() => {
339
631
  this.events.push({ type: 'session.ended', payload: { reason: 'stopped' } })
340
632
  this.events.push({ type: 'session.state.changed', payload: { state: 'stopped' } })
341
633
  this.events.end()
342
- })()
343
- await this.stoppingTask
634
+ }, stopping)
635
+ await stopping.promise
344
636
  }
345
637
 
346
- private async prepareTurn({ prompt, commandId, origin = 'user', author, options }: { prompt: AgentPrompt, commandId: string, origin?: 'user' | 'supervisor', author?: AgentAuthor, options: AgentOperationOptions }): Promise<PreparedCodexTurn> {
638
+ private startStopping(finalize: () => void, stopping: PromiseWithResolvers<void>): void {
639
+ const revocation = new Promise<void>((resolve) => {
640
+ resolve(this.onStop())
641
+ })
642
+ void Promise.all([this.client.stop(), revocation])
643
+ .then(finalize)
644
+ .then(stopping.resolve, stopping.reject)
645
+ }
646
+
647
+ private async prepareTurn({ prompt, commandId, origin = 'user', author, options }: { prompt: AgentPrompt, commandId: string, origin?: 'user' | 'supervisor', author?: AgentAuthor, options: AgentOperationOptions }): Promise<PreparedCodexPromptTurn> {
347
648
  options.signal?.throwIfAborted()
348
- const mapperInput = { prompt, origin, author, senderContext: options.senderContext }
649
+ const sessionContext = this.sessionContext
650
+ const mapperInput = { prompt, origin, author, senderContext: options.senderContext, authorContext: options.authorContext, sessionContext }
349
651
  const input = options.inputAttachmentPreparation
350
652
  ? await prepareCodexUserInput({ ...mapperInput, preparation: options.inputAttachmentPreparation })
351
653
  : toCodexUserInput(mapperInput)
352
654
  options.signal?.throwIfAborted()
353
- return { input, commandId, origin }
655
+ return { input, commandId, origin, sessionContext }
354
656
  }
355
657
 
356
- private async startPreparedTurn(prepared: PreparedCodexTurn): Promise<AgentPromptAcceptance> {
357
- const publicModel = prepared.model ?? this.metadata.model
658
+ private async startTurn(prepared: PreparedCodexTurnStart, options: AgentOperationOptions): Promise<CodexTurnStartResult> {
659
+ // Provider state can change during prompt preparation or notification serialization. Keep
660
+ // the last no-write guard and abort check at the shared turn/start boundary.
661
+ this.assertRunning()
662
+ options.signal?.throwIfAborted()
663
+ if (prepared.content.kind === 'notification')
664
+ this.deferNotificationForUnresolvedUserInput()
665
+ else if (this.activeTurnId)
666
+ this.deferNextWhileTurnActive('next')
667
+ const activeTurnIdAtSubmission = this.activeTurnId
668
+ const publicModel = prepared.settings.model
358
669
  const inferenceModel = publicModel ? this.resolveInferenceModel(publicModel) : undefined
359
- const clientUserMessageId = this.rememberPromptCommand(prepared.commandId, prepared.origin)
670
+
671
+ const clientUserMessageId = prepared.content.kind === 'prompt'
672
+ ? this.rememberPromptCommand(prepared.commandId, prepared.content.origin)
673
+ : deriveMessageId(prepared.commandId)
674
+ const notificationParams = prepared.content.kind === 'notification'
675
+ ? {
676
+ turnTrigger: codexNotificationTurnTrigger,
677
+ toolOutput: {
678
+ namespace: codexNotificationToolNamespace,
679
+ name: codexNotificationToolName,
680
+ output: prepared.content.output,
681
+ },
682
+ }
683
+ : {}
684
+
360
685
  let result: ClientRequestResponsesByMethod['turn/start']
361
686
  try {
362
687
  result = await this.client.request('turn/start', {
363
688
  threadId: this.metadata.providerSessionId,
364
- input: prepared.input,
689
+ input: prepared.content.kind === 'prompt' ? prepared.content.input : [],
365
690
  cwd: this.metadata.cwd,
366
691
  model: inferenceModel,
367
- effort: codexEffort(this.reasoningEffort),
692
+ effort: codexEffort(prepared.settings.effort),
368
693
  summary: codexReasoningSummary,
369
694
  approvalPolicy: 'never',
370
695
  sandboxPolicy: { type: 'dangerFullAccess' },
371
696
  clientUserMessageId,
697
+ ...notificationParams,
372
698
  })
373
699
  }
374
700
  catch (error) {
701
+ if (prepared.content.kind === 'notification' && isNotificationNotAcceptedResponse(error)) {
702
+ throw new AgentPromptDeferredError({
703
+ message: 'Codex active turn cannot accept a notification.',
704
+ cause: error,
705
+ })
706
+ }
375
707
  throw mapTurnTransportLoss(error)
376
708
  }
377
- this.activeTurnId = result.turn.id
378
- if (prepared.model)
379
- this.metadata.model = prepared.model
380
- return { kind: 'turn', turnId: result.turn.id, placement: 'started' }
709
+ const placement = prepared.content.kind === 'notification'
710
+ && activeTurnIdAtSubmission === result.turn.id
711
+ ? 'steered'
712
+ : 'started'
713
+ if (placement === 'started') {
714
+ this.activeTurnId = result.turn.id
715
+ this.current = prepared.settings
716
+ this.metadata.model = prepared.settings.model
717
+ }
718
+ return {
719
+ receipt: { kind: 'turn', turnId: result.turn.id },
720
+ placement,
721
+ }
381
722
  }
382
723
 
383
724
  private rememberPromptCommand(commandId: string, origin: 'user' | 'supervisor'): string {
@@ -406,6 +747,45 @@ function isSteerNotAcceptedResponse(error: unknown): error is CodexJsonRpcRespon
406
747
  return error instanceof CodexJsonRpcResponseError && error.method === 'turn/steer' && error.responseErrorCode === -32600
407
748
  }
408
749
 
750
+ function withSettingsChange(current: CodexTurnSettings, change: AgentSettingsChange | undefined): CodexTurnSettings {
751
+ return {
752
+ model: change?.model ?? current.model,
753
+ effort: change?.reasoningEffort ?? current.effort,
754
+ }
755
+ }
756
+
757
+ // A running turn that cannot take new settings is not a failure: they apply from the next turn.
758
+ // Pinned Codex answers -32600 with its reason as the message when `features.step_model_switching` is off
759
+ // (live-verified on 0.153.4) or when its legacy turn-safety check refuses the destination model
760
+ // (core/src/session/step_activation.rs at rust-v0.156.1, live-verified for gpt-6-astra -> gpt-5.5).
761
+ function isTurnSettingsUnavailableResponse(error: unknown): error is CodexJsonRpcResponseError {
762
+ return error instanceof CodexJsonRpcResponseError && error.method === 'turn/settings/update' && error.responseErrorCode === -32600
763
+ }
764
+
765
+ // Settings requests run before the prompt is written, so losing the connection during one proves
766
+ // the prompt was not delivered, whatever the settings request's own write state.
767
+ function mapSettingsTransportLoss(error: unknown): unknown {
768
+ if (!(error instanceof CodexJsonRpcTransportClosedError))
769
+ return error
770
+ return new AgentNotAcceptedError({
771
+ message: 'Codex app-server connection was lost while applying prompt settings; the prompt was not written.',
772
+ recovery: { kind: 'retry', reason: 'provider_connection_lost' },
773
+ cause: error,
774
+ })
775
+ }
776
+
777
+ // Pinned Codex reports a standalone tool output as definitively not submitted when the active turn is
778
+ // review or compaction work. The app-server currently exposes that Core reason only in this exact error.
779
+ function isNotificationNotAcceptedResponse(error: unknown): error is CodexJsonRpcResponseError {
780
+ if (!(error instanceof CodexJsonRpcResponseError)
781
+ || error.method !== 'turn/start'
782
+ || error.responseErrorCode !== -32603) {
783
+ return false
784
+ }
785
+ return error.responseErrorMessage === 'failed to submit turn input: ActiveTurnNotSteerable { turn_kind: Review }'
786
+ || error.responseErrorMessage === 'failed to submit turn input: ActiveTurnNotSteerable { turn_kind: Compact }'
787
+ }
788
+
409
789
  // Transport loss on an input-carrying request maps to write-state truth: possible_write means the
410
790
  // input may already be in the provider thread (delivery unknown, never blind-retryable);
411
791
  // before_write is proven non-delivery and safe to retry. Anything else passes through unchanged.