@adecore/agents 0.38.0 → 0.40.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 (170) hide show
  1. package/dist/async.d.ts +5 -0
  2. package/dist/async.js +13 -5
  3. package/dist/chat/acp-protocol.js +3 -5
  4. package/dist/chat/chat-core.d.ts +1 -0
  5. package/dist/chat/chat-core.js +9 -2
  6. package/dist/chat/chat-process.d.ts +1 -0
  7. package/dist/chat/chat-process.js +1 -1
  8. package/dist/chat/chat-session.d.ts +2 -0
  9. package/dist/chat/chat-session.js +9 -10
  10. package/dist/chat/claude-protocol.js +1 -6
  11. package/dist/chat/claude-transcript.js +1 -3
  12. package/dist/chat/codex-protocol.js +1 -6
  13. package/dist/chat/projector.js +1 -3
  14. package/dist/chat/request-summary.js +1 -3
  15. package/dist/coded-error.js +1 -5
  16. package/dist/context/argv.js +1 -2
  17. package/dist/context/verb.js +1 -3
  18. package/dist/error-text.d.ts +1 -0
  19. package/dist/error-text.js +4 -0
  20. package/dist/fields.d.ts +3 -0
  21. package/dist/fields.js +12 -0
  22. package/dist/fs.d.ts +1 -0
  23. package/dist/fs.js +8 -4
  24. package/dist/lineage.js +11 -5
  25. package/dist/messages/notice-store.js +3 -4
  26. package/dist/providers/accounts/acp-account.js +3 -20
  27. package/dist/providers/accounts/service.d.ts +1 -2
  28. package/dist/providers/accounts/service.js +5 -9
  29. package/dist/providers/accounts/status.js +1 -3
  30. package/dist/providers/acp-agent-state.js +1 -3
  31. package/dist/providers/claude.js +0 -1
  32. package/dist/providers/codex.js +0 -1
  33. package/dist/providers/detect.js +1 -1
  34. package/dist/providers/shell-commands.js +1 -2
  35. package/dist/run-process.js +1 -3
  36. package/dist/skills.d.ts +1 -1
  37. package/dist/skills.js +5 -8
  38. package/dist/tasks/background-limit.js +3 -2
  39. package/dist/tasks/task-coordinator.js +11 -17
  40. package/dist/testing/watch.js +0 -1
  41. package/dist/usage/limits/monitor.js +1 -2
  42. package/dist/usage/readers/claude.js +3 -10
  43. package/dist/usage/readers/codex.js +2 -9
  44. package/dist/usage/readers/grok.js +3 -10
  45. package/dist/usage/readers/host-log.js +3 -10
  46. package/dist/usage/record.d.ts +2 -0
  47. package/dist/usage/record.js +13 -0
  48. package/package.json +6 -6
  49. package/src/async.ts +23 -5
  50. package/src/chat/acp-backend.ts +121 -0
  51. package/src/chat/acp-protocol.ts +172 -6
  52. package/src/chat/attachment-store.ts +21 -0
  53. package/src/chat/backend.ts +0 -2
  54. package/src/chat/bookmark-store.ts +23 -0
  55. package/src/chat/chat-core.ts +218 -7
  56. package/src/chat/chat-log.ts +19 -0
  57. package/src/chat/chat-process.ts +52 -1
  58. package/src/chat/chat-session.ts +414 -13
  59. package/src/chat/chat-store.ts +50 -0
  60. package/src/chat/chat-title.ts +9 -0
  61. package/src/chat/claude-backend.ts +50 -0
  62. package/src/chat/claude-protocol.ts +155 -8
  63. package/src/chat/claude-title.ts +10 -0
  64. package/src/chat/claude-transcript.ts +47 -4
  65. package/src/chat/codex-backend.ts +131 -0
  66. package/src/chat/codex-protocol.ts +180 -8
  67. package/src/chat/codex-thread.ts +23 -0
  68. package/src/chat/composer-preferences.ts +6 -0
  69. package/src/chat/context-breakdown.ts +11 -0
  70. package/src/chat/delta-coalescer.ts +8 -0
  71. package/src/chat/external-sessions.ts +28 -0
  72. package/src/chat/generated-image.ts +25 -0
  73. package/src/chat/index.ts +5 -0
  74. package/src/chat/input.ts +13 -0
  75. package/src/chat/json-rpc-transport.ts +28 -0
  76. package/src/chat/limit-resume.ts +13 -0
  77. package/src/chat/projector.ts +160 -4
  78. package/src/chat/readable-text.ts +3 -0
  79. package/src/chat/request-summary.ts +18 -4
  80. package/src/chat/subagent-projection.ts +3 -0
  81. package/src/chat/subagent-reader.ts +76 -0
  82. package/src/chat/subagent-settlement.ts +24 -0
  83. package/src/chat/thread-search.ts +13 -0
  84. package/src/chat/thread.ts +31 -0
  85. package/src/chat/ui-queries.ts +122 -0
  86. package/src/chat/ui-replies.ts +34 -0
  87. package/src/chat/visual-store.ts +49 -0
  88. package/src/chat/wake-chat.ts +3 -0
  89. package/src/client-sinks.ts +2 -0
  90. package/src/coded-error.ts +1 -5
  91. package/src/context/argv.ts +18 -2
  92. package/src/context/index.ts +1 -0
  93. package/src/context/refusal.ts +3 -0
  94. package/src/context/verb.ts +32 -4
  95. package/src/coordination/index.ts +10 -0
  96. package/src/error-text.ts +7 -0
  97. package/src/fields.ts +14 -0
  98. package/src/fs.ts +20 -4
  99. package/src/host/agent-host.ts +20 -0
  100. package/src/host/environment.ts +6 -0
  101. package/src/host/handlers.ts +18 -0
  102. package/src/host/memory-port.ts +4 -0
  103. package/src/host/wiring.ts +10 -0
  104. package/src/index.ts +1 -0
  105. package/src/lineage.ts +24 -5
  106. package/src/messages/deliver-message.ts +6 -0
  107. package/src/messages/deliver-notice.ts +13 -0
  108. package/src/messages/notice-store.ts +16 -4
  109. package/src/modes.ts +1 -0
  110. package/src/outbox/outbox-worker.ts +31 -0
  111. package/src/outbox/outbox.ts +5 -0
  112. package/src/providers/accounts/accounts.ts +36 -0
  113. package/src/providers/accounts/acp-account.ts +22 -27
  114. package/src/providers/accounts/launch.ts +4 -0
  115. package/src/providers/accounts/service.ts +110 -12
  116. package/src/providers/accounts/shadow-home.ts +10 -0
  117. package/src/providers/accounts/status.ts +13 -4
  118. package/src/providers/accounts/store.ts +25 -0
  119. package/src/providers/accounts/variables.ts +11 -1
  120. package/src/providers/acp-agent-state.ts +24 -3
  121. package/src/providers/acp-provider.ts +5 -0
  122. package/src/providers/acp-usage-log.ts +2 -0
  123. package/src/providers/catalog.ts +17 -0
  124. package/src/providers/claude.ts +10 -1
  125. package/src/providers/codex-rules.ts +3 -0
  126. package/src/providers/codex.ts +2 -1
  127. package/src/providers/detect.ts +6 -1
  128. package/src/providers/provider.ts +1 -1
  129. package/src/providers/registry.ts +18 -2
  130. package/src/providers/shell-commands.ts +37 -2
  131. package/src/record-directory.ts +19 -0
  132. package/src/run-process.ts +24 -3
  133. package/src/serializer.ts +3 -0
  134. package/src/skills.ts +73 -10
  135. package/src/tasks/background-limit.ts +9 -2
  136. package/src/tasks/end-children.ts +9 -0
  137. package/src/tasks/give-task.ts +8 -0
  138. package/src/tasks/task-coordinator.ts +81 -17
  139. package/src/tasks/task-store.ts +28 -0
  140. package/src/tasks/waiting-child.ts +21 -0
  141. package/src/tasks/wake-parent.ts +26 -0
  142. package/src/tasks/wiring.ts +22 -0
  143. package/src/testing/accounts.ts +2 -0
  144. package/src/testing/fake-acp-capture.ts +16 -0
  145. package/src/testing/fake-acp.ts +66 -0
  146. package/src/testing/fake-claude.ts +177 -0
  147. package/src/testing/fake-cli.ts +29 -0
  148. package/src/testing/fake-codex.ts +122 -0
  149. package/src/testing/manual-clock.ts +2 -0
  150. package/src/testing/watch.ts +7 -1
  151. package/src/title-file.ts +12 -0
  152. package/src/usage/accounts.ts +6 -0
  153. package/src/usage/aggregate.ts +19 -0
  154. package/src/usage/exchange.ts +12 -0
  155. package/src/usage/index-file.ts +27 -0
  156. package/src/usage/limits/monitor.ts +31 -3
  157. package/src/usage/limits/normalize.ts +30 -0
  158. package/src/usage/limits/probe.ts +17 -0
  159. package/src/usage/pricing.ts +35 -0
  160. package/src/usage/projects.ts +8 -0
  161. package/src/usage/readers/claude.ts +12 -9
  162. package/src/usage/readers/codex.ts +40 -8
  163. package/src/usage/readers/grok.ts +17 -9
  164. package/src/usage/readers/host-log.ts +9 -9
  165. package/src/usage/record.ts +22 -0
  166. package/src/usage/roots.ts +6 -0
  167. package/src/usage/scanner.ts +61 -0
  168. package/src/usage/usage-service.ts +14 -0
  169. package/src/watch-seam.ts +8 -0
  170. package/src/windows-command.ts +12 -0
@@ -5,6 +5,7 @@ import { readCodexLimits } from '../usage/limits/normalize.ts';
5
5
  import { codexRuntimeMode } from '../providers/codex.ts';
6
6
  import type { ApprovalDecision, BackendEvent } from './backend.ts';
7
7
  import type { CodexFrame } from './codex-transport.ts';
8
+ import { num, str } from '../fields.ts';
8
9
  import { cleanTitle } from '../title-file.ts';
9
10
 
10
11
  type CodexRpcId = number | string;
@@ -14,14 +15,6 @@ type CodexAnswer = { kind: 'respond'; rpcId: CodexRpcId; result: unknown } | { k
14
15
 
15
16
  type Frame = Record<string, unknown>;
16
17
 
17
- function str(value: unknown): string | null {
18
- return typeof value === 'string' ? value : null;
19
- }
20
-
21
- function num(value: unknown): number {
22
- return typeof value === 'number' && Number.isFinite(value) ? value : 0;
23
- }
24
-
25
18
  const GOAL_STATUSES: Record<string, ChatGoalStatus> = {
26
19
  active: 'active',
27
20
  paused: 'paused',
@@ -36,15 +29,20 @@ export function codexGoal(raw: unknown): ChatGoal | null {
36
29
  if (!isRecord(raw)) {
37
30
  return null;
38
31
  }
32
+
39
33
  const objective = str(raw.objective)?.trim() ?? '';
40
34
  const status = GOAL_STATUSES[str(raw.status) ?? ''];
35
+
41
36
  if (objective === '' || status === undefined) {
42
37
  return null;
43
38
  }
39
+
44
40
  const count = (value: unknown): number | undefined => (typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : undefined);
41
+
45
42
  const tokensUsed = count(raw.tokensUsed);
46
43
  const tokenBudget = count(raw.tokenBudget);
47
44
  const secondsUsed = count(raw.timeUsedSeconds);
45
+
48
46
  return {
49
47
  objective,
50
48
  status,
@@ -59,11 +57,14 @@ const SHELL_WRAPPER = /^(?:\/bin\/|\/usr\/bin\/)?(?:zsh|bash|sh) -lc (.*)$/s;
59
57
 
60
58
  export function unwrapCommand(command: string): string {
61
59
  const match = SHELL_WRAPPER.exec(command);
60
+
62
61
  if (!match) {
63
62
  return command;
64
63
  }
64
+
65
65
  const inner = match[1]!;
66
66
  const quoted = /^(["'])([\s\S]*)\1$/.exec(inner);
67
+
67
68
  return quoted ? quoted[2]! : inner;
68
69
  }
69
70
 
@@ -71,9 +72,11 @@ function textOf(content: unknown): string {
71
72
  if (typeof content === 'string') {
72
73
  return content;
73
74
  }
75
+
74
76
  if (!Array.isArray(content)) {
75
77
  return '';
76
78
  }
79
+
77
80
  return content
78
81
  .map((block) => (isRecord(block) ? (str(block.text) ?? null) : null))
79
82
  .filter((text): text is string => text !== null)
@@ -88,10 +91,12 @@ function collabAgentOutput(states: unknown): string {
88
91
  if (!isRecord(states)) {
89
92
  return '';
90
93
  }
94
+
91
95
  return Object.entries(states)
92
96
  .map(([thread, state]) => {
93
97
  const status = isRecord(state) ? (str(state.status) ?? 'unknown') : 'unknown';
94
98
  const message = isRecord(state) ? str(state.message) : null;
99
+
95
100
  return message ? `${thread}: ${status}, ${message}` : `${thread}: ${status}`;
96
101
  })
97
102
  .join('\n');
@@ -102,6 +107,7 @@ function parseBlockingQuestions(questions: unknown): ChatQuestion[] {
102
107
  if (!Array.isArray(questions)) {
103
108
  return [];
104
109
  }
110
+
105
111
  return questions.filter(isRecord).map((question, index) => ({
106
112
  id: str(question.id) ?? String(index),
107
113
  header: str(question.header) ?? '',
@@ -118,6 +124,7 @@ function parseAsyncQuestions(questions: unknown): ChatQuestion[] {
118
124
  if (!Array.isArray(questions)) {
119
125
  return [];
120
126
  }
127
+
121
128
  return questions.filter(isRecord).map((question, index) => ({
122
129
  id: String(index),
123
130
  header: '',
@@ -140,8 +147,10 @@ function parseChanges(value: unknown): ChatFileChange[] {
140
147
  if (!Array.isArray(value)) {
141
148
  return [];
142
149
  }
150
+
143
151
  return value.filter(isRecord).map((change) => {
144
152
  const kind = isRecord(change.kind) ? str(change.kind.type) : str(change.kind);
153
+
145
154
  return {
146
155
  path: str(change.path) ?? '',
147
156
  kind: kind !== null && CHANGE_KINDS.has(kind as ChatFileChange['kind']) ? (kind as ChatFileChange['kind']) : 'update',
@@ -153,13 +162,17 @@ function parseChanges(value: unknown): ChatFileChange[] {
153
162
  // Partial command output arrives as text or as the raw bytes of it, depending on the build.
154
163
  function outputChunk(params: Frame): string | null {
155
164
  const text = str(params.delta) ?? str(params.chunk);
165
+
156
166
  if (text !== null) {
157
167
  return text;
158
168
  }
169
+
159
170
  const bytes = params.chunk ?? params.delta;
171
+
160
172
  if (!Array.isArray(bytes)) {
161
173
  return null;
162
174
  }
175
+
163
176
  return new TextDecoder().decode(Uint8Array.from(bytes.filter((byte): byte is number => typeof byte === 'number')));
164
177
  }
165
178
 
@@ -187,26 +200,31 @@ function isConfirmation(params: Frame): boolean {
187
200
  /* The widest remembering the MCP server offers, which the one "always" button of a card stands for. */
188
201
  function elicitationPersist(meta: Frame): ElicitationPersist | null {
189
202
  const offered = Array.isArray(meta.persist) ? meta.persist : [];
203
+
190
204
  return offered.includes('always') ? 'always' : offered.includes('session') ? 'session' : null;
191
205
  }
192
206
 
193
207
  /* The arguments as the MCP server shows them to a person, keyed by name, so the card sums up the call. */
194
208
  function elicitationInput(meta: Frame): Frame {
195
209
  const input: Frame = {};
210
+
196
211
  for (const param of Array.isArray(meta.tool_params_display) ? meta.tool_params_display : []) {
197
212
  if (isRecord(param) && typeof param.name === 'string') {
198
213
  input[param.name] = param.value;
199
214
  }
200
215
  }
216
+
201
217
  return input;
202
218
  }
203
219
 
204
220
  // The latest reset of every spent window; one without a known reset leaves resuming to a person.
205
221
  function spentUntil(windows: readonly UsageWindow[]): number | null {
206
222
  const spent = windows.filter((window) => window.used >= 1);
223
+
207
224
  if (spent.length === 0 || spent.some((window) => window.resetsAt === null)) {
208
225
  return null;
209
226
  }
227
+
210
228
  return Math.max(...spent.map((window) => window.resetsAt!));
211
229
  }
212
230
 
@@ -247,6 +265,7 @@ export class CodexProtocol {
247
265
  const policy = isRecord(result) ? str(result.approvalPolicy) : null;
248
266
  const sandbox = isRecord(result) && isRecord(result.sandbox) ? str(result.sandbox.type) : null;
249
267
  const effectiveRuntimeMode = policy && sandbox ? codexRuntimeMode(policy, sandbox) : undefined;
268
+
250
269
  return [
251
270
  {
252
271
  type: 'session',
@@ -261,28 +280,38 @@ export class CodexProtocol {
261
280
 
262
281
  handle(frame: CodexFrame): BackendEvent[] {
263
282
  const events: BackendEvent[] = [];
283
+
264
284
  if (typeof frame.method !== 'string') {
265
285
  return events;
266
286
  }
287
+
267
288
  const params = isRecord(frame.params) ? frame.params : {};
289
+
268
290
  if (frame.id !== undefined) {
269
291
  this.handleServerRequest(frame.method, frame.id as CodexRpcId, params, events);
292
+
270
293
  return events;
271
294
  }
295
+
272
296
  if (frame.method === 'serverRequest/resolved') {
273
297
  const rpcId = params.requestId;
298
+
274
299
  if (typeof rpcId === 'number' || typeof rpcId === 'string') {
275
300
  const requestId = this.requestId(rpcId);
276
301
  this.pending.delete(requestId);
277
302
  events.push({ type: 'request.withdrawn', requestId });
278
303
  }
304
+
279
305
  return events;
280
306
  }
307
+
281
308
  if (frame.method === 'item/started' || frame.method === 'item/completed') {
282
309
  this.rememberPatch(params);
283
310
  }
311
+
284
312
  // Codex 0.156.1 streams a spawned agent's turns, items and usage here too, under that agent's thread id.
285
313
  const threadId = frame.method === 'thread/started' ? (isRecord(params.thread) ? str(params.thread.id) : null) : str(params.threadId);
314
+
286
315
  if (threadId !== null && this.ownThreadId !== null && threadId !== this.ownThreadId) {
287
316
  if (frame.method === 'turn/completed') {
288
317
  this.withdrawOwner(
@@ -294,167 +323,213 @@ export class CodexProtocol {
294
323
  } else if (frame.method === 'thread/closed') {
295
324
  this.withdrawOwner(threadId, null, events);
296
325
  }
326
+
297
327
  return events;
298
328
  }
329
+
299
330
  switch (frame.method) {
300
331
  case 'thread/started':
301
332
  this.ownThreadId = threadId ?? this.ownThreadId;
302
333
  events.push({ type: 'session', agentSessionId: threadId, model: null });
334
+
303
335
  break;
304
336
  case 'thread/name/updated': {
305
337
  const title = cleanTitle(params.threadName);
338
+
306
339
  if (title !== null) {
307
340
  events.push({ type: 'title', title });
308
341
  }
342
+
309
343
  break;
310
344
  }
311
345
  case 'turn/started':
312
346
  this.codexTurnId = isRecord(params.turn) ? str(params.turn.id) : null;
347
+
313
348
  break;
314
349
  case 'turn/completed':
315
350
  this.handleTurnCompleted(params, events);
351
+
316
352
  break;
317
353
  case 'thread/goal/updated':
318
354
  events.push({ type: 'goal', goal: codexGoal(params.goal) });
355
+
319
356
  break;
320
357
  case 'thread/goal/cleared':
321
358
  events.push({ type: 'goal', goal: null });
359
+
322
360
  break;
323
361
  case 'thread/closed':
324
362
  this.withdrawOwner(threadId ?? this.ownThreadId, null, events);
363
+
325
364
  break;
326
365
  case 'item/started':
327
366
  case 'item/completed':
328
367
  this.handleItem(params, frame.method === 'item/completed', events);
368
+
329
369
  break;
330
370
  case 'item/agentMessage/delta': {
331
371
  const ref = str(params.itemId);
332
372
  const text = str(params.delta);
373
+
333
374
  if (ref && text) {
334
375
  events.push({ type: 'text.delta', ref, text });
335
376
  }
377
+
336
378
  break;
337
379
  }
338
380
  case 'item/reasoning/summaryTextDelta':
339
381
  case 'item/reasoning/textDelta': {
340
382
  const ref = str(params.itemId);
341
383
  const text = str(params.delta);
384
+
342
385
  if (ref && text) {
343
386
  events.push({ type: 'thinking.delta', ref, text });
344
387
  }
388
+
345
389
  break;
346
390
  }
347
391
  case 'item/reasoning/summaryPartAdded': {
348
392
  // A new part of the same summary; without a break the parts run into each other.
349
393
  const ref = str(params.itemId);
394
+
350
395
  if (ref && num(params.summaryIndex) > 0) {
351
396
  events.push({ type: 'thinking.delta', ref, text: '\n\n' });
352
397
  }
398
+
353
399
  break;
354
400
  }
355
401
  case 'item/commandExecution/outputDelta': {
356
402
  const ref = str(params.itemId);
357
403
  const text = outputChunk(params);
404
+
358
405
  if (ref && text) {
359
406
  events.push({ type: 'tool.output', ref, text });
360
407
  }
408
+
361
409
  break;
362
410
  }
363
411
  case 'thread/tokenUsage/updated': {
364
412
  const usage = isRecord(params.tokenUsage) ? params.tokenUsage : {};
365
413
  const last = isRecord(usage.last) ? usage.last : {};
366
414
  const contextWindow = num(usage.modelContextWindow);
415
+
367
416
  events.push({
368
417
  type: 'usage',
369
418
  contextTokens: num(last.totalTokens),
370
419
  ...(contextWindow > 0 ? { contextWindow } : {})
371
420
  });
421
+
372
422
  break;
373
423
  }
374
424
  case 'error': {
375
425
  const error = isRecord(params.error) ? params.error : {};
376
426
  events.push({ type: 'note', level: params.willRetry === true ? 'warning' : 'error', text: str(error.message) ?? 'Codex reported an error' });
427
+
377
428
  break;
378
429
  }
379
430
  case 'account/rateLimits/updated': {
380
431
  // The plan's own numbers, sent beside a token usage tick; the usage monitor keeps them.
381
432
  const reading = readCodexLimits(isRecord(params.rateLimits) ? params.rateLimits : params);
433
+
382
434
  if (reading !== null) {
383
435
  this.spentUntil = spentUntil(reading.windows);
436
+
384
437
  if (reading.windows.length > 0) {
385
438
  events.push({ type: 'limits', update: { kind: 'codex', plan: reading.plan, windows: reading.windows } });
386
439
  }
387
440
  }
441
+
388
442
  break;
389
443
  }
390
444
  case 'model/rerouted': {
391
445
  const model = str(params.toModel);
446
+
392
447
  if (model) {
393
448
  events.push({ type: 'model', model });
394
449
  }
450
+
395
451
  break;
396
452
  }
397
453
  default:
398
454
  break;
399
455
  }
456
+
400
457
  return events;
401
458
  }
402
459
 
403
460
  /* The reply that settles an approval, or null when nothing waits under that id. */
404
461
  approvalDecision(requestId: string, decision: ApprovalDecision): { rpcId: CodexRpcId; result: unknown } | null {
405
462
  const pending = this.pending.get(requestId);
463
+
406
464
  if (pending?.type === 'permissions') {
407
465
  this.pending.delete(requestId);
466
+
408
467
  return {
409
468
  rpcId: pending.rpcId,
410
469
  result: { permissions: decision === 'deny' ? {} : pending.permissions, scope: decision === 'allow-always' ? 'session' : 'turn' }
411
470
  };
412
471
  }
472
+
413
473
  if (pending?.type === 'elicitation') {
414
474
  this.pending.delete(requestId);
475
+
415
476
  return { rpcId: pending.rpcId, result: elicitationAnswer(decision, pending.persist) };
416
477
  }
478
+
417
479
  if (pending?.type !== 'approval') {
418
480
  return null;
419
481
  }
482
+
420
483
  this.pending.delete(requestId);
484
+
421
485
  return { rpcId: pending.rpcId, result: { decision: codexDecision(decision, pending) } };
422
486
  }
423
487
 
424
488
  /* An RPC reply, or plain text that continues the chat, for a question that still waits. */
425
489
  questionAnswer(requestId: string, answers: Record<string, string>): CodexAnswer | null {
426
490
  const pending = this.pending.get(requestId);
491
+
427
492
  if (pending?.type !== 'question') {
428
493
  return null;
429
494
  }
495
+
430
496
  this.pending.delete(requestId);
431
497
  const given = pending.questionIds.map((id) => answers[id]).filter((answer): answer is string => answer !== undefined);
498
+
432
499
  if (pending.rpcId === null) {
433
500
  // An async question is answered like a message from the person, into the running turn.
434
501
  return { kind: 'steer', text: given.join('\n') };
435
502
  }
503
+
436
504
  const byId: Record<string, { answers: string[] }> = {};
505
+
437
506
  for (const id of pending.questionIds) {
438
507
  const answer = answers[id];
508
+
439
509
  if (answer !== undefined) {
440
510
  byId[id] = { answers: [answer] };
441
511
  }
442
512
  }
513
+
443
514
  return { kind: 'respond', rpcId: pending.rpcId, result: { answers: byId } };
444
515
  }
445
516
 
446
517
  dismissQuestionAnswer(requestId: string): CodexAnswer | null {
447
518
  const pending = this.pending.get(requestId);
519
+
448
520
  return pending?.type === 'question' && pending.async === true && pending.rpcId !== null ? this.questionAnswer(requestId, {}) : null;
449
521
  }
450
522
 
451
523
  /* Drops an asynchronous question the person walked away from; a blocking one has to be answered. */
452
524
  dismissQuestion(requestId: string): boolean {
453
525
  const pending = this.pending.get(requestId);
526
+
454
527
  if (pending?.type !== 'question' || pending.rpcId !== null) {
455
528
  return false;
456
529
  }
530
+
457
531
  this.pending.delete(requestId);
532
+
458
533
  return true;
459
534
  }
460
535
 
@@ -466,18 +541,23 @@ export class CodexProtocol {
466
541
 
467
542
  declineRequest(requestId: string): { rpcId: CodexRpcId; result: unknown } | null {
468
543
  const pending = this.pending.get(requestId);
544
+
469
545
  if (!pending) {
470
546
  return null;
471
547
  }
548
+
472
549
  if (pending.type !== 'question') {
473
550
  return this.approvalDecision(requestId, 'deny');
474
551
  }
552
+
475
553
  this.pending.delete(requestId);
554
+
476
555
  return pending.rpcId === null ? null : { rpcId: pending.rpcId, result: { answers: {} } };
477
556
  }
478
557
 
479
558
  private owner(params: Frame): RequestOwner {
480
559
  const threadId = str(params.threadId) ?? this.ownThreadId;
560
+
481
561
  return { threadId, turnId: str(params.turnId) ?? (threadId === this.ownThreadId ? this.codexTurnId : null) };
482
562
  }
483
563
 
@@ -487,12 +567,15 @@ export class CodexProtocol {
487
567
 
488
568
  private rememberPatch(params: Frame): void {
489
569
  const item = isRecord(params.item) ? params.item : null;
570
+
490
571
  if (item?.type !== 'fileChange' || typeof item.id !== 'string') {
491
572
  return;
492
573
  }
574
+
493
575
  const owner = this.owner(params);
494
576
  const changes = parseChanges(item.changes);
495
577
  this.toolInputs.set(this.toolKey(owner.threadId, item.id), { ...owner, input: { summary: changes.map((change) => change.path).join(', ') }, changes });
578
+
496
579
  // An approval copies its input into the event, so evicting cached metadata cannot change an open card.
497
580
  if (this.toolInputs.size > 512) {
498
581
  this.toolInputs.delete(this.toolInputs.keys().next().value!);
@@ -501,12 +584,14 @@ export class CodexProtocol {
501
584
 
502
585
  private withdrawOwner(threadId: string | null, turnId: string | null, events: BackendEvent[], keepAsyncQuestions = false): void {
503
586
  const belongs = (owner: RequestOwner): boolean => owner.threadId === threadId && (turnId === null || owner.turnId === null || owner.turnId === turnId);
587
+
504
588
  for (const [requestId, pending] of this.pending) {
505
589
  if (belongs(pending) && !(keepAsyncQuestions && pending.type === 'question' && pending.async === true)) {
506
590
  this.pending.delete(requestId);
507
591
  events.push({ type: 'request.withdrawn', requestId });
508
592
  }
509
593
  }
594
+
510
595
  for (const [key, input] of this.toolInputs) {
511
596
  if (belongs(input)) {
512
597
  this.toolInputs.delete(key);
@@ -516,6 +601,7 @@ export class CodexProtocol {
516
601
 
517
602
  private isChildRequest(params: Frame): boolean {
518
603
  const threadId = str(params.threadId);
604
+
519
605
  return threadId !== null && this.ownThreadId !== null && threadId !== this.ownThreadId;
520
606
  }
521
607
 
@@ -523,8 +609,10 @@ export class CodexProtocol {
523
609
  if (!this.isChildRequest(params)) {
524
610
  return description;
525
611
  }
612
+
526
613
  const threadId = str(params.threadId)!;
527
614
  const label = this.agentRows.get(threadId)?.label || threadId;
615
+
528
616
  return [`Requested by subagent ${label}.`, description].filter(Boolean).join(' ');
529
617
  }
530
618
 
@@ -534,11 +622,14 @@ export class CodexProtocol {
534
622
 
535
623
  private handleServerRequest(method: string, rpcId: CodexRpcId, params: Frame, events: BackendEvent[]): void {
536
624
  const requestId = this.requestId(rpcId);
625
+
537
626
  if (method === 'item/tool/requestUserInput') {
538
627
  const questions = parseBlockingQuestions(params.questions);
628
+
539
629
  if (questions.length === 0) {
540
630
  return;
541
631
  }
632
+
542
633
  this.pending.set(requestId, {
543
634
  ...this.owner(params),
544
635
  type: 'question',
@@ -546,6 +637,7 @@ export class CodexProtocol {
546
637
  questionIds: questions.map((question) => question.id),
547
638
  async: params.isBlocking === false
548
639
  });
640
+
549
641
  events.push({
550
642
  type: 'question.requested',
551
643
  requestId,
@@ -553,14 +645,18 @@ export class CodexProtocol {
553
645
  ...(params.isBlocking === false ? { async: true } : {}),
554
646
  ...(this.isChildRequest(params) ? { background: true } : {})
555
647
  });
648
+
556
649
  return;
557
650
  }
651
+
558
652
  if (method === 'item/permissions/requestApproval') {
559
653
  if (!isRecord(params.permissions)) {
560
654
  return;
561
655
  }
656
+
562
657
  const permissions = params.permissions;
563
658
  this.pending.set(requestId, { ...this.owner(params), type: 'permissions', rpcId, permissions });
659
+
564
660
  events.push({
565
661
  type: 'approval.requested',
566
662
  requestId,
@@ -572,31 +668,41 @@ export class CodexProtocol {
572
668
  allowAlways: { label: 'Allow for this session', description: 'Grant these permissions for the rest of this session.' },
573
669
  ...(this.isChildRequest(params) ? { background: true } : {})
574
670
  });
671
+
575
672
  return;
576
673
  }
674
+
577
675
  if (method === 'mcpServer/elicitation/request') {
578
676
  this.elicitation(requestId, rpcId, params, events);
677
+
579
678
  return;
580
679
  }
680
+
581
681
  const kind = method === 'item/commandExecution/requestApproval' ? 'command' : method === 'item/fileChange/requestApproval' ? 'fileChange' : null;
682
+
582
683
  if (!kind) {
583
684
  return;
584
685
  }
686
+
585
687
  const itemId = str(params.itemId);
586
688
  const known = itemId ? this.toolInputs.get(this.toolKey(this.owner(params).threadId, itemId)) : undefined;
689
+
587
690
  const decisions = Array.isArray(params.availableDecisions)
588
691
  ? params.availableDecisions.map((decision) =>
589
692
  typeof decision === 'string' ? decision : (Object.keys(isRecord(decision) ? decision : {})[0] ?? '')
590
693
  )
591
694
  : [];
695
+
592
696
  const amendment = Array.isArray(params.proposedExecpolicyAmendment)
593
697
  ? params.proposedExecpolicyAmendment.filter((word): word is string => typeof word === 'string')
594
698
  : null;
699
+
595
700
  // The person must see what they are approving, so a file change repeats its diffs here.
596
701
  const input =
597
702
  kind === 'command'
598
703
  ? { command: unwrapCommand(str(params.command) ?? ''), cwd: str(params.cwd) ?? undefined }
599
704
  : { ...(isRecord(known?.input) ? known.input : { itemId }), changes: known?.changes ?? [] };
705
+
600
706
  this.pending.set(requestId, {
601
707
  ...this.owner(params),
602
708
  type: 'approval',
@@ -605,6 +711,7 @@ export class CodexProtocol {
605
711
  amendment: amendment && amendment.length > 0 ? amendment : null,
606
712
  decisions
607
713
  });
714
+
608
715
  events.push({
609
716
  type: 'approval.requested',
610
717
  requestId,
@@ -635,9 +742,11 @@ export class CodexProtocol {
635
742
  if (!isConfirmation(params)) {
636
743
  return;
637
744
  }
745
+
638
746
  const meta = isRecord(params._meta) ? params._meta : {};
639
747
  const persist = elicitationPersist(meta);
640
748
  this.pending.set(requestId, { ...this.owner(params), type: 'elicitation', rpcId, persist });
749
+
641
750
  events.push({
642
751
  type: 'approval.requested',
643
752
  requestId,
@@ -658,17 +767,21 @@ export class CodexProtocol {
658
767
  private handleItem(params: Frame, completed: boolean, events: BackendEvent[]): void {
659
768
  const item = isRecord(params.item) ? params.item : null;
660
769
  const ref = item ? str(item.id) : null;
770
+
661
771
  if (!item || !ref) {
662
772
  return;
663
773
  }
774
+
664
775
  switch (item.type) {
665
776
  case 'agentMessage': {
666
777
  const questions = parseAsyncQuestions(item.questions);
778
+
667
779
  if (questions.length > 0) {
668
780
  // Codex keeps polling for an answer to this one; the id it carries is the item's own.
669
781
  if (!completed || this.pending.has(ref)) {
670
782
  return;
671
783
  }
784
+
672
785
  this.pending.set(ref, {
673
786
  ...this.owner(params),
674
787
  type: 'question',
@@ -676,21 +789,28 @@ export class CodexProtocol {
676
789
  questionIds: questions.map((question) => question.id),
677
790
  async: true
678
791
  });
792
+
679
793
  events.push({ type: 'question.requested', requestId: ref, questions, async: true });
794
+
680
795
  return;
681
796
  }
797
+
682
798
  this.text(ref, str(item.text) ?? '', completed, events, str(item.phase) ?? undefined);
799
+
683
800
  return;
684
801
  }
685
802
  case 'plan':
686
803
  this.text(`plan-${ref}`, str(item.text) ?? '', completed, events);
804
+
687
805
  return;
688
806
  case 'reasoning': {
689
807
  // The deltas already streamed this; the completed item is what a resumed thread replays.
690
808
  const lines = [...toLines(item.summary), ...toLines(item.content)];
809
+
691
810
  if (completed && lines.length > 0) {
692
811
  events.push({ type: 'thinking.done', ref, text: lines.join('\n\n') });
693
812
  }
813
+
694
814
  return;
695
815
  }
696
816
  case 'commandExecution': {
@@ -698,18 +818,23 @@ export class CodexProtocol {
698
818
  const succeeded = str(item.status) === 'completed';
699
819
  // Its turn is long over, so only the row learns how it ended; a start again would open a turn nobody ends.
700
820
  const backgroundId = this.backgroundTerminals.get(ref);
821
+
701
822
  if (completed && backgroundId !== undefined) {
702
823
  this.backgroundTerminals.delete(ref);
703
824
  events.push({ type: 'tool.done', ref, output, state: succeeded ? 'done' : 'error' });
704
825
  events.push({ type: 'background.ended', taskId: backgroundId });
826
+
705
827
  return;
706
828
  }
829
+
707
830
  const processId = str(item.processId);
831
+
708
832
  if (!completed && processId !== null) {
709
833
  this.openTerminals.set(ref, processId);
710
834
  } else {
711
835
  this.openTerminals.delete(ref);
712
836
  }
837
+
713
838
  this.tool(
714
839
  ref,
715
840
  'Bash',
@@ -719,10 +844,12 @@ export class CodexProtocol {
719
844
  succeeded,
720
845
  events
721
846
  );
847
+
722
848
  return;
723
849
  }
724
850
  case 'fileChange': {
725
851
  const changes = parseChanges(item.changes);
852
+
726
853
  this.tool(
727
854
  ref,
728
855
  'ApplyPatch',
@@ -733,11 +860,13 @@ export class CodexProtocol {
733
860
  events,
734
861
  changes
735
862
  );
863
+
736
864
  return;
737
865
  }
738
866
  case 'mcpToolCall': {
739
867
  const result = isRecord(item.result) ? textOf(item.result.content) : '';
740
868
  const error = isRecord(item.error) ? (str(item.error.message) ?? '') : '';
869
+
741
870
  this.tool(
742
871
  ref,
743
872
  `${str(item.server) ?? 'mcp'}/${str(item.tool) ?? 'tool'}`,
@@ -747,62 +876,78 @@ export class CodexProtocol {
747
876
  str(item.status) === 'completed',
748
877
  events
749
878
  );
879
+
750
880
  return;
751
881
  }
752
882
  case 'dynamicToolCall':
753
883
  this.tool(ref, str(item.tool) ?? 'tool', item.arguments ?? {}, completed, textOf(item.contentItems), item.success !== false, events);
884
+
754
885
  return;
755
886
  case 'imageGeneration': {
756
887
  const revisedPrompt = str(item.revisedPrompt) ?? undefined;
757
888
  const transparentBackground = typeof item.transparentBackground === 'boolean' ? item.transparentBackground : undefined;
889
+
758
890
  const input = {
759
891
  ...(revisedPrompt === undefined ? {} : { revisedPrompt }),
760
892
  ...(transparentBackground === undefined ? {} : { transparentBackground })
761
893
  };
894
+
762
895
  if (!completed) {
763
896
  this.tool(ref, 'ImageGeneration', input, false, '', true, events);
897
+
764
898
  return;
765
899
  }
900
+
766
901
  try {
767
902
  if (item.failure || item.status !== 'completed') {
768
903
  throw new Error(
769
904
  isRecord(item.failure) ? (str(item.failure.message) ?? 'Image generation failed') : (str(item.failure) ?? 'Image generation failed')
770
905
  );
771
906
  }
907
+
772
908
  const image = generatedImage(str(item.result) ?? '', revisedPrompt);
773
909
  events.push({ type: 'image.generated', ref, ...image, ...input });
774
910
  } catch (error) {
775
911
  this.tool(ref, 'ImageGeneration', input, true, error instanceof Error ? error.message : 'Image generation failed', false, events);
776
912
  }
913
+
777
914
  return;
778
915
  }
779
916
  case 'imageView':
780
917
  this.tool(ref, 'ImageView', { path: str(item.path) ?? '' }, completed, '', true, events);
918
+
781
919
  return;
782
920
  case 'webSearch': {
783
921
  const action = isRecord(item.action) ? item.action : {};
784
922
  this.tool(ref, 'WebSearch', { query: str(item.query) ?? str(action.query) ?? '' }, completed, '', true, events);
923
+
785
924
  return;
786
925
  }
787
926
  case 'collabAgentToolCall': {
788
927
  // Codex's own multi-agent calls; spawnAgent is the one handled as a delegation.
789
928
  const tool = str(item.tool) ?? 'collabAgent';
790
929
  const input = { tool, prompt: str(item.prompt) ?? undefined, model: str(item.model) ?? undefined, threads: item.receiverThreadIds ?? [] };
930
+
791
931
  if (tool === 'spawnAgent') {
792
932
  this.spawnedAgent(ref, item, input, events);
933
+
793
934
  return;
794
935
  }
936
+
795
937
  this.tool(ref, tool, input, completed, collabAgentOutput(item.agentsStates), str(item.status) === 'completed', events);
796
938
  this.settleEndedAgents(item.agentsStates, events);
939
+
797
940
  return;
798
941
  }
799
942
  case 'subAgentActivity':
800
943
  this.subAgentActivity(item, events);
944
+
801
945
  return;
802
946
  case 'contextCompaction':
803
947
  if (completed) {
804
948
  events.push({ type: 'compaction', preTokens: null });
805
949
  }
950
+
806
951
  return;
807
952
  default:
808
953
  return;
@@ -817,12 +962,15 @@ export class CodexProtocol {
817
962
  const turnId = str(turn.id) ?? this.codexTurnId;
818
963
  this.codexTurnId = null;
819
964
  this.withdrawOwner(str(params.threadId) ?? this.ownThreadId, turnId, events, status === 'completed');
965
+
820
966
  // A terminal session still open as the turn ends is one Codex left running; it only completes once its process does.
821
967
  for (const [ref, processId] of this.openTerminals) {
822
968
  this.backgroundTerminals.set(ref, processId);
823
969
  events.push({ type: 'background.started', taskId: processId, ref, monitor: false, description: null });
824
970
  }
971
+
825
972
  this.openTerminals.clear();
973
+
826
974
  events.push({
827
975
  type: 'turn.done',
828
976
  state: status === 'interrupted' ? 'aborted' : status === 'failed' ? 'error' : 'done',
@@ -843,9 +991,11 @@ export class CodexProtocol {
843
991
  if (info === 'usageLimitExceeded') {
844
992
  return { kind: 'usage', ...(this.spentUntil === null ? {} : { resetsAt: this.spentUntil }) };
845
993
  }
994
+
846
995
  if (info === 'serverOverloaded' || info === 'rateLimitExceeded') {
847
996
  return { kind: 'overload' };
848
997
  }
998
+
849
999
  return null;
850
1000
  }
851
1001
 
@@ -863,6 +1013,7 @@ export class CodexProtocol {
863
1013
  // One spawn opens one thread; Codex only knows its id once the call went through.
864
1014
  const threadId = Array.isArray(item.receiverThreadIds) ? str(item.receiverThreadIds[0]) : null;
865
1015
  events.push({ type: 'tool.started', ref, name: 'Agent', input, parentRef: null });
1016
+
866
1017
  events.push({
867
1018
  type: 'task.started',
868
1019
  ref,
@@ -873,15 +1024,19 @@ export class CodexProtocol {
873
1024
  background: true,
874
1025
  ...(threadId ? { threadId } : {})
875
1026
  });
1027
+
876
1028
  if (threadId === null) {
877
1029
  if (str(item.status) === 'failed') {
878
1030
  events.push({ type: 'task.done', ref, summary: summary || null, ok: false });
879
1031
  }
1032
+
880
1033
  return;
881
1034
  }
1035
+
882
1036
  if (!this.agentRows.has(threadId)) {
883
1037
  this.agentRows.set(threadId, { ref, label: (str(item.prompt) ?? '').split('\n')[0] ?? '', settled: false });
884
1038
  }
1039
+
885
1040
  if (!this.settleEndedAgents(item.agentsStates, events)) {
886
1041
  events.push({ type: 'task.progress', ref, summary: summary || null, lastTool: null, usage: null });
887
1042
  }
@@ -890,17 +1045,21 @@ export class CodexProtocol {
890
1045
  /* Settles the row of every agent a collab call reports stopped; true when one of them did just now. */
891
1046
  private settleEndedAgents(states: unknown, events: BackendEvent[]): boolean {
892
1047
  let settled = false;
1048
+
893
1049
  for (const [threadId, state] of Object.entries(isRecord(states) ? states : {})) {
894
1050
  const status = isRecord(state) ? str(state.status) : null;
895
1051
  const row = this.agentRows.get(threadId);
1052
+
896
1053
  if (row === undefined || row.settled || status === null || !ENDED_AGENT_STATES.has(status)) {
897
1054
  continue;
898
1055
  }
1056
+
899
1057
  row.settled = true;
900
1058
  this.withdrawOwner(threadId, null, events, status === 'completed');
901
1059
  settled = true;
902
1060
  events.push({ type: 'task.done', ref: row.ref, summary: collabAgentOutput({ [threadId]: state }), ok: status === 'completed' });
903
1061
  }
1062
+
904
1063
  return settled;
905
1064
  }
906
1065
 
@@ -913,21 +1072,27 @@ export class CodexProtocol {
913
1072
  const ref = str(item.id);
914
1073
  const threadId = str(item.agentThreadId);
915
1074
  const kind = str(item.kind);
1075
+
916
1076
  if (ref === null || threadId === null) {
917
1077
  return;
918
1078
  }
1079
+
919
1080
  const row = this.agentRows.get(threadId);
1081
+
920
1082
  if ((kind === 'started' || kind === 'interacted') && row === undefined) {
921
1083
  // The path ends in the name the parent gave the agent, which is all the stream says about its task.
922
1084
  const path = str(item.agentPath) ?? '';
923
1085
  this.agentRows.set(threadId, { ref, label: path.split('/').at(-1) ?? '', settled: false, ...(kind === 'interacted' ? { interaction: ref } : {}) });
924
1086
  events.push({ type: 'tool.started', ref, name: 'Agent', input: { agentPath: path }, parentRef: null });
925
1087
  events.push({ type: 'task.started', ref, description: path.split('/').at(-1) ?? '', subagentType: null, prompt: null, background: true, threadId });
1088
+
926
1089
  return;
927
1090
  }
1091
+
928
1092
  if (kind === 'interacted' && row !== undefined && row.interaction !== ref) {
929
1093
  row.interaction = ref;
930
1094
  row.settled = false;
1095
+
931
1096
  events.push({
932
1097
  type: 'task.started',
933
1098
  ref: row.ref,
@@ -938,8 +1103,10 @@ export class CodexProtocol {
938
1103
  threadId,
939
1104
  resumed: true
940
1105
  });
1106
+
941
1107
  return;
942
1108
  }
1109
+
943
1110
  if ((kind === 'completed' || kind === 'interrupted') && row !== undefined && !row.settled) {
944
1111
  row.settled = true;
945
1112
  this.withdrawOwner(threadId, null, events, kind === 'completed');
@@ -958,6 +1125,7 @@ export class CodexProtocol {
958
1125
  changes?: ChatFileChange[]
959
1126
  ): void {
960
1127
  events.push({ type: 'tool.started', ref, name, input, parentRef: null, ...(changes && changes.length > 0 ? { changes } : {}) });
1128
+
961
1129
  if (completed) {
962
1130
  events.push({ type: 'tool.done', ref, output, state: succeeded ? 'done' : 'error', ...(changes && changes.length > 0 ? { changes } : {}) });
963
1131
  }
@@ -972,12 +1140,15 @@ function codexDecision(decision: ApprovalDecision, pending: Extract<Pending, { t
972
1140
  if (decision === 'deny') {
973
1141
  return 'decline';
974
1142
  }
1143
+
975
1144
  if (decision === 'allow') {
976
1145
  return 'accept';
977
1146
  }
1147
+
978
1148
  if (pending.amendment) {
979
1149
  return { acceptWithExecpolicyAmendment: { execpolicy_amendment: pending.amendment } };
980
1150
  }
1151
+
981
1152
  return pending.kind === 'fileChange' || pending.decisions.includes('acceptForSession') ? 'acceptForSession' : 'accept';
982
1153
  }
983
1154
 
@@ -986,5 +1157,6 @@ function elicitationAnswer(decision: ApprovalDecision, persist: ElicitationPersi
986
1157
  if (decision === 'deny') {
987
1158
  return { action: 'decline' };
988
1159
  }
1160
+
989
1161
  return decision === 'allow-always' && persist !== null ? { action: 'accept', content: {}, _meta: { persist } } : { action: 'accept', content: {} };
990
1162
  }