@meistrari/agent-core 0.2.0-experimental.1 → 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 (71) hide show
  1. package/README.md +50 -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 +864 -135
  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 +893 -130
  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 +423 -53
  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 +52 -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 +3 -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/supervisor-protocol/agent-event-wrapper.ts +30 -0
@@ -1,49 +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
39
- reasoningEffort?: AgentReasoningEffort
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'
40
73
  }
41
74
 
42
75
  interface CodexRunInput {
43
76
  beforePrompt?: (signal?: AbortSignal) => Promise<void>
44
77
  resolveInferenceModel?: (model: string) => string
45
78
  metadata: AgentRunMetadata
46
- 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
47
83
  client: CodexJsonRpcClient
48
84
  requestedCwd: string
49
85
  workspaceRepositoryRoots: readonly string[]
@@ -53,7 +89,7 @@ interface CodexRunInput {
53
89
  userInputRequests: UserInputRequestStore
54
90
  eventMapperState: CodexEventMapperState
55
91
  abortSignal: AbortSignal
56
- onStop: () => void
92
+ onStop: () => void | Promise<void>
57
93
  }
58
94
 
59
95
  export class CodexRun implements AgentRun {
@@ -62,8 +98,8 @@ export class CodexRun implements AgentRun {
62
98
  private readonly client: CodexJsonRpcClient
63
99
  private readonly beforePrompt: ((signal?: AbortSignal) => Promise<void>) | undefined
64
100
  private readonly resolveInferenceModel: (model: string) => string
65
- private reasoningEffort: AgentReasoningEffort | undefined
66
- private readonly onStop: () => void
101
+ private current: CodexTurnSettings
102
+ private readonly onStop: () => void | Promise<void>
67
103
  private readonly userInputRequests: UserInputRequestStore
68
104
  private readonly eventMapperState: CodexEventMapperState
69
105
  private readonly skillCatalog: CodexSkillCatalog
@@ -75,14 +111,16 @@ export class CodexRun implements AgentRun {
75
111
  private stopped = false
76
112
  private stoppingTask: Promise<void> | undefined
77
113
  private activeTurnId: string | undefined
114
+ private sessionContext: string | undefined
78
115
  private readonly interruptingTurnIds = new Set<string>()
79
116
 
80
117
  constructor(input: CodexRunInput) {
81
118
  this.metadata = input.metadata
119
+ this.sessionContext = input.sessionContext
82
120
  this.client = input.client
83
121
  this.beforePrompt = input.beforePrompt
84
122
  this.resolveInferenceModel = input.resolveInferenceModel ?? (model => model)
85
- this.reasoningEffort = input.reasoningEffort
123
+ this.current = { ...input.settings }
86
124
  this.onStop = input.onStop
87
125
  this.userInputRequests = input.userInputRequests
88
126
  this.eventMapperState = input.eventMapperState
@@ -205,6 +243,10 @@ export class CodexRun implements AgentRun {
205
243
  }
206
244
 
207
245
  async failActiveTurnAndEndRun(input: CodexRunAuthFailureInput): Promise<void> {
246
+ if (this.stoppingTask) {
247
+ await this.stoppingTask
248
+ return
249
+ }
208
250
  if (this.stopped)
209
251
  return
210
252
  this.pushEvent({ type: 'error', payload: { message: 'Codex auth refresh failed.', fatal: false, source: 'provider', error: input.error } })
@@ -220,23 +262,38 @@ export class CodexRun implements AgentRun {
220
262
  }
221
263
  this.pushEvent({ type: 'session.state.changed', payload: { state: 'failed' } })
222
264
  this.stopped = true
223
- // Pinned Codex forwards preceding stdin lines before EOF, preserving the refresh error ordering.
224
- await this.client.closeInputAndStop()
225
- this.events.push({ type: 'session.ended', payload: { reason: 'failed', error: input.error } })
226
- 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
227
274
  }
228
275
 
229
- async sendPrompt(command: SendPromptCommand, options: AgentOperationOptions = {}): Promise<AgentPromptAcceptance> {
276
+ async sendPrompt(command: SendPromptCommand, options: AgentPromptOperationOptions = {}): Promise<AgentPromptAcceptance> {
230
277
  this.assertCommandTargetsRun(command)
231
278
  this.assertRunning()
279
+ options.signal?.throwIfAborted()
280
+ this.assertSubmissionOrder(command.mode, options.submissionOrder)
232
281
  // Released 0.145.0 accepts turn/start during an active turn by silently steering the input
233
- // and answering a phantom turn id, so the next-vs-active race must be refused from adapter
234
- // state before any request — nothing is prepared or retained.
235
- this.deferNextWhileTurnActive(command.mode)
236
- 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) {
237
290
  await this.beforePrompt(options.signal)
238
- this.assertRunning()
239
- this.deferNextWhileTurnActive(command.mode)
291
+ this.assertRunning()
292
+ if (submissionOrder === 'in-order')
293
+ this.deferNextWhileTurnActive(command.mode)
294
+ else
295
+ this.deferBypassWithoutTurn()
296
+ }
240
297
  const prepared = await this.prepareTurn({
241
298
  prompt: command.prompt,
242
299
  commandId: command.commandId,
@@ -244,24 +301,49 @@ export class CodexRun implements AgentRun {
244
301
  author: command.author,
245
302
  options,
246
303
  })
247
- prepared.model = command.model
248
- prepared.reasoningEffort = command.reasoningEffort
249
- // A turn/started fact may have reduced during attachment preparation.
250
- 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()
251
312
 
252
313
  if (command.turnId && (command.mode !== 'steer' || command.turnId !== this.activeTurnId))
253
314
  throw new AgentNotAcceptedError({ message: 'The targeted codex turn is no longer active.', recovery: { kind: 'terminal', reason: 'target_turn_not_active' } })
254
315
  const activeTurnId = this.activeTurnId
255
316
  if (activeTurnId && command.model && command.model !== this.metadata.model)
256
317
  throw new AgentPromptDeferredError({ message: 'Codex model changes require an idle turn boundary.' })
257
- if (activeTurnId && command.reasoningEffort && command.reasoningEffort !== this.reasoningEffort)
318
+ if (activeTurnId && command.reasoningEffort && command.reasoningEffort !== this.current.effort)
258
319
  throw new AgentPromptDeferredError({ message: 'Codex reasoning effort changes require an idle turn boundary.' })
259
- if (command.mode === 'next' || !activeTurnId)
260
- return await this.startPreparedTurn(prepared)
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
+ }
261
333
 
262
- 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
263
341
  let result: ClientRequestResponsesByMethod['turn/steer']
264
342
  try {
343
+ this.assertRunning()
344
+ options.signal?.throwIfAborted()
345
+ this.rememberPromptCommand(prepared.commandId, prepared.origin)
346
+ steerRequested = true
265
347
  result = await this.client.request('turn/steer', {
266
348
  threadId: this.metadata.providerSessionId,
267
349
  expectedTurnId: activeTurnId,
@@ -270,13 +352,32 @@ export class CodexRun implements AgentRun {
270
352
  })
271
353
  }
272
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 })
273
361
  if (isSteerNotAcceptedResponse(error)) {
362
+ if (submissionOrder === 'bypass')
363
+ this.eventMapperState.promptCommandByClientUserMessageId.delete(clientUserMessageId)
274
364
  throw new AgentPromptDeferredError({
275
365
  message: 'Codex rejected the steer at a turn boundary race.',
276
366
  cause: error,
277
367
  })
278
368
  }
279
- 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
+ })
280
381
  }
281
382
  return { kind: 'turn', turnId: result.turnId, placement: 'steered' }
282
383
  }
@@ -285,6 +386,97 @@ export class CodexRun implements AgentRun {
285
386
  return this.activeTurnId !== undefined
286
387
  }
287
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
+
288
480
  private deferNextWhileTurnActive(mode: SendPromptCommand['mode']): void {
289
481
  if (mode !== 'next' || !this.activeTurnId)
290
482
  return
@@ -293,6 +485,93 @@ export class CodexRun implements AgentRun {
293
485
  })
294
486
  }
295
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
+
296
575
  async respondUserInput(command: RespondUserInputAgentCommand, options: AgentOperationOptions = {}): Promise<AgentUserInputSubmission> {
297
576
  this.assertCommandTargetsRun(command)
298
577
  this.assertRunning()
@@ -303,7 +582,12 @@ export class CodexRun implements AgentRun {
303
582
  if (resolved.event)
304
583
  this.pushEvent(resolved.event)
305
584
  const prepared = await this.prepareTurn({ prompt: resolved.prompt, commandId: command.commandId, options })
306
- 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)
307
591
  this.userInputRequests.markDelivered(command.requestId)
308
592
  return 'submitted'
309
593
  }
@@ -341,53 +625,100 @@ export class CodexRun implements AgentRun {
341
625
  options.signal?.throwIfAborted()
342
626
  this.stopped = true
343
627
  this.events.push({ type: 'session.state.changed', payload: { state: 'stopping' } })
344
- this.onStop()
345
- this.stoppingTask = (async () => {
346
- await this.client.stop()
628
+ const stopping = Promise.withResolvers<void>()
629
+ this.stoppingTask = stopping.promise
630
+ this.startStopping(() => {
347
631
  this.events.push({ type: 'session.ended', payload: { reason: 'stopped' } })
348
632
  this.events.push({ type: 'session.state.changed', payload: { state: 'stopped' } })
349
633
  this.events.end()
350
- })()
351
- await this.stoppingTask
634
+ }, stopping)
635
+ await stopping.promise
352
636
  }
353
637
 
354
- 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> {
355
648
  options.signal?.throwIfAborted()
356
- 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 }
357
651
  const input = options.inputAttachmentPreparation
358
652
  ? await prepareCodexUserInput({ ...mapperInput, preparation: options.inputAttachmentPreparation })
359
653
  : toCodexUserInput(mapperInput)
360
654
  options.signal?.throwIfAborted()
361
- return { input, commandId, origin }
655
+ return { input, commandId, origin, sessionContext }
362
656
  }
363
657
 
364
- private async startPreparedTurn(prepared: PreparedCodexTurn): Promise<AgentPromptAcceptance> {
365
- 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
366
669
  const inferenceModel = publicModel ? this.resolveInferenceModel(publicModel) : undefined
367
- 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
+
368
685
  let result: ClientRequestResponsesByMethod['turn/start']
369
686
  try {
370
687
  result = await this.client.request('turn/start', {
371
688
  threadId: this.metadata.providerSessionId,
372
- input: prepared.input,
689
+ input: prepared.content.kind === 'prompt' ? prepared.content.input : [],
373
690
  cwd: this.metadata.cwd,
374
691
  model: inferenceModel,
375
- effort: codexEffort(prepared.reasoningEffort ?? this.reasoningEffort),
692
+ effort: codexEffort(prepared.settings.effort),
376
693
  summary: codexReasoningSummary,
377
694
  approvalPolicy: 'never',
378
695
  sandboxPolicy: { type: 'dangerFullAccess' },
379
696
  clientUserMessageId,
697
+ ...notificationParams,
380
698
  })
381
699
  }
382
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
+ }
383
707
  throw mapTurnTransportLoss(error)
384
708
  }
385
- this.activeTurnId = result.turn.id
386
- if (prepared.reasoningEffort)
387
- this.reasoningEffort = prepared.reasoningEffort
388
- if (prepared.model)
389
- this.metadata.model = prepared.model
390
- 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
+ }
391
722
  }
392
723
 
393
724
  private rememberPromptCommand(commandId: string, origin: 'user' | 'supervisor'): string {
@@ -416,6 +747,45 @@ function isSteerNotAcceptedResponse(error: unknown): error is CodexJsonRpcRespon
416
747
  return error instanceof CodexJsonRpcResponseError && error.method === 'turn/steer' && error.responseErrorCode === -32600
417
748
  }
418
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
+
419
789
  // Transport loss on an input-carrying request maps to write-state truth: possible_write means the
420
790
  // input may already be in the provider thread (delivery unknown, never blind-retryable);
421
791
  // before_write is proven non-delivery and safe to retry. Anything else passes through unchanged.