@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
@@ -108,28 +108,37 @@ export class ChatUiQueries {
108
108
  const key = `${session.id}:${item.id}`;
109
109
  // Once a capture started, every delta of a streaming reply would scan its whole text again for nothing.
110
110
  const captured = this.captures.has(key) || item.uiQueries?.authorChatId === session.id;
111
+
111
112
  if (!captured && !uiMayReferenceHost(item.text) && !item.ui?.some((block) => Object.keys(block.queries).length)) {
112
113
  return;
113
114
  }
115
+
114
116
  const capture = this.access(session, item);
117
+
115
118
  if (!final || !item.ui?.length) {
116
119
  return;
117
120
  }
121
+
118
122
  if (this.freezing.has(key)) {
119
123
  return;
120
124
  }
125
+
121
126
  const frozen = capture
122
127
  .then(async () => {
123
128
  for (const block of (item.ui ?? []).slice(0, 16)) {
124
129
  const current = session.thread.get(item.id);
130
+
125
131
  if (current?.kind !== 'assistant' || current.ui?.find((entry) => entry.id === block.id)?.revision !== block.revision) {
126
132
  return;
127
133
  }
134
+
128
135
  if (current.uiQueries?.blocks[block.id]?.revision === block.revision) {
129
136
  continue;
130
137
  }
138
+
131
139
  const readings: Record<string, ChatUiQueryReading> = Object.create(null);
132
140
  const values: Record<string, unknown> = Object.create(null);
141
+
133
142
  for (const name of Object.keys(block.queries).slice(0, UI_HOST_LIMITS.queries)) {
134
143
  const reading = await this.query(session, {
135
144
  chatId: session.id,
@@ -138,15 +147,20 @@ export class ChatUiQueries {
138
147
  revision: block.revision!,
139
148
  query: name
140
149
  });
150
+
141
151
  readings[name] = reading;
152
+
142
153
  if (reading.state === 'fresh') {
143
154
  values[name] = reading.value;
144
155
  }
145
156
  }
157
+
146
158
  const links: Record<string, ChatUiLinkReading> = Object.create(null);
159
+
147
160
  if (this.host.link) {
148
161
  const targets = uiLinkTargets(block, {}, values);
149
162
  const access = await this.access(session, current);
163
+
150
164
  for (const [id, target] of Object.entries(targets).slice(0, UI_HOST_LIMITS.links)) {
151
165
  try {
152
166
  links[id] = this.linkReading(await this.host.link(session.info, access, target));
@@ -155,21 +169,28 @@ export class ChatUiQueries {
155
169
  }
156
170
  }
157
171
  }
172
+
158
173
  const { places, found, ended } = await this.lookUpPlaces(session, item.id, block, values);
174
+
159
175
  if (ended) {
160
176
  return;
161
177
  }
178
+
162
179
  const latest = session.thread.get(item.id);
180
+
163
181
  if (latest?.kind !== 'assistant' || !latest.uiQueries || latest.ui?.find((entry) => entry.id === block.id)?.revision !== block.revision) {
164
182
  return;
165
183
  }
184
+
166
185
  const names = Object.entries(block.queries)
167
186
  .map(([name, query]) => `${name}: ${query.source}`)
168
187
  .join(', ');
188
+
169
189
  const failures = Object.entries(readings)
170
190
  .filter(([, reading]) => reading.state !== 'fresh')
171
191
  .map(([name, reading]) => `${name}: ${reading.reason}`)
172
192
  .join('\n');
193
+
173
194
  const frozen: ChatUiQueryState['blocks'][string] = {
174
195
  revision: block.revision!,
175
196
  readings,
@@ -177,7 +198,9 @@ export class ChatUiQueries {
177
198
  ...(Object.keys(places).length > 0 ? { places } : {}),
178
199
  fallback: `${uiQueryFallback(block, values).slice(0, 8192)}${names ? `\nSources: ${names}` : ''}${failures ? `\n${failures}` : ''}${found.length > 0 ? `\nPlaces: ${found.join('; ')}` : ''}`
179
200
  };
201
+
180
202
  const blocks = { ...latest.uiQueries.blocks, [block.id]: frozen };
203
+
181
204
  if (Buffer.byteLength(JSON.stringify(blocks)) > MAX_SNAPSHOT_BYTES) {
182
205
  frozen.readings = Object.fromEntries(
183
206
  Object.keys(readings).map((name) => [
@@ -185,9 +208,11 @@ export class ChatUiQueries {
185
208
  { state: 'failed', readAt: Date.now(), code: 'snapshot-too-large', reason: 'The frozen query values are too large.' }
186
209
  ])
187
210
  );
211
+
188
212
  frozen.links = Object.fromEntries(
189
213
  Object.keys(links).map((id) => [id, { state: 'plain', code: 'link-unchecked', reason: 'This link must be checked again.' }])
190
214
  );
215
+
191
216
  if (frozen.places) {
192
217
  frozen.places = Object.fromEntries(
193
218
  Object.entries(places).map(([id, reading]) => [
@@ -196,17 +221,21 @@ export class ChatUiQueries {
196
221
  ])
197
222
  );
198
223
  }
224
+
199
225
  frozen.fallback = `${block.fallback.slice(0, 2048)}\nSources: ${names}\nThe frozen query values are too large.`;
200
226
  }
227
+
201
228
  if (Buffer.byteLength(JSON.stringify(blocks)) > MAX_SNAPSHOT_BYTES) {
202
229
  // The block stays marked as frozen with nothing in it, so a reader stops waiting for readings that will not come.
203
230
  blocks[block.id] = { revision: block.revision!, readings: {}, fallback: '' };
204
231
  }
232
+
205
233
  session.updateUiQueries(item.id, { ...latest.uiQueries, blocks });
206
234
  }
207
235
  })
208
236
  .catch(() => {})
209
237
  .finally(() => this.freezing.delete(key));
238
+
210
239
  this.freezing.set(key, frozen);
211
240
  }
212
241
 
@@ -229,15 +258,19 @@ export class ChatUiQueries {
229
258
  ): Promise<{ places: Record<string, ChatUiPlaceReading>; found: string[]; ended: boolean }> {
230
259
  const places: Record<string, ChatUiPlaceReading> = Object.create(null);
231
260
  const found: string[] = [];
261
+
232
262
  if (!this.host.place) {
233
263
  return { places, found, ended: false };
234
264
  }
265
+
235
266
  let queries: Array<[string, UiPlaceQuery]>;
267
+
236
268
  try {
237
269
  queries = Object.entries(uiPlaceQueries(block, {}, values)).slice(0, UI_HOST_LIMITS.places);
238
270
  } catch {
239
271
  return { places, found, ended: false };
240
272
  }
273
+
241
274
  const life = this.lifeOf(session.id).signal;
242
275
  const budget = new AbortController();
243
276
  const timer = setTimeout(() => budget.abort(), PLACE_BLOCK_BUDGET_MS);
@@ -246,49 +279,64 @@ export class ChatUiQueries {
246
279
  const readings = new Map<string, ChatUiPlaceReading>();
247
280
  let timeouts = 0;
248
281
  let next = 0;
282
+
249
283
  const stillThere = (): boolean => {
250
284
  const current = session.thread.get(itemId);
285
+
251
286
  return !life.aborted && current?.kind === 'assistant' && current.ui?.find((entry) => entry.id === block.id)?.revision === block.revision;
252
287
  };
288
+
253
289
  const worker = async (): Promise<void> => {
254
290
  for (let index = next++; index < queries.length; index = next++) {
255
291
  const [id, query] = queries[index]!;
292
+
256
293
  if (!stillThere()) {
257
294
  return;
258
295
  }
296
+
259
297
  if (budget.signal.aborted || timeouts >= PLACE_TIMEOUTS_IN_A_ROW) {
260
298
  readings.set(id, { state: 'missing', address: query.address, code: 'timed-out', reason: 'The place lookup timed out.' });
299
+
261
300
  continue;
262
301
  }
302
+
263
303
  const reading = await this.placeReading(session.info, query, signal);
264
304
  timeouts = reading.code === 'timed-out' ? timeouts + 1 : 0;
265
305
  readings.set(id, reading);
266
306
  }
267
307
  };
308
+
268
309
  try {
269
310
  await Promise.all(Array.from({ length: Math.min(PLACE_PARALLEL, queries.length) }, worker));
270
311
  } finally {
271
312
  clearTimeout(timer);
272
313
  }
314
+
273
315
  for (const [id, query] of queries) {
274
316
  const reading = readings.get(id);
317
+
275
318
  if (reading === undefined) {
276
319
  continue;
277
320
  }
321
+
278
322
  places[id] = reading;
323
+
279
324
  if (reading.state === 'found') {
280
325
  found.push(`${query.name || query.address}: ${reading.lat}, ${reading.lon}`);
281
326
  }
282
327
  }
328
+
283
329
  return { places, found, ended: life.aborted };
284
330
  }
285
331
 
286
332
  private lifeOf(chatId: string): AbortController {
287
333
  let life = this.lives.get(chatId);
334
+
288
335
  if (life === undefined) {
289
336
  life = new AbortController();
290
337
  this.lives.set(chatId, life);
291
338
  }
339
+
292
340
  return life;
293
341
  }
294
342
 
@@ -302,6 +350,7 @@ export class ChatUiQueries {
302
350
  const timer = setTimeout(() => timeout.abort(), PLACE_TIMEOUT_MS);
303
351
  timer.unref?.();
304
352
  const signal = AbortSignal.any([timeout.signal, outer]);
353
+
305
354
  try {
306
355
  const reading = await Promise.race([
307
356
  this.host.place!(info, query, signal),
@@ -311,7 +360,9 @@ export class ChatUiQueries {
311
360
  })
312
361
  )
313
362
  ]);
363
+
314
364
  const parsed = ChatUiPlaceReadingSchema.parse({ ...(copyUiValue(reading, new UiBudget()) as object), address: query.address });
365
+
315
366
  return parsed.state === 'found' && (parsed.lat === undefined || parsed.lon === undefined)
316
367
  ? { state: 'missing', address: query.address, code: 'place-unreadable', reason: 'The host found no point for this address.' }
317
368
  : parsed;
@@ -328,28 +379,37 @@ export class ChatUiQueries {
328
379
  private access(session: ChatSession, item: ChatAssistantItem): Promise<unknown> {
329
380
  const key = `${session.id}:${item.id}`;
330
381
  const existing = this.captures.get(key);
382
+
331
383
  if (existing) {
332
384
  return existing;
333
385
  }
386
+
334
387
  if (item.uiQueries?.authorChatId === session.id) {
335
388
  return Promise.resolve(session.uiAccess(item.id) ?? null);
336
389
  }
390
+
337
391
  // A pending capture is persisted as denied, so a restart cannot widen the writer's rights.
338
392
  session.setUiAccess(item.id, null);
339
393
  session.updateUiQueries(item.id, { authorChatId: session.id, blocks: {} });
394
+
340
395
  const captured = this.host
341
396
  .capture(session.info)
342
397
  .then((access) => {
343
398
  const captured = copyUiValue(access, new UiBudget());
399
+
344
400
  if (Buffer.byteLength(JSON.stringify(captured)) > 32 * 1024) {
345
401
  throw new Error('The UI access snapshot is too large.');
346
402
  }
403
+
347
404
  session.setUiAccess(item.id, captured);
405
+
348
406
  return captured as unknown;
349
407
  })
350
408
  .catch(() => null)
351
409
  .finally(() => this.captures.delete(key));
410
+
352
411
  this.captures.set(key, captured);
412
+
353
413
  return captured;
354
414
  }
355
415
 
@@ -358,23 +418,30 @@ export class ChatUiQueries {
358
418
  payload: Pick<ChatUiQueryPayload, 'itemId' | 'blockId' | 'revision'>
359
419
  ): { item: ChatAssistantItem; block: UiBlock } {
360
420
  const item = session.thread.get(payload.itemId);
421
+
361
422
  const block =
362
423
  item?.kind === 'assistant' && !item.streaming && !item.parentToolUseId ? item.ui?.find((entry) => entry.id === payload.blockId) : undefined;
424
+
363
425
  if (item?.kind !== 'assistant' || !block?.complete || block.revision !== payload.revision) {
364
426
  throw new UiQueryError('stale-ui-block', 'block-stale', 'This completed UI block is no longer available.');
365
427
  }
428
+
366
429
  return { item, block };
367
430
  }
368
431
 
369
432
  private stored(session: ChatSession, payload: ChatUiQueryPayload): { item: ChatAssistantItem; block: UiBlock; source: ChatUiSource } {
370
433
  const { item, block } = this.storedBlock(session, payload);
434
+
371
435
  if (!Object.hasOwn(block.queries, payload.query) || Object.keys(block.queries).length > UI_HOST_LIMITS.queries) {
372
436
  throw new UiQueryError('refused-query', 'query-undeclared', 'This block does not declare an allowed query.');
373
437
  }
438
+
374
439
  const name = block.queries[payload.query].source;
440
+
375
441
  if (!Object.hasOwn(this.host.sources, name)) {
376
442
  throw new UiQueryError('refused-query', 'source-unregistered', 'This query source is not registered.');
377
443
  }
444
+
378
445
  return { item, block, source: this.host.sources[name] };
379
446
  }
380
447
 
@@ -384,49 +451,63 @@ export class ChatUiQueries {
384
451
  let block: UiBlock;
385
452
  let item: ChatAssistantItem;
386
453
  let authorAccess: unknown;
454
+
387
455
  try {
388
456
  ({ source, block, item } = this.stored(session, payload));
389
457
  args = uiQueryArguments(block, payload.query, payload.values, {}, this.heldValues(session, item, block));
390
458
  args = source.args.parse(args) as Record<string, UiValue>;
391
459
  authorAccess = await this.access(session, item);
460
+
392
461
  if (authorAccess === null) {
393
462
  throw new UiQueryError('refused-query', 'access-unavailable', 'The writer’s original access is unavailable.');
394
463
  }
464
+
395
465
  await source.authorize(session.info, authorAccess, args);
396
466
  } catch (error) {
397
467
  return { state: 'refused', readAt: Date.now(), ...this.failure(error) };
398
468
  }
469
+
399
470
  const key = JSON.stringify([session.id, item.id, block.id, block.revision, payload.query, args]);
400
471
  const cached = this.cache.get(this.latest.get(key) ?? '');
472
+
401
473
  if (cached && Date.now() - cached.reading.readAt < UI_HOST_LIMITS.refreshMilliseconds) {
402
474
  return cached.reading;
403
475
  }
476
+
404
477
  const pending = this.reading.get(key);
478
+
405
479
  if (pending) {
406
480
  return pending;
407
481
  }
482
+
408
483
  const identity = JSON.stringify([session.id, item.id, block.id, block.revision, payload.query]);
409
484
  const now = Date.now();
485
+
410
486
  if (
411
487
  now - (this.attempts.get(key) ?? -Infinity) < UI_HOST_LIMITS.refreshMilliseconds ||
412
488
  now - (this.attempts.get(identity) ?? -Infinity) < INPUT_READ_MS
413
489
  ) {
414
490
  return { state: 'failed', readAt: now, code: 'refresh-limit', reason: 'This query may refresh once every ten seconds.' };
415
491
  }
492
+
416
493
  if ((this.active.get(session.id) ?? 0) >= 2 || this.totalActive >= 16) {
417
494
  return { state: 'failed', readAt: now, code: 'busy', reason: 'Too many UI queries are being read.' };
418
495
  }
496
+
419
497
  this.attempts.set(key, now);
420
498
  this.attempts.set(identity, now);
499
+
421
500
  while (this.attempts.size > 256) {
422
501
  this.attempts.delete(this.attempts.keys().next().value!);
423
502
  }
503
+
424
504
  this.active.set(session.id, (this.active.get(session.id) ?? 0) + 1);
425
505
  this.totalActive++;
426
506
  const controller = new AbortController();
427
507
  const work = Promise.resolve().then(() => source.read(session.info, args, controller.signal, authorAccess));
428
508
  const timeout = setTimeout(() => controller.abort(), READ_TIMEOUT_MS);
429
509
  timeout.unref?.();
510
+
430
511
  const read = Promise.race([
431
512
  work,
432
513
  new Promise<never>((_, reject) =>
@@ -437,16 +518,20 @@ export class ChatUiQueries {
437
518
  ])
438
519
  .then((result): ChatUiQueryReading => {
439
520
  const value = copyUiValue(source.result.parse(result), new UiBudget());
521
+
440
522
  if (Buffer.byteLength(JSON.stringify(value)) > MAX_QUERY_BYTES) {
441
523
  throw new UiQueryError('refused-query', 'result-too-large', 'The UI query result is too large.');
442
524
  }
525
+
443
526
  const reading: ChatUiQueryReading = { state: 'fresh', value, readId: randomUUID(), readAt: Date.now() };
444
527
  this.cache.set(reading.readId!, { key, identity, chatId: session.id, reading });
445
528
  this.latest.set(key, reading.readId!);
446
529
  this.newest.set(identity, reading.readId!);
530
+
447
531
  while (this.cache.size > 128) {
448
532
  this.evict(this.cache.keys().next().value!);
449
533
  }
534
+
450
535
  return reading;
451
536
  })
452
537
  .catch((error): ChatUiQueryReading => ({ state: 'failed', readAt: Date.now(), ...this.failure(error) }))
@@ -454,19 +539,24 @@ export class ChatUiQueries {
454
539
  clearTimeout(timeout);
455
540
  this.reading.delete(key);
456
541
  });
542
+
457
543
  // Timed-out sources still occupy their slot until their actual work ends.
458
544
  void work
459
545
  .catch(() => {})
460
546
  .finally(() => {
461
547
  const count = (this.active.get(session.id) ?? 1) - 1;
548
+
462
549
  if (count) {
463
550
  this.active.set(session.id, count);
464
551
  } else {
465
552
  this.active.delete(session.id);
466
553
  }
554
+
467
555
  this.totalActive--;
468
556
  });
557
+
469
558
  this.reading.set(key, read);
559
+
470
560
  return read;
471
561
  }
472
562
 
@@ -474,36 +564,48 @@ export class ChatUiQueries {
474
564
  async choiceValues(session: ChatSession, payload: ChatUiChoicePayload | ChatUiLinkPayload): Promise<Record<string, unknown>> {
475
565
  const item = session.thread.get(payload.itemId);
476
566
  const block = item?.kind === 'assistant' ? item.ui?.find((entry) => entry.id === payload.blockId) : undefined;
567
+
477
568
  if (!block || item?.kind !== 'assistant' || ('choiceId' in payload && item.uiAnswers?.[block.id]?.revision === payload.revision)) {
478
569
  return {};
479
570
  }
571
+
480
572
  const names = Object.keys(block.queries);
481
573
  const values: Record<string, unknown> = Object.create(null);
574
+
482
575
  if (names.length === 0) {
483
576
  return values;
484
577
  }
578
+
485
579
  const access = await this.access(session, item);
580
+
486
581
  if (access === null) {
487
582
  throw new UiQueryError('refused-query', 'access-unavailable', 'The writer’s original access is unavailable.');
488
583
  }
584
+
489
585
  const frozen = item.uiQueries?.blocks[block.id];
586
+
490
587
  const readIdOf = (name: string): string | undefined =>
491
588
  payload.reads !== undefined && Object.hasOwn(payload.reads, name) ? payload.reads[name] : undefined;
589
+
492
590
  // What the person saw, by the read ids it names; each one is checked against its arguments below.
493
591
  const seen: Record<string, unknown> = Object.create(null);
592
+
494
593
  for (const name of names) {
495
594
  const readId = readIdOf(name);
496
595
  const cached = readId === undefined ? undefined : this.cache.get(readId);
596
+
497
597
  const reading =
498
598
  cached?.identity === JSON.stringify([session.id, item.id, block.id, block.revision, name])
499
599
  ? cached.reading
500
600
  : frozen?.revision === block.revision
501
601
  ? frozen?.readings[name]
502
602
  : undefined;
603
+
503
604
  if (readId !== undefined && reading?.readId === readId && reading.state === 'fresh') {
504
605
  seen[name] = reading.value;
505
606
  }
506
607
  }
608
+
507
609
  for (const name of names) {
508
610
  const { source } = this.stored(session, { ...payload, query: name });
509
611
  const args = source.args.parse(uiQueryArguments(block, name, payload.values, {}, seen)) as Record<string, UiValue>;
@@ -512,6 +614,7 @@ export class ChatUiQueries {
512
614
  const key = JSON.stringify([session.id, item.id, block.id, block.revision, name, args]);
513
615
  const cached = readId === undefined ? undefined : this.cache.get(readId);
514
616
  const snapshot = frozen?.revision === block.revision ? frozen?.readings[name] : undefined;
617
+
515
618
  const reading =
516
619
  readId === undefined
517
620
  ? undefined
@@ -520,11 +623,14 @@ export class ChatUiQueries {
520
623
  : snapshot?.readId === readId && JSON.stringify(args) === JSON.stringify(source.args.parse(uiQueryArguments(block, name)))
521
624
  ? snapshot
522
625
  : undefined;
626
+
523
627
  if (!reading || reading.state !== 'fresh') {
524
628
  throw new UiQueryError('stale-ui-query', 'stale-read', 'Refresh this query before sending its choice.');
525
629
  }
630
+
526
631
  values[name] = reading.value;
527
632
  }
633
+
528
634
  return values;
529
635
  }
530
636
 
@@ -533,19 +639,26 @@ export class ChatUiQueries {
533
639
  if (!this.host.link) {
534
640
  throw new UiQueryError('refused-query', 'links-unsupported', 'This host does not provide UI links.');
535
641
  }
642
+
536
643
  const { item, block } = this.storedBlock(session, payload);
644
+
537
645
  if (!item.uiQueries) {
538
646
  throw new UiQueryError('refused-query', 'access-unavailable', 'The writer’s original access is unavailable.');
539
647
  }
648
+
540
649
  const access = await this.access(session, item);
650
+
541
651
  if (access === null) {
542
652
  throw new UiQueryError('refused-query', 'access-unavailable', 'The writer’s original access is unavailable.');
543
653
  }
654
+
544
655
  const queries = await this.choiceValues(session, payload);
545
656
  const target = uiLinkTargets(block, payload.values, queries)[payload.nodeId];
657
+
546
658
  if (!target) {
547
659
  throw new UiQueryError('refused-query', 'link-unsupported', 'This visible node is not a supported link.');
548
660
  }
661
+
549
662
  return this.linkReading(await this.host.link(session.info, access, target));
550
663
  } catch (error) {
551
664
  return { state: 'plain', ...this.failure(error) };
@@ -555,6 +668,7 @@ export class ChatUiQueries {
555
668
  forget(chatId: string): void {
556
669
  this.lives.get(chatId)?.abort();
557
670
  this.lives.delete(chatId);
671
+
558
672
  for (const [id, cached] of this.cache) {
559
673
  if (cached.chatId === chatId) {
560
674
  this.evict(id);
@@ -565,9 +679,11 @@ export class ChatUiQueries {
565
679
  private evict(readId: string): void {
566
680
  const cached = this.cache.get(readId);
567
681
  this.cache.delete(readId);
682
+
568
683
  if (cached && this.latest.get(cached.key) === readId) {
569
684
  this.latest.delete(cached.key);
570
685
  }
686
+
571
687
  if (cached && this.newest.get(cached.identity) === readId) {
572
688
  this.newest.delete(cached.identity);
573
689
  }
@@ -577,23 +693,29 @@ export class ChatUiQueries {
577
693
  private heldValues(session: ChatSession, item: ChatAssistantItem, block: UiBlock): Record<string, unknown> {
578
694
  const frozen = item.uiQueries?.blocks[block.id];
579
695
  const values: Record<string, unknown> = Object.create(null);
696
+
580
697
  for (const name of Object.keys(block.queries)) {
581
698
  const readId = this.newest.get(JSON.stringify([session.id, item.id, block.id, block.revision, name]));
699
+
582
700
  const reading =
583
701
  (readId === undefined ? undefined : this.cache.get(readId)?.reading) ??
584
702
  (frozen?.revision === block.revision ? frozen?.readings[name] : undefined);
703
+
585
704
  if (reading?.state === 'fresh') {
586
705
  values[name] = reading.value;
587
706
  }
588
707
  }
708
+
589
709
  return values;
590
710
  }
591
711
 
592
712
  private linkReading(value: unknown): ChatUiLinkReading {
593
713
  const reading = UiLinkResolutionSchema.parse(copyUiValue(value, new UiBudget()));
714
+
594
715
  if (reading.state === 'chip' && !reading.target) {
595
716
  throw new Error('The host did not resolve a link target.');
596
717
  }
718
+
597
719
  return reading;
598
720
  }
599
721