@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
@@ -1,5 +1,6 @@
1
1
  import { isRecord, type AgentKind, type ChatFileChange, type ChatQuestion, type ChatTurnLimit, type RuntimeMode } from '@adecore/agent-contracts';
2
2
  import { z } from 'zod';
3
+ import { filledStr } from '../fields.ts';
3
4
  import { MODE_ORDER } from '../modes.ts';
4
5
  import { cleanTitle } from '../title-file.ts';
5
6
  import type { ApprovalDecision, BackendEvent } from './backend.ts';
@@ -113,17 +114,21 @@ export interface AcpTurnUsage {
113
114
  */
114
115
  export function promptUsageOf(result: unknown): AcpTurnUsage | null {
115
116
  const response = z.looseObject({ usage: PromptUsageSchema }).safeParse(result);
117
+
116
118
  if (!response.success) {
117
119
  return null;
118
120
  }
121
+
119
122
  const usage = response.data.usage;
120
123
  const cacheRead = usage.cachedReadTokens ?? 0;
121
124
  const cacheWrite = usage.cachedWriteTokens ?? 0;
122
125
  const apart = usage.inputTokens + usage.outputTokens + cacheRead + cacheWrite <= usage.totalTokens;
123
126
  const input = apart ? usage.inputTokens : Math.max(0, usage.inputTokens - cacheRead - cacheWrite);
127
+
124
128
  if (input + cacheRead + cacheWrite + usage.outputTokens === 0) {
125
129
  return null;
126
130
  }
131
+
127
132
  return { input, cacheRead, cacheWrite, output: usage.outputTokens, reasoning: usage.thoughtTokens ?? 0 };
128
133
  }
129
134
 
@@ -156,6 +161,7 @@ const ModelEntrySchema = z.looseObject({ modelId: z.string(), name: z.string(),
156
161
  /* The context window an agent names for a model in the model's `_meta` (`totalContextTokens`, not part of the protocol); null where it names none. */
157
162
  function contextWindowOf(meta: unknown): number | null {
158
163
  const tokens = isRecord(meta) ? meta.totalContextTokens : undefined;
164
+
159
165
  return typeof tokens === 'number' && Number.isSafeInteger(tokens) && tokens > 0 ? tokens : null;
160
166
  }
161
167
 
@@ -165,6 +171,7 @@ function contextWindowOf(meta: unknown): number | null {
165
171
  */
166
172
  function vendorContextTokens(meta: unknown): number | null {
167
173
  const tokens = isRecord(meta) ? meta.totalTokens : undefined;
174
+
168
175
  return typeof tokens === 'number' && Number.isSafeInteger(tokens) && tokens > 0 ? tokens : null;
169
176
  }
170
177
 
@@ -195,6 +202,7 @@ const SessionResponseSchema = z.looseObject({ sessionId: z.string().optional() }
195
202
  function validItems<T>(schema: z.ZodType<T>, items: readonly unknown[] | null | undefined): T[] {
196
203
  return (items ?? []).flatMap((item) => {
197
204
  const parsed = schema.safeParse(item);
205
+
198
206
  return parsed.success ? [parsed.data] : [];
199
207
  });
200
208
  }
@@ -222,12 +230,15 @@ export interface AcpAgentCapabilities {
222
230
 
223
231
  export function parseInitialize(result: unknown): AcpAgentCapabilities {
224
232
  const parsed = InitializeResponseSchema.safeParse(result);
233
+
225
234
  if (!parsed.success) {
226
235
  throw new Error('initialize failed: the agent answered with something that is not an initialize response');
227
236
  }
237
+
228
238
  const capabilities = parsed.data.agentCapabilities;
229
239
  const sessions = capabilities?.sessionCapabilities;
230
240
  const agent = parsed.data.agentInfo;
241
+
231
242
  return {
232
243
  protocolVersion: parsed.data.protocolVersion,
233
244
  loadSession: capabilities?.loadSession === true,
@@ -296,6 +307,7 @@ type ConfigOption = z.infer<typeof ConfigOptionSchema>;
296
307
  /* The values of a select option, grouped or not. */
297
308
  function selectValues(option: ConfigOption): Array<z.infer<typeof SelectValueSchema>> {
298
309
  const flat = (option.options ?? []).flatMap((entry) => (isRecord(entry) && Array.isArray(entry.options) ? entry.options : [entry]));
310
+
299
311
  return validItems(SelectValueSchema, flat);
300
312
  }
301
313
 
@@ -316,12 +328,14 @@ export function thoughtLevelOf(result: unknown): AcpThoughtLevel | null | undefi
316
328
  if (!isRecord(result) || !Array.isArray(result.configOptions)) {
317
329
  return undefined;
318
330
  }
331
+
319
332
  return sessionState(validItems(ConfigOptionSchema, result.configOptions), null, null).thoughtLevel;
320
333
  }
321
334
 
322
335
  export function parseSessionInfo(result: unknown, sessionId: string, agent: AcpAgentInfo | null): AcpSessionInfo {
323
336
  const record = isRecord(result) ? result : {};
324
337
  const options = validItems(ConfigOptionSchema, Array.isArray(record.configOptions) ? record.configOptions : []);
338
+
325
339
  return { sessionId, agent, ...sessionState(options, record.modes, record.models) };
326
340
  }
327
341
 
@@ -334,6 +348,7 @@ function sessionState(options: ConfigOption[], modes: unknown, models: unknown):
334
348
  const entries = modelState.success ? validItems(ModelEntrySchema, modelState.data.availableModels) : [];
335
349
  // Grok CLI 1.0.62 offers its models as a config option too, but names their windows only in the older field.
336
350
  const windows = new Map(entries.map((model) => [model.modelId, contextWindowOf(model._meta)]));
351
+
337
352
  return {
338
353
  models: modelOption
339
354
  ? {
@@ -388,10 +403,13 @@ function commandNames(commands: unknown): string[] {
388
403
  /* The session a `session/update` notification is for, and the commands it lists when it is an `available_commands_update`; null when it does not parse. */
389
404
  export function sessionUpdateOf(params: unknown): { sessionId: string; commands: string[] | null } | null {
390
405
  const notification = SessionNotificationSchema.safeParse(params);
406
+
391
407
  if (!notification.success) {
392
408
  return null;
393
409
  }
410
+
394
411
  const update = notification.data.update;
412
+
395
413
  return {
396
414
  sessionId: notification.data.sessionId,
397
415
  commands: update.sessionUpdate === 'available_commands_update' ? commandNames(update.availableCommands) : null
@@ -400,6 +418,7 @@ export function sessionUpdateOf(params: unknown): { sessionId: string; commands:
400
418
 
401
419
  export function sessionIdOf(result: unknown): string | null {
402
420
  const parsed = SessionResponseSchema.safeParse(result);
421
+
403
422
  return parsed.success ? (parsed.data.sessionId ?? null) : null;
404
423
  }
405
424
 
@@ -414,10 +433,12 @@ export function resolveAcpMode(requested: RuntimeMode, map: AcpModeMap, offered:
414
433
  for (let index = MODE_ORDER.indexOf(requested); index >= 0; index--) {
415
434
  const runtimeMode = MODE_ORDER[index]!;
416
435
  const modeId = (map[runtimeMode] ?? []).find((candidate) => offered.includes(candidate));
436
+
417
437
  if (modeId !== undefined) {
418
438
  return { modeId, runtimeMode };
419
439
  }
420
440
  }
441
+
421
442
  return null;
422
443
  }
423
444
 
@@ -425,10 +446,12 @@ export function resolveAcpMode(requested: RuntimeMode, map: AcpModeMap, offered:
425
446
  export function runtimeModeOf(modeId: string, map: AcpModeMap): RuntimeMode | null {
426
447
  for (let index = MODE_ORDER.length - 1; index >= 0; index--) {
427
448
  const runtimeMode = MODE_ORDER[index]!;
449
+
428
450
  if ((map[runtimeMode] ?? []).includes(modeId)) {
429
451
  return runtimeMode;
430
452
  }
431
453
  }
454
+
432
455
  return null;
433
456
  }
434
457
 
@@ -442,6 +465,7 @@ const AUTO_ALLOWED: Record<RuntimeMode, ReadonlySet<string> | 'all'> = {
442
465
 
443
466
  export function autoAllows(mode: RuntimeMode, kind: string | null): boolean {
444
467
  const allowed = AUTO_ALLOWED[mode];
468
+
445
469
  return allowed === 'all' || (kind !== null && allowed.has(kind));
446
470
  }
447
471
 
@@ -449,27 +473,37 @@ export function autoAllows(mode: RuntimeMode, kind: string | null): boolean {
449
473
  export function unifiedDiff(oldText: string, newText: string): string | null {
450
474
  const lines = (text: string): string[] => {
451
475
  const split = text === '' ? [] : text.split('\n');
476
+
452
477
  if (split.at(-1) === '') {
453
478
  split.pop();
454
479
  }
480
+
455
481
  return split;
456
482
  };
483
+
457
484
  const before = lines(oldText);
458
485
  const after = lines(newText);
459
486
  let prefix = 0;
487
+
460
488
  while (prefix < before.length && prefix < after.length && before[prefix] === after[prefix]) {
461
489
  prefix++;
462
490
  }
491
+
463
492
  let suffix = 0;
493
+
464
494
  while (suffix < before.length - prefix && suffix < after.length - prefix && before[before.length - 1 - suffix] === after[after.length - 1 - suffix]) {
465
495
  suffix++;
466
496
  }
497
+
467
498
  const removed = before.slice(prefix, before.length - suffix);
468
499
  const added = after.slice(prefix, after.length - suffix);
500
+
469
501
  if (removed.length === 0 && added.length === 0) {
470
502
  return null;
471
503
  }
504
+
472
505
  const range = (count: number): string => `${count === 0 ? prefix : prefix + 1},${count}`;
506
+
473
507
  return (
474
508
  [`@@ -${range(removed.length)} +${range(added.length)} @@`, ...removed.map((line) => `-${line}`), ...added.map((line) => `+${line}`)].join('\n') + '\n'
475
509
  );
@@ -525,27 +559,27 @@ function foldSpace(text: string): string {
525
559
  return text.replace(/\s+/g, ' ').trim();
526
560
  }
527
561
 
528
- function str(value: unknown): string | null {
529
- return typeof value === 'string' && value !== '' ? value : null;
530
- }
531
-
532
- /* A field of the agent's own input, which no schema promises; the first one that is a non-empty string. */
533
562
  // Where agents put the command a call runs; Antigravity names it `CommandLine`.
534
563
  const COMMAND_KEYS = ['command', 'cmd', 'CommandLine'];
535
564
 
565
+ /* A field of the agent's own input, which no schema promises; the first one that is a non-empty string. */
536
566
  function rawField(raw: unknown, ...keys: string[]): string | null {
537
567
  if (!isRecord(raw)) {
538
568
  return null;
539
569
  }
570
+
540
571
  for (const key of keys) {
541
572
  const value = raw[key];
573
+
542
574
  if (Array.isArray(value) && value.every((part) => typeof part === 'string') && value.length > 0) {
543
575
  return value.join(' ');
544
576
  }
545
- if (str(value) !== null) {
577
+
578
+ if (filledStr(value) !== null) {
546
579
  return value as string;
547
580
  }
548
581
  }
582
+
549
583
  return null;
550
584
  }
551
585
 
@@ -557,28 +591,38 @@ export function toolRow(tool: Pick<ToolState, 'title' | 'name' | 'kind' | 'rawIn
557
591
  } {
558
592
  const description = tool.title;
559
593
  const path = tool.locations[0] ?? rawField(tool.rawInput, 'file_path', 'path', 'filePath') ?? null;
594
+
560
595
  if (tool.diffs.length > 0) {
561
596
  const paths = new Set(tool.diffs.map((diff) => diff.path));
597
+
562
598
  if (paths.size === 1) {
563
599
  const [first] = tool.diffs;
600
+
564
601
  if (tool.diffs.length === 1 && (first!.oldText ?? null) === null) {
565
602
  return { name: 'Write', input: { file_path: first!.path, content: first!.newText, description } };
566
603
  }
604
+
567
605
  if (tool.diffs.length === 1) {
568
606
  return { name: 'Edit', input: { file_path: first!.path, old_string: first!.oldText, new_string: first!.newText, description } };
569
607
  }
608
+
570
609
  return {
571
610
  name: 'MultiEdit',
572
611
  input: { file_path: first!.path, edits: tool.diffs.map((diff) => ({ old_string: diff.oldText ?? '', new_string: diff.newText })), description }
573
612
  };
574
613
  }
614
+
575
615
  const changes = tool.diffs.flatMap((diff): ChatFileChange[] => {
576
616
  const text = unifiedDiff(diff.oldText ?? '', diff.newText);
617
+
577
618
  return text === null ? [] : [{ path: diff.path, kind: (diff.oldText ?? null) === null ? 'add' : 'update', diff: text }];
578
619
  });
620
+
579
621
  return { name: 'ApplyPatch', input: { summary: [...paths].join(', '), description }, changes };
580
622
  }
623
+
581
624
  const raw = isRecord(tool.rawInput) ? tool.rawInput : {};
625
+
582
626
  switch (tool.kind) {
583
627
  case 'execute':
584
628
  return { name: 'Bash', input: { ...raw, description, command: rawField(tool.rawInput, ...COMMAND_KEYS) ?? description } };
@@ -588,6 +632,7 @@ export function toolRow(tool: Pick<ToolState, 'title' | 'name' | 'kind' | 'rawIn
588
632
  return { name: 'Grep', input: { ...raw, description, pattern: rawField(tool.rawInput, 'pattern', 'query', 'regex') ?? description } };
589
633
  case 'fetch': {
590
634
  const url = rawField(tool.rawInput, 'url', 'uri');
635
+
591
636
  return url === null
592
637
  ? { name: 'WebSearch', input: { ...raw, description, query: rawField(tool.rawInput, 'query') ?? description } }
593
638
  : { name: 'WebFetch', input: { ...raw, description, url } };
@@ -604,13 +649,17 @@ function toolOutput(tool: ToolState): string | null {
604
649
  if (tool.text.length > 0) {
605
650
  return tool.text.join('\n');
606
651
  }
652
+
607
653
  if (typeof tool.rawOutput === 'string') {
608
654
  return tool.rawOutput;
609
655
  }
656
+
610
657
  if (isRecord(tool.rawOutput)) {
611
658
  const text = rawField(tool.rawOutput, 'output', 'stdout', 'content', 'result', 'error', 'message');
659
+
612
660
  return text ?? JSON.stringify(tool.rawOutput);
613
661
  }
662
+
614
663
  return null;
615
664
  }
616
665
 
@@ -673,14 +722,18 @@ const OVERLOADED_TYPES = /\b(overloaded_error|service_unavailable_error)\b/;
673
722
  */
674
723
  export function acpErrorLimit(code: number | null, data: unknown, kind: AgentKind | null): ChatTurnLimit | null {
675
724
  const text = typeof data === 'string' ? data : isRecord(data) && typeof data.message === 'string' ? data.message : '';
725
+
676
726
  if (code === GROK_RATE_LIMITED && kind === 'grok') {
677
727
  return text.includes(GROK_FREE_USAGE) ? { kind: 'usage' } : { kind: 'overload' };
678
728
  }
729
+
679
730
  if (code !== -32603) {
680
731
  return null;
681
732
  }
733
+
682
734
  const status = isRecord(data) && typeof data.http_status === 'number' ? data.http_status : null;
683
735
  const overloaded = status === 529 || OVERLOADED_TYPES.test(text) || (status !== null && status >= 500 && /overloaded/i.test(text));
736
+
684
737
  return overloaded ? { kind: 'overload' } : null;
685
738
  }
686
739
 
@@ -699,25 +752,33 @@ const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', '
699
752
  */
700
753
  export function quotaResetOf(text: string, now: number): number | null {
701
754
  const relative = /will reset in ([^.]+)\./.exec(text);
755
+
702
756
  if (relative) {
703
757
  let total = 0;
704
758
  let smallest = Infinity;
759
+
705
760
  for (const part of relative[1]!.split(',')) {
706
761
  const amount = /^(\d+) (second|minute|hour|day)s?$/.exec(part.trim());
762
+
707
763
  if (!amount) {
708
764
  return null;
709
765
  }
766
+
710
767
  const unit = UNIT_MS[amount[2]!]!;
711
768
  total += Number(amount[1]) * unit;
712
769
  smallest = Math.min(smallest, unit);
713
770
  }
771
+
714
772
  return now + total + smallest;
715
773
  }
774
+
716
775
  const absolute = /will reset on ([A-Z][a-z]{2}) (\d{1,2}), (\d{4}) (\d{2}):(\d{2}) UTC\./.exec(text);
717
776
  const month = absolute ? MONTHS.indexOf(absolute[1]!) : -1;
777
+
718
778
  if (!absolute || month === -1) {
719
779
  return null;
720
780
  }
781
+
721
782
  return Date.UTC(Number(absolute[3]), month, Number(absolute[2]), Number(absolute[4]), Number(absolute[5])) + UNIT_MS.minute!;
722
783
  }
723
784
 
@@ -726,19 +787,24 @@ function pickedLabels(answer: string, labels: readonly string[], multiSelect: bo
726
787
  if (labels.includes(answer)) {
727
788
  return [answer];
728
789
  }
790
+
729
791
  if (!multiSelect) {
730
792
  return null;
731
793
  }
794
+
732
795
  // A label may hold `, ` itself, so the pieces join up again until they make one.
733
796
  const picked: string[] = [];
734
797
  let part = '';
798
+
735
799
  for (const piece of answer.split(', ')) {
736
800
  part = part === '' ? piece : `${part}, ${piece}`;
801
+
737
802
  if (labels.includes(part)) {
738
803
  picked.push(part);
739
804
  part = '';
740
805
  }
741
806
  }
807
+
742
808
  return part === '' && picked.length > 0 ? picked : null;
743
809
  }
744
810
 
@@ -792,9 +858,11 @@ export class AcpProtocol {
792
858
  /* One `session/update` notification; empty for another session, an unknown update or one that does not parse. */
793
859
  update(sessionId: string, params: unknown): BackendEvent[] {
794
860
  const notification = SessionNotificationSchema.safeParse(params);
861
+
795
862
  if (!notification.success || notification.data.sessionId !== sessionId) {
796
863
  return [];
797
864
  }
865
+
798
866
  return [...this.mapUpdate(notification.data.update), ...this.vendorUsage(notification.data._meta)];
799
867
  }
800
868
 
@@ -807,6 +875,7 @@ export class AcpProtocol {
807
875
  case 'tool_call':
808
876
  case 'tool_call_update': {
809
877
  const call = ToolCallSchema.safeParse(update);
878
+
810
879
  return call.success ? [...this.closeText(), ...this.closeThought(), ...this.toolCall(call.data)] : [];
811
880
  }
812
881
  case 'plan':
@@ -818,6 +887,7 @@ export class AcpProtocol {
818
887
  case 'config_option_update': {
819
888
  const options = validItems(ConfigOptionSchema, Array.isArray(update.configOptions) ? update.configOptions : []);
820
889
  const state = sessionState(options, null, null);
890
+
821
891
  return [
822
892
  ...(state.models === null ? [] : [{ type: 'model', model: state.models.current } as const]),
823
893
  ...(state.modes === null ? [] : [this.modeEvent(state.modes.current)])
@@ -825,21 +895,27 @@ export class AcpProtocol {
825
895
  }
826
896
  case 'usage_update': {
827
897
  const usage = UsageSchema.safeParse(update);
898
+
828
899
  if (!usage.success) {
829
900
  return [];
830
901
  }
902
+
831
903
  this.standardUsage = true;
832
904
  const cost = usage.data.cost?.currency === 'USD' ? usage.data.cost.amount : undefined;
905
+
833
906
  if (cost !== undefined) {
834
907
  this.costUsd = cost;
835
908
  }
909
+
836
910
  return [{ type: 'usage', contextTokens: usage.data.used, contextWindow: usage.data.size, ...(cost === undefined ? {} : { costUsd: cost }) }];
837
911
  }
838
912
  case 'session_info_update': {
839
913
  if (!this.titles) {
840
914
  return [];
841
915
  }
916
+
842
917
  const title = cleanTitle(update.title);
918
+
843
919
  return title === null || this.restatesPrompt(title) ? [] : [{ type: 'title', title }];
844
920
  }
845
921
  default:
@@ -850,10 +926,13 @@ export class AcpProtocol {
850
926
  /* The fallback for an agent that sends no `usage_update`: its own count of the context, once each time it changes. */
851
927
  private vendorUsage(meta: unknown): BackendEvent[] {
852
928
  const tokens = this.standardUsage ? null : vendorContextTokens(meta);
929
+
853
930
  if (tokens === null || tokens === this.vendorTokens) {
854
931
  return [];
855
932
  }
933
+
856
934
  this.vendorTokens = tokens;
935
+
857
936
  return [{ type: 'usage', contextTokens: tokens }];
858
937
  }
859
938
 
@@ -870,26 +949,31 @@ export class AcpProtocol {
870
949
  */
871
950
  private restatesPrompt(title: string): boolean {
872
951
  const start = foldSpace(title.endsWith('…') ? title.slice(0, -1) : title);
952
+
873
953
  return start !== '' && this.prompts.some((prompt) => prompt.startsWith(start));
874
954
  }
875
955
 
876
956
  modeEvent(modeId: string): BackendEvent {
877
957
  const runtimeMode = runtimeModeOf(modeId, this.modes);
958
+
878
959
  return { type: 'permissions', permissionMode: modeId, ...(runtimeMode === null ? {} : { effectiveRuntimeMode: runtimeMode }) };
879
960
  }
880
961
 
881
962
  /* A `session/request_permission` request; null when it does not parse, which the caller answers with an error. */
882
963
  permission(rpcId: number | string, sessionId: string, params: unknown): AcpPermission | null {
883
964
  const request = PermissionRequestSchema.safeParse(params);
965
+
884
966
  if (!request.success || request.data.sessionId !== sessionId) {
885
967
  return null;
886
968
  }
969
+
887
970
  const requestId = `acp-${this.generation}-${String(rpcId)}`;
888
971
  const events = [...this.closeText(), ...this.closeThought(), ...this.toolCall(request.data.toolCall, false)];
889
972
  const tool = this.tools.get(request.data.toolCall.toolCallId)!;
890
973
  const row = toolRow(tool);
891
974
  const options = request.data.options;
892
975
  this.permissions.set(requestId, { rpcId, toolCallId: request.data.toolCall.toolCallId, options });
976
+
893
977
  events.push({
894
978
  type: 'approval.requested',
895
979
  requestId,
@@ -899,8 +983,10 @@ export class AcpProtocol {
899
983
  description: tool.title === '' ? null : tool.title,
900
984
  canAllowAlways: options.some((option) => option.kind === 'allow_always')
901
985
  });
986
+
902
987
  const command = rawField(tool.rawInput, ...COMMAND_KEYS);
903
988
  const asked = { kind: tool.kind, title: tool.title === '' ? null : tool.title, commands: [...(command === null ? [] : [command]), ...tool.text] };
989
+
904
990
  return { requestId, rpcId, kind: tool.kind, request: asked, events };
905
991
  }
906
992
 
@@ -911,41 +997,53 @@ export class AcpProtocol {
911
997
  */
912
998
  answer(requestId: string, decision: ApprovalDecision): { rpcId: number | string; result: unknown; events: BackendEvent[] } | null {
913
999
  const pending = this.permissions.get(requestId);
1000
+
914
1001
  if (!pending) {
915
1002
  return null;
916
1003
  }
1004
+
917
1005
  this.permissions.delete(requestId);
1006
+
918
1007
  const byKind = (...kinds: string[]): string | null => {
919
1008
  for (const kind of kinds) {
920
1009
  const option = pending.options.find((candidate) => candidate.kind === kind);
1010
+
921
1011
  if (option) {
922
1012
  return option.optionId;
923
1013
  }
924
1014
  }
1015
+
925
1016
  return null;
926
1017
  };
1018
+
927
1019
  const optionId =
928
1020
  decision === 'allow-always'
929
1021
  ? byKind('allow_always', 'allow_once')
930
1022
  : decision === 'allow'
931
1023
  ? byKind('allow_once', 'allow_always')
932
1024
  : byKind('reject_once', 'reject_always');
1025
+
933
1026
  const result = { outcome: optionId === null ? { outcome: 'cancelled' } : { outcome: 'selected', optionId } };
934
1027
  const tool = this.tools.get(pending.toolCallId);
1028
+
935
1029
  if (decision !== 'deny' || tool === undefined) {
936
1030
  return { rpcId: pending.rpcId, result, events: [] };
937
1031
  }
1032
+
938
1033
  tool.refused = true;
939
1034
  this.refusalLine = true;
1035
+
940
1036
  return { rpcId: pending.rpcId, result, events: this.toolCall({ toolCallId: pending.toolCallId }, false) };
941
1037
  }
942
1038
 
943
1039
  /* A Grok `ask_user_question` request; null when it does not parse or asks nothing, which the caller answers with an error. */
944
1040
  question(rpcId: number | string, sessionId: string, params: unknown): AcpQuestion | null {
945
1041
  const request = GrokQuestionRequestSchema.safeParse(params);
1042
+
946
1043
  if (!request.success || request.data.sessionId !== sessionId) {
947
1044
  return null;
948
1045
  }
1046
+
949
1047
  const questions = validItems(GrokQuestionSchema, request.data.questions).map((question, index): ChatQuestion => ({
950
1048
  id: String(index),
951
1049
  header: question.header ?? '',
@@ -953,9 +1051,11 @@ export class AcpProtocol {
953
1051
  choices: validItems(GrokChoiceSchema, question.options).map((choice) => ({ label: choice.label, description: choice.description ?? '' })),
954
1052
  multiSelect: question.multiSelect === true
955
1053
  }));
1054
+
956
1055
  if (questions.length === 0) {
957
1056
  return null;
958
1057
  }
1058
+
959
1059
  return this.ask(rpcId, { shape: 'grok', rpcId, questions });
960
1060
  }
961
1061
 
@@ -965,15 +1065,19 @@ export class AcpProtocol {
965
1065
  */
966
1066
  interaction(rpcId: number | string, sessionId: string, params: unknown): AcpQuestion | null {
967
1067
  const request = PermissionRequestSchema.safeParse(params);
1068
+
968
1069
  if (!request.success || request.data.sessionId !== sessionId) {
969
1070
  return null;
970
1071
  }
1072
+
971
1073
  const call = request.data.toolCall;
972
1074
  const raw = call.rawInput;
973
1075
  const bare = raw === undefined || raw === null || (isRecord(raw) && Object.keys(raw).length === 0);
1076
+
974
1077
  if (!call.toolCallId.startsWith(INTERACTION_PREFIX) || !bare || call.kind || request.data.options.length === 0) {
975
1078
  return null;
976
1079
  }
1080
+
977
1081
  const question: ChatQuestion = {
978
1082
  id: '0',
979
1083
  header: '',
@@ -981,6 +1085,7 @@ export class AcpProtocol {
981
1085
  choices: request.data.options.map((option) => ({ label: option.name, description: '' })),
982
1086
  multiSelect: false
983
1087
  };
1088
+
984
1089
  return this.ask(rpcId, { shape: 'interaction', rpcId, question, options: request.data.options });
985
1090
  }
986
1091
 
@@ -988,19 +1093,24 @@ export class AcpProtocol {
988
1093
  const requestId = `acp-${this.generation}-${String(rpcId)}`;
989
1094
  this.questions.set(requestId, pending);
990
1095
  const questions = pending.shape === 'grok' ? pending.questions : [pending.question];
1096
+
991
1097
  return { requestId, rpcId, events: [...this.closeText(), ...this.closeThought(), { type: 'question.requested', requestId, questions }] };
992
1098
  }
993
1099
 
994
1100
  /* The answer to a question in the agent's own shape, from the card's answers keyed by question id. */
995
1101
  answerQuestion(requestId: string, answers: Record<string, string>): { rpcId: number | string; result: unknown; events: BackendEvent[] } | null {
996
1102
  const pending = this.questions.get(requestId);
1103
+
997
1104
  if (!pending) {
998
1105
  return null;
999
1106
  }
1107
+
1000
1108
  this.questions.delete(requestId);
1109
+
1001
1110
  if (pending.shape === 'interaction') {
1002
1111
  const given = (answers[pending.question.id] ?? '').trim();
1003
1112
  const option = pending.options.find((candidate) => candidate.name === given) ?? pending.options.find((candidate) => candidate.optionId === given);
1113
+
1004
1114
  if (option === undefined) {
1005
1115
  return {
1006
1116
  rpcId: pending.rpcId,
@@ -1008,25 +1118,33 @@ export class AcpProtocol {
1008
1118
  events: [{ type: 'note', level: 'warning', text: this.words.ownAnswer }]
1009
1119
  };
1010
1120
  }
1121
+
1011
1122
  return { rpcId: pending.rpcId, result: { outcome: { outcome: 'selected', optionId: option.optionId } }, events: [] };
1012
1123
  }
1124
+
1013
1125
  const picked: Record<string, string[]> = {};
1014
1126
  const notes: Record<string, { notes: string }> = {};
1127
+
1015
1128
  for (const question of pending.questions) {
1016
1129
  const given = answers[question.id];
1130
+
1017
1131
  if (given === undefined) {
1018
1132
  continue;
1019
1133
  }
1134
+
1020
1135
  const labels = pickedLabels(
1021
1136
  given,
1022
1137
  question.choices.map((choice) => choice.label),
1023
1138
  question.multiSelect
1024
1139
  );
1140
+
1025
1141
  picked[question.question] = labels ?? [GROK_OWN_ANSWER];
1142
+
1026
1143
  if (labels === null) {
1027
1144
  notes[question.question] = { notes: given };
1028
1145
  }
1029
1146
  }
1147
+
1030
1148
  return {
1031
1149
  rpcId: pending.rpcId,
1032
1150
  result: { outcome: 'accepted', answers: picked, ...(Object.keys(notes).length === 0 ? {} : { annotations: notes }) },
@@ -1037,10 +1155,13 @@ export class AcpProtocol {
1037
1155
  /* Turns one question down, in the shape its agent takes a refusal in. */
1038
1156
  cancelQuestion(requestId: string): { rpcId: number | string; result: unknown } | null {
1039
1157
  const pending = this.questions.get(requestId);
1158
+
1040
1159
  if (!pending) {
1041
1160
  return null;
1042
1161
  }
1162
+
1043
1163
  this.questions.delete(requestId);
1164
+
1044
1165
  return { rpcId: pending.rpcId, result: cancelledAnswer(pending) };
1045
1166
  }
1046
1167
 
@@ -1054,8 +1175,10 @@ export class AcpProtocol {
1054
1175
  })),
1055
1176
  ...[...this.questions].map(([requestId, pending]) => ({ requestId, rpcId: pending.rpcId, result: cancelledAnswer(pending) }))
1056
1177
  ];
1178
+
1057
1179
  this.permissions.clear();
1058
1180
  this.questions.clear();
1181
+
1059
1182
  return cancelled;
1060
1183
  }
1061
1184
 
@@ -1081,29 +1204,38 @@ export class AcpProtocol {
1081
1204
  const base = { ...(promptId === undefined ? {} : { promptId }), costUsd: this.costUsd };
1082
1205
  const quota = this.quota;
1083
1206
  this.quota = null;
1207
+
1084
1208
  if (quota !== null && reason !== 'cancelled') {
1085
1209
  events.push({ type: 'turn.done', ...base, state: 'error', error: quota.text, limit: quotaLimit(quota) });
1210
+
1086
1211
  return events;
1087
1212
  }
1213
+
1088
1214
  if (quota !== null) {
1089
1215
  events.push({ type: 'text.done', ref: quota.ref, text: quota.text });
1090
1216
  }
1217
+
1091
1218
  switch (reason) {
1092
1219
  case 'cancelled':
1093
1220
  events.push({ type: 'turn.done', ...base, state: 'aborted' });
1221
+
1094
1222
  break;
1095
1223
  case 'refusal':
1096
1224
  events.push({ type: 'turn.done', ...base, state: 'error', error: this.words.refusal });
1225
+
1097
1226
  break;
1098
1227
  case 'max_tokens':
1099
1228
  events.push({ type: 'note', level: 'warning', text: this.words.maxTokens }, { type: 'turn.done', ...base, state: 'done' });
1229
+
1100
1230
  break;
1101
1231
  case 'max_turn_requests':
1102
1232
  events.push({ type: 'note', level: 'warning', text: this.words.maxTurnRequests }, { type: 'turn.done', ...base, state: 'done' });
1233
+
1103
1234
  break;
1104
1235
  default:
1105
1236
  events.push({ type: 'turn.done', ...base, state: 'done' });
1106
1237
  }
1238
+
1107
1239
  return events;
1108
1240
  }
1109
1241
 
@@ -1114,81 +1246,103 @@ export class AcpProtocol {
1114
1246
  const events = [...this.closeText(), ...this.closeThought()];
1115
1247
  const quota = this.quota;
1116
1248
  this.quota = null;
1249
+
1117
1250
  if (quota !== null) {
1118
1251
  events.push({ type: 'text.done', ref: quota.ref, text: quota.text });
1119
1252
  }
1253
+
1120
1254
  // A failed request after the limit message still ran into that limit, so it pauses as one when the error names none.
1121
1255
  const named = limit ?? (quota === null ? null : quotaLimit(quota));
1256
+
1122
1257
  events.push(
1123
1258
  error === null
1124
1259
  ? { type: 'turn.done', ...base, state: 'aborted' }
1125
1260
  : { type: 'turn.done', ...base, state: 'error', error, ...(named === null ? {} : { limit: named }) }
1126
1261
  );
1262
+
1127
1263
  return events;
1128
1264
  }
1129
1265
 
1130
1266
  private messageChunk(update: Record<string, unknown>): BackendEvent[] {
1131
1267
  const chunk = ContentChunkSchema.safeParse(update);
1268
+
1132
1269
  if (!chunk.success || chunk.data.content.type !== 'text' || chunk.data.content.text === undefined) {
1133
1270
  return [];
1134
1271
  }
1272
+
1135
1273
  const messageId = chunk.data.messageId ?? null;
1136
1274
  const events = this.closeThought();
1275
+
1137
1276
  if (this.text !== null && messageId !== null && this.text.messageId !== messageId) {
1138
1277
  events.push(...this.closeText());
1139
1278
  }
1279
+
1140
1280
  if (this.text === null) {
1141
1281
  const ref = messageId === null ? `message-${++this.counter}` : `message-${messageId}`;
1142
1282
  this.text = { ref, messageId, text: '', held: true, afterRefusal: this.refusalLine };
1143
1283
  this.refusalLine = false;
1144
1284
  }
1285
+
1145
1286
  const text = this.text;
1146
1287
  text.text += chunk.data.content.text;
1288
+
1147
1289
  if (text.held && !withheld(text.text, text.afterRefusal)) {
1148
1290
  text.held = false;
1149
1291
  events.push({ type: 'text.delta', ref: text.ref, text: text.text });
1150
1292
  } else if (!text.held) {
1151
1293
  events.push({ type: 'text.delta', ref: text.ref, text: chunk.data.content.text });
1152
1294
  }
1295
+
1153
1296
  return events;
1154
1297
  }
1155
1298
 
1156
1299
  private thoughtChunk(update: Record<string, unknown>): BackendEvent[] {
1157
1300
  const chunk = ContentChunkSchema.safeParse(update);
1301
+
1158
1302
  if (!chunk.success || chunk.data.content.type !== 'text' || chunk.data.content.text === undefined) {
1159
1303
  return [];
1160
1304
  }
1305
+
1161
1306
  const events = this.closeText();
1307
+
1162
1308
  if (this.thought === null) {
1163
1309
  this.thought = { ref: `thought-${++this.counter}`, text: '' };
1164
1310
  }
1311
+
1165
1312
  this.thought.text += chunk.data.content.text;
1166
1313
  events.push({ type: 'thinking.delta', ref: this.thought.ref, text: chunk.data.content.text });
1314
+
1167
1315
  return events;
1168
1316
  }
1169
1317
 
1170
1318
  private closeText(): BackendEvent[] {
1171
1319
  const text = this.text;
1172
1320
  this.text = null;
1321
+
1173
1322
  if (text === null || (text.held && text.afterRefusal && text.text === REFUSAL_LINE)) {
1174
1323
  return [];
1175
1324
  }
1325
+
1176
1326
  if (text.held && text.text.startsWith(QUOTA_HEAD)) {
1177
1327
  this.quota = { ref: text.ref, text: text.text.trim(), resetsAt: quotaResetOf(text.text, this.now()) };
1328
+
1178
1329
  return [];
1179
1330
  }
1331
+
1180
1332
  return [{ type: 'text.done', ref: text.ref, text: text.text }];
1181
1333
  }
1182
1334
 
1183
1335
  private closeThought(): BackendEvent[] {
1184
1336
  const thought = this.thought;
1185
1337
  this.thought = null;
1338
+
1186
1339
  return thought === null ? [] : [{ type: 'thinking.done', ref: thought.ref, text: thought.text }];
1187
1340
  }
1188
1341
 
1189
1342
  /* A tool call or an update to one; fields an update leaves out keep what the call said before, and a list it sends replaces the old one. */
1190
1343
  private toolCall(call: z.infer<typeof ToolCallSchema>, settle = true): BackendEvent[] {
1191
1344
  const known = this.tools.get(call.toolCallId);
1345
+
1192
1346
  const tool: ToolState = known ?? {
1193
1347
  title: '',
1194
1348
  name: null,
@@ -1203,27 +1357,34 @@ export class AcpProtocol {
1203
1357
  settled: false,
1204
1358
  refused: false
1205
1359
  };
1360
+
1206
1361
  tool.title = call.title ?? tool.title;
1207
1362
  tool.name = call.name ?? tool.name;
1208
1363
  tool.kind = call.kind ?? tool.kind;
1209
1364
  tool.status = call.status ?? tool.status;
1210
1365
  tool.rawInput = call.rawInput ?? tool.rawInput;
1211
1366
  tool.rawOutput = call.rawOutput ?? tool.rawOutput;
1367
+
1212
1368
  if (call.locations) {
1213
1369
  tool.locations = validItems(LocationSchema, call.locations).map((location) => location.path);
1214
1370
  }
1371
+
1215
1372
  if (call.content) {
1216
1373
  tool.diffs = validItems(DiffSchema, call.content);
1374
+
1217
1375
  tool.text = validItems(ToolContentSchema, call.content).flatMap((entry) =>
1218
1376
  entry.content.type === 'text' && entry.content.text ? [entry.content.text] : []
1219
1377
  );
1220
1378
  }
1379
+
1221
1380
  this.tools.set(call.toolCallId, tool);
1222
1381
  const events: BackendEvent[] = [];
1223
1382
  const row = toolRow(tool.refused ? { ...tool, diffs: [] } : tool);
1224
1383
  const shown = JSON.stringify(row);
1384
+
1225
1385
  if (shown !== tool.shown) {
1226
1386
  tool.shown = shown;
1387
+
1227
1388
  events.push({
1228
1389
  type: 'tool.started',
1229
1390
  ref: call.toolCallId,
@@ -1233,8 +1394,10 @@ export class AcpProtocol {
1233
1394
  ...(row.changes ? { changes: row.changes } : {})
1234
1395
  });
1235
1396
  }
1397
+
1236
1398
  if (settle && !tool.settled && (tool.status === 'completed' || tool.status === 'failed')) {
1237
1399
  tool.settled = true;
1400
+
1238
1401
  events.push({
1239
1402
  type: 'tool.done',
1240
1403
  ref: call.toolCallId,
@@ -1243,6 +1406,7 @@ export class AcpProtocol {
1243
1406
  ...(row.changes ? { changes: row.changes } : {})
1244
1407
  });
1245
1408
  }
1409
+
1246
1410
  return events;
1247
1411
  }
1248
1412
 
@@ -1250,12 +1414,14 @@ export class AcpProtocol {
1250
1414
  private plan(update: Record<string, unknown>): BackendEvent[] {
1251
1415
  const entries = validItems(PlanEntrySchema, Array.isArray(update.entries) ? update.entries : []);
1252
1416
  const ref = `plan-${++this.counter}`;
1417
+
1253
1418
  const todos = entries.map((entry) => ({
1254
1419
  content: entry.content,
1255
1420
  status: entry.status,
1256
1421
  activeForm: entry.content,
1257
1422
  ...(entry.priority ? { priority: entry.priority } : {})
1258
1423
  }));
1424
+
1259
1425
  return [
1260
1426
  ...this.closeText(),
1261
1427
  ...this.closeThought(),