@adecore/agents 0.39.0 → 0.40.1

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 (172) 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.js +0 -2
  5. package/dist/chat/chat-process.d.ts +1 -0
  6. package/dist/chat/chat-process.js +1 -1
  7. package/dist/chat/chat-session.js +6 -8
  8. package/dist/chat/claude-protocol.js +1 -6
  9. package/dist/chat/claude-transcript.js +1 -3
  10. package/dist/chat/codex-protocol.js +1 -6
  11. package/dist/chat/projector.js +1 -3
  12. package/dist/chat/request-summary.js +1 -3
  13. package/dist/coded-error.js +1 -5
  14. package/dist/context/argv.js +1 -2
  15. package/dist/context/verb.js +1 -3
  16. package/dist/error-text.d.ts +1 -0
  17. package/dist/error-text.js +4 -0
  18. package/dist/fields.d.ts +3 -0
  19. package/dist/fields.js +12 -0
  20. package/dist/fs.d.ts +1 -0
  21. package/dist/fs.js +8 -4
  22. package/dist/lineage.js +11 -5
  23. package/dist/messages/notice-store.js +3 -4
  24. package/dist/providers/accounts/acp-account.js +3 -20
  25. package/dist/providers/accounts/service.d.ts +1 -2
  26. package/dist/providers/accounts/service.js +5 -9
  27. package/dist/providers/accounts/status.js +1 -3
  28. package/dist/providers/acp-agent-state.js +1 -3
  29. package/dist/providers/claude.js +0 -1
  30. package/dist/providers/codex-models.json +3 -2
  31. package/dist/providers/codex.js +0 -1
  32. package/dist/providers/detect.js +1 -1
  33. package/dist/providers/shell-commands.js +1 -2
  34. package/dist/run-process.js +1 -3
  35. package/dist/skills.d.ts +1 -1
  36. package/dist/skills.js +5 -8
  37. package/dist/tasks/background-limit.js +3 -2
  38. package/dist/tasks/task-coordinator.js +11 -17
  39. package/dist/testing/watch.js +0 -1
  40. package/dist/usage/limits/monitor.js +1 -2
  41. package/dist/usage/prices-snapshot.json +8 -0
  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 +208 -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 +401 -10
  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-models.json +3 -2
  126. package/src/providers/codex-rules.ts +3 -0
  127. package/src/providers/codex.ts +2 -1
  128. package/src/providers/detect.ts +6 -1
  129. package/src/providers/provider.ts +1 -1
  130. package/src/providers/registry.ts +18 -2
  131. package/src/providers/shell-commands.ts +37 -2
  132. package/src/record-directory.ts +19 -0
  133. package/src/run-process.ts +24 -3
  134. package/src/serializer.ts +3 -0
  135. package/src/skills.ts +73 -10
  136. package/src/tasks/background-limit.ts +9 -2
  137. package/src/tasks/end-children.ts +9 -0
  138. package/src/tasks/give-task.ts +8 -0
  139. package/src/tasks/task-coordinator.ts +81 -17
  140. package/src/tasks/task-store.ts +28 -0
  141. package/src/tasks/waiting-child.ts +21 -0
  142. package/src/tasks/wake-parent.ts +26 -0
  143. package/src/tasks/wiring.ts +22 -0
  144. package/src/testing/accounts.ts +2 -0
  145. package/src/testing/fake-acp-capture.ts +16 -0
  146. package/src/testing/fake-acp.ts +66 -0
  147. package/src/testing/fake-claude.ts +177 -0
  148. package/src/testing/fake-cli.ts +29 -0
  149. package/src/testing/fake-codex.ts +122 -0
  150. package/src/testing/manual-clock.ts +2 -0
  151. package/src/testing/watch.ts +7 -1
  152. package/src/title-file.ts +12 -0
  153. package/src/usage/accounts.ts +6 -0
  154. package/src/usage/aggregate.ts +19 -0
  155. package/src/usage/exchange.ts +12 -0
  156. package/src/usage/index-file.ts +27 -0
  157. package/src/usage/limits/monitor.ts +31 -3
  158. package/src/usage/limits/normalize.ts +30 -0
  159. package/src/usage/limits/probe.ts +17 -0
  160. package/src/usage/prices-snapshot.json +8 -0
  161. package/src/usage/pricing.ts +35 -0
  162. package/src/usage/projects.ts +8 -0
  163. package/src/usage/readers/claude.ts +12 -9
  164. package/src/usage/readers/codex.ts +40 -8
  165. package/src/usage/readers/grok.ts +17 -9
  166. package/src/usage/readers/host-log.ts +9 -9
  167. package/src/usage/record.ts +22 -0
  168. package/src/usage/roots.ts +6 -0
  169. package/src/usage/scanner.ts +61 -0
  170. package/src/usage/usage-service.ts +14 -0
  171. package/src/watch-seam.ts +8 -0
  172. package/src/windows-command.ts +12 -0
@@ -81,13 +81,17 @@ function liftUiAccess(record: unknown): unknown {
81
81
  if (typeof record !== 'object' || record === null || 'uiAccess' in record || !Array.isArray((record as { items?: unknown }).items)) {
82
82
  return record;
83
83
  }
84
+
84
85
  const uiAccess: Record<string, unknown> = {};
86
+
85
87
  for (const item of (record as { items: unknown[] }).items) {
86
88
  const queries = (item as { uiQueries?: unknown } | null)?.uiQueries;
89
+
87
90
  if (typeof queries === 'object' && queries !== null && 'access' in queries) {
88
91
  uiAccess[String((item as { id: unknown }).id)] = queries.access;
89
92
  }
90
93
  }
94
+
91
95
  return Object.keys(uiAccess).length === 0 ? record : { ...record, uiAccess };
92
96
  }
93
97
 
@@ -119,26 +123,31 @@ export class ChatStore {
119
123
  /* Every chat with a record or a log, for the scan at start. */
120
124
  async list(): Promise<string[]> {
121
125
  let names: string[];
126
+
122
127
  try {
123
128
  names = await readdir(this.dir);
124
129
  } catch (e) {
125
130
  if (isNotFound(e)) {
126
131
  return [];
127
132
  }
133
+
128
134
  throw e;
129
135
  }
136
+
130
137
  const ids = names
131
138
  // A chat's bookmarks and visuals, and whatever a host keeps per chat, sit beside its record under names that also end in .json.
132
139
  .filter(
133
140
  (name) => (name.endsWith('.json') && !isBookmarkFileName(name) && !isVisualFileName(name) && !this.isSidecar(name)) || name.endsWith('.log')
134
141
  )
135
142
  .map((name) => decodeURIComponent(name.replace(/\.(json|log)$/, '')));
143
+
136
144
  return [...new Set(ids)];
137
145
  }
138
146
 
139
147
  /* Whether anything of the chat is on disk, without reading it: a record or a log. */
140
148
  async has(chatId: string): Promise<boolean> {
141
149
  const found = await Promise.all([exists(join(this.dir, recordFileName(chatId))), exists(this.logPath(chatId))]);
150
+
142
151
  return found.includes(true);
143
152
  }
144
153
 
@@ -146,6 +155,7 @@ export class ChatStore {
146
155
  async discardLog(chatId: string): Promise<number> {
147
156
  const lines = parseLog((await readOrNull(this.logPath(chatId))) ?? '');
148
157
  await rm(this.logPath(chatId), { force: true });
158
+
149
159
  return lines.at(-1)?.seq ?? 0;
150
160
  }
151
161
 
@@ -162,6 +172,7 @@ export class ChatStore {
162
172
  const body = recordBody(info, items, at, preambles, extras, uiAccess);
163
173
  await mkdir(this.dir, { recursive: true, mode: 0o700 });
164
174
  await writeAtomic(join(this.dir, recordFileName(chatId)), body);
175
+
165
176
  return body.length;
166
177
  }
167
178
 
@@ -186,25 +197,32 @@ export class ChatStore {
186
197
  async read(chatId: string): Promise<ChatRecord | null> {
187
198
  const lines = parseLog((await readOrNull(this.logPath(chatId))) ?? '');
188
199
  const snapshot = await this.readSnapshot(chatId);
200
+
189
201
  if (snapshot === UNREADABLE) {
190
202
  return null;
191
203
  }
204
+
192
205
  if (snapshot === null) {
193
206
  return fromLogAlone(lines);
194
207
  }
208
+
195
209
  const seq = snapshot.seq ?? 0;
196
210
  const thread = new ChatThread(snapshot.info, snapshot.items);
197
211
  let resetSeq = snapshot.resetSeq ?? 0;
212
+
198
213
  for (const line of lines) {
199
214
  // A crash between writing a snapshot and folding the log into it leaves lines the snapshot already holds.
200
215
  if (line.seq <= seq) {
201
216
  continue;
202
217
  }
218
+
203
219
  thread.apply(line.event);
220
+
204
221
  if (line.event.type === 'reset') {
205
222
  resetSeq = line.seq;
206
223
  }
207
224
  }
225
+
208
226
  return {
209
227
  ...thread.snapshot(),
210
228
  seq,
@@ -233,30 +251,39 @@ export class ChatStore {
233
251
  if (!settledInfo(await leadingInfo(join(this.dir, recordFileName(chatId))))) {
234
252
  return false;
235
253
  }
254
+
236
255
  const last = lastLoggedInfo((await readOrNull(this.logPath(chatId))) ?? '');
256
+
237
257
  return last === undefined || settledInfo(last);
238
258
  }
239
259
 
240
260
  private async readSnapshot(chatId: string): Promise<z.infer<typeof RecordSchema> | typeof UNREADABLE | null> {
241
261
  const raw = await readOrNull(join(this.dir, recordFileName(chatId)));
262
+
242
263
  if (raw === null) {
243
264
  return null;
244
265
  }
266
+
245
267
  let record: unknown;
268
+
246
269
  try {
247
270
  record = JSON.parse(raw);
248
271
  } catch {
249
272
  return UNREADABLE;
250
273
  }
274
+
251
275
  // Before the schema sees it: an image written inline no longer has a shape the schema knows.
252
276
  if (this.attachments) {
253
277
  const migrated = await migrateInlineAttachments(chatId, record, this.attachments).catch(() => null);
278
+
254
279
  if (migrated !== null) {
255
280
  record = migrated;
256
281
  await writeAtomic(join(this.dir, recordFileName(chatId)), JSON.stringify(migrated));
257
282
  }
258
283
  }
284
+
259
285
  const parsed = RecordSchema.safeParse(liftUiAccess(record));
286
+
260
287
  return parsed.success ? parsed.data : UNREADABLE;
261
288
  }
262
289
 
@@ -281,14 +308,17 @@ const CLOSE_BRACE = 0x7d;
281
308
  */
282
309
  async function leadingInfo(path: string): Promise<unknown> {
283
310
  let handle: Awaited<ReturnType<typeof open>>;
311
+
284
312
  try {
285
313
  handle = await open(path, 'r');
286
314
  } catch (e) {
287
315
  if (isNotFound(e)) {
288
316
  return undefined;
289
317
  }
318
+
290
319
  throw e;
291
320
  }
321
+
292
322
  try {
293
323
  let head = Buffer.alloc(0);
294
324
  let at = RECORD_HEAD.length;
@@ -297,13 +327,17 @@ async function leadingInfo(path: string): Promise<unknown> {
297
327
  let escaped = false;
298
328
  const chunk = Buffer.alloc(HEAD_CHUNK_BYTES);
299
329
  let { bytesRead } = await handle.read(chunk, 0, chunk.length, 0);
330
+
300
331
  while (bytesRead > 0) {
301
332
  head = Buffer.concat([head, chunk.subarray(0, bytesRead)]);
333
+
302
334
  if (!head.subarray(0, RECORD_HEAD.length).equals(RECORD_HEAD.subarray(0, Math.min(head.length, RECORD_HEAD.length)))) {
303
335
  return undefined;
304
336
  }
337
+
305
338
  for (; at < head.length; at++) {
306
339
  const byte = head[at]!;
340
+
307
341
  if (escaped) {
308
342
  escaped = false;
309
343
  } else if (inString) {
@@ -315,13 +349,16 @@ async function leadingInfo(path: string): Promise<unknown> {
315
349
  depth += 1;
316
350
  } else if (byte === CLOSE_BRACE) {
317
351
  depth -= 1;
352
+
318
353
  if (depth === 0) {
319
354
  return parseOrUndefined(head.subarray(RECORD_HEAD.length - 1, at + 1).toString('utf8'));
320
355
  }
321
356
  }
322
357
  }
358
+
323
359
  ({ bytesRead } = await handle.read(chunk, 0, chunk.length, head.length));
324
360
  }
361
+
325
362
  return undefined;
326
363
  } finally {
327
364
  await handle.close();
@@ -331,16 +368,20 @@ async function leadingInfo(path: string): Promise<unknown> {
331
368
  /* The info of the last info or reset line in a log, unvalidated; undefined when no line carries one. */
332
369
  function lastLoggedInfo(raw: string): unknown {
333
370
  const lines = raw.split('\n');
371
+
334
372
  for (let i = lines.length - 1; i >= 0; i--) {
335
373
  // Most lines are items and deltas, which are larger and never carry this key.
336
374
  if (!lines[i]!.includes('"info"')) {
337
375
  continue;
338
376
  }
377
+
339
378
  const event = (parseOrUndefined(lines[i]!) as { event?: { type?: unknown; info?: unknown } } | null | undefined)?.event;
379
+
340
380
  if (event?.type === 'info' || event?.type === 'reset') {
341
381
  return event.info;
342
382
  }
343
383
  }
384
+
344
385
  return undefined;
345
386
  }
346
387
 
@@ -359,11 +400,13 @@ function parseOrUndefined(text: string): unknown {
359
400
  async function exists(path: string): Promise<boolean> {
360
401
  try {
361
402
  await stat(path);
403
+
362
404
  return true;
363
405
  } catch (e) {
364
406
  if (isNotFound(e)) {
365
407
  return false;
366
408
  }
409
+
367
410
  throw e;
368
411
  }
369
412
  }
@@ -375,6 +418,7 @@ async function readOrNull(path: string): Promise<string | null> {
375
418
  if (isNotFound(e)) {
376
419
  return null;
377
420
  }
421
+
378
422
  throw e;
379
423
  }
380
424
  }
@@ -389,17 +433,23 @@ function fromLogAlone(lines: ChatLogLine[]): ChatRecord | null {
389
433
  if (lines.length === 0) {
390
434
  return null;
391
435
  }
436
+
392
437
  const first = lines.find((line) => line.event.type === 'info' || line.event.type === 'reset')?.event;
438
+
393
439
  if (lines[0]!.seq !== 1 || (first?.type !== 'info' && first?.type !== 'reset')) {
394
440
  return null;
395
441
  }
442
+
396
443
  const thread = new ChatThread(first.info);
397
444
  let resetSeq = 0;
445
+
398
446
  for (const line of lines) {
399
447
  thread.apply(line.event);
448
+
400
449
  if (line.event.type === 'reset') {
401
450
  resetSeq = line.seq;
402
451
  }
403
452
  }
453
+
404
454
  return { ...thread.snapshot(), seq: 0, resetSeq, preambles: [], uiAccess: {}, extras: {}, lines };
405
455
  }
@@ -45,19 +45,25 @@ export function buildTitlePrompt(prompt: string, answer: string): string {
45
45
  export function parseTitle(output: string): string | null {
46
46
  const start = [...output.matchAll(/\{\s*"title"/g)].at(-1)?.index;
47
47
  const end = output.lastIndexOf('}');
48
+
48
49
  if (start === undefined || end < start) {
49
50
  return null;
50
51
  }
52
+
51
53
  let parsed: unknown;
54
+
52
55
  try {
53
56
  parsed = JSON.parse(output.slice(start, end + 1));
54
57
  } catch {
55
58
  return null;
56
59
  }
60
+
57
61
  if (typeof parsed !== 'object' || parsed === null || !('title' in parsed)) {
58
62
  return null;
59
63
  }
64
+
60
65
  const title = cleanTitle(parsed.title);
66
+
61
67
  return title === null ? null : cleanTitle(title.replace(/^["'`]+|["'`.]+$/g, ''));
62
68
  }
63
69
 
@@ -74,11 +80,14 @@ export async function suggestChatTitle(
74
80
  ): Promise<string | null> {
75
81
  const provider = await registry.oneShotProvider(preferred);
76
82
  const args = provider?.oneShotArgs?.(buildTitlePrompt(input.prompt, input.answer)) ?? null;
83
+
77
84
  if (provider === null || args === null) {
78
85
  return null;
79
86
  }
87
+
80
88
  try {
81
89
  const result = await runProcess([provider.command[0]!, ...args], { cwd: input.cwd, timeoutMs: TIMEOUT_MS, ...(env === undefined ? {} : { env }) });
90
+
82
91
  return result.exitCode === 0 ? parseTitle(result.stdout) : null;
83
92
  } catch {
84
93
  return null;
@@ -56,7 +56,9 @@ export class ClaudeBackend implements ChatBackend {
56
56
  if (this.child) {
57
57
  return Promise.resolve();
58
58
  }
59
+
59
60
  const { selection, runtimeMode, resume, folders, projectConfig } = this.launch;
61
+
60
62
  const args = [
61
63
  ...this.launch.command,
62
64
  ...claudeArgs({
@@ -68,10 +70,13 @@ export class ClaudeBackend implements ChatBackend {
68
70
  ...(projectConfig === undefined ? {} : { projectConfig })
69
71
  })
70
72
  ];
73
+
71
74
  if (this.launch.instructions !== null) {
72
75
  args.push('--append-system-prompt', this.launch.instructions);
73
76
  }
77
+
74
78
  this.stdinClosed = false;
79
+
75
80
  const child: ChatChild = new ChatChild({
76
81
  command: args,
77
82
  cwd: this.launch.cwd,
@@ -80,8 +85,10 @@ export class ClaudeBackend implements ChatBackend {
80
85
  onError: (error) => this.broken(error),
81
86
  onExit: (exitCode, stderr) => this.handleExit(child, exitCode, stderr)
82
87
  });
88
+
83
89
  this.child = child;
84
90
  void this.readLines(child);
91
+
85
92
  return Promise.resolve();
86
93
  }
87
94
 
@@ -96,10 +103,13 @@ export class ClaudeBackend implements ChatBackend {
96
103
  if (!this.child || !this.active || this.stdinClosed) {
97
104
  return Promise.resolve(false);
98
105
  }
106
+
99
107
  const promptId = input.promptId ?? randomUUID();
108
+
100
109
  return new Promise((resolve, reject) => {
101
110
  this.steers.set(promptId, { resolve, reject });
102
111
  this.protocol.beginSteer(promptId);
112
+
103
113
  try {
104
114
  this.write(this.userMessage(input, promptId));
105
115
  } catch (error) {
@@ -118,6 +128,7 @@ export class ClaudeBackend implements ChatBackend {
118
128
  this.interrupted = true;
119
129
  this.protocol.noteInterrupt();
120
130
  this.write({ type: 'control_request', request_id: `interrupt-${Date.now()}`, request: { subtype: 'interrupt' } });
131
+
121
132
  // A steer the CLI has not taken in survives a plain interrupt and would run after the Stop; it goes back to the queue instead.
122
133
  for (const promptId of this.steers.keys()) {
123
134
  this.write({
@@ -134,25 +145,31 @@ export class ClaudeBackend implements ChatBackend {
134
145
 
135
146
  respondApproval(requestId: string, decision: ApprovalDecision, message?: string): boolean {
136
147
  const frame = this.protocol.approvalResponse(requestId, decision, message);
148
+
137
149
  if (frame === null) {
138
150
  return false;
139
151
  }
152
+
140
153
  return this.respond(requestId, frame);
141
154
  }
142
155
 
143
156
  respondQuestion(requestId: string, answers: Record<string, string>): boolean {
144
157
  const frame = this.protocol.questionResponse(requestId, answers);
158
+
145
159
  if (frame === null) {
146
160
  return false;
147
161
  }
162
+
148
163
  return this.respond(requestId, frame);
149
164
  }
150
165
 
151
166
  declineRequest(requestId: string, message: string): boolean {
152
167
  const frame = this.protocol.declineResponse(requestId, message);
168
+
153
169
  if (frame === null) {
154
170
  return false;
155
171
  }
172
+
156
173
  return this.respond(requestId, frame);
157
174
  }
158
175
 
@@ -165,12 +182,14 @@ export class ClaudeBackend implements ChatBackend {
165
182
  this.rejectSteers('Claude stopped before acknowledging your message');
166
183
  const child = this.child;
167
184
  this.child = null;
185
+
168
186
  return child?.end() ?? Promise.resolve();
169
187
  }
170
188
 
171
189
  private userMessage(input: TurnInput, promptId: string): Record<string, unknown> {
172
190
  // What the CLI must see first (ultrathink), then the preamble, then what was typed.
173
191
  const prefix = `${promptPrefix(this.launch.selection)}${input.preamble === null ? '' : `${input.preamble}\n\n`}`;
192
+
174
193
  return buildUserMessage({ text: input.text, attachments: input.attachments, prefix, skills: input.skills, promptId });
175
194
  }
176
195
 
@@ -196,33 +215,43 @@ export class ClaudeBackend implements ChatBackend {
196
215
  if (this.child !== child || line.trim() === '') {
197
216
  return;
198
217
  }
218
+
199
219
  let frame: unknown;
220
+
200
221
  try {
201
222
  frame = JSON.parse(line);
202
223
  } catch {
203
224
  return;
204
225
  }
226
+
205
227
  const echoed = typeof frame === 'object' && frame !== null ? (frame as Record<string, unknown>) : null;
228
+
206
229
  if (echoed?.type === 'control_response' && this.cancelledSteer(echoed.response)) {
207
230
  return;
208
231
  }
232
+
209
233
  if (echoed?.type === 'user' && typeof echoed.uuid === 'string' && echoed.parent_tool_use_id == null) {
210
234
  this.steers.get(echoed.uuid)?.resolve(true);
211
235
  this.steers.delete(echoed.uuid);
236
+
212
237
  if (this.steers.size === 0) {
213
238
  this.clearSteerTimer();
214
239
  }
215
240
  }
241
+
216
242
  for (const event of this.protocol.handle(frame)) {
217
243
  if (event.type === 'turn.done') {
218
244
  this.active = false;
219
245
  this.awaitSteers();
220
246
  }
247
+
221
248
  if (event.type === 'turn.done' && this.interrupted) {
222
249
  this.interrupted = false;
223
250
  this.host.onEvent({ ...event, state: 'aborted' });
251
+
224
252
  continue;
225
253
  }
254
+
226
255
  this.host.onEvent(event);
227
256
  }
228
257
  }
@@ -240,18 +269,24 @@ export class ClaudeBackend implements ChatBackend {
240
269
  if (typeof response !== 'object' || response === null) {
241
270
  return false;
242
271
  }
272
+
243
273
  const { request_id: requestId, response: body } = response as { request_id?: unknown; response?: unknown };
274
+
244
275
  if (typeof requestId !== 'string' || !requestId.startsWith(CANCEL_STEER)) {
245
276
  return false;
246
277
  }
278
+
247
279
  const promptId = requestId.slice(CANCEL_STEER.length);
280
+
248
281
  if (typeof body === 'object' && body !== null && (body as { cancelled?: unknown }).cancelled === true) {
249
282
  this.steers.get(promptId)?.resolve(false);
250
283
  this.steers.delete(promptId);
284
+
251
285
  if (this.steers.size === 0) {
252
286
  this.clearSteerTimer();
253
287
  }
254
288
  }
289
+
255
290
  return true;
256
291
  }
257
292
 
@@ -259,13 +294,17 @@ export class ClaudeBackend implements ChatBackend {
259
294
  if (this.steers.size === 0 || this.steerTimer !== null) {
260
295
  return;
261
296
  }
297
+
262
298
  this.steerTimer = setTimeout(() => {
263
299
  this.steerTimer = null;
300
+
264
301
  for (const pending of this.steers.values()) {
265
302
  pending.reject(new Error('Claude ended its turn without taking your message'));
266
303
  }
304
+
267
305
  this.steers.clear();
268
306
  }, STEER_GRACE_MS);
307
+
269
308
  this.steerTimer.unref?.();
270
309
  }
271
310
 
@@ -279,9 +318,11 @@ export class ClaudeBackend implements ChatBackend {
279
318
  private rejectSteers(reason: string): void {
280
319
  this.active = false;
281
320
  this.clearSteerTimer();
321
+
282
322
  for (const pending of this.steers.values()) {
283
323
  pending.reject(new Error(reason));
284
324
  }
325
+
285
326
  this.steers.clear();
286
327
  }
287
328
 
@@ -294,6 +335,7 @@ export class ClaudeBackend implements ChatBackend {
294
335
  private failed(error: unknown): void {
295
336
  this.rejectSteers(`Lost the connection to Claude: ${errorText(error)}`);
296
337
  this.protocol.forgetPending();
338
+
297
339
  try {
298
340
  this.host.onEvent({ type: 'failed', message: `Lost the connection to Claude: ${errorText(error)}`, processAlive: this.running });
299
341
  } catch (reportError) {
@@ -304,24 +346,29 @@ export class ClaudeBackend implements ChatBackend {
304
346
  private respond(requestId: string, frame: unknown): boolean {
305
347
  try {
306
348
  this.write(frame);
349
+
307
350
  return true;
308
351
  } catch (error) {
309
352
  this.host.onEvent({ type: 'request.withdrawn', requestId });
310
353
  this.failed(error);
354
+
311
355
  return false;
312
356
  }
313
357
  }
314
358
 
315
359
  private write(frame: unknown): void {
316
360
  const child = this.child;
361
+
317
362
  if (!child || this.stdinClosed) {
318
363
  throw new Error('Claude stdin is closed');
319
364
  }
365
+
320
366
  try {
321
367
  child.process.stdin.write(`${JSON.stringify(frame)}\n`);
322
368
  child.process.stdin.flush();
323
369
  } catch (error) {
324
370
  this.stdinClosed = true;
371
+
325
372
  throw error;
326
373
  }
327
374
  }
@@ -330,7 +377,9 @@ export class ClaudeBackend implements ChatBackend {
330
377
  if (!this.child || this.stdinClosed) {
331
378
  return;
332
379
  }
380
+
333
381
  this.stdinClosed = true;
382
+
334
383
  try {
335
384
  this.child.process.stdin.end();
336
385
  } catch {
@@ -342,6 +391,7 @@ export class ClaudeBackend implements ChatBackend {
342
391
  if (this.child !== child) {
343
392
  return;
344
393
  }
394
+
345
395
  this.child = null;
346
396
  this.rejectSteers('Claude stopped before it got your message');
347
397
  this.protocol.forgetPending();