@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
@@ -34,7 +34,7 @@ import { readableAssistantText } from './readable-text.ts';
34
34
  import type { SubagentSettlement } from './subagent-settlement.ts';
35
35
  import { ChatThread } from './thread.ts';
36
36
  import { UiReplies, type UiReplyHost } from './ui-replies.ts';
37
- import { errorText } from '../error-text.ts';
37
+ import { asError, errorText } from '../error-text.ts';
38
38
  import { systemClock, type OutboxClock } from '../outbox/outbox-worker.ts';
39
39
  import { cleanTitle } from '../title-file.ts';
40
40
 
@@ -56,13 +56,13 @@ export interface PromptNotes {
56
56
  reset(): void;
57
57
  }
58
58
 
59
- /* Chats a person attached to a message: the ones the host knows, and what the agent is told about them. */
60
59
  /* What a message pointed at: other chats with `@`, and views and nodes with `#`, by id. */
61
60
  export interface ChatAttached {
62
61
  chats: string[];
63
62
  references: string[];
64
63
  }
65
64
 
65
+ /* Chats a person attached to a message: the ones the host knows, and what the agent is told about them. */
66
66
  export type ChatReferences = (attached: {
67
67
  readonly chats?: readonly string[];
68
68
  readonly references?: readonly string[];
@@ -77,7 +77,6 @@ export interface ChatSessionOptions {
77
77
  // The access a UI host captured per reply, from the record; never on the wire.
78
78
  uiAccess?: Readonly<Record<string, unknown>>;
79
79
  provider: ChatProvider;
80
- // The executable and leading arguments; a test points this at a fake CLI.
81
80
  command: string[];
82
81
  /* The environment of the CLI under the chat's account, asked at every start; throws for an account that cannot start. */
83
82
  env(account: string | undefined): Record<string, string>;
@@ -163,8 +162,10 @@ function newId(prefix: string): string {
163
162
  * the ones before it, as long as it handed out fewer than one per millisecond it ran.
164
163
  */
165
164
  let lastGeneration = Date.now();
165
+
166
166
  function nextGeneration(): number {
167
167
  lastGeneration = Math.max(lastGeneration + 1, Date.now());
168
+
168
169
  return lastGeneration;
169
170
  }
170
171
 
@@ -235,6 +236,7 @@ export class ChatSession {
235
236
  this.capturedUiAccess = new Map(Object.entries(options.uiAccess ?? {}));
236
237
  this.thread = new ChatThread(options.info, options.items);
237
238
  this.projector = new ThreadProjector(this.thread, { providerName: options.provider.name });
239
+
238
240
  this.uiReplies = new UiReplies({
239
241
  thread: this.thread,
240
242
  fenceLanguage: options.uiFenceLanguage ?? UI_FENCE_LANGUAGE,
@@ -286,6 +288,7 @@ export class ChatSession {
286
288
  get busy(): boolean {
287
289
  const turnId = this.thread.info.activeTurnId;
288
290
  const turn = turnId ? this.thread.get(turnId) : undefined;
291
+
289
292
  // A turn the CLI opened toward a goal is the goal's own: a message waits for it like for any turn of the person's.
290
293
  return turnId !== null && (turn?.kind !== 'turn' || turn.origin !== 'agent' || this.goalWorks());
291
294
  }
@@ -302,20 +305,27 @@ export class ChatSession {
302
305
  */
303
306
  async goal(action: ChatGoalAction): Promise<void> {
304
307
  const supported = this.options.provider.capabilities.goals;
308
+
305
309
  if (supported === undefined || (supported === 'command' && action !== 'clear')) {
306
310
  throw new ChatError('chat-unsupported', `${this.options.provider.name} cannot ${action} a goal`);
307
311
  }
312
+
308
313
  if (this.thread.info.goal === undefined) {
309
314
  throw new ChatError('goal-not-found', 'This chat has no goal');
310
315
  }
316
+
311
317
  const backend = this.backend;
318
+
312
319
  if (supported === 'native' && backend?.goal && backend.running && (action !== 'resume' || this.busy)) {
313
320
  await backend.goal(action);
321
+
314
322
  return;
315
323
  }
324
+
316
325
  if (this.busy) {
317
326
  throw new ChatError('chat-busy', 'Stop the turn that works toward the goal first');
318
327
  }
328
+
319
329
  this.send(`/goal ${action}`);
320
330
  }
321
331
 
@@ -326,22 +336,29 @@ export class ChatSession {
326
336
  this.options.persist();
327
337
  this.resumeSettingChanged();
328
338
  }
339
+
329
340
  const catalog = this.options.provider.catalog;
330
341
  const selection = patch.selection ? catalog.normalize(patch.selection) : this.thread.info.selection;
342
+
331
343
  const next: Partial<ChatInfo> = {
332
344
  selection,
333
345
  runtimeMode: patch.runtimeMode ?? this.thread.info.runtimeMode
334
346
  };
347
+
335
348
  const changed = JSON.stringify(next.selection) !== JSON.stringify(this.thread.info.selection) || next.runtimeMode !== this.thread.info.runtimeMode;
349
+
336
350
  if (!changed) {
337
351
  return this.thread.info;
338
352
  }
353
+
339
354
  if (patch.selection) {
340
355
  next.usage = { ...this.thread.info.usage, contextWindow: catalog.contextWindowFor(selection) };
341
356
  }
357
+
342
358
  this.emit([this.thread.patchInfo(next)]);
343
359
  this.options.persist();
344
360
  this.drainQueue();
361
+
345
362
  return this.thread.info;
346
363
  }
347
364
 
@@ -350,9 +367,11 @@ export class ChatSession {
350
367
  if (account === this.thread.info.account) {
351
368
  return this.thread.info;
352
369
  }
370
+
353
371
  this.emit([this.thread.patchInfo({ account })]);
354
372
  this.options.persist();
355
373
  this.drainQueue();
374
+
356
375
  return this.thread.info;
357
376
  }
358
377
 
@@ -361,8 +380,10 @@ export class ChatSession {
361
380
  if (cwd === this.thread.info.cwd) {
362
381
  return this.thread.info;
363
382
  }
383
+
364
384
  this.emit([this.thread.patchInfo({ cwd })]);
365
385
  this.options.persist();
386
+
366
387
  return this.thread.info;
367
388
  }
368
389
 
@@ -396,17 +417,17 @@ export class ChatSession {
396
417
  this.emit([this.thread.patchInfo({})]);
397
418
  }
398
419
 
399
- /*
400
- * Internal follow-ups wait for a new turn. User input goes through `sendInput`, which can steer native work.
401
- */
420
+ /* Internal follow-ups wait for a new turn. User input goes through `sendInput`, which can steer native work. */
402
421
  send(text: string, sent: ChatSendExtras = {}): { queued: boolean; turnId: string } {
403
422
  const extras = this.vetted(sent);
404
423
  const turnId = newId('turn');
424
+
405
425
  // Behind whatever still waits (a CLI that crashed leaves its queue), so everything goes out in the order it was sent.
406
426
  if (this.busy || this.queue.length > 0 || this.replacementWaits() || this.relocating) {
407
427
  if (this.busy && this.info.queuePaused) {
408
428
  this.sendAfterStop = true;
409
429
  }
430
+
410
431
  const message: ChatQueuedMessage = {
411
432
  id: newId('queued'),
412
433
  turnId,
@@ -420,12 +441,16 @@ export class ChatSession {
420
441
  ...(extras.attachments?.length ? { attachments: extras.attachments } : {}),
421
442
  ...(extras.references?.length ? { references: extras.references } : {})
422
443
  };
444
+
423
445
  this.setQueue([...this.queue, message]);
424
446
  this.lapseResume();
425
447
  this.drainQueue(true);
448
+
426
449
  return { queued: true, turnId };
427
450
  }
451
+
428
452
  this.dispatch(text, extras, turnId);
453
+
429
454
  return { queued: false, turnId };
430
455
  }
431
456
 
@@ -438,6 +463,7 @@ export class ChatSession {
438
463
  if (this.frozen) {
439
464
  return;
440
465
  }
466
+
441
467
  this.capturedUiAccess.set(itemId, access);
442
468
  this.options.persistSoon();
443
469
  }
@@ -449,9 +475,11 @@ export class ChatSession {
449
475
 
450
476
  updateUiQueries(itemId: string, uiQueries: ChatUiQueryState): void {
451
477
  const item = this.thread.get(itemId);
478
+
452
479
  if (this.frozen || item?.kind !== 'assistant' || item.parentToolUseId) {
453
480
  return;
454
481
  }
482
+
455
483
  this.thread.upsert({ ...item, uiQueries });
456
484
  this.options.emit({ type: 'delta', itemId, text: '', uiQueries });
457
485
  this.options.persistSoon();
@@ -459,35 +487,46 @@ export class ChatSession {
459
487
 
460
488
  choose(payload: ChatUiChoicePayload, queries: Readonly<Record<string, unknown>> = {}): { queued: boolean; turnId: string } {
461
489
  const item = this.thread.get(payload.itemId);
490
+
462
491
  const block =
463
492
  item?.kind === 'assistant' && !item.streaming && !item.parentToolUseId ? item.ui?.find((block) => block.id === payload.blockId) : undefined;
493
+
464
494
  if (item?.kind !== 'assistant' || !block?.revision || block.revision !== payload.revision) {
465
495
  throw new ChatError('stale-ui-block', 'This completed UI block is no longer available at the requested revision');
466
496
  }
497
+
467
498
  const sameBlock = (origin: ChatUiChoiceOrigin | undefined) =>
468
499
  origin?.itemId === item.id && origin.blockId === block.id && origin.revision === block.revision;
500
+
469
501
  let previous = item.uiAnswers?.[block.id];
502
+
470
503
  if (previous?.revision !== block.revision) {
471
504
  previous = undefined;
472
505
  }
506
+
473
507
  // The message is logged first: a crash before its answer marker must not send it a second time.
474
508
  if (!previous) {
475
509
  const sent = this.thread.list().find((entry) => entry.kind === 'user' && sameBlock(entry.uiChoice));
476
510
  const queued = this.queue.find((entry) => sameBlock(entry.uiChoice));
477
511
  const origin = sent?.kind === 'user' ? sent.uiChoice : queued?.uiChoice;
478
512
  const turnId = sent?.turnId ?? queued?.turnId;
513
+
479
514
  if (origin && turnId) {
480
515
  previous = { ...origin, turnId, queued: !sent };
481
516
  this.recordUiAnswer(previous);
482
517
  }
483
518
  }
519
+
484
520
  if (previous) {
485
521
  if (previous.choiceId !== payload.choiceId) {
486
522
  throw new ChatError('ui-already-answered', 'This UI block was already answered');
487
523
  }
524
+
488
525
  return { queued: previous.queued, turnId: previous.turnId };
489
526
  }
527
+
490
528
  let selected;
529
+
491
530
  try {
492
531
  selected = resolveUiChoice(block, payload.choiceId, payload.values, queries);
493
532
  } catch (error) {
@@ -496,6 +535,7 @@ export class ChatSession {
496
535
  error instanceof Error ? error.message : 'This choice could not be read'
497
536
  );
498
537
  }
538
+
499
539
  const origin: ChatUiChoiceOrigin = {
500
540
  itemId: item.id,
501
541
  blockId: block.id,
@@ -507,19 +547,24 @@ export class ChatSession {
507
547
  values: selected.values,
508
548
  at: Date.now()
509
549
  };
550
+
510
551
  const result = this.send(selected.context, { uiChoice: origin });
511
552
  this.recordUiAnswer({ ...origin, ...result });
553
+
512
554
  return result;
513
555
  }
514
556
 
515
557
  private recordUiAnswer(answer: ChatUiAnswer): void {
516
558
  const item = this.thread.get(answer.itemId);
559
+
517
560
  if (item?.kind !== 'assistant') {
518
561
  return;
519
562
  }
563
+
520
564
  if (JSON.stringify(item.uiAnswers?.[answer.blockId]) === JSON.stringify(answer)) {
521
565
  return;
522
566
  }
567
+
523
568
  this.options.emit(this.thread.upsert({ ...item, uiAnswers: { ...item.uiAnswers, [answer.blockId]: answer } }));
524
569
  this.options.persist();
525
570
  }
@@ -529,8 +574,10 @@ export class ChatSession {
529
574
  if (extras.quoteSource === undefined || this.thread.get(extras.quoteSource.itemId)?.kind === 'assistant') {
530
575
  return extras;
531
576
  }
577
+
532
578
  const plain = { ...extras };
533
579
  delete plain.quoteSource;
580
+
534
581
  return plain;
535
582
  }
536
583
 
@@ -540,6 +587,7 @@ export class ChatSession {
540
587
  if (text.startsWith('/') || !this.options.references) {
541
588
  return { chats: [], references: [], note: null };
542
589
  }
590
+
543
591
  return this.options.references({ chats: extras.chats, references: extras.references });
544
592
  }
545
593
 
@@ -551,6 +599,7 @@ export class ChatSession {
551
599
  const extras = this.vetted(sent);
552
600
  const backend = this.backend;
553
601
  const turnId = this.info.activeTurnId;
602
+
554
603
  if (
555
604
  extras.delivery !== 'steer' ||
556
605
  this.info.queuePaused ||
@@ -565,26 +614,34 @@ export class ChatSession {
565
614
  ) {
566
615
  return this.send(text, extras);
567
616
  }
617
+
568
618
  const queued = this.send(text, extras);
569
619
  const message = this.queue.find((entry) => entry.turnId === queued.turnId);
620
+
570
621
  if (!message) {
571
622
  return queued;
572
623
  }
624
+
573
625
  this.steering.add(message.id);
574
626
  const steer = this.steer(message, text, extras, backend.steerTurn.bind(backend), turnId, queued);
575
627
  // Claude takes a steer only between tool calls, so a long command would outlast the client's request.
576
628
  const clock = this.options.clock ?? systemClock;
577
629
  let timer: unknown = null;
630
+
578
631
  const waited = new Promise<null>((resolve) => {
579
632
  timer = clock.setTimeout(() => resolve(null), STEER_ANSWER_MS);
580
633
  });
634
+
581
635
  try {
582
636
  const answer = await Promise.race([steer, waited]);
637
+
583
638
  if (answer !== null) {
584
639
  return answer;
585
640
  }
641
+
586
642
  // Still in the queue until it is taken; a later failure pauses the queue as it does now.
587
643
  steer.catch(() => undefined);
644
+
588
645
  return queued;
589
646
  } finally {
590
647
  clock.clearTimeout(timer);
@@ -601,6 +658,7 @@ export class ChatSession {
601
658
  ): Promise<{ queued: boolean; turnId: string }> {
602
659
  try {
603
660
  const attached = this.attached(text, extras);
661
+
604
662
  const accepted = await steerTurn({
605
663
  promptId: message.id,
606
664
  text,
@@ -609,9 +667,11 @@ export class ChatSession {
609
667
  mentions: extras.mentions ?? [],
610
668
  skills: extras.skills ?? []
611
669
  });
670
+
612
671
  if (!accepted) {
613
672
  return queued;
614
673
  }
674
+
615
675
  this.emit([
616
676
  this.thread.upsert({
617
677
  id: `user-${message.id}`,
@@ -628,12 +688,15 @@ export class ChatSession {
628
688
  }),
629
689
  this.thread.patchInfo({ queue: this.queue.filter((entry) => entry.id !== message.id) })
630
690
  ]);
691
+
631
692
  this.options.persist();
693
+
632
694
  return { queued: false, turnId };
633
695
  } catch (error) {
634
696
  // A lost acknowledgement does not prove rejection. Keep the message for an explicit retry.
635
697
  this.pauseQueue(true);
636
698
  this.options.persist();
699
+
637
700
  throw error;
638
701
  } finally {
639
702
  this.steering.delete(message.id);
@@ -645,10 +708,13 @@ export class ChatSession {
645
708
  unqueue(messageId: string): ChatQueuedMessage | null {
646
709
  const queue = this.queue;
647
710
  const message = queue.find((entry) => entry.id === messageId);
711
+
648
712
  if (!message || this.steering.has(message.id)) {
649
713
  return null;
650
714
  }
715
+
651
716
  this.setQueue(queue.filter((entry) => entry.id !== messageId));
717
+
652
718
  return message;
653
719
  }
654
720
 
@@ -656,16 +722,22 @@ export class ChatSession {
656
722
  sendNow(messageId: string): boolean {
657
723
  const queue = this.queue;
658
724
  const message = queue.find((entry) => entry.id === messageId);
725
+
659
726
  if (!message || this.steering.has(message.id)) {
660
727
  return false;
661
728
  }
729
+
662
730
  this.setQueue([message, ...queue.filter((entry) => entry.id !== messageId)]);
731
+
663
732
  if (this.busy) {
664
733
  this.cancel(true);
734
+
665
735
  return true;
666
736
  }
737
+
667
738
  this.lapseResume();
668
739
  this.drainQueue(true);
740
+
669
741
  return true;
670
742
  }
671
743
 
@@ -683,6 +755,7 @@ export class ChatSession {
683
755
  const [next, ...rest] = this.queue;
684
756
  const limited = limitedTurn(this.thread.list());
685
757
  const explicit = asked || this.sendAfterReplacement;
758
+
686
759
  if (
687
760
  !next ||
688
761
  this.steering.size > 0 ||
@@ -692,9 +765,11 @@ export class ChatSession {
692
765
  ) {
693
766
  return;
694
767
  }
768
+
695
769
  if (this.replacementWaits()) {
696
770
  this.sendAfterReplacement = explicit;
697
771
  const id = `settings-wait-${this.generation}`;
772
+
698
773
  if (!this.thread.get(id)) {
699
774
  this.emit([
700
775
  this.thread.upsert({
@@ -706,13 +781,17 @@ export class ChatSession {
706
781
  text: 'Your message is queued until the current work and open requests finish. The changes to this chat will apply then.'
707
782
  })
708
783
  ]);
784
+
709
785
  this.options.persist();
710
786
  }
787
+
711
788
  return;
712
789
  }
790
+
713
791
  this.sendAfterReplacement = false;
714
792
  this.pauseQueue(false);
715
793
  this.setQueue(rest);
794
+
716
795
  this.dispatch(
717
796
  next.text,
718
797
  {
@@ -731,17 +810,22 @@ export class ChatSession {
731
810
  private dispatch(text: string, extras: ChatSendExtras, requestedTurnId?: string): void {
732
811
  this.pauseQueue(false);
733
812
  this.settleAgentTurn();
813
+
734
814
  // A goal that was met has said what it had to; the next message of the person leaves it behind.
735
815
  if (this.thread.info.goal?.status === 'complete') {
736
816
  this.emit([this.thread.patchInfo({ goal: undefined })]);
737
817
  }
818
+
738
819
  const { preamble, note } = this.contextNote(text);
739
820
  const attached = this.attached(text, extras);
740
821
  const turnId = this.openTurn(text, note, { ...extras, chats: attached.chats, references: attached.references }, requestedTurnId);
822
+
741
823
  const choiceNote = extras.uiChoice
742
824
  ? `The person chose ${JSON.stringify(extras.uiChoice.label)} from your UI reply. Their message is the visible context of that choice.`
743
825
  : null;
826
+
744
827
  const said = [preamble, attached.note, choiceNote].filter((part): part is string => part !== null);
828
+
745
829
  const input = {
746
830
  text,
747
831
  preamble: said.length === 0 ? null : said.join('\n\n'),
@@ -749,6 +833,7 @@ export class ChatSession {
749
833
  mentions: extras.mentions ?? [],
750
834
  skills: extras.skills ?? []
751
835
  };
836
+
752
837
  // The prompt waits for the checkpoint, so the tree is the folder as it was before the agent edited it.
753
838
  this.turnReady = this.checkpoint(turnId);
754
839
  this.submit(turnId, input);
@@ -757,16 +842,21 @@ export class ChatSession {
757
842
  /* Asks the CLI to fold its context: a call of its own, or the slash command as a turn. */
758
843
  compact(): void {
759
844
  const compaction = this.options.provider.capabilities.compaction;
845
+
760
846
  if (compaction === 'none') {
761
847
  throw new ChatError('chat-unsupported', `${this.options.provider.name} cannot compact its context`);
762
848
  }
849
+
763
850
  if (compaction === 'prompt') {
764
851
  this.send('/compact');
852
+
765
853
  return;
766
854
  }
855
+
767
856
  if (this.replacementWaits()) {
768
857
  throw new ChatError('chat-busy', 'The current work and open requests must finish before the changes to this chat can apply');
769
858
  }
859
+
770
860
  // A native compaction has no message of the person in front of it, only a turn to fold behind.
771
861
  this.openTurn(null, null, {});
772
862
  // Folding the context changes no file, so this turn needs no checkpoint.
@@ -780,12 +870,15 @@ export class ChatSession {
780
870
  this.pauseQueue(true);
781
871
  this.lapseResume();
782
872
  const turnId = this.thread.info.activeTurnId;
873
+
783
874
  // A turn still waiting to be resumed has no process to interrupt; stopping it is ending it here.
784
875
  if (!this.backend && turnId !== null) {
785
876
  this.clearStartWarning();
786
877
  this.abandon(turnId, null);
878
+
787
879
  return;
788
880
  }
881
+
789
882
  if (!this.backend || turnId === null) {
790
883
  if (turnId === null && this.backend) {
791
884
  for (const item of this.thread.pending()) {
@@ -793,10 +886,13 @@ export class ChatSession {
793
886
  this.receive(this.generation, { type: 'request.withdrawn', requestId: item.requestId });
794
887
  }
795
888
  }
889
+
796
890
  this.options.persist();
797
891
  }
892
+
798
893
  return;
799
894
  }
895
+
800
896
  if (this.pendingStart?.turnId === turnId) {
801
897
  this.clearStartWarning();
802
898
  const backend = this.backend;
@@ -805,8 +901,10 @@ export class ChatSession {
805
901
  backend.stop();
806
902
  this.emit([this.thread.patchInfo({ running: false })]);
807
903
  this.receive(this.generation, { type: 'turn.done', state: 'aborted', costUsd: 0 });
904
+
808
905
  return;
809
906
  }
907
+
810
908
  this.backend.interrupt();
811
909
  }
812
910
 
@@ -815,32 +913,41 @@ export class ChatSession {
815
913
  const workflow = this.thread
816
914
  .list()
817
915
  .some((item) => item.kind === 'tool' && item.name === 'Workflow' && item.state === 'running' && item.workflow?.taskId === taskId);
916
+
818
917
  if (!workflow && !this.thread.info.background?.some((task) => task.id === taskId)) {
819
918
  throw new ChatError('task-not-found', 'This chat runs no such task');
820
919
  }
920
+
821
921
  if (!this.backend?.stopTask) {
822
922
  throw new ChatError('chat-unsupported', 'This CLI cannot stop a task on its own');
823
923
  }
924
+
824
925
  this.backend.stopTask(taskId);
825
926
  }
826
927
 
827
928
  /* Answers a pending approval; false when nothing waits under that id. */
828
929
  approve(requestId: string, decision: 'allow' | 'allow-always' | 'deny', message?: string): boolean {
829
930
  const item = this.thread.get(`approval-${requestId}`);
931
+
830
932
  if (item?.kind !== 'approval' || item.decision !== 'pending' || this.backend?.respondApproval(requestId, decision, message) !== true) {
831
933
  return false;
832
934
  }
935
+
833
936
  this.emit([this.thread.upsert({ ...item, decision }), this.thread.setStatus(this.thread.statusFor(this.thread.info.activeTurnId))]);
937
+
834
938
  return true;
835
939
  }
836
940
 
837
941
  /* Answers a pending question; false when nothing waits under that id. */
838
942
  answer(requestId: string, answers: Record<string, string>): boolean {
839
943
  const item = this.thread.get(`question-${requestId}`);
944
+
840
945
  if (item?.kind !== 'question' || item.state !== 'pending' || this.backend?.respondQuestion(requestId, answers) !== true) {
841
946
  return false;
842
947
  }
948
+
843
949
  this.emit([this.thread.upsert({ ...item, answers, state: 'answered' }), this.thread.setStatus(this.thread.statusFor(this.thread.info.activeTurnId))]);
950
+
844
951
  return true;
845
952
  }
846
953
 
@@ -850,11 +957,14 @@ export class ChatSession {
850
957
  */
851
958
  dismiss(itemId: string): boolean {
852
959
  const item = this.thread.get(itemId);
960
+
853
961
  if (item?.kind !== 'question' || item.state !== 'pending' || item.async !== true) {
854
962
  return false;
855
963
  }
964
+
856
965
  this.backend?.dismissRequest?.(item.requestId);
857
966
  this.emit([this.thread.upsert({ ...item, state: 'dismissed' }), this.thread.setStatus(this.thread.statusFor(this.thread.info.activeTurnId))]);
967
+
858
968
  return true;
859
969
  }
860
970
 
@@ -883,6 +993,7 @@ export class ChatSession {
883
993
  if (this.busy && !force) {
884
994
  throw new ChatError('chat-busy', `Chat ${this.id} is still working on the previous message`);
885
995
  }
996
+
886
997
  const ended = this.dispose();
887
998
  this.generation = nextGeneration();
888
999
  this.projector.reset();
@@ -900,6 +1011,7 @@ export class ChatSession {
900
1011
  this.turnReady = Promise.resolve();
901
1012
  this.lapseResume();
902
1013
  const usage = this.thread.info.usage;
1014
+
903
1015
  this.emit([
904
1016
  this.thread.reset({
905
1017
  agentSessionId: null,
@@ -926,6 +1038,7 @@ export class ChatSession {
926
1038
  }
927
1039
  })
928
1040
  ]);
1041
+
929
1042
  return ended;
930
1043
  }
931
1044
 
@@ -938,23 +1051,31 @@ export class ChatSession {
938
1051
  const events: ChatEvent[] = [];
939
1052
  const resumeTurnId = resume.resumeTurnId;
940
1053
  const now = Date.now();
1054
+
941
1055
  for (const item of this.thread.list()) {
942
1056
  const settled = settleStoredItem(item, resumeTurnId);
1057
+
943
1058
  if (settled === item) {
944
1059
  continue;
945
1060
  }
1061
+
946
1062
  if (isBackgroundWork(item)) {
947
1063
  this.recordLostBackground(item);
948
1064
  }
1065
+
949
1066
  events.push(this.thread.upsert(settled));
1067
+
950
1068
  if (item.kind === 'turn' && settled.kind === 'turn' && settled.state === 'aborted') {
951
1069
  const reason = item.id === this.thread.info.activeTurnId && resume.reason !== null ? resume.reason : 'it was not the turn the chat was running';
1070
+
952
1071
  events.push(
953
1072
  this.thread.upsert({ id: newId('note'), kind: 'note', createdAt: now, turnId: item.id, level: 'warning', text: notResumedNote(reason) })
954
1073
  );
955
1074
  }
956
1075
  }
1076
+
957
1077
  const info = this.thread.info;
1078
+
958
1079
  const patch: Partial<ChatInfo> = {
959
1080
  selection,
960
1081
  running: false,
@@ -963,19 +1084,24 @@ export class ChatSession {
963
1084
  effectiveRuntimeMode: undefined,
964
1085
  permissionMode: undefined
965
1086
  };
1087
+
966
1088
  // No process of the host that went down is left to run what it kept in the background.
967
1089
  if (info.background?.length) {
968
1090
  for (const command of info.background) {
969
1091
  const text = `The machine restarted while background command "${commandLabel(command)}" was running. It will not send a result. Do not wait for it.`;
970
1092
  this.deliverNote({ noteId: `lost-command-${command.id}`, note: text, preamble: text });
971
1093
  }
1094
+
972
1095
  patch.background = [];
973
1096
  }
1097
+
974
1098
  // The window stored is whatever the session that went left behind; the next CLI starts on the pick.
975
1099
  const contextWindow = this.options.provider.catalog.contextWindowFor(selection);
1100
+
976
1101
  if (contextWindow !== info.usage.contextWindow) {
977
1102
  patch.usage = { ...info.usage, contextWindow };
978
1103
  }
1104
+
979
1105
  if (
980
1106
  JSON.stringify(selection) !== JSON.stringify(info.selection) ||
981
1107
  patch.usage !== undefined ||
@@ -988,6 +1114,7 @@ export class ChatSession {
988
1114
  ) {
989
1115
  events.push(this.thread.patchInfo(patch));
990
1116
  }
1117
+
991
1118
  this.emit(events);
992
1119
  }
993
1120
 
@@ -1001,55 +1128,75 @@ export class ChatSession {
1001
1128
  if (!this.awaitsResume(turnId, attempt)) {
1002
1129
  return;
1003
1130
  }
1131
+
1004
1132
  const original = this.thread.get(turnId);
1133
+
1005
1134
  if (original?.kind !== 'turn') {
1006
1135
  return;
1007
1136
  }
1137
+
1008
1138
  const now = Date.now();
1139
+
1009
1140
  this.emit([
1010
1141
  this.thread.upsert({ ...original, attempt, resumePending: true }),
1011
1142
  this.thread.upsert({ id: `resume-${turnId}-${attempt}`, kind: 'note', createdAt: now, turnId, level: 'info', text: words.note })
1012
1143
  ]);
1144
+
1013
1145
  this.options.persist();
1014
1146
  const pending = this.watchStart(turnId);
1015
1147
  let backend: ChatBackend;
1148
+
1016
1149
  try {
1017
1150
  await this.options.save?.();
1151
+
1018
1152
  if (this.pendingStart !== pending || !this.awaitsResume(turnId, attempt)) {
1019
1153
  return;
1020
1154
  }
1155
+
1021
1156
  backend = await this.ensureBackend();
1022
1157
  } catch (e) {
1023
1158
  if (this.pendingStart !== pending) {
1024
1159
  return;
1025
1160
  }
1161
+
1026
1162
  this.clearStartWarning();
1163
+
1027
1164
  if (this.backend === null) {
1028
1165
  this.emit([this.thread.patchInfo({ running: false })]);
1029
1166
  }
1167
+
1030
1168
  this.addNote('warning', `The resume attempt could not start: ${errorText(e)}`);
1169
+
1031
1170
  throw e;
1032
1171
  }
1172
+
1033
1173
  const turn = this.thread.get(turnId);
1174
+
1034
1175
  if (this.pendingStart !== pending || !this.awaitsResume(turnId, attempt) || this.backend !== backend || turn?.kind !== 'turn') {
1035
1176
  return;
1036
1177
  }
1178
+
1037
1179
  this.clearStartWarning();
1038
1180
  // The turn keeps the checkpoint it started with, so its card still shows everything it changed.
1039
1181
  this.turnReady = Promise.resolve();
1040
1182
  const context = this.contextNote(words.prompt);
1041
1183
  const preamble = [words.preamble, context.preamble].filter(Boolean).join('\n\n') || null;
1184
+
1042
1185
  if (context.note) {
1043
1186
  this.emit([this.thread.upsert({ id: newId('note'), kind: 'note', createdAt: now, turnId, level: 'info', text: context.note })]);
1044
1187
  }
1188
+
1045
1189
  const input: TurnInput = { promptId: randomUUID(), text: words.prompt, preamble, attachments: [], mentions: [], skills: [] };
1046
1190
  this.submission = { turnId, input };
1047
1191
  this.options.persist();
1048
1192
  await this.options.save?.();
1193
+
1049
1194
  if (this.backend !== backend || this.info.activeTurnId !== turnId || this.frozen) {
1050
1195
  return;
1051
1196
  }
1197
+
1052
1198
  backend.sendTurn(input);
1199
+
1053
1200
  if (!backend.acknowledgesTurns) {
1054
1201
  this.acceptPrompt(input.promptId!);
1055
1202
  }
@@ -1058,16 +1205,20 @@ export class ChatSession {
1058
1205
  /* Ends a running turn nobody is working on and keeps queued messages for a person's next send. */
1059
1206
  abandon(turnId: string, reason: string | null): void {
1060
1207
  const turn = this.thread.get(turnId);
1208
+
1061
1209
  if (turn?.kind !== 'turn' || turn.state !== 'running' || this.thread.info.activeTurnId !== turnId || this.running) {
1062
1210
  return;
1063
1211
  }
1212
+
1064
1213
  const now = Date.now();
1065
1214
  this.pauseQueue(true);
1215
+
1066
1216
  this.emit([
1067
1217
  this.thread.upsert({ ...turn, state: 'aborted', endedAt: now }),
1068
1218
  ...(reason === null ? [] : [this.thread.upsert({ id: newId('note'), kind: 'note', createdAt: now, turnId, level: 'warning', text: reason })]),
1069
1219
  this.thread.patchInfo({ status: 'idle', activeTurnId: null })
1070
1220
  ]);
1221
+
1071
1222
  this.options.persist();
1072
1223
  this.drainQueue(this.sendAfterStop);
1073
1224
  this.sendAfterStop = false;
@@ -1078,16 +1229,28 @@ export class ChatSession {
1078
1229
  * turn carrying what woke it, so a turn it opens cannot itself wake anyone. A caller passes `note`
1079
1230
  * only when its news is not already written in the thread. Checked and opened in one step, so null
1080
1231
  * means a turn (or a shutdown) was in the way. Never through `send`, which would step on a turn
1081
- * with `origin: 'agent'` and close the wake it just made.
1232
+ * with `origin: 'agent'` and close the wake it just made. `mentions` and `skills` reach a CLI that
1233
+ * expands them itself, as a person's picks in a message do.
1082
1234
  */
1083
- wake(wake: { text: string; label: string; note?: string; taskIds: string[]; summaryFor?: string; messageFrom?: string[] }): string | null {
1235
+ wake(wake: {
1236
+ text: string;
1237
+ label: string;
1238
+ note?: string;
1239
+ taskIds: string[];
1240
+ summaryFor?: string;
1241
+ messageFrom?: string[];
1242
+ mentions?: string[];
1243
+ skills?: string[];
1244
+ }): string | null {
1084
1245
  if (this.frozen || this.relocating || this.thread.info.activeTurnId !== null || this.replacementWaits()) {
1085
1246
  return null;
1086
1247
  }
1248
+
1087
1249
  this.turnOpened();
1088
1250
  const { preamble, note } = this.contextNote(wake.text);
1089
1251
  const turnId = newId('turn');
1090
1252
  const now = Date.now();
1253
+
1091
1254
  this.emit([
1092
1255
  this.thread.upsert({
1093
1256
  id: turnId,
@@ -1110,9 +1273,11 @@ export class ChatSession {
1110
1273
  ...(note === null ? [] : [this.thread.upsert({ id: newId('note'), kind: 'note', createdAt: now, turnId, level: 'info', text: note })]),
1111
1274
  this.thread.patchInfo({ status: this.thread.statusFor(turnId), activeTurnId: turnId })
1112
1275
  ]);
1276
+
1113
1277
  this.options.persist();
1114
1278
  this.turnReady = this.checkpoint(turnId);
1115
- this.submit(turnId, { text: wake.text, preamble, attachments: [], mentions: [], skills: [] });
1279
+ this.submit(turnId, { text: wake.text, preamble, attachments: [], mentions: wake.mentions ?? [], skills: wake.skills ?? [] });
1280
+
1116
1281
  return turnId;
1117
1282
  }
1118
1283
 
@@ -1133,20 +1298,27 @@ export class ChatSession {
1133
1298
  */
1134
1299
  takeUpAfterLimit(turnId: string): 'opened' | 'wait' | 'lapsed' {
1135
1300
  const turn = limitedTurn(this.thread.list());
1301
+
1136
1302
  if (turn === null || turn.id !== turnId || !this.resumeAllowed()) {
1137
1303
  this.lapseResume();
1304
+
1138
1305
  return 'lapsed';
1139
1306
  }
1307
+
1140
1308
  const wake = limitResumeWake(turn.limit.kind);
1309
+
1141
1310
  const opened = this.wake({
1142
1311
  ...wake,
1143
1312
  taskIds: turn.taskIds ?? [],
1144
1313
  ...(turn.messageFrom === undefined ? {} : { messageFrom: turn.messageFrom })
1145
1314
  });
1315
+
1146
1316
  if (opened === null) {
1147
1317
  return 'wait';
1148
1318
  }
1319
+
1149
1320
  this.pauseQueue(false);
1321
+
1150
1322
  return 'opened';
1151
1323
  }
1152
1324
 
@@ -1172,17 +1344,23 @@ export class ChatSession {
1172
1344
  private oweResume(fresh: boolean): void {
1173
1345
  const hooks = this.options.limitResume;
1174
1346
  const turn = limitedTurn(this.thread.list());
1347
+
1175
1348
  if (!hooks || turn === null || this.thread.info.activeTurnId !== null || this.frozen || !this.resumeAllowed() || this.wentOn(turn)) {
1176
1349
  return;
1177
1350
  }
1351
+
1178
1352
  const now = hooks.now();
1353
+
1179
1354
  if (!fresh && (turn.limit.kind !== 'usage' || (turn.limit.resetsAt ?? 0) <= now)) {
1180
1355
  return;
1181
1356
  }
1357
+
1182
1358
  const at = limitResumeAt(this.thread.list(), turn, now);
1359
+
1183
1360
  if (at === null || at === this.thread.info.resumeAt) {
1184
1361
  return;
1185
1362
  }
1363
+
1186
1364
  this.emit([this.thread.patchInfo({ resumeAt: at })]);
1187
1365
  this.options.persist();
1188
1366
  void hooks.owe(turn.id, at).catch((e: unknown) => console.error(`Owing a resume of chat ${this.id} failed:`, errorText(e)));
@@ -1195,15 +1373,20 @@ export class ChatSession {
1195
1373
  settleOwedResume(): void {
1196
1374
  const hooks = this.options.limitResume;
1197
1375
  const at = this.thread.info.resumeAt;
1376
+
1198
1377
  if (at === undefined || hooks?.owed() === true) {
1199
1378
  return;
1200
1379
  }
1380
+
1201
1381
  const turn = limitedTurn(this.thread.list());
1382
+
1202
1383
  if (!hooks || turn === null || this.thread.info.activeTurnId !== null || !this.resumeAllowed()) {
1203
1384
  this.emit([this.thread.patchInfo({ resumeAt: undefined })]);
1204
1385
  this.options.persist();
1386
+
1205
1387
  return;
1206
1388
  }
1389
+
1207
1390
  void hooks.owe(turn.id, at).catch((e: unknown) => console.error(`Owing a resume of chat ${this.id} failed:`, errorText(e)));
1208
1391
  }
1209
1392
 
@@ -1212,6 +1395,7 @@ export class ChatSession {
1212
1395
  if (this.thread.info.limit !== undefined) {
1213
1396
  this.emit([this.thread.patchInfo({ limit: undefined })]);
1214
1397
  }
1398
+
1215
1399
  this.lapseResume();
1216
1400
  }
1217
1401
 
@@ -1222,9 +1406,11 @@ export class ChatSession {
1222
1406
  continuedElsewhere(note: string): void {
1223
1407
  this.lapseResume();
1224
1408
  const turn = limitedTurn(this.thread.list());
1409
+
1225
1410
  if (turn === null || this.wentOn(turn)) {
1226
1411
  return;
1227
1412
  }
1413
+
1228
1414
  this.emit([this.thread.upsert({ id: continuedNoteId(turn.id), kind: 'note', createdAt: Date.now(), turnId: turn.id, level: 'info', text: note })]);
1229
1415
  this.options.persist();
1230
1416
  }
@@ -1232,9 +1418,11 @@ export class ChatSession {
1232
1418
  /* Drops the resume owed after a limit, the time the thread shows for it included. */
1233
1419
  lapseResume(): void {
1234
1420
  const hooks = this.options.limitResume;
1421
+
1235
1422
  if (this.thread.info.resumeAt === undefined || !hooks) {
1236
1423
  return;
1237
1424
  }
1425
+
1238
1426
  this.emit([this.thread.patchInfo({ resumeAt: undefined })]);
1239
1427
  this.options.persist();
1240
1428
  void hooks.lapse().catch((e: unknown) => console.error(`Dropping the resume of chat ${this.id} failed:`, errorText(e)));
@@ -1249,7 +1437,9 @@ export class ChatSession {
1249
1437
  if (this.thread.get(delivery.noteId) !== undefined) {
1250
1438
  return false;
1251
1439
  }
1440
+
1252
1441
  this.pendingPreambles = [...this.pendingPreambles, delivery.preamble];
1442
+
1253
1443
  this.emit([
1254
1444
  this.thread.upsert({
1255
1445
  id: delivery.noteId,
@@ -1261,16 +1451,20 @@ export class ChatSession {
1261
1451
  ...(delivery.from === undefined ? {} : { from: delivery.from })
1262
1452
  })
1263
1453
  ]);
1454
+
1264
1455
  this.options.persist();
1456
+
1265
1457
  return true;
1266
1458
  }
1267
1459
 
1268
1460
  /* Writes items a host draws in the thread itself (a row for work it delegated), leaving the ones that did not change alone. */
1269
1461
  upsertItems(items: readonly ChatItem[]): void {
1270
1462
  const changed = items.filter((item) => JSON.stringify(this.thread.get(item.id)) !== JSON.stringify(item));
1463
+
1271
1464
  if (changed.length === 0) {
1272
1465
  return;
1273
1466
  }
1467
+
1274
1468
  this.emit(changed.map((item) => this.thread.upsert(item)));
1275
1469
  this.options.persistSoon();
1276
1470
  }
@@ -1300,6 +1494,7 @@ export class ChatSession {
1300
1494
  const now = Date.now();
1301
1495
  const turnId = this.thread.info.activeTurnId;
1302
1496
  const events: ChatEvent[] = [];
1497
+
1303
1498
  for (const item of this.thread.list()) {
1304
1499
  const settled =
1305
1500
  item.kind === 'turn' && item.state === 'running'
@@ -1307,13 +1502,16 @@ export class ChatSession {
1307
1502
  : item.kind === 'subagent' && item.origin !== HOST_TASK_ORIGIN && item.status === 'running'
1308
1503
  ? { ...item, status: 'failed' as const, finishedAt: now }
1309
1504
  : settleStoredItem(item, null);
1505
+
1310
1506
  if (settled !== item) {
1311
1507
  events.push(this.thread.upsert(settled));
1312
1508
  }
1313
1509
  }
1510
+
1314
1511
  if (turnId !== null) {
1315
1512
  events.push(this.thread.upsert({ id: newId(`${ENDED_NOTE_PREFIX}note`), kind: 'note', createdAt: now, turnId, level: 'warning', text: reason }));
1316
1513
  }
1514
+
1317
1515
  events.push(
1318
1516
  this.thread.patchInfo({
1319
1517
  running: false,
@@ -1325,6 +1523,7 @@ export class ChatSession {
1325
1523
  ...(this.thread.info.background?.length ? { background: [] } : {})
1326
1524
  })
1327
1525
  );
1526
+
1328
1527
  this.emit(events);
1329
1528
  this.options.persist();
1330
1529
  }
@@ -1338,6 +1537,7 @@ export class ChatSession {
1338
1537
  if (this.thread.info.activeTurnId !== null || this.processInUse()) {
1339
1538
  throw new ChatError('chat-busy', `Chat ${this.id} is still working; stop it before restarting its agent session`);
1340
1539
  }
1540
+
1341
1541
  return this.dropProcess();
1342
1542
  }
1343
1543
 
@@ -1355,6 +1555,7 @@ export class ChatSession {
1355
1555
  // What the init frame of the process that goes announced; a skill added since would be filtered out by it.
1356
1556
  this.emit([this.thread.patchInfo({ running: false, effectiveRuntimeMode: undefined, permissionMode: undefined, skills: undefined })]);
1357
1557
  this.options.persist();
1558
+
1358
1559
  return backend?.dispose() ?? Promise.resolve();
1359
1560
  }
1360
1561
 
@@ -1372,50 +1573,63 @@ export class ChatSession {
1372
1573
  this.uiReplies.clear();
1373
1574
  this.clearStartWarning();
1374
1575
  this.clearWorkflowWarnings();
1576
+
1375
1577
  if (this.titleTimer !== null) {
1376
1578
  clearTimeout(this.titleTimer);
1377
1579
  this.titleTimer = null;
1378
1580
  }
1581
+
1379
1582
  const ended = this.backend?.dispose() ?? Promise.resolve();
1380
1583
  this.backend = null;
1381
1584
  this.starting = null;
1585
+
1382
1586
  return Promise.all([ended, ...this.checkpointWork]).then(() => undefined);
1383
1587
  }
1384
1588
 
1385
1589
  /* What a turn changed against its checkpoint: the stored answer, or one taken now while it runs. */
1386
1590
  async turnDiff(turnId: string): Promise<ChatCheckpointDiff | null> {
1387
1591
  const turn = this.thread.get(turnId);
1592
+
1388
1593
  if (turn?.kind !== 'turn') {
1389
1594
  throw new ChatError('request-not-found', `No turn ${turnId} in chat ${this.id}`);
1390
1595
  }
1596
+
1391
1597
  if (turn.checkpointDiff) {
1392
1598
  return turn.checkpointDiff;
1393
1599
  }
1600
+
1394
1601
  if (turn.checkpoint === undefined || !this.options.checkpoints) {
1395
1602
  return null;
1396
1603
  }
1604
+
1397
1605
  return await this.options.checkpoints.diff(this.thread.info.cwd, turn.checkpoint);
1398
1606
  }
1399
1607
 
1400
1608
  /* A page of a thread the CLI keeps, through the process that runs now; null when none runs that can answer. */
1401
1609
  listThreadItems(params: { threadId: string; cursor?: string; limit: number; sortDirection: 'asc' | 'desc' }): Promise<unknown> | null {
1402
1610
  const backend = this.backend;
1611
+
1403
1612
  return backend?.running === true && backend.listThreadItems ? backend.listThreadItems(params) : null;
1404
1613
  }
1405
1614
 
1406
1615
  /* A completed transcript survives the process; unfinished work cannot send a result after a restart. */
1407
1616
  async settleOrphanedSubagents(): Promise<void> {
1408
1617
  const rows = this.runningBackgroundRows();
1618
+
1409
1619
  if (this.info.provider === 'claude') {
1410
1620
  for (const row of rows) {
1411
1621
  this.orphans.add(row.toolUseId);
1412
1622
  }
1623
+
1413
1624
  await this.settleFromTranscripts(rows);
1414
1625
  }
1626
+
1415
1627
  const lost = this.runningBackgroundRows();
1628
+
1416
1629
  for (const row of lost) {
1417
1630
  this.recordLostBackground(row);
1418
1631
  }
1632
+
1419
1633
  this.settleRows(lost.map((row) => ({ ...row, status: 'failed', finishedAt: Date.now() })));
1420
1634
  }
1421
1635
 
@@ -1434,15 +1648,20 @@ export class ChatSession {
1434
1648
  const row = this.thread.find('subagent', (item) => item.toolUseId === toolUseId);
1435
1649
  const agentId = row?.native?.agentId;
1436
1650
  const owned = this.subagentOwners.get(toolUseId) === this.generation;
1651
+
1437
1652
  if (agentId !== undefined && owned && this.backend?.stopTask) {
1438
1653
  this.stopping.set(agentId, toolUseId);
1439
1654
  this.backend.stopTask(agentId);
1655
+
1440
1656
  return 'stopping';
1441
1657
  }
1658
+
1442
1659
  if (agentId !== undefined && !owned && this.thread.info.activeTurnId !== null) {
1443
1660
  throw new ChatError('subagent-stale', 'This sub-agent belongs to a process that has ended');
1444
1661
  }
1662
+
1445
1663
  this.markSubagentStopped(toolUseId);
1664
+
1446
1665
  return 'marked';
1447
1666
  }
1448
1667
 
@@ -1451,6 +1670,7 @@ export class ChatSession {
1451
1670
  this.subagentOwners.set(event.ref, generation);
1452
1671
  } else if (event.type === 'task.done') {
1453
1672
  this.noteStopped(event);
1673
+
1454
1674
  if (event.ref !== null) {
1455
1675
  this.subagentOwners.delete(event.ref);
1456
1676
  }
@@ -1462,18 +1682,24 @@ export class ChatSession {
1462
1682
  /* The CLI would not stop it: with no turn running the row is only marked, and while one runs a note says it still goes on. */
1463
1683
  private refuseStop(agentId: string, message: string): void {
1464
1684
  const toolUseId = this.stopping.get(agentId);
1685
+
1465
1686
  if (toolUseId === undefined) {
1466
1687
  return;
1467
1688
  }
1689
+
1468
1690
  this.stopping.delete(agentId);
1469
1691
  const row = this.thread.find('subagent', (item) => item.toolUseId === toolUseId);
1692
+
1470
1693
  if (row?.status !== 'running') {
1471
1694
  return;
1472
1695
  }
1696
+
1473
1697
  if (this.thread.info.activeTurnId === null) {
1474
1698
  this.markSubagentStopped(toolUseId);
1699
+
1475
1700
  return;
1476
1701
  }
1702
+
1477
1703
  this.emit([
1478
1704
  this.thread.upsert({
1479
1705
  id: `stop-refused-${toolUseId}-${Date.now()}`,
@@ -1484,25 +1710,33 @@ export class ChatSession {
1484
1710
  text: `"${row.description || row.subagentType || 'Sub-agent'}" could not be stopped: ${message}`
1485
1711
  })
1486
1712
  ]);
1713
+
1487
1714
  this.options.persist();
1488
1715
  }
1489
1716
 
1490
1717
  /* The line under a subagent that `stop_task` ended, once the CLI reported it settled. */
1491
1718
  private noteStopped(event: Extract<BackendEvent, { type: 'task.done' }>): void {
1492
1719
  const stopped = [...this.stopping].find(([agentId, toolUseId]) => agentId === event.taskId || toolUseId === event.ref);
1720
+
1493
1721
  if (stopped === undefined) {
1494
1722
  return;
1495
1723
  }
1724
+
1496
1725
  this.stopping.delete(stopped[0]);
1726
+
1497
1727
  // It finished on its own just before the stop arrived; the row already says so.
1498
1728
  if (event.ok) {
1499
1729
  return;
1500
1730
  }
1731
+
1501
1732
  const row = this.thread.find('subagent', (item) => item.toolUseId === stopped[1]);
1733
+
1502
1734
  if (!row) {
1503
1735
  return;
1504
1736
  }
1737
+
1505
1738
  const now = Date.now();
1739
+
1506
1740
  this.emit([
1507
1741
  ...(row.status === 'running' ? [this.thread.upsert({ ...row, status: 'failed' as const, finishedAt: now })] : []),
1508
1742
  this.thread.upsert({
@@ -1514,6 +1748,7 @@ export class ChatSession {
1514
1748
  text: `"${row.description || row.subagentType || 'Sub-agent'}" was stopped.`
1515
1749
  })
1516
1750
  ]);
1751
+
1517
1752
  this.options.persist();
1518
1753
  }
1519
1754
 
@@ -1525,10 +1760,13 @@ export class ChatSession {
1525
1760
  if (this.thread.info.activeTurnId !== null) {
1526
1761
  throw new ChatError('chat-busy', 'A turn is running; stop the turn instead');
1527
1762
  }
1763
+
1528
1764
  const row = this.thread.find('subagent', (item) => item.toolUseId === toolUseId);
1765
+
1529
1766
  if (row?.status !== 'running') {
1530
1767
  return;
1531
1768
  }
1769
+
1532
1770
  this.markStopped([row]);
1533
1771
  }
1534
1772
 
@@ -1545,11 +1783,15 @@ export class ChatSession {
1545
1783
  if (rows.length === 0) {
1546
1784
  return;
1547
1785
  }
1786
+
1548
1787
  const now = Date.now();
1788
+
1549
1789
  for (const row of rows) {
1550
1790
  this.orphans.delete(row.toolUseId);
1551
1791
  }
1792
+
1552
1793
  const names = rows.map((row) => `"${row.description || row.subagentType || 'Sub-agent'}"`).join(', ');
1794
+
1553
1795
  this.emit([
1554
1796
  ...rows.map((row) => this.thread.upsert({ ...row, status: 'failed', finishedAt: now })),
1555
1797
  this.thread.upsert({
@@ -1561,12 +1803,14 @@ export class ChatSession {
1561
1803
  text: `${names} ${rows.length === 1 ? 'was' : 'were'} marked as stopped. ${this.options.provider.name} cannot stop one sub-agent on its own, so ${rows.length === 1 ? 'it' : 'they'} may keep working until the chat's process ends.`
1562
1804
  })
1563
1805
  ]);
1806
+
1564
1807
  this.options.persist();
1565
1808
  }
1566
1809
 
1567
1810
  /* Asks the transcript again for a row a load left running, when somebody looks at it. */
1568
1811
  async recheckOrphan(toolUseId: string): Promise<void> {
1569
1812
  const row = this.thread.find('subagent', (item) => item.toolUseId === toolUseId);
1813
+
1570
1814
  if (this.orphans.has(toolUseId) && (row?.status === 'running' || row?.status === 'failed')) {
1571
1815
  await this.settleFromTranscripts([row]);
1572
1816
  }
@@ -1575,9 +1819,11 @@ export class ChatSession {
1575
1819
  /* Writes down where a subagent's conversation was found, so the next question about it reads no folder. */
1576
1820
  noteSubagentNative(toolUseId: string, native: { agentId?: string; threadId?: string }): void {
1577
1821
  const item = this.thread.find('subagent', (candidate) => candidate.toolUseId === toolUseId);
1822
+
1578
1823
  if (!item || (item.native?.agentId === native.agentId && item.native?.threadId === native.threadId)) {
1579
1824
  return;
1580
1825
  }
1826
+
1581
1827
  this.emit([this.thread.upsert({ ...item, native: { ...item.native, ...native } })]);
1582
1828
  this.options.persistSoon();
1583
1829
  }
@@ -1589,9 +1835,11 @@ export class ChatSession {
1589
1835
  */
1590
1836
  async skills(scan: () => Promise<ChatSkill[]>): Promise<ChatSkill[]> {
1591
1837
  const backend = this.backend;
1838
+
1592
1839
  if (backend?.running === true && backend.listSkills) {
1593
1840
  try {
1594
1841
  const own = await backend.listSkills();
1842
+
1595
1843
  if (own.length > 0) {
1596
1844
  return own;
1597
1845
  }
@@ -1599,12 +1847,16 @@ export class ChatSession {
1599
1847
  // The CLI could not answer; the scan below is what is left.
1600
1848
  }
1601
1849
  }
1850
+
1602
1851
  const scanned = await scan();
1603
1852
  const announced = this.thread.info.skills;
1853
+
1604
1854
  if (!announced?.length) {
1605
1855
  return scanned;
1606
1856
  }
1857
+
1607
1858
  const known = new Set(announced);
1859
+
1608
1860
  return scanned.filter((skill) => known.has(skill.name));
1609
1861
  }
1610
1862
 
@@ -1613,14 +1865,17 @@ export class ChatSession {
1613
1865
  const turnId = requestedTurnId ?? newId('turn');
1614
1866
  const now = Date.now();
1615
1867
  const events = [this.thread.upsert({ id: turnId, kind: 'turn', createdAt: now, turnId, state: 'running', origin: 'user', endedAt: null, costUsd: 0 })];
1868
+
1616
1869
  if (note !== null) {
1617
1870
  events.push(this.thread.upsert({ id: newId('note'), kind: 'note', createdAt: now, turnId, level: 'info', text: note }));
1618
1871
  }
1872
+
1619
1873
  if (text !== null) {
1620
1874
  const mentions = extras.mentions?.length ? extras.mentions : undefined;
1621
1875
  const skills = extras.skills?.length ? extras.skills : undefined;
1622
1876
  const chats = extras.chats?.length ? extras.chats : undefined;
1623
1877
  const attachments = extras.attachments?.length ? extras.attachments : undefined;
1878
+
1624
1879
  events.push(
1625
1880
  this.thread.upsert({
1626
1881
  id: newId('user'),
@@ -1638,13 +1893,17 @@ export class ChatSession {
1638
1893
  })
1639
1894
  );
1640
1895
  }
1896
+
1641
1897
  events.push(this.thread.patchInfo({ status: this.thread.statusFor(turnId), activeTurnId: turnId }));
1642
1898
  this.emit(events);
1899
+
1643
1900
  if (extras.uiChoice) {
1644
1901
  this.recordUiAnswer({ ...extras.uiChoice, turnId, queued: false });
1645
1902
  }
1903
+
1646
1904
  // On disk before the CLI answers, so a host that goes down mid-turn still shows the question.
1647
1905
  this.options.persist();
1906
+
1648
1907
  return turnId;
1649
1908
  }
1650
1909
 
@@ -1655,12 +1914,15 @@ export class ChatSession {
1655
1914
  private settleAgentTurn(): void {
1656
1915
  const turnId = this.thread.info.activeTurnId;
1657
1916
  const turn = turnId ? this.thread.get(turnId) : undefined;
1917
+
1658
1918
  if (turn?.kind !== 'turn' || turn.origin !== 'agent') {
1659
1919
  return;
1660
1920
  }
1921
+
1661
1922
  if (this.spokenTurnId === turn.id) {
1662
1923
  this.staleResults += 1;
1663
1924
  }
1925
+
1664
1926
  this.emit([this.thread.upsert({ ...turn, state: 'done', endedAt: Date.now() }), this.thread.patchInfo({ status: 'idle', activeTurnId: null })]);
1665
1927
  this.settleCheckpoint(turn.id);
1666
1928
  }
@@ -1668,16 +1930,21 @@ export class ChatSession {
1668
1930
  /* Records the folder's tree on the turn; a folder without git leaves the turn as it is. */
1669
1931
  private checkpoint(turnId: string): Promise<void> {
1670
1932
  const checkpoints = this.options.checkpoints;
1933
+
1671
1934
  if (!checkpoints) {
1672
1935
  return Promise.resolve();
1673
1936
  }
1937
+
1674
1938
  const cwd = this.thread.info.cwd;
1939
+
1675
1940
  return this.trackCheckpoint(
1676
1941
  Promise.all([checkpoints.take(cwd), checkpoints.head?.(cwd) ?? null]).then(([tree, head]) => {
1677
1942
  const turn = this.thread.get(turnId);
1943
+
1678
1944
  if (tree === null || turn?.kind !== 'turn' || turn.state !== 'running' || this.info.activeTurnId !== turnId) {
1679
1945
  return;
1680
1946
  }
1947
+
1681
1948
  this.emit([this.thread.upsert({ ...turn, checkpoint: tree, ...(head === null ? {} : { checkpointHead: head }) })]);
1682
1949
  })
1683
1950
  );
@@ -1686,17 +1953,22 @@ export class ChatSession {
1686
1953
  /* The diff of a turn that just ended, so a reload shows it without asking git again, and the tree a fork after it starts from. */
1687
1954
  private settleCheckpoint(turnId: string): void {
1688
1955
  const turn = this.thread.get(turnId);
1956
+
1689
1957
  if (turn?.kind !== 'turn' || turn.checkpoint === undefined || turn.checkpointDiff || !this.options.checkpoints) {
1690
1958
  return;
1691
1959
  }
1960
+
1692
1961
  const { checkpoints } = this.options;
1693
1962
  const cwd = this.thread.info.cwd;
1963
+
1694
1964
  void this.trackCheckpoint(
1695
1965
  Promise.all([checkpoints.settle(cwd, turn.checkpoint), checkpoints.head?.(cwd) ?? null]).then(([answer, head]) => {
1696
1966
  const settled = this.thread.get(turnId);
1967
+
1697
1968
  if (answer === null || settled?.kind !== 'turn') {
1698
1969
  return;
1699
1970
  }
1971
+
1700
1972
  this.emit([
1701
1973
  this.thread.upsert({
1702
1974
  ...settled,
@@ -1705,6 +1977,7 @@ export class ChatSession {
1705
1977
  ...(head === null ? {} : { checkpointHeadAfter: head })
1706
1978
  })
1707
1979
  ]);
1980
+
1708
1981
  this.options.persist();
1709
1982
  })
1710
1983
  );
@@ -1714,6 +1987,7 @@ export class ChatSession {
1714
1987
  private trackCheckpoint(work: Promise<void>): Promise<void> {
1715
1988
  const tracked = work.catch(() => undefined).finally(() => this.checkpointWork.delete(tracked));
1716
1989
  this.checkpointWork.add(tracked);
1990
+
1717
1991
  return tracked;
1718
1992
  }
1719
1993
 
@@ -1726,6 +2000,7 @@ export class ChatSession {
1726
2000
  if (text.startsWith('/')) {
1727
2001
  return { preamble: null, note: null };
1728
2002
  }
2003
+
1729
2004
  const host = this.options.promptNotes?.next() ?? { shown: [], heard: [] };
1730
2005
  // Keep consumed notes in the submission until the CLI acknowledges them.
1731
2006
  const preambles = this.preambles;
@@ -1734,7 +2009,9 @@ export class ChatSession {
1734
2009
  const hints = this.pendingHints;
1735
2010
  this.pendingHints = [];
1736
2011
  const read = [...preambles, ...host.shown];
2012
+
1737
2013
  const joined = (parts: string[]): string | null => (parts.length === 0 ? null : parts.join('\n\n'));
2014
+
1738
2015
  return { preamble: joined([...read, ...host.heard, ...hints]), note: joined(read) };
1739
2016
  }
1740
2017
 
@@ -1743,8 +2020,10 @@ export class ChatSession {
1743
2020
  this.submission = { turnId, input: { ...input, promptId: randomUUID() } };
1744
2021
  const submission = this.submission;
1745
2022
  this.options.persist();
2023
+
1746
2024
  this.run((backend) => {
1747
2025
  backend.sendTurn(submission.input);
2026
+
1748
2027
  if (!backend.acknowledgesTurns) {
1749
2028
  this.acceptPrompt(submission.input.promptId!);
1750
2029
  }
@@ -1753,43 +2032,54 @@ export class ChatSession {
1753
2032
 
1754
2033
  private acceptPrompt(promptId: string): void {
1755
2034
  const submission = this.submission;
2035
+
1756
2036
  if (submission?.input.promptId !== promptId) {
1757
2037
  return;
1758
2038
  }
2039
+
1759
2040
  this.submission = null;
1760
2041
  const turn = this.thread.get(submission.turnId);
2042
+
1761
2043
  if (turn?.kind === 'turn' && (turn.deliveryPending || turn.resumePending)) {
1762
2044
  this.emit([this.thread.upsert({ ...turn, deliveryPending: undefined, resumePending: undefined })]);
1763
2045
  }
2046
+
1764
2047
  this.options.persist();
1765
2048
  }
1766
2049
 
1767
2050
  /* Runs one turn against the backend; a backend that will not start ends the turn with the reason. */
1768
2051
  private run(work: (backend: ChatBackend) => void): void {
1769
2052
  const turnId = this.info.activeTurnId;
2053
+
1770
2054
  if (turnId === null) {
1771
2055
  return;
1772
2056
  }
2057
+
1773
2058
  const pending = this.watchStart(turnId);
1774
2059
  let backend: Promise<ChatBackend>;
2060
+
1775
2061
  try {
1776
2062
  backend = this.ensureBackend();
1777
2063
  } catch (error) {
1778
2064
  // What the host prepares for a launch (its folders, its instructions) can throw too; the turn already opened, so it fails.
1779
- backend = Promise.reject(error instanceof Error ? error : new Error(String(error)));
2065
+ backend = Promise.reject(asError(error));
1780
2066
  }
2067
+
1781
2068
  const generation = this.generation;
2069
+
1782
2070
  void Promise.all([backend, this.turnReady, this.options.save?.()])
1783
2071
  .then(([started]) => {
1784
2072
  if (this.pendingStart !== pending || this.backend !== started || this.info.activeTurnId !== turnId || this.frozen) {
1785
2073
  return;
1786
2074
  }
2075
+
1787
2076
  this.clearStartWarning();
1788
2077
  work(started);
1789
2078
  })
1790
2079
  .catch((error: unknown) => {
1791
2080
  if (this.pendingStart === pending || (this.backend !== null && this.generation === generation && this.info.activeTurnId === turnId)) {
1792
2081
  this.clearStartWarning();
2082
+
1793
2083
  if (error instanceof ChatError && error.code === 'chat-busy') {
1794
2084
  this.pendingPreambles = this.preambles;
1795
2085
  this.submission = null;
@@ -1797,8 +2087,10 @@ export class ChatSession {
1797
2087
  this.emit(this.projector.project(generation, { type: 'turn.done', state: 'error', costUsd: 0, error: error.message }));
1798
2088
  this.settleCheckpoint(turnId);
1799
2089
  this.options.persist();
2090
+
1800
2091
  return;
1801
2092
  }
2093
+
1802
2094
  this.receive(generation, { type: 'failed', message: errorText(error), processAlive: this.running });
1803
2095
  }
1804
2096
  });
@@ -1807,6 +2099,7 @@ export class ChatSession {
1807
2099
  private watchStart(turnId: string): { turnId: string; warning: unknown } {
1808
2100
  this.clearStartWarning();
1809
2101
  const clock = this.options.clock ?? systemClock;
2102
+
1810
2103
  const pending = {
1811
2104
  turnId,
1812
2105
  warning: clock.setTimeout(() => {
@@ -1821,11 +2114,14 @@ export class ChatSession {
1821
2114
  text: 'The agent is still starting. You can stop this turn and try again.'
1822
2115
  })
1823
2116
  ]);
2117
+
1824
2118
  this.options.persist();
1825
2119
  }
1826
2120
  }, 30_000)
1827
2121
  };
2122
+
1828
2123
  this.pendingStart = pending;
2124
+
1829
2125
  return pending;
1830
2126
  }
1831
2127
 
@@ -1833,6 +2129,7 @@ export class ChatSession {
1833
2129
  if (this.pendingStart === null) {
1834
2130
  return;
1835
2131
  }
2132
+
1836
2133
  (this.options.clock ?? systemClock).clearTimeout(this.pendingStart.warning);
1837
2134
  this.pendingStart = null;
1838
2135
  }
@@ -1871,27 +2168,34 @@ export class ChatSession {
1871
2168
  if (this.processInUse()) {
1872
2169
  return Promise.reject(new ChatError('chat-busy', 'The current work and open requests must finish before the changes to this chat can apply'));
1873
2170
  }
2171
+
1874
2172
  // Let the old one go quietly; its events are dropped and its exit must not end the chat.
1875
2173
  this.backend.stop();
1876
2174
  this.backend = null;
1877
2175
  this.starting = null;
1878
2176
  }
2177
+
1879
2178
  if (this.backend && this.starting) {
1880
2179
  return this.starting;
1881
2180
  }
2181
+
1882
2182
  const projectConfig = this.loadsProjectConfig();
2183
+
1883
2184
  if (!projectConfig && !this.options.provider.ignoresProjectConfig) {
1884
2185
  return Promise.reject(
1885
2186
  new ChatError('project-config-unsupported', `${this.options.provider.name} cannot run without the configuration of its folder`)
1886
2187
  );
1887
2188
  }
2189
+
1888
2190
  let env: Record<string, string>;
2191
+
1889
2192
  try {
1890
2193
  env = this.options.env(this.thread.info.account);
1891
2194
  this.launchedAccount = this.thread.info.account;
1892
2195
  } catch (error) {
1893
- return Promise.reject(error instanceof Error ? error : new Error(String(error)));
2196
+ return Promise.reject(asError(error));
1894
2197
  }
2198
+
1895
2199
  this.generation = nextGeneration();
1896
2200
  const generation = this.generation;
1897
2201
  const info = this.thread.info;
@@ -1901,6 +2205,7 @@ export class ChatSession {
1901
2205
  const folders = this.options.folders?.() ?? [];
1902
2206
  this.launchedFolders = folders;
1903
2207
  this.launchedProjectConfig = projectConfig;
2208
+
1904
2209
  const launch: BackendLaunch = {
1905
2210
  command: this.options.command,
1906
2211
  cwd: info.cwd,
@@ -1921,7 +2226,9 @@ export class ChatSession {
1921
2226
  ...(projectConfig ? {} : { projectConfig }),
1922
2227
  ...(this.options.spawn ? { spawn: this.options.spawn } : {})
1923
2228
  };
2229
+
1924
2230
  const made: { backend: ChatBackend | null } = { backend: null };
2231
+
1925
2232
  try {
1926
2233
  made.backend = this.options.provider.createBackend(launch, {
1927
2234
  onEvent: (event) => {
@@ -1931,19 +2238,22 @@ export class ChatSession {
1931
2238
  }
1932
2239
  });
1933
2240
  } catch (error) {
1934
- return Promise.reject(error instanceof Error ? error : new Error(String(error)));
2241
+ return Promise.reject(asError(error));
1935
2242
  }
2243
+
1936
2244
  const backend = made.backend;
1937
2245
  this.backend = backend;
1938
2246
  this.replaceProcess = false;
1939
2247
  this.emit([this.thread.patchInfo({ running: true, effectiveRuntimeMode: undefined, permissionMode: undefined })]);
1940
2248
  let started: Promise<void>;
2249
+
1941
2250
  try {
1942
2251
  started = backend.start();
1943
2252
  } catch (error) {
1944
2253
  // A spawn that throws on the spot (no such executable) is a start that failed like any other.
1945
- started = Promise.reject(error instanceof Error ? error : new Error(String(error)));
2254
+ started = Promise.reject(asError(error));
1946
2255
  }
2256
+
1947
2257
  this.starting = started
1948
2258
  .then(() => backend)
1949
2259
  .catch((error: unknown) => {
@@ -1952,13 +2262,16 @@ export class ChatSession {
1952
2262
  this.backend = null;
1953
2263
  this.starting = null;
1954
2264
  }
2265
+
1955
2266
  throw error;
1956
2267
  });
2268
+
1957
2269
  return this.starting;
1958
2270
  }
1959
2271
 
1960
2272
  private awaitsResume(turnId: string, attempt: number): boolean {
1961
2273
  const turn = this.thread.get(turnId);
2274
+
1962
2275
  return (
1963
2276
  !this.frozen &&
1964
2277
  turn?.kind === 'turn' &&
@@ -1972,33 +2285,45 @@ export class ChatSession {
1972
2285
  if (this.frozen) {
1973
2286
  return;
1974
2287
  }
2288
+
1975
2289
  if (event.type === 'turn.accepted') {
1976
2290
  this.acceptPrompt(event.promptId);
2291
+
1977
2292
  return;
1978
2293
  }
2294
+
1979
2295
  if (event.type === 'limits') {
1980
2296
  this.options.onLimits?.(this.launchedAccount === undefined ? event.update : { ...event.update, account: this.launchedAccount });
2297
+
1981
2298
  return;
1982
2299
  }
2300
+
1983
2301
  if (event.type === 'title') {
1984
2302
  this.applyTitle(event.title);
2303
+
1985
2304
  return;
1986
2305
  }
2306
+
1987
2307
  if (event.type === 'stop.refused') {
1988
2308
  this.refuseStop(event.taskId, event.message);
2309
+
1989
2310
  return;
1990
2311
  }
2312
+
1991
2313
  /*
1992
2314
  * A CLI that counts its tokens but names no window, in a chat that has none yet: the catalog may have learned the
1993
2315
  * model's window since the chat began, as an ACP agent's catalog does from the first session that reports its models.
1994
2316
  */
1995
2317
  if (event.type === 'usage' && event.contextWindow === undefined && event.contextTokens !== undefined && this.thread.info.usage.contextWindow === null) {
1996
2318
  const contextWindow = this.options.provider.catalog.contextWindowFor(this.launchedSelection ?? this.thread.info.selection);
2319
+
1997
2320
  if (contextWindow !== null) {
1998
2321
  this.emit(this.projector.project(generation, { ...event, contextWindow }));
2322
+
1999
2323
  return;
2000
2324
  }
2001
2325
  }
2326
+
2002
2327
  /*
2003
2328
  * A CLI reports the window it was started on. A pick made since waits for the restart the next
2004
2329
  * send does, so its report may not put the old size back under a meter that already reads the new one.
@@ -2007,66 +2332,88 @@ export class ChatSession {
2007
2332
  if (event.contextTokens === undefined) {
2008
2333
  return;
2009
2334
  }
2335
+
2010
2336
  this.emit(this.projector.project(generation, { type: 'usage', contextTokens: event.contextTokens }));
2337
+
2011
2338
  return;
2012
2339
  }
2340
+
2013
2341
  if (event.type === 'exit') {
2014
2342
  this.backend = null;
2015
2343
  this.starting = null;
2016
2344
  this.awaitingBackgroundReport = false;
2017
2345
  }
2346
+
2018
2347
  // A turn we settled ourselves still has its own `result` coming; it may not close the turn after it.
2019
2348
  if (event.type === 'turn.done' && event.promptId === undefined && this.staleResults > 0) {
2020
2349
  this.staleResults -= 1;
2350
+
2021
2351
  return;
2022
2352
  }
2353
+
2023
2354
  if (event.type === 'exit' || event.type === 'failed') {
2024
2355
  this.staleResults = 0;
2025
2356
  }
2357
+
2026
2358
  if (event.type === 'turn.done') {
2027
2359
  if (event.promptId !== undefined && this.submission !== null && this.submission.input.promptId !== event.promptId) {
2028
2360
  return;
2029
2361
  }
2362
+
2030
2363
  if (event.promptId !== undefined) {
2031
2364
  this.staleResults = 0;
2032
2365
  }
2366
+
2033
2367
  if (event.replaceProcess === true) {
2034
2368
  this.replaceProcess = true;
2035
2369
  }
2370
+
2036
2371
  const promptId = event.promptId ?? this.submission?.input.promptId;
2372
+
2037
2373
  if (promptId !== undefined) {
2038
2374
  this.acceptPrompt(promptId);
2039
2375
  }
2040
2376
  }
2377
+
2041
2378
  const openTurnId = this.thread.info.activeTurnId;
2379
+
2042
2380
  if ((event.type === 'turn.done' && event.state !== 'done') || ((event.type === 'failed' || event.type === 'exit') && openTurnId !== null)) {
2043
2381
  this.pauseQueue(true);
2044
2382
  }
2383
+
2045
2384
  // Settled as failed by the exit below; the transcript may still show that they finished first.
2046
2385
  const orphaned = event.type === 'exit' && this.info.provider === 'claude' ? this.runningBackgroundRows() : [];
2386
+
2047
2387
  if (event.type === 'turn.done' && event.state === 'aborted') {
2048
2388
  // A request of work beside the turn outlives the interrupt; turned down, that work goes on instead of waiting forever.
2049
2389
  for (const item of this.thread.pending()) {
2050
2390
  this.backend?.declineRequest?.(item.requestId, 'The user stopped the turn');
2051
2391
  }
2052
2392
  }
2393
+
2053
2394
  const hadNativeWork = (event.type === 'task.done' || event.type === 'tool.done' || event.type === 'background.ended') && this.hasNativeWork();
2054
2395
  const projected = this.projector.project(generation, event);
2396
+
2055
2397
  if (event.type === 'turn.done' || (event.type === 'failed' && event.processAlive !== true)) {
2056
2398
  this.awaitingBackgroundReport = false;
2057
2399
  } else if (event.type !== 'exit' && event.type !== 'failed' && hadNativeWork && !this.hasNativeWork() && reportsOnBackgroundWork(this.info.provider)) {
2058
2400
  this.awaitingBackgroundReport = true;
2059
2401
  }
2402
+
2060
2403
  this.emit(projected);
2061
2404
  this.trackSubagentOwner(generation, event);
2062
2405
  this.watchWorkflows(generation, event);
2406
+
2063
2407
  if (isMainAgentOutput(event)) {
2064
2408
  this.spokenTurnId = this.thread.info.activeTurnId;
2065
2409
  }
2410
+
2066
2411
  if (orphaned.length > 0) {
2067
2412
  void this.settleFromTranscripts(orphaned.flatMap((row) => this.thread.get(row.id) ?? []).filter((item) => item.kind === 'subagent'));
2068
2413
  }
2414
+
2069
2415
  const activeTurnId = this.thread.info.activeTurnId;
2416
+
2070
2417
  if (openTurnId === null && activeTurnId !== null) {
2071
2418
  // The CLI opened this turn itself; it takes a checkpoint like any other, so the card can
2072
2419
  // show what the agent changed while nobody was watching.
@@ -2074,20 +2421,26 @@ export class ChatSession {
2074
2421
  void this.checkpoint(activeTurnId);
2075
2422
  this.options.persist();
2076
2423
  }
2424
+
2077
2425
  if (event.type === 'turn.done' || event.type === 'exit' || event.type === 'failed') {
2078
2426
  this.options.persist();
2079
2427
  }
2428
+
2080
2429
  if (event.type === 'permissions' || event.type === 'session') {
2081
2430
  this.options.persist();
2082
2431
  }
2432
+
2083
2433
  if (openTurnId !== null && activeTurnId === null) {
2084
2434
  this.settleCheckpoint(openTurnId);
2085
2435
  }
2436
+
2086
2437
  if (event.type === 'session' && typeof event.title === 'string') {
2087
2438
  this.applyTitle(event.title);
2088
2439
  }
2440
+
2089
2441
  if (event.type === 'session' && this.thread.info.agentSessionId !== null) {
2090
2442
  this.refreshTitle();
2443
+
2091
2444
  if (this.titleTimer === null && this.thread.info.suggestedTitle === undefined) {
2092
2445
  this.titleTimer = setTimeout(() => {
2093
2446
  this.titleTimer = null;
@@ -2095,15 +2448,19 @@ export class ChatSession {
2095
2448
  }, TITLE_RECHECK_MS);
2096
2449
  }
2097
2450
  }
2451
+
2098
2452
  // The turn is over and the CLI is still there, so whatever waited behind it can go out now.
2099
2453
  if (event.type === 'turn.done') {
2100
2454
  this.refreshTitle();
2455
+
2101
2456
  if (event.state === 'done') {
2102
2457
  this.nameThread();
2103
2458
  }
2459
+
2104
2460
  if (event.limit !== undefined) {
2105
2461
  this.oweResume(true);
2106
2462
  }
2463
+
2107
2464
  this.drainQueue(event.state === 'aborted' && this.sendAfterStop);
2108
2465
  this.sendAfterStop = false;
2109
2466
  } else if (
@@ -2123,9 +2480,11 @@ export class ChatSession {
2123
2480
 
2124
2481
  private clearWorkflowWarnings(): void {
2125
2482
  const clock = this.options.clock ?? systemClock;
2483
+
2126
2484
  for (const timer of this.workflowWarnings.values()) {
2127
2485
  clock.clearTimeout(timer);
2128
2486
  }
2487
+
2129
2488
  this.workflowWarnings.clear();
2130
2489
  }
2131
2490
 
@@ -2133,15 +2492,20 @@ export class ChatSession {
2133
2492
  if (event.type === 'tool.output' || event.type === 'tool.progress') {
2134
2493
  // These come by the thousand, and only the call they name can be a workflow they move.
2135
2494
  const item = this.projector.itemOf(generation, event.ref);
2495
+
2136
2496
  if (item?.kind === 'tool' && item.name === 'Workflow') {
2137
2497
  this.watchWorkflow(item, true);
2138
2498
  }
2499
+
2139
2500
  return;
2140
2501
  }
2502
+
2141
2503
  if (!['tool.started', 'tool.done', 'workflow.progress', 'task.started', 'task.done', 'exit', 'failed'].includes(event.type)) {
2142
2504
  return;
2143
2505
  }
2506
+
2144
2507
  const ref = 'ref' in event ? event.ref : null;
2508
+
2145
2509
  for (const item of this.thread.list()) {
2146
2510
  if (item.kind === 'tool' && item.name === 'Workflow') {
2147
2511
  this.watchWorkflow(item, ref === item.toolUseId && ['workflow.progress', 'tool.started', 'tool.done', 'task.started'].includes(event.type));
@@ -2152,15 +2516,18 @@ export class ChatSession {
2152
2516
  private watchWorkflow(item: ChatToolItem, progress: boolean): void {
2153
2517
  const clock = this.options.clock ?? systemClock;
2154
2518
  const existing = this.workflowWarnings.get(item.id);
2519
+
2155
2520
  if (item.state !== 'running' || progress) {
2156
2521
  if (existing !== undefined) {
2157
2522
  clock.clearTimeout(existing);
2158
2523
  this.workflowWarnings.delete(item.id);
2159
2524
  }
2160
2525
  }
2526
+
2161
2527
  if (item.state !== 'running' || (!progress && existing !== undefined) || (!progress && item.workflow?.stalledAt !== undefined)) {
2162
2528
  return;
2163
2529
  }
2530
+
2164
2531
  if (progress) {
2165
2532
  this.emit([
2166
2533
  this.thread.upsert({
@@ -2169,14 +2536,17 @@ export class ChatSession {
2169
2536
  })
2170
2537
  ]);
2171
2538
  }
2539
+
2172
2540
  this.workflowWarnings.set(
2173
2541
  item.id,
2174
2542
  clock.setTimeout(() => {
2175
2543
  this.workflowWarnings.delete(item.id);
2176
2544
  const current = this.thread.get(item.id);
2545
+
2177
2546
  if (this.frozen || current?.kind !== 'tool' || current.state !== 'running') {
2178
2547
  return;
2179
2548
  }
2549
+
2180
2550
  this.emit([this.thread.upsert({ ...current, workflow: { name: null, phases: [], agents: [], ...current.workflow, stalledAt: clock.now() } })]);
2181
2551
  this.options.persist();
2182
2552
  }, 5 * 60_000)
@@ -2186,15 +2556,18 @@ export class ChatSession {
2186
2556
  /* Whether the CLI that reports was started on a pick standing for the same window as the chat's own. */
2187
2557
  private reportsCurrentWindow(): boolean {
2188
2558
  const catalog = this.options.provider.catalog;
2559
+
2189
2560
  return this.launchedSelection === null || catalog.contextWindowFor(this.launchedSelection) === catalog.contextWindowFor(this.thread.info.selection);
2190
2561
  }
2191
2562
 
2192
2563
  /* Rides on the info patch: a client that has the chat open hears it, and a reload finds it stored. */
2193
2564
  private refreshTitle(): void {
2194
2565
  const agentSessionId = this.thread.info.agentSessionId;
2566
+
2195
2567
  if (!this.options.readTitle || agentSessionId === null) {
2196
2568
  return;
2197
2569
  }
2570
+
2198
2571
  void this.options
2199
2572
  .readTitle(agentSessionId)
2200
2573
  .then((title) => {
@@ -2202,6 +2575,7 @@ export class ChatSession {
2202
2575
  if (title === null || title === this.thread.info.suggestedTitle || this.thread.info.agentSessionId !== agentSessionId) {
2203
2576
  return;
2204
2577
  }
2578
+
2205
2579
  this.emit([this.thread.patchInfo({ suggestedTitle: title })]);
2206
2580
  this.options.persistSoon();
2207
2581
  })
@@ -2211,9 +2585,11 @@ export class ChatSession {
2211
2585
  private applyTitle(raw: string): void {
2212
2586
  // Every info a client reads carries the title, so one it would refuse would leave the chat stuck there.
2213
2587
  const title = cleanTitle(raw);
2588
+
2214
2589
  if (title === null || title === this.thread.info.suggestedTitle) {
2215
2590
  return;
2216
2591
  }
2592
+
2217
2593
  this.emit([this.thread.patchInfo({ suggestedTitle: title })]);
2218
2594
  this.options.persistSoon();
2219
2595
  }
@@ -2225,28 +2601,36 @@ export class ChatSession {
2225
2601
  */
2226
2602
  private nameThread(): void {
2227
2603
  const name = this.options.nameThread;
2604
+
2228
2605
  if (!name || this.naming || this.thread.info.suggestedTitle !== undefined) {
2229
2606
  return;
2230
2607
  }
2608
+
2231
2609
  const items = this.thread.list();
2232
2610
  const done = items.filter((item) => item.kind === 'turn' && item.state === 'done');
2233
2611
  const prompt = items.find((item) => item.kind === 'user');
2612
+
2234
2613
  if (done.length !== 1 || prompt?.kind !== 'user' || prompt.text.trim() === '') {
2235
2614
  return;
2236
2615
  }
2616
+
2237
2617
  this.naming = true;
2238
2618
  const turnId = done[0]!.id;
2619
+
2239
2620
  const answer = items
2240
2621
  .map((item) => (item.kind === 'assistant' && item.turnId === turnId ? readableAssistantText(item) : ''))
2241
2622
  .filter((text) => text !== '')
2242
2623
  .join('\n\n');
2624
+
2243
2625
  const agentSessionId = this.thread.info.agentSessionId;
2626
+
2244
2627
  void name({ cwd: this.thread.info.cwd, prompt: prompt.text, answer })
2245
2628
  .then((title) => {
2246
2629
  // A clear in between started another conversation, and a name that came meanwhile is the CLI's own.
2247
2630
  if (title === null || this.thread.info.agentSessionId !== agentSessionId || this.thread.info.suggestedTitle !== undefined) {
2248
2631
  return;
2249
2632
  }
2633
+
2250
2634
  this.applyTitle(title);
2251
2635
  this.backend?.setTitle?.(title);
2252
2636
  })
@@ -2260,17 +2644,23 @@ export class ChatSession {
2260
2644
  /* Done for every row whose transcript shows the end, unless something else settled the row while it was read. */
2261
2645
  private async settleFromTranscripts(rows: ChatSubagentItem[]): Promise<void> {
2262
2646
  const lookup = this.options.subagentSettlement;
2647
+
2263
2648
  if (!lookup || rows.length === 0) {
2264
2649
  return;
2265
2650
  }
2651
+
2266
2652
  const settled: ChatSubagentItem[] = [];
2653
+
2267
2654
  for (const row of rows) {
2268
2655
  const settlement = await lookup(row.toolUseId);
2269
2656
  const current = this.thread.get(row.id);
2657
+
2270
2658
  if (settlement === null || current?.kind !== 'subagent' || current.status !== row.status || current.finishedAt !== row.finishedAt) {
2271
2659
  continue;
2272
2660
  }
2661
+
2273
2662
  this.orphans.delete(row.toolUseId);
2663
+
2274
2664
  settled.push({
2275
2665
  ...current,
2276
2666
  status: 'done',
@@ -2278,6 +2668,7 @@ export class ChatSession {
2278
2668
  result: settlement.report ?? current.result
2279
2669
  });
2280
2670
  }
2671
+
2281
2672
  this.settleRows(settled);
2282
2673
  }
2283
2674
 
@@ -2285,12 +2676,14 @@ export class ChatSession {
2285
2676
  if (this.frozen || rows.length === 0) {
2286
2677
  return;
2287
2678
  }
2679
+
2288
2680
  this.emit(rows.map((row) => this.thread.upsert(row)));
2289
2681
  this.options.persist();
2290
2682
  }
2291
2683
 
2292
2684
  private emit(events: ChatEvent[]): void {
2293
2685
  const delegating = this.delegatingChange(events);
2686
+
2294
2687
  for (const event of delegating === null ? events : [...events, delegating]) {
2295
2688
  this.uiReplies.pass(event);
2296
2689
  }
@@ -2301,10 +2694,13 @@ export class ChatSession {
2301
2694
  if (!events.some((event) => event.type === 'item' && isBackgroundWork(event.item))) {
2302
2695
  return null;
2303
2696
  }
2697
+
2304
2698
  const delegating = runningInBackground(this.thread.list()).length > 0;
2699
+
2305
2700
  if (delegating === (this.thread.info.delegating ?? false)) {
2306
2701
  return null;
2307
2702
  }
2703
+
2308
2704
  return this.thread.patchInfo({ delegating: delegating || undefined });
2309
2705
  }
2310
2706
  }
@@ -2320,18 +2716,23 @@ function settleStoredItem(item: ChatItem, resumeTurnId: string | null): ChatItem
2320
2716
  if (item.kind === 'assistant' && item.streaming) {
2321
2717
  return { ...item, streaming: false };
2322
2718
  }
2719
+
2323
2720
  if (item.kind === 'approval' && item.decision === 'pending') {
2324
2721
  return { ...item, decision: 'cancelled' };
2325
2722
  }
2723
+
2326
2724
  if (item.kind === 'question' && item.state === 'pending') {
2327
2725
  return { ...item, state: 'cancelled' };
2328
2726
  }
2727
+
2329
2728
  if (item.kind === 'tool' && item.state === 'running') {
2330
2729
  return { ...item, state: 'error' };
2331
2730
  }
2731
+
2332
2732
  // Aborted rather than error: the turn did not fail, the machine went down under it.
2333
2733
  if (item.kind === 'turn' && item.state === 'running' && item.id !== resumeTurnId) {
2334
2734
  return { ...item, state: 'aborted', endedAt: item.endedAt ?? item.createdAt };
2335
2735
  }
2736
+
2336
2737
  return item;
2337
2738
  }