@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
@@ -108,6 +108,7 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
108
108
  const turnCompleted = (status: 'completed' | 'interrupted' | 'failed', error: string | null = null, codexErrorInfo: string | null = null): void => {
109
109
  usage();
110
110
  notify('thread/status/changed', { threadId, status: { type: 'idle' } });
111
+
111
112
  notify('turn/completed', {
112
113
  threadId,
113
114
  turn: {
@@ -118,6 +119,7 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
118
119
  error: error ? { message: error, codexErrorInfo, additionalDetails: null } : null
119
120
  }
120
121
  });
122
+
121
123
  turnId = '';
122
124
  };
123
125
 
@@ -137,11 +139,14 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
137
139
  goalSteps += 1;
138
140
  agentMessage(`goal step ${goalSteps}`);
139
141
  const needed = Number(/steps:(\d+)/.exec(String(goal?.objective ?? ''))?.[1] ?? '1');
142
+
140
143
  if (goal !== null) {
141
144
  goal = { ...goal, tokensUsed: Number(goal.tokensUsed) + 100, status: goalSteps >= needed ? 'complete' : goal.status };
142
145
  goalChanged();
143
146
  }
147
+
144
148
  turnCompleted('completed');
149
+
145
150
  if (goal?.status === 'active') {
146
151
  io.later(() => {
147
152
  if (goal?.status === 'active') {
@@ -160,6 +165,7 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
160
165
  const askApproval = (kind: 'command' | 'fileChange', entry: Frame, params: Frame): void => {
161
166
  pendingApproval = { rpcId: serverRequestId, entry, kind };
162
167
  notify('thread/status/changed', { threadId, status: { type: 'active', activeFlags: ['waitingOnApproval'] } });
168
+
163
169
  out({
164
170
  method: kind === 'command' ? 'item/commandExecution/requestApproval' : 'item/fileChange/requestApproval',
165
171
  id: serverRequestId++,
@@ -173,56 +179,77 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
173
179
  const raw = input?.text ?? '';
174
180
  const gap = raw.indexOf('\n\n');
175
181
  const noted = options.resumeLead !== undefined && raw.startsWith(options.resumeLead) && gap !== -1;
182
+
176
183
  if (noted) {
177
184
  pasted = raw.slice(0, gap);
178
185
  }
186
+
179
187
  const text = noted ? raw.slice(gap + 2) : raw;
188
+
180
189
  if (text === 'name?') {
181
190
  turnStarted();
182
191
  agentMessage(threadName ?? 'unnamed');
183
192
  turnCompleted('completed');
193
+
184
194
  return;
185
195
  }
196
+
186
197
  if (text === 'note?' || text === 'pasted?') {
187
198
  turnStarted();
188
199
  agentMessage((text === 'note?' ? threadInstructions.get(threadId) : pasted) ?? 'nothing');
189
200
  turnCompleted('completed');
201
+
190
202
  return;
191
203
  }
204
+
192
205
  turnStarted();
193
206
  const user = item('userMessage', { clientId: null, content: [{ type: 'text', text, text_elements: [] }] });
194
207
  started(user);
195
208
  completed(user);
209
+
196
210
  if (goal !== null && (text === goal.objective || text === '')) {
197
211
  io.later(goalStep);
212
+
198
213
  return;
199
214
  }
215
+
200
216
  if (text === 'crash') {
201
217
  io.exit(1);
218
+
202
219
  return;
203
220
  }
221
+
204
222
  if (text === 'crash loudly') {
205
223
  io.err('Error: the fake lost its thread\n');
206
224
  io.exit(1);
225
+
207
226
  return;
208
227
  }
228
+
209
229
  if (text === 'fail') {
210
230
  turnCompleted('failed', 'The model is overloaded');
231
+
211
232
  return;
212
233
  }
234
+
213
235
  // Lines of their own, so a task brief around them still fails the way Codex 0.156.1 does.
214
236
  const lines = text.split('\n');
237
+
215
238
  // A dev server Codex leaves running past the turn; its process ends once the test says later is.
216
239
  if (lines.includes('serve')) {
217
240
  const command = item('commandExecution', { command: "/bin/zsh -lc 'bun dev'", cwd: io.cwd, processId: `pty-${nonce}`, status: 'inProgress' });
218
241
  started(command);
219
242
  io.later(() => completed({ ...command, status: 'completed', aggregatedOutput: '', exitCode: 0 }));
220
243
  }
244
+
221
245
  if (lines.includes('overloaded')) {
222
246
  turnCompleted('failed', 'Selected model is at capacity. Please try a different model.', 'serverOverloaded');
247
+
223
248
  return;
224
249
  }
250
+
225
251
  const limit = lines.find((line) => line.startsWith('limit:'));
252
+
226
253
  if (limit !== undefined) {
227
254
  notify('account/rateLimits/updated', {
228
255
  rateLimits: {
@@ -231,11 +258,15 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
231
258
  secondary: { usedPercent: 40, windowDurationMins: 10080, resetsAt: Number(limit.slice(6)) + 86_400 }
232
259
  }
233
260
  });
261
+
234
262
  turnCompleted('failed', "You've hit your usage limit.", 'usageLimitExceeded');
263
+
235
264
  return;
236
265
  }
266
+
237
267
  if (text.startsWith('image:')) {
238
268
  const scenario = text.slice(6);
269
+
239
270
  const entry = item('imageGeneration', {
240
271
  status: 'inProgress',
241
272
  result: '',
@@ -243,7 +274,9 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
243
274
  revisedPrompt: 'A rabbit',
244
275
  transparentBackground: true
245
276
  });
277
+
246
278
  started(entry);
279
+
247
280
  const result =
248
281
  scenario === 'empty'
249
282
  ? ''
@@ -254,27 +287,36 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
254
287
  : scenario === 'too-large'
255
288
  ? Buffer.alloc(10 * 1024 * 1024 + 1).toString('base64')
256
289
  : 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII=';
290
+
257
291
  const done = {
258
292
  ...entry,
259
293
  status: scenario === 'failed' ? 'failed' : 'completed',
260
294
  result,
261
295
  ...(scenario === 'failed' ? { failure: { message: 'Image generation failed' } } : {})
262
296
  };
297
+
263
298
  completed(done);
299
+
264
300
  if (scenario === 'replay') {
265
301
  completed(done);
266
302
  }
303
+
267
304
  agentMessage('Here is the generated image.');
268
305
  turnCompleted('completed');
306
+
269
307
  return;
270
308
  }
309
+
271
310
  if (text === 'slow') {
272
311
  slow = true;
273
312
  started(item('agentMessage', { text: '', phase: 'final_answer', memoryCitation: null, delivery: null, questions: null }));
313
+
274
314
  return;
275
315
  }
316
+
276
317
  if (text.startsWith('tool:')) {
277
318
  const command = text.slice(5).trim();
319
+
278
320
  const entry = item('commandExecution', {
279
321
  command: `/bin/zsh -lc ${command}`,
280
322
  cwd: io.cwd,
@@ -282,7 +324,9 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
282
324
  aggregatedOutput: null,
283
325
  exitCode: null
284
326
  });
327
+
285
328
  started(entry);
329
+
286
330
  askApproval('command', entry, {
287
331
  kind: 'command',
288
332
  command: `/bin/zsh -lc ${command}`,
@@ -291,20 +335,26 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
291
335
  proposedExecpolicyAmendment: [command.split(' ')[0]],
292
336
  availableDecisions: ['accept', { acceptWithExecpolicyAmendment: { execpolicy_amendment: [command.split(' ')[0]] } }, 'cancel']
293
337
  });
338
+
294
339
  return;
295
340
  }
341
+
296
342
  if (text.startsWith('edit:')) {
297
343
  const path = text.slice(5).trim();
298
344
  const entry = item('fileChange', { changes: [{ path, kind: { type: 'add' }, diff: `+++ ${path}\n+hello\n` }], status: 'inProgress' });
299
345
  started(entry);
300
346
  askApproval('fileChange', entry, { reason: 'Write outside the sandbox' });
347
+
301
348
  return;
302
349
  }
350
+
303
351
  const finish = text.startsWith('spawn and finish:');
352
+
304
353
  if (finish || text.startsWith('spawn:')) {
305
354
  const prompt = text.slice(text.indexOf(':') + 1).trim();
306
355
  const childId = `child-${nonce}-${++itemCounter}`;
307
356
  const steps: Frame[] = [{ type: 'userMessage', id: `${childId}-prompt`, content: [{ type: 'text', text: prompt, text_elements: [] }] }];
357
+
308
358
  for (let step = 1; step <= FAKE_CHILD_STEPS; step++) {
309
359
  steps.push({
310
360
  type: 'commandExecution',
@@ -316,7 +366,9 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
316
366
  exitCode: 0
317
367
  });
318
368
  }
369
+
319
370
  childThreads.set(childId, steps);
371
+
320
372
  const spawn = item('collabAgentToolCall', {
321
373
  tool: 'spawnAgent',
322
374
  prompt,
@@ -326,22 +378,28 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
326
378
  agentsStates: {},
327
379
  status: 'inProgress'
328
380
  });
381
+
329
382
  started(spawn);
330
383
  const running = { ...spawn, receiverThreadIds: [childId], agentsStates: { [childId]: { status: 'running', message: null } } };
384
+
331
385
  if (!finish) {
332
386
  completed(running);
333
387
  agentMessage('spawned');
334
388
  turnCompleted('completed');
389
+
335
390
  return;
336
391
  }
392
+
337
393
  completed({ ...running, status: 'completed', agentsStates: { [childId]: { status: 'completed', message: 'the docs are read' } } });
338
394
  agentMessage('spawned');
339
395
  const spawnedIn = turnId;
340
396
  turnCompleted('completed');
341
397
  // A snapshot of the call from while its agent still ran, delivered after the agent finished.
342
398
  io.later(() => notify('item/completed', { item: running, threadId, turnId: spawnedIn, completedAtMs: Date.now() }));
399
+
343
400
  return;
344
401
  }
402
+
345
403
  /*
346
404
  * A spawn the way Codex 0.156.1 reports it: no spawnAgent call, a `subAgentActivity` that started under
347
405
  * the call id, the agent's own thread streaming over the same connection, and a `completed` activity
@@ -352,14 +410,17 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
352
410
  const childId = `child-${nonce}-${++itemCounter}`;
353
411
  const childTurn = `turn-${nonce}-${++itemCounter}`;
354
412
  childThreads.set(childId, [{ type: 'userMessage', id: `${childId}-prompt`, content: [{ type: 'text', text: prompt, text_elements: [] }] }]);
413
+
355
414
  const activity = (id: string, kind: string): void => {
356
415
  const entry = { type: 'subAgentActivity', id, kind, agentThreadId: childId, agentPath: '/root/survey' };
357
416
  started(entry);
358
417
  completed(entry);
359
418
  };
419
+
360
420
  const inChild = (method: string, params: Frame): void => {
361
421
  notify(method, { threadId: childId, ...params });
362
422
  };
423
+
363
424
  activity(`call-spawn-${nonce}-${++itemCounter}`, 'started');
364
425
  inChild('thread/status/changed', { status: { type: 'active', activeFlags: [] } });
365
426
  inChild('turn/started', { turn: { id: childTurn, items: [], itemsView: 'notLoaded', status: 'inProgress', error: null } });
@@ -371,11 +432,14 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
371
432
  agentMessage('spawned');
372
433
  turnCompleted('completed');
373
434
  io.later(() => activity(`subagent-completed-${childTurn}`, 'completed'));
435
+
374
436
  return;
375
437
  }
438
+
376
439
  if (text.startsWith('ask:')) {
377
440
  const question = text.slice(4).trim();
378
441
  pendingQuestion = { rpcId: serverRequestId, questionId: 'color' };
442
+
379
443
  out({
380
444
  method: 'item/tool/requestUserInput',
381
445
  id: serverRequestId++,
@@ -399,11 +463,14 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
399
463
  ]
400
464
  }
401
465
  });
466
+
402
467
  return;
403
468
  }
469
+
404
470
  if (text.startsWith('async:')) {
405
471
  const question = text.slice(6).trim();
406
472
  pendingSteer = true;
473
+
407
474
  const entry = item('agentMessage', {
408
475
  text: `${question}\n- Red\n- Blue`,
409
476
  phase: 'final_answer',
@@ -411,10 +478,13 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
411
478
  delivery: 'async',
412
479
  questions: [{ title: question, options: ['Red', 'Blue'] }]
413
480
  });
481
+
414
482
  started(entry);
415
483
  completed(entry);
484
+
416
485
  return;
417
486
  }
487
+
418
488
  agentMessage(`echo: ${text}${turnEffort ? ` (${turnEffort})` : ''}`);
419
489
  turnCompleted('completed');
420
490
  };
@@ -425,8 +495,10 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
425
495
  const decision = result.decision;
426
496
  const accepted = decision === 'accept' || decision === 'acceptForSession' || (typeof decision === 'object' && decision !== null);
427
497
  const always = decision === 'acceptForSession' || (typeof decision === 'object' && decision !== null);
498
+
428
499
  if (approval.kind === 'command') {
429
500
  const command = String(approval.entry.command).replace('/bin/zsh -lc ', '');
501
+
430
502
  completed({
431
503
  ...approval.entry,
432
504
  status: accepted ? 'completed' : 'declined',
@@ -436,6 +508,7 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
436
508
  } else {
437
509
  completed({ ...approval.entry, status: accepted ? 'completed' : 'declined' });
438
510
  }
511
+
439
512
  agentMessage(accepted ? (always ? 'done, remembered' : 'done') : 'denied');
440
513
  turnCompleted('completed');
441
514
  };
@@ -451,6 +524,7 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
451
524
  switch (method) {
452
525
  case 'initialize':
453
526
  out({ id, result: { userAgent: 'fake-codex/0.0.0', codexHome: io.cwd, platformFamily: 'unix', platformOs: 'test' } });
527
+
454
528
  return;
455
529
  case 'model/list':
456
530
  out({
@@ -460,24 +534,31 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
460
534
  nextCursor: null
461
535
  }
462
536
  });
537
+
463
538
  return;
464
539
  case 'thread/start':
465
540
  case 'thread/resume': {
466
541
  const resumed = method === 'thread/resume' ? String(params.threadId) : null;
542
+
467
543
  if (resumed === 'gone') {
468
544
  out({ id, error: { code: -32600, message: 'no thread gone' } });
545
+
469
546
  return;
470
547
  }
548
+
471
549
  threadId = resumed ?? `fake-${Math.random().toString(36).slice(2, 8)}`;
550
+
472
551
  // Codex 0.154 ignores developer instructions on a resume; only a start sets them.
473
552
  if (resumed === null) {
474
553
  threadInstructions.set(threadId, typeof params.developerInstructions === 'string' ? params.developerInstructions : null);
475
554
  }
555
+
476
556
  threadModel = typeof params.model === 'string' ? params.model : 'fake-model';
477
557
  threadSandbox = typeof params.sandbox === 'string' ? params.sandbox : '';
478
558
  threadApproval = typeof params.approvalPolicy === 'string' ? params.approvalPolicy : '';
479
559
  threadName = threadId.startsWith('named-') ? 'Named before' : null;
480
560
  const thread = { id: threadId, model: threadModel, cwd: params.cwd, turns: [], name: threadName };
561
+
481
562
  out({
482
563
  id,
483
564
  result: {
@@ -489,51 +570,67 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
489
570
  sandbox: { type: threadSandbox }
490
571
  }
491
572
  });
573
+
492
574
  notify('thread/started', { thread });
575
+
493
576
  return;
494
577
  }
495
578
  case 'thread/inject_items': {
496
579
  if (options.injectItems === false) {
497
580
  out({ id, error: { code: -32601, message: 'unknown method thread/inject_items' } });
581
+
498
582
  return;
499
583
  }
584
+
500
585
  // The newest developer message is the one the model follows, so `note?` answers with it.
501
586
  const items = Array.isArray(params.items) ? (params.items as Frame[]) : [];
587
+
502
588
  for (const injected of items) {
503
589
  const content = Array.isArray(injected.content) ? (injected.content[0] as { text?: unknown } | undefined) : undefined;
590
+
504
591
  if (injected.role === 'developer' && typeof content?.text === 'string') {
505
592
  threadInstructions.set(String(params.threadId), content.text);
506
593
  }
507
594
  }
595
+
508
596
  out({ id, result: {} });
597
+
509
598
  return;
510
599
  }
511
600
  case 'thread/fork': {
512
601
  const source = String(params.threadId);
513
602
  const turns = threadTurns.get(source) ?? [];
514
603
  const last = typeof params.lastTurnId === 'string' ? turns.indexOf(params.lastTurnId) : turns.length - 1;
604
+
515
605
  if (!threadTurns.has(source) || (typeof params.lastTurnId === 'string' && last < 0)) {
516
606
  out({ id, error: { code: -32600, message: `no rollout found for thread id ${source}` } });
607
+
517
608
  return;
518
609
  }
610
+
519
611
  fakeCodexForks.push(params);
520
612
  const forked = `fork-${Math.random().toString(36).slice(2, 8)}`;
521
613
  threadTurns.set(forked, turns.slice(0, last + 1));
522
614
  threadInstructions.set(forked, threadInstructions.get(source) ?? null);
523
615
  out({ id, result: { thread: { id: forked, forkedFromId: source, cwd: params.cwd, turns: [] }, model: params.model ?? 'fake-model' } });
616
+
524
617
  return;
525
618
  }
526
619
  case 'thread/turns/list': {
527
620
  const turns = threadTurns.get(String(params.threadId)) ?? [];
528
621
  out({ id, result: { data: turns.map((turn) => ({ id: turn, items: [], status: 'completed', error: null })), nextCursor: null } });
622
+
529
623
  return;
530
624
  }
531
625
  case 'thread/items/list': {
532
626
  const steps = childThreads.get(String(params.threadId));
627
+
533
628
  if (!steps) {
534
629
  out({ id, error: { code: -32600, message: `no rollout found for thread id ${String(params.threadId)}` } });
630
+
535
631
  return;
536
632
  }
633
+
537
634
  // The cursor is where the next page starts, counted from the end the list is sorted from.
538
635
  const ordered = params.sortDirection === 'asc' ? steps : [...steps].reverse();
539
636
  const start = typeof params.cursor === 'string' ? Number(params.cursor) : 0;
@@ -541,16 +638,19 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
541
638
  const page = ordered.slice(start, start + limit);
542
639
  const next = start + limit < ordered.length ? String(start + limit) : null;
543
640
  out({ id, result: { data: page.map((entry) => ({ item: entry, turnId: 'child-turn' })), nextCursor: next, backwardsCursor: null } });
641
+
544
642
  return;
545
643
  }
546
644
  case 'thread/name/set':
547
645
  threadName = String(params.name);
548
646
  out({ id, result: {} });
549
647
  notify('thread/name/updated', { threadId, threadName });
648
+
550
649
  return;
551
650
  case 'turn/start':
552
651
  out({ id, result: { turn: { id: `turn-${nonce}-${itemCounter + 1}`, items: [], status: 'inProgress', error: null } } });
553
652
  handleTurnStart(params);
653
+
554
654
  return;
555
655
  case 'turn/steer': {
556
656
  // The real app-server refuses a steer that does not name the turn it is meant for.
@@ -560,29 +660,37 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
560
660
  // assertion rather than on a timeout.
561
661
  pendingSteer = false;
562
662
  turnCompleted('failed');
663
+
563
664
  return;
564
665
  }
666
+
565
667
  out({ id, result: { turnId } });
668
+
566
669
  if (pendingSteer) {
567
670
  pendingSteer = false;
568
671
  const input = Array.isArray(params.input) ? (params.input[0] as { text?: string } | undefined) : undefined;
569
672
  agentMessage(`you chose ${input?.text ?? 'no answer'}`);
570
673
  turnCompleted('completed');
571
674
  }
675
+
572
676
  return;
573
677
  }
574
678
  case 'turn/interrupt':
575
679
  out({ id, result: {} });
680
+
576
681
  if (slow) {
577
682
  slow = false;
578
683
  turnCompleted('interrupted');
579
684
  }
685
+
580
686
  return;
581
687
  case 'thread/goal/get':
582
688
  out({ id, result: { goal } });
689
+
583
690
  return;
584
691
  case 'thread/goal/set': {
585
692
  const at = Math.floor(Date.now() / 1000);
693
+
586
694
  if (typeof params.objective === 'string') {
587
695
  goal = {
588
696
  threadId,
@@ -594,26 +702,34 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
594
702
  createdAt: at,
595
703
  updatedAt: at
596
704
  };
705
+
597
706
  goalSteps = 0;
598
707
  }
708
+
599
709
  if (goal === null) {
600
710
  out({ id, error: { code: -32600, message: 'no goal to update' } });
711
+
601
712
  return;
602
713
  }
714
+
603
715
  if (typeof params.status === 'string') {
604
716
  goal = { ...goal, status: params.status, updatedAt: at };
605
717
  }
718
+
606
719
  out({ id, result: { goal } });
607
720
  goalChanged();
721
+
608
722
  return;
609
723
  }
610
724
  case 'thread/goal/clear': {
611
725
  const cleared = goal !== null;
612
726
  goal = null;
613
727
  out({ id, result: { cleared } });
728
+
614
729
  if (cleared) {
615
730
  notify('thread/goal/cleared', { threadId });
616
731
  }
732
+
617
733
  return;
618
734
  }
619
735
  case 'thread/compact/start':
@@ -622,6 +738,7 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
622
738
  started(item('contextCompaction', {}));
623
739
  completed(item('contextCompaction', {}));
624
740
  turnCompleted('completed');
741
+
625
742
  return;
626
743
  default:
627
744
  out({ id, error: { code: -32601, message: `unknown method ${method}` } });
@@ -632,16 +749,21 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
632
749
  onLine: (line) => {
633
750
  const frame = JSON.parse(line) as Frame;
634
751
  const params = (frame.params ?? {}) as Frame;
752
+
635
753
  if (typeof frame.method === 'string' && frame.id !== undefined) {
636
754
  handleRequest(frame.id, frame.method, params);
755
+
637
756
  return;
638
757
  }
758
+
639
759
  if (frame.id === undefined || frame.result === undefined) {
640
760
  return;
641
761
  }
762
+
642
763
  const result = frame.result as Frame;
643
764
  const approval = pendingApproval;
644
765
  const question = pendingQuestion;
766
+
645
767
  if (approval && approval.rpcId === frame.id) {
646
768
  handleApprovalResponse(approval, result);
647
769
  } else if (question && question.rpcId === frame.id) {
@@ -13,6 +13,7 @@ export class ManualClock implements OutboxClock {
13
13
  setTimeout(run: () => void, ms: number): unknown {
14
14
  const id = this.nextId++;
15
15
  this.timers.push({ at: this.time + ms, run, id });
16
+
16
17
  return id;
17
18
  }
18
19
 
@@ -24,6 +25,7 @@ export class ManualClock implements OutboxClock {
24
25
  this.time += ms;
25
26
  const due = this.timers.filter((timer) => timer.at <= this.time);
26
27
  this.timers = this.timers.filter((timer) => timer.at > this.time);
28
+
27
29
  for (const timer of due) {
28
30
  timer.run();
29
31
  }
@@ -17,6 +17,7 @@ export class FakeDirectoryWatcher implements DirectoryWatcher {
17
17
  if (this.closed) {
18
18
  return;
19
19
  }
20
+
20
21
  this.listener(event, filename);
21
22
  }
22
23
 
@@ -41,18 +42,19 @@ export class FakeWatch implements WatchSeams {
41
42
  readonly watch: WatchDirectory = (path, options, listener) => {
42
43
  const watcher = new FakeDirectoryWatcher(path, options.recursive, listener);
43
44
  this.watchers.push(watcher);
45
+
44
46
  return watcher;
45
47
  };
46
48
 
47
49
  readonly schedule = (callback: () => unknown, _ms: number): (() => void) => {
48
50
  const id = this.nextId++;
49
51
  this.due.set(id, callback);
52
+
50
53
  return () => {
51
54
  this.due.delete(id);
52
55
  };
53
56
  };
54
57
 
55
- /* How many timers are waiting to run. */
56
58
  get pending(): number {
57
59
  return this.due.size;
58
60
  }
@@ -64,9 +66,11 @@ export class FakeWatch implements WatchSeams {
64
66
  /* The one open watcher on `path`, which a test that emits on it expects to exist. */
65
67
  on(path: string): FakeDirectoryWatcher {
66
68
  const [watcher, ...rest] = this.openOn(path);
69
+
67
70
  if (!watcher || rest.length > 0) {
68
71
  throw new Error(`Expected one open watcher on ${path}, found ${rest.length + (watcher ? 1 : 0)}`);
69
72
  }
73
+
70
74
  return watcher;
71
75
  }
72
76
 
@@ -86,6 +90,7 @@ export class EventLog<T> {
86
90
  this.events.push(event);
87
91
  const ready = this.waiters.filter((waiter) => waiter.check(this.events));
88
92
  this.waiters = this.waiters.filter((waiter) => !ready.includes(waiter));
93
+
89
94
  for (const waiter of ready) {
90
95
  waiter.resolve();
91
96
  }
@@ -95,6 +100,7 @@ export class EventLog<T> {
95
100
  if (check(this.events)) {
96
101
  return Promise.resolve();
97
102
  }
103
+
98
104
  return new Promise((resolve) => {
99
105
  this.waiters.push({ check, resolve });
100
106
  });
package/src/title-file.ts CHANGED
@@ -12,10 +12,13 @@ export function cleanTitle(raw: unknown): string | null {
12
12
  if (typeof raw !== 'string') {
13
13
  return null;
14
14
  }
15
+
15
16
  const text = raw.replace(CONTROL, ' ').replace(/\s+/g, ' ').trim();
17
+
16
18
  if (text === '') {
17
19
  return null;
18
20
  }
21
+
19
22
  return text.length <= SUGGESTED_TITLE_LIMIT ? text : `${clipText(text, SUGGESTED_TITLE_LIMIT - 1).trimEnd()}…`;
20
23
  }
21
24
 
@@ -28,30 +31,39 @@ export function cleanTitle(raw: unknown): string | null {
28
31
  export async function readLines(path: string, from: number, size: number, chunkBytes: number, onLine: (line: string) => void): Promise<number> {
29
32
  const handle = await open(path, 'r');
30
33
  let offset = from;
34
+
31
35
  try {
32
36
  let carry = Buffer.alloc(0);
33
37
  let position = from;
38
+
34
39
  while (position < size) {
35
40
  const piece = Buffer.alloc(Math.min(chunkBytes, size - position));
36
41
  const { bytesRead } = await handle.read(piece, 0, piece.length, position);
42
+
37
43
  if (bytesRead === 0) {
38
44
  break;
39
45
  }
46
+
40
47
  position += bytesRead;
41
48
  const bytes = Buffer.concat([carry, piece.subarray(0, bytesRead)]);
42
49
  const end = bytes.lastIndexOf(0x0a);
50
+
43
51
  if (end < 0) {
44
52
  carry = bytes;
53
+
45
54
  continue;
46
55
  }
56
+
47
57
  for (const line of bytes.subarray(0, end).toString('utf8').split('\n')) {
48
58
  onLine(line);
49
59
  }
60
+
50
61
  carry = bytes.subarray(end + 1);
51
62
  offset = position - carry.length;
52
63
  }
53
64
  } finally {
54
65
  await handle.close();
55
66
  }
67
+
56
68
  return offset;
57
69
  }