@agentshouse/mdruntime 0.1.0-cfbf80d8d4644 → 0.1.0-cfcf93be6310b

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 (187) hide show
  1. package/README.md +24 -11
  2. package/dist/adapter.d.ts +182 -47
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/attached-model.d.ts +5 -1
  5. package/dist/attached-model.d.ts.map +1 -1
  6. package/dist/attached-model.js +53 -6
  7. package/dist/attempt.d.ts +16 -0
  8. package/dist/attempt.d.ts.map +1 -0
  9. package/dist/attempt.js +70 -0
  10. package/dist/binary.d.ts +18 -0
  11. package/dist/binary.d.ts.map +1 -0
  12. package/dist/binary.js +8 -0
  13. package/dist/bootstrap.d.ts +33 -5
  14. package/dist/bootstrap.d.ts.map +1 -1
  15. package/dist/bootstrap.js +202 -48
  16. package/dist/chain.d.ts +11 -0
  17. package/dist/chain.d.ts.map +1 -0
  18. package/dist/chain.js +64 -0
  19. package/dist/change-set.d.ts +9 -14
  20. package/dist/change-set.d.ts.map +1 -1
  21. package/dist/change-set.js +11 -56
  22. package/dist/change.d.ts +45 -0
  23. package/dist/change.d.ts.map +1 -1
  24. package/dist/change.js +34 -0
  25. package/dist/client.d.ts +38 -17
  26. package/dist/client.d.ts.map +1 -1
  27. package/dist/client.js +156 -20
  28. package/dist/collection.d.ts +13 -14
  29. package/dist/collection.d.ts.map +1 -1
  30. package/dist/collection.js +52 -56
  31. package/dist/compile.d.ts +16 -0
  32. package/dist/compile.d.ts.map +1 -0
  33. package/dist/compile.js +789 -0
  34. package/dist/connection.d.ts +59 -20
  35. package/dist/connection.d.ts.map +1 -1
  36. package/dist/connection.js +4080 -879
  37. package/dist/context.d.ts +2 -0
  38. package/dist/context.d.ts.map +1 -0
  39. package/dist/context.js +7 -0
  40. package/dist/conversation.d.ts +51 -20
  41. package/dist/conversation.d.ts.map +1 -1
  42. package/dist/conversation.js +732 -242
  43. package/dist/digest.d.ts +7 -0
  44. package/dist/digest.d.ts.map +1 -0
  45. package/dist/digest.js +113 -0
  46. package/dist/durability.d.ts +4 -1
  47. package/dist/durability.d.ts.map +1 -1
  48. package/dist/durability.js +5 -0
  49. package/dist/entry.d.ts +20 -0
  50. package/dist/entry.d.ts.map +1 -0
  51. package/dist/entry.js +152 -0
  52. package/dist/expectation.d.ts +21 -0
  53. package/dist/expectation.d.ts.map +1 -0
  54. package/dist/expectation.js +70 -0
  55. package/dist/failure.d.ts +13 -0
  56. package/dist/failure.d.ts.map +1 -0
  57. package/dist/failure.js +19 -0
  58. package/dist/fence.d.ts +6 -0
  59. package/dist/fence.d.ts.map +1 -0
  60. package/dist/fence.js +10 -0
  61. package/dist/file.d.ts +2 -19
  62. package/dist/file.d.ts.map +1 -1
  63. package/dist/file.js +7 -122
  64. package/dist/host.d.ts +11 -2
  65. package/dist/host.d.ts.map +1 -1
  66. package/dist/host.js +128 -24
  67. package/dist/identity.d.ts +4 -0
  68. package/dist/identity.d.ts.map +1 -1
  69. package/dist/identity.js +16 -1
  70. package/dist/index.d.ts +22 -9
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +11 -0
  73. package/dist/job.d.ts +14 -0
  74. package/dist/job.d.ts.map +1 -0
  75. package/dist/job.js +28 -0
  76. package/dist/live.d.ts +23 -0
  77. package/dist/live.d.ts.map +1 -0
  78. package/dist/live.js +127 -0
  79. package/dist/media.d.ts +8 -0
  80. package/dist/media.d.ts.map +1 -0
  81. package/dist/media.js +218 -0
  82. package/dist/memory.d.ts +20 -7
  83. package/dist/memory.d.ts.map +1 -1
  84. package/dist/memory.js +253 -92
  85. package/dist/messages.d.ts +28 -0
  86. package/dist/messages.d.ts.map +1 -0
  87. package/dist/messages.js +89 -0
  88. package/dist/model.d.ts +19 -4
  89. package/dist/model.d.ts.map +1 -1
  90. package/dist/model.js +10 -2
  91. package/dist/name.d.ts +4 -0
  92. package/dist/name.d.ts.map +1 -0
  93. package/dist/name.js +122 -0
  94. package/dist/observation.d.ts +22 -4
  95. package/dist/observation.d.ts.map +1 -1
  96. package/dist/observation.js +120 -14
  97. package/dist/open.d.ts +2 -0
  98. package/dist/open.d.ts.map +1 -0
  99. package/dist/open.js +1 -0
  100. package/dist/opened.d.ts +22 -0
  101. package/dist/opened.d.ts.map +1 -0
  102. package/dist/opened.js +107 -0
  103. package/dist/outcome.d.ts +72 -5
  104. package/dist/outcome.d.ts.map +1 -1
  105. package/dist/outcome.js +81 -1
  106. package/dist/overlay.d.ts +15 -5
  107. package/dist/overlay.d.ts.map +1 -1
  108. package/dist/overlay.js +136 -180
  109. package/dist/paths.d.ts +1 -0
  110. package/dist/paths.d.ts.map +1 -1
  111. package/dist/paths.js +4 -0
  112. package/dist/phrase.d.ts +3 -0
  113. package/dist/phrase.d.ts.map +1 -0
  114. package/dist/phrase.js +8 -0
  115. package/dist/query.d.ts +75 -5
  116. package/dist/query.d.ts.map +1 -1
  117. package/dist/query.js +119 -26
  118. package/dist/reach.d.ts +18 -4
  119. package/dist/reach.d.ts.map +1 -1
  120. package/dist/reach.js +234 -69
  121. package/dist/react.d.ts +32 -19
  122. package/dist/react.d.ts.map +1 -1
  123. package/dist/react.js +13 -21
  124. package/dist/readiness.d.ts +50 -0
  125. package/dist/readiness.d.ts.map +1 -0
  126. package/dist/readiness.js +27 -0
  127. package/dist/recovery.d.ts +26 -0
  128. package/dist/recovery.d.ts.map +1 -0
  129. package/dist/recovery.js +224 -0
  130. package/dist/refresh.d.ts +20 -7
  131. package/dist/refresh.d.ts.map +1 -1
  132. package/dist/refresh.js +167 -59
  133. package/dist/replica.d.ts +59 -33
  134. package/dist/replica.d.ts.map +1 -1
  135. package/dist/replica.js +324 -157
  136. package/dist/room-draft.d.ts +59 -11
  137. package/dist/room-draft.d.ts.map +1 -1
  138. package/dist/room-draft.js +276 -80
  139. package/dist/rows.d.ts +40 -34
  140. package/dist/rows.d.ts.map +1 -1
  141. package/dist/rows.js +202 -80
  142. package/dist/save.d.ts +73 -4
  143. package/dist/save.d.ts.map +1 -1
  144. package/dist/save.js +825 -152
  145. package/dist/search.d.ts +15 -54
  146. package/dist/search.d.ts.map +1 -1
  147. package/dist/search.js +10 -121
  148. package/dist/seed.d.ts +16 -0
  149. package/dist/seed.d.ts.map +1 -0
  150. package/dist/seed.js +75 -0
  151. package/dist/shared.d.ts +15 -0
  152. package/dist/shared.d.ts.map +1 -0
  153. package/dist/shared.js +421 -0
  154. package/dist/status.d.ts +7 -2
  155. package/dist/status.d.ts.map +1 -1
  156. package/dist/status.js +3 -9
  157. package/dist/store.d.ts +118 -59
  158. package/dist/store.d.ts.map +1 -1
  159. package/dist/store.js +354 -344
  160. package/dist/stored.d.ts +162 -0
  161. package/dist/stored.d.ts.map +1 -0
  162. package/dist/stored.js +549 -0
  163. package/dist/tables.d.ts +353 -0
  164. package/dist/tables.d.ts.map +1 -0
  165. package/dist/tables.js +585 -0
  166. package/dist/transfer.d.ts +0 -5
  167. package/dist/transfer.d.ts.map +1 -1
  168. package/dist/turn.d.ts +9 -0
  169. package/dist/turn.d.ts.map +1 -0
  170. package/dist/turn.js +75 -0
  171. package/dist/validation.d.ts +29 -0
  172. package/dist/validation.d.ts.map +1 -0
  173. package/dist/validation.js +104 -0
  174. package/dist/windows.d.ts +22 -0
  175. package/dist/windows.d.ts.map +1 -0
  176. package/dist/windows.js +89 -0
  177. package/dist/wire.d.ts +170 -33
  178. package/dist/wire.d.ts.map +1 -1
  179. package/dist/wire.js +69 -31
  180. package/package.json +26 -15
  181. package/AGENTS.md +0 -232
  182. package/dist/attachment.d.ts +0 -3
  183. package/dist/attachment.d.ts.map +0 -1
  184. package/dist/attachment.js +0 -8
  185. package/dist/read-side.d.ts +0 -18
  186. package/dist/read-side.d.ts.map +0 -1
  187. package/dist/read-side.js +0 -377
@@ -1,12 +1,31 @@
1
1
  import { conversationChangeSet } from './change-set.js';
2
- import { declaredFile } from './file.js';
2
+ import { runtimeError } from './failure.js';
3
+ import { refusedForSpace } from './durability.js';
4
+ import { changeBatchIdentity, chatDraftRevision } from './identity.js';
5
+ import { OFFLINE } from './outcome.js';
6
+ import { untilAnswered } from './recovery.js';
7
+ import { capped, cursorOf, decodedCursor, FROM_START, messageOrder, precedes } from './windows.js';
3
8
  const QUIET = {
4
9
  turn: null,
5
10
  questions: [],
6
11
  process: null,
7
12
  commands: [],
8
13
  options: [],
14
+ starting: null,
15
+ subagents: [],
16
+ link: null,
17
+ deliveryUnknown: [],
9
18
  };
19
+ const FACTS = [
20
+ 'questions',
21
+ 'process',
22
+ 'commands',
23
+ 'options',
24
+ 'starting',
25
+ 'subagents',
26
+ 'link',
27
+ 'deliveryUnknown',
28
+ ];
10
29
  export function unheldView(conversation) {
11
30
  return {
12
31
  conversation,
@@ -14,11 +33,28 @@ export function unheldView(conversation) {
14
33
  turn: null,
15
34
  questions: [],
16
35
  process: null,
36
+ processEnded: null,
17
37
  commands: [],
18
38
  options: [],
39
+ starting: null,
40
+ subagents: [],
41
+ link: null,
42
+ deliveryUnknown: [],
19
43
  chatDraft: null,
20
44
  };
21
45
  }
46
+ export const SESSION_STARTED = 'Session started';
47
+ const NEW_CHAT = 'new:';
48
+ export const HISTORY_CONVERSATIONS = 64;
49
+ export function newChat(agent) {
50
+ return `${NEW_CHAT}${agent}`;
51
+ }
52
+ function creationOf(input) {
53
+ if (typeof input !== 'object' || input === null)
54
+ return null;
55
+ const { agent_id: agent, operation_id: operation } = input;
56
+ return typeof agent === 'string' && typeof operation === 'string' ? { agent, operation } : null;
57
+ }
22
58
  function startedTurn(turn, operation) {
23
59
  if (operation.op === 'begin')
24
60
  return { turn, started: operation.at, parts: [], context: null };
@@ -68,18 +104,28 @@ function appliedTurn(held, operation) {
68
104
  parts: held.parts.map((kept, at) => (at === operation.index ? changed : kept)),
69
105
  };
70
106
  }
71
- function ordered(left, right) {
72
- if (left.position !== right.position)
73
- return left.position < right.position ? -1 : 1;
74
- if (left.identity !== right.identity)
75
- return left.identity < right.identity ? -1 : 1;
76
- return 0;
77
- }
78
- function merged(held, arriving) {
107
+ function windowOf(held, arriving, limit) {
79
108
  const byIdentity = new Map(held.map((message) => [message.identity, message]));
80
109
  for (const message of arriving)
81
110
  byIdentity.set(message.identity, message);
82
- return [...byIdentity.values()].sort(ordered);
111
+ const next = [...byIdentity.values()].sort(messageOrder);
112
+ const start = Math.max(0, next.length - limit);
113
+ return { messages: next.slice(start), dropped: start > 0 };
114
+ }
115
+ function frameOf(asked, messages, more, total) {
116
+ const oldest = messages[0];
117
+ const next = !more
118
+ ? undefined
119
+ : oldest === undefined
120
+ ? (asked.after ?? FROM_START)
121
+ : cursorOf([oldest.position, oldest.identity]);
122
+ return {
123
+ ...(next === undefined ? {} : { next }),
124
+ ...(total === null ? {} : { total }),
125
+ };
126
+ }
127
+ function newestOf(arriving, from) {
128
+ return arriving.reduce((top, message) => (top === null || message.position > top ? message.position : top), from);
83
129
  }
84
130
  function acceptedMessage(value) {
85
131
  if (typeof value !== 'object' || value === null)
@@ -138,86 +184,172 @@ function historyPages(result) {
138
184
  }
139
185
  return pages;
140
186
  }
141
- function turnOf(result) {
187
+ function answered(result, field) {
142
188
  if (result.kind !== 'action')
143
189
  return null;
144
190
  const answer = result.result;
145
191
  if (typeof answer !== 'object' || answer === null)
146
192
  return null;
147
- const record = answer;
148
- return typeof record.turn === 'string' ? record.turn : null;
193
+ const value = answer[field];
194
+ return typeof value === 'string' ? value : null;
195
+ }
196
+ function beforeOf(after) {
197
+ if (after === undefined)
198
+ return null;
199
+ const decoded = typeof after === 'string' ? decodedCursor(after) : null;
200
+ if (decoded?.length === 0)
201
+ return null;
202
+ const [position, identity] = decoded ?? [];
203
+ if (decoded?.length !== 2 || typeof position !== 'string' || typeof identity !== 'string') {
204
+ throw runtimeError('invalid-after', `conversation after must be the next an earlier view returned, got ${String(after)}`);
205
+ }
206
+ return [position, identity];
207
+ }
208
+ function askedOf(options) {
209
+ const { limit, after, total } = options;
210
+ if (limit !== undefined && (!Number.isInteger(limit) || limit < 0)) {
211
+ throw runtimeError('invalid-limit', `conversation limit must be a non-negative integer, got ${String(limit)}`);
212
+ }
213
+ return { limit: capped(limit), after, before: beforeOf(after), total: total === true };
214
+ }
215
+ function phraseOf(text) {
216
+ if (text === undefined)
217
+ return null;
218
+ if (typeof text !== 'string' || text === '') {
219
+ throw runtimeError('invalid-text', 'a conversation is found by a phrase');
220
+ }
221
+ return text;
222
+ }
223
+ const EXPIRED = { kind: 'rejected', code: 'expired', fields: {}, retryable: false };
224
+ const DURABLE_WRITE = {
225
+ kind: 'rejected',
226
+ code: 'durable-write',
227
+ fields: {},
228
+ retryable: true,
229
+ };
230
+ function unwritten(draft) {
231
+ return draft.text === '';
232
+ }
233
+ function pending(draft) {
234
+ return {
235
+ ...(draft.submitted === undefined ? {} : { submitted: draft.submitted }),
236
+ ...(draft.moves === undefined ? {} : { moves: draft.moves }),
237
+ };
238
+ }
239
+ function empty(draft) {
240
+ return unwritten(draft) && draft.submitted === undefined && draft.moves === undefined;
149
241
  }
150
- function limitOf(options) {
151
- const { limit } = options;
152
- if (limit === undefined)
153
- return undefined;
154
- if (!Number.isInteger(limit) || limit < 0) {
155
- throw new Error(`conversation limit must be a non-negative integer, got ${String(limit)}`);
156
- }
157
- return limit;
242
+ function withSubmitted(draft, submitted) {
243
+ const { submitted: _held, ...unsent } = draft;
244
+ return submitted.length === 0 ? unsent : { ...unsent, submitted };
245
+ }
246
+ function prepended(draft, said) {
247
+ return { text: [said.text, draft.text].filter((one) => one !== '').join('\n') };
248
+ }
249
+ function returned(draft, submission, others) {
250
+ return withSubmitted({
251
+ conversation: draft.conversation,
252
+ ...prepended(draft, submission),
253
+ revision: chatDraftRevision(),
254
+ }, others);
158
255
  }
159
256
  export function createConversationStore(deps) {
160
- const messages = new Map();
257
+ const stored = new Set();
161
258
  const reads = new Map();
259
+ const partials = new Set();
162
260
  const drafts = new Map();
163
261
  const sending = new Map();
164
262
  const behind = new Set();
263
+ const catching = new Map();
165
264
  const gaps = new Map();
166
265
  const lives = new Map();
266
+ const unsent = new Map();
167
267
  const views = new Map();
168
268
  const listing = new Map();
269
+ let relisting = null;
270
+ let listCursor = null;
271
+ const creations = new Set();
169
272
  const listWatchers = new Set();
273
+ const unlisted = new Set();
170
274
  let listKnown = false;
171
- let agentNames = new Map();
172
- let agentsRead = null;
275
+ let listHeld = false;
276
+ let listKept = false;
277
+ const agentWatchers = new Set();
278
+ const unnamed = new Set();
279
+ let agentsShown = null;
280
+ let listedAgents = null;
281
+ let agentsReading = false;
173
282
  let replicated = new Set();
174
283
  let handedReplicated;
175
284
  function liveOf(conversation) {
176
285
  return lives.get(conversation) ?? QUIET;
177
286
  }
178
- function shownMessages(conversation, limit) {
179
- const held = messages.get(conversation) ?? [];
180
- const shown = limit === undefined ? held : held.slice(Math.max(0, held.length - limit));
181
- return shown.map(({ identity, turn, text, message }) => ({
182
- identity,
183
- turn,
184
- text,
185
- message: structuredClone(message),
186
- }));
187
- }
188
- function viewOf(conversation, limit, shown = shownMessages(conversation, limit)) {
287
+ function viewOf(conversation, held, frame) {
189
288
  const draft = drafts.get(conversation);
190
289
  const facts = liveOf(conversation);
191
290
  return {
192
291
  conversation,
193
- messages: shown,
292
+ messages: held.map(({ identity, turn, text, message }) => ({
293
+ identity,
294
+ turn,
295
+ text,
296
+ message,
297
+ })),
298
+ ...frame,
194
299
  turn: structuredClone(facts.turn),
195
300
  questions: structuredClone(facts.questions),
196
301
  process: facts.process === null ? null : { ...facts.process },
302
+ processEnded: listing.get(conversation)?.processEnded ?? null,
197
303
  commands: structuredClone(facts.commands),
198
304
  options: structuredClone(facts.options),
199
- chatDraft: draft === undefined || draft.turn !== null || sending.get(conversation)?.draft === draft
305
+ starting: facts.starting,
306
+ subagents: structuredClone(facts.subagents),
307
+ link: facts.link === null ? null : { ...facts.link },
308
+ deliveryUnknown: [...facts.deliveryUnknown],
309
+ chatDraft: draft === undefined || unwritten(draft)
200
310
  ? null
201
- : { text: draft.text, files: [...draft.files] },
311
+ : { text: draft.text, revision: draft.revision },
202
312
  };
203
313
  }
204
- function emit(conversation, messagesChanged = true) {
314
+ function emit(conversation) {
205
315
  if (deps.closed())
206
316
  return;
207
317
  for (const view of views.values()) {
208
- if (view.conversation !== conversation)
318
+ if (view.conversation !== conversation || view.window === null)
209
319
  continue;
210
- if (messagesChanged)
211
- view.messages = null;
212
320
  const ready = [...view.watchers].filter((watcher) => watcher.ready);
213
321
  if (ready.length === 0)
214
322
  continue;
215
- view.messages ??= shownMessages(conversation, view.limit);
216
- const value = viewOf(conversation, view.limit, view.messages);
323
+ const value = viewOf(conversation, view.window, sharedFrame(view, view.window));
217
324
  for (const watcher of ready)
218
325
  watcher.notify(value);
219
326
  }
220
327
  }
328
+ function sharedFrame(view, window) {
329
+ return frameOf(view.asked, window, view.more, view.total);
330
+ }
331
+ async function slide(local, conversation, arriving) {
332
+ let count;
333
+ for (const view of views.values()) {
334
+ if (view.conversation !== conversation || view.window === null)
335
+ continue;
336
+ if (arriving === null) {
337
+ view.window = [];
338
+ view.more = false;
339
+ if (view.total !== null)
340
+ view.total = 0;
341
+ continue;
342
+ }
343
+ const joining = arriving.filter((message) => precedes(message, view.asked.before));
344
+ const next = windowOf(view.window, joining, view.asked.limit);
345
+ view.window = next.messages;
346
+ view.more ||= next.dropped;
347
+ if (view.total !== null && arriving.length > 0) {
348
+ count ??= await local.messageCount(conversation);
349
+ view.total = count;
350
+ }
351
+ }
352
+ }
221
353
  function stateOf(conversation) {
222
354
  const facts = liveOf(conversation);
223
355
  if (facts.questions.length > 0)
@@ -227,20 +359,114 @@ export function createConversationStore(deps) {
227
359
  function entries() {
228
360
  return [...listing.values()].map((entry) => ({
229
361
  conversation: entry.conversation,
230
- agent: { identity: entry.agent, name: agentNames.get(entry.agent) ?? null },
362
+ agent: { identity: entry.agent, name: entry.agentName },
231
363
  name: entry.name,
232
364
  created: entry.created,
233
365
  finished: entry.finished,
234
366
  outcome: entry.outcome,
367
+ waiting: entry.waiting,
235
368
  state: stateOf(entry.conversation),
236
369
  started: lives.get(entry.conversation)?.turn?.started ?? null,
237
370
  }));
238
371
  }
372
+ function keepList() {
373
+ if (!listKnown || listKept)
374
+ return;
375
+ listKept = true;
376
+ queueMicrotask(() => {
377
+ listKept = false;
378
+ if (deps.closed())
379
+ return;
380
+ const entries = [...listing.values()];
381
+ const cursor = listCursor;
382
+ void deps
383
+ .durably(async (local) => {
384
+ await local.fact('conversationList', entries);
385
+ await local.fact('conversationCursor', cursor);
386
+ })
387
+ .catch(() => { });
388
+ });
389
+ }
390
+ function listed(entry) {
391
+ const ended = listing.get(entry.conversation)?.processEnded ?? null;
392
+ listing.set(entry.conversation, { ...entry });
393
+ creations.delete(entry.conversation);
394
+ if (entry.processEnded !== ended)
395
+ emit(entry.conversation);
396
+ }
239
397
  function emitList() {
240
- if (deps.closed() || listWatchers.size === 0)
398
+ if (deps.closed())
399
+ return;
400
+ if (listWatchers.size > 0) {
401
+ const value = entries();
402
+ for (const watcher of listWatchers)
403
+ watcher(value);
404
+ }
405
+ emitAgents();
406
+ learned();
407
+ }
408
+ function learned() {
409
+ if (deps.closed())
410
+ return;
411
+ if (!listKnown && !listHeld && deps.stream() !== undefined && deps.online())
241
412
  return;
242
- const value = entries();
243
- for (const watcher of listWatchers)
413
+ if (unlisted.size > 0) {
414
+ const value = entries();
415
+ for (const watcher of [...unlisted]) {
416
+ unlisted.delete(watcher);
417
+ listWatchers.add(watcher);
418
+ watcher(value);
419
+ }
420
+ }
421
+ if (unnamed.size > 0) {
422
+ const value = agents();
423
+ for (const watcher of [...unnamed]) {
424
+ unnamed.delete(watcher);
425
+ agentWatchers.add(watcher);
426
+ watcher(value);
427
+ }
428
+ }
429
+ }
430
+ function agents() {
431
+ const named = new Map();
432
+ for (const agent of listedAgents ?? [])
433
+ named.set(agent.agent, agent);
434
+ for (const entry of listing.values()) {
435
+ named.set(entry.agent, { name: entry.agentName, retired: entry.agentRetired });
436
+ }
437
+ return [...named]
438
+ .filter(([, agent]) => !agent.retired)
439
+ .map(([identity, agent]) => ({ identity, name: agent.name }))
440
+ .sort((left, right) => left.name === right.name
441
+ ? left.identity.localeCompare(right.identity)
442
+ : left.name.localeCompare(right.name));
443
+ }
444
+ function readAgents() {
445
+ const stream = deps.stream();
446
+ if (stream?.agents === undefined || !deps.online() || agentsReading)
447
+ return;
448
+ agentsReading = true;
449
+ void stream
450
+ .agents()
451
+ .then((read) => {
452
+ if (read === null)
453
+ return;
454
+ listedAgents = read;
455
+ emitAgents();
456
+ }, () => { })
457
+ .finally(() => {
458
+ agentsReading = false;
459
+ });
460
+ }
461
+ function emitAgents() {
462
+ if (deps.closed() || agentWatchers.size === 0)
463
+ return;
464
+ const value = agents();
465
+ const shown = JSON.stringify(value);
466
+ if (shown === agentsShown)
467
+ return;
468
+ agentsShown = shown;
469
+ for (const watcher of agentWatchers)
244
470
  watcher(value);
245
471
  }
246
472
  function setLive(conversation, next) {
@@ -255,34 +481,41 @@ export function createConversationStore(deps) {
255
481
  if (stateOf(conversation) !== before || next?.turn?.started !== began)
256
482
  emitList();
257
483
  }
258
- async function readAgents() {
259
- const declaration = deps.agents();
260
- if (declaration === undefined)
261
- return;
262
- const { documents } = await deps.documents(declaration.locator);
263
- const names = new Map();
264
- for (const document of documents) {
265
- if (document.type !== declaration.type)
266
- continue;
267
- const identity = document.fields?.[declaration.identity];
268
- const name = document.fields?.[declaration.name];
269
- if (typeof identity === 'string' && typeof name === 'string')
270
- names.set(identity, name);
271
- }
272
- if (JSON.stringify([...names]) === JSON.stringify([...agentNames]))
273
- return;
274
- agentNames = names;
275
- emitList();
276
- }
277
- function agentsReady() {
278
- agentsRead ??= readAgents().catch(() => {
279
- agentsRead = null;
280
- });
281
- return agentsRead;
484
+ function retiredChat(conversation) {
485
+ if (!conversation.startsWith(NEW_CHAT))
486
+ return false;
487
+ const agent = conversation.slice(NEW_CHAT.length);
488
+ return [...listing.values()].some((entry) => entry.agent === agent && entry.agentRetired);
282
489
  }
283
490
  function live(conversation, change) {
491
+ const owed = unsent.get(conversation);
492
+ for (const fact of FACTS.filter((one) => one in change))
493
+ owed?.delete(fact);
284
494
  setLive(conversation, { ...liveOf(conversation), ...change });
285
- emit(conversation, false);
495
+ emit(conversation);
496
+ }
497
+ function openingEnded() {
498
+ const owed = [...unsent];
499
+ unsent.clear();
500
+ for (const [conversation, facts] of owed) {
501
+ const held = lives.get(conversation);
502
+ if (held === undefined || facts.size === 0)
503
+ continue;
504
+ setLive(conversation, {
505
+ turn: held.turn,
506
+ questions: facts.has('questions') ? QUIET.questions : held.questions,
507
+ process: facts.has('process') ? QUIET.process : held.process,
508
+ commands: facts.has('commands') ? QUIET.commands : held.commands,
509
+ options: facts.has('options') ? QUIET.options : held.options,
510
+ starting: facts.has('starting') ? QUIET.starting : held.starting,
511
+ subagents: facts.has('subagents') ? QUIET.subagents : held.subagents,
512
+ link: facts.has('link') ? QUIET.link : held.link,
513
+ deliveryUnknown: facts.has('deliveryUnknown')
514
+ ? QUIET.deliveryUnknown
515
+ : held.deliveryUnknown,
516
+ });
517
+ emit(conversation);
518
+ }
286
519
  }
287
520
  function relayed(conversation, turn, operation) {
288
521
  const held = liveOf(conversation).turn;
@@ -311,25 +544,38 @@ export function createConversationStore(deps) {
311
544
  async function remove(leaving) {
312
545
  await deps.durably(async (local) => {
313
546
  await local.dropConversations(leaving);
314
- const removed = [];
315
547
  const changed = [];
316
548
  for (const conversation of leaving) {
317
- for (const message of messages.get(conversation) ?? []) {
318
- removed.push({ conversation, identity: message.identity });
319
- }
320
- if (messages.has(conversation) || drafts.has(conversation) || lives.has(conversation)) {
549
+ if (stored.has(conversation) || drafts.has(conversation) || lives.has(conversation)) {
321
550
  changed.push(conversation);
322
551
  }
323
- messages.delete(conversation);
552
+ stored.delete(conversation);
324
553
  reads.delete(conversation);
554
+ partials.delete(conversation);
325
555
  drafts.delete(conversation);
326
556
  behind.delete(conversation);
557
+ unsent.delete(conversation);
327
558
  setLive(conversation, null);
559
+ await slide(local, conversation, null);
328
560
  }
329
561
  if (changed.length > 0)
330
- deps.publish(conversationChangeSet(removed, changed));
562
+ deps.publish(conversationChangeSet(changed));
331
563
  });
332
564
  }
565
+ function missed(conversation) {
566
+ const read = reads.get(conversation);
567
+ const newest = listing.get(conversation)?.newest;
568
+ return (read === undefined ||
569
+ newest === undefined ||
570
+ partials.has(conversation) ||
571
+ (newest !== null && (read === null || newest > read)));
572
+ }
573
+ function noted(conversation, message) {
574
+ const entry = listing.get(conversation);
575
+ if (entry === undefined || (entry.newest !== null && entry.newest >= message.position))
576
+ return;
577
+ listing.set(conversation, { ...entry, newest: message.position });
578
+ }
333
579
  function gap(conversation) {
334
580
  gaps.set(conversation, (gaps.get(conversation) ?? 0) + 1);
335
581
  behind.add(conversation);
@@ -342,33 +588,29 @@ export function createConversationStore(deps) {
342
588
  const advances = read !== undefined &&
343
589
  !behind.has(conversation) &&
344
590
  (read === null || message.position > read);
345
- const sealed = drafts.get(conversation)?.turn === message.turn;
591
+ await sealed(local, conversation, new Set([message.turn]));
346
592
  await local.keepMessages({
347
593
  conversation,
348
594
  messages: [message],
349
- ...(advances ? { read: message.position } : {}),
350
- sealed,
595
+ ...(advances ? { read: { at: message.position, partial: false } } : {}),
351
596
  });
352
- messages.set(conversation, merged(messages.get(conversation) ?? [], [message]));
597
+ stored.add(conversation);
353
598
  if (advances)
354
599
  reads.set(conversation, message.position);
355
- if (sealed)
356
- drafts.delete(conversation);
357
600
  sealTurns(conversation, new Set([message.turn]));
358
- deps.publish(conversationChangeSet([{ conversation, identity: message.identity }], [conversation]));
601
+ await slide(local, conversation, [message]);
602
+ deps.publish(conversationChangeSet([conversation]));
359
603
  });
360
604
  }
361
- async function readHistories(conversations) {
605
+ async function readHistories(conversations, ended) {
362
606
  const generations = new Map(conversations.map((one) => [one, gaps.get(one) ?? 0]));
363
- const starts = new Map(conversations.map((one) => [one, reads.get(one)]));
364
- const arriving = new Map();
365
- const complete = new Set();
366
- let asking = conversations.map((conversation) => ({
607
+ let waiting = conversations.map((conversation) => ({
367
608
  conversation,
368
- after: starts.get(conversation) ?? undefined,
609
+ after: reads.get(conversation) ?? undefined,
369
610
  }));
370
- while (asking.length > 0) {
371
- const result = await deps.call('conversation.history', {
611
+ while (waiting.length > 0) {
612
+ const asking = waiting.slice(0, HISTORY_CONVERSATIONS);
613
+ const result = await deps.history({
372
614
  conversations: asking.map(({ conversation, after }) => ({
373
615
  conversation,
374
616
  ...(after === undefined ? {} : { after }),
@@ -377,46 +619,64 @@ export function createConversationStore(deps) {
377
619
  const pages = historyPages(result);
378
620
  if (pages === null)
379
621
  return result;
380
- const asked = new Set(asking.map(({ conversation }) => conversation));
622
+ const asked = new Map(asking.map(({ conversation, after }) => [conversation, after]));
381
623
  const following = [];
382
624
  for (const page of pages) {
383
625
  if (!asked.has(page.conversation))
384
626
  continue;
385
- arriving.set(page.conversation, [
386
- ...(arriving.get(page.conversation) ?? []),
387
- ...page.messages,
388
- ]);
389
- if (page.next === undefined)
390
- complete.add(page.conversation);
391
- else
627
+ await keepPage(page.conversation, page.messages, asked.get(page.conversation) ?? null, page.next, generations.get(page.conversation) ?? 0);
628
+ if (page.next !== undefined) {
392
629
  following.push({ conversation: page.conversation, after: page.next });
630
+ }
393
631
  }
394
- asking = following;
395
- }
396
- for (const [conversation, held] of arriving) {
397
- if (!complete.has(conversation))
398
- continue;
399
- await keepHistory(conversation, held, starts.get(conversation), generations.get(conversation) ?? 0);
632
+ for (const { conversation } of asking) {
633
+ if (!following.some((next) => next.conversation === conversation))
634
+ ended(conversation);
635
+ }
636
+ waiting = [...waiting.slice(HISTORY_CONVERSATIONS), ...following];
400
637
  }
401
638
  return null;
402
639
  }
403
- function keepHistory(conversation, arriving, from, generation) {
640
+ function caughtUp(conversations) {
641
+ const ends = new Map();
642
+ for (const conversation of conversations) {
643
+ const held = new Promise((end) => ends.set(conversation, end));
644
+ catching.set(conversation, held);
645
+ void held.then(() => {
646
+ if (catching.get(conversation) === held)
647
+ catching.delete(conversation);
648
+ });
649
+ }
650
+ const ended = (conversation) => ends.get(conversation)?.();
651
+ const read = readHistories(conversations, ended);
652
+ const endAll = () => {
653
+ for (const conversation of conversations)
654
+ ended(conversation);
655
+ };
656
+ read.then(endAll, endAll);
657
+ return read;
658
+ }
659
+ function keepPage(conversation, arriving, from, next, generation) {
404
660
  return deps.durably(async (local) => {
405
661
  if (!replicated.has(conversation))
406
662
  return;
407
- const newest = arriving.reduce((top, message) => (top === null || message.position > top ? message.position : top), from ?? null);
408
- const turn = drafts.get(conversation)?.turn;
409
- const sealed = arriving.some((message) => message.turn === turn);
410
- await local.keepMessages({ conversation, messages: arriving, read: newest, sealed });
411
- messages.set(conversation, merged(messages.get(conversation) ?? [], arriving));
412
- reads.set(conversation, newest);
413
- if (sealed)
414
- drafts.delete(conversation);
663
+ const whole = next === undefined;
664
+ const cursor = next ?? newestOf(arriving, from);
665
+ const partial = !whole && (partials.has(conversation) || !reads.has(conversation));
666
+ const sealing = await sealed(local, conversation, new Set(arriving.map((message) => message.turn)));
667
+ await local.keepMessages({ conversation, messages: arriving, read: { at: cursor, partial } });
668
+ stored.add(conversation);
669
+ reads.set(conversation, cursor);
670
+ if (partial)
671
+ partials.add(conversation);
672
+ else
673
+ partials.delete(conversation);
415
674
  sealTurns(conversation, new Set(arriving.map((message) => message.turn)));
416
- if ((gaps.get(conversation) ?? 0) === generation)
675
+ if (whole && (gaps.get(conversation) ?? 0) === generation)
417
676
  behind.delete(conversation);
418
- if (arriving.length > 0 || sealed) {
419
- deps.publish(conversationChangeSet(arriving.map((message) => ({ conversation, identity: message.identity })), [conversation]));
677
+ await slide(local, conversation, arriving);
678
+ if (arriving.length > 0 || sealing || whole) {
679
+ deps.publish(conversationChangeSet([conversation]));
420
680
  }
421
681
  });
422
682
  }
@@ -425,11 +685,8 @@ export function createConversationStore(deps) {
425
685
  if (event.kind === 'gap') {
426
686
  if (replicated.has(conversation))
427
687
  gap(conversation);
428
- const held = lives.get(conversation);
429
- if (held === undefined)
430
- return;
431
- setLive(conversation, { ...QUIET, turn: held.turn });
432
- emit(conversation, false);
688
+ if (lives.has(conversation))
689
+ unsent.set(conversation, new Set(FACTS));
433
690
  return;
434
691
  }
435
692
  if (event.kind === 'turn') {
@@ -452,142 +709,292 @@ export function createConversationStore(deps) {
452
709
  live(conversation, { options: structuredClone(event.options) });
453
710
  return;
454
711
  }
712
+ if (event.kind === 'starting') {
713
+ live(conversation, { starting: event.step });
714
+ return;
715
+ }
716
+ if (event.kind === 'subagents') {
717
+ live(conversation, { subagents: structuredClone(event.subagents) });
718
+ return;
719
+ }
720
+ if (event.kind === 'link') {
721
+ live(conversation, { link: { connected: event.connected } });
722
+ return;
723
+ }
724
+ if (event.kind === 'deliveries') {
725
+ live(conversation, { deliveryUnknown: [...event.unknown] });
726
+ return;
727
+ }
455
728
  const arriving = acceptedMessage(event.message);
456
729
  if (arriving === null)
457
730
  return;
731
+ noted(conversation, arriving);
458
732
  if (replicated.has(conversation)) {
459
- void keep(conversation, arriving).catch(() => { });
733
+ if (deps.full()) {
734
+ gap(conversation);
735
+ return;
736
+ }
737
+ void keep(conversation, arriving).catch((error) => {
738
+ if (refusedForSpace(error))
739
+ gap(conversation);
740
+ });
460
741
  return;
461
742
  }
462
743
  if (sealTurns(conversation, new Set([arriving.turn])))
463
744
  emit(conversation);
464
745
  }
746
+ async function kept(local, conversation, following) {
747
+ if (following === null || empty(following)) {
748
+ await local.dropChatDraft(conversation);
749
+ drafts.delete(conversation);
750
+ }
751
+ else {
752
+ await local.keepChatDrafts([following]);
753
+ drafts.set(conversation, following);
754
+ }
755
+ }
756
+ async function sealed(local, conversation, turns) {
757
+ const held = drafts.get(conversation);
758
+ const submitted = held?.submitted ?? [];
759
+ const left = submitted.filter((one) => one.turn === null || !turns.has(one.turn));
760
+ if (held === undefined || left.length === submitted.length)
761
+ return false;
762
+ await kept(local, conversation, withSubmitted(held, left));
763
+ return true;
764
+ }
765
+ function moved(agent, operation, to) {
766
+ const from = newChat(agent);
767
+ return deps.durably(async (local) => {
768
+ const held = drafts.get(from);
769
+ const earlier = (held?.moves ?? []).filter((one) => !deps.expired(one));
770
+ if (earlier.includes(operation))
771
+ return;
772
+ const blank = {
773
+ conversation: from,
774
+ text: '',
775
+ revision: chatDraftRevision(),
776
+ };
777
+ const { moves: _answered, ...unsent } = held ?? blank;
778
+ const taken = drafts.has(to) || empty(unsent) ? [] : [{ ...unsent, conversation: to }];
779
+ const left = { ...(taken.length === 0 ? unsent : blank), moves: [...earlier, operation] };
780
+ await local.keepChatDrafts([left, ...taken]);
781
+ drafts.set(from, left);
782
+ for (const one of taken)
783
+ drafts.set(to, one);
784
+ deps.publish(conversationChangeSet([from, to]));
785
+ });
786
+ }
465
787
  function withDraft(conversation, next) {
466
788
  return deps.durably(async (local) => {
467
- const following = next(drafts.get(conversation));
789
+ const following = await next(drafts.get(conversation), local);
468
790
  if (following === undefined)
469
791
  return;
470
- if (following === null) {
471
- await local.dropChatDraft(conversation);
472
- drafts.delete(conversation);
473
- }
474
- else {
475
- await local.keepChatDraft(following);
476
- drafts.set(conversation, following);
477
- }
478
- deps.publish(conversationChangeSet([], [conversation]));
792
+ await kept(local, conversation, following);
793
+ deps.publish(conversationChangeSet([conversation]));
794
+ });
795
+ }
796
+ async function submitted(conversation, operation, draft) {
797
+ let submission;
798
+ let earlier = false;
799
+ await withDraft(conversation, (current) => {
800
+ submission = current?.submitted?.find((one) => one.operation === operation);
801
+ earlier = submission !== undefined;
802
+ if (submission !== undefined)
803
+ return undefined;
804
+ const said = draft ?? current;
805
+ submission = { operation, text: said?.text ?? '', turn: null };
806
+ return {
807
+ conversation,
808
+ ...(draft === undefined || current === undefined
809
+ ? { text: '', revision: chatDraftRevision() }
810
+ : { text: current.text, revision: current.revision }),
811
+ submitted: [...(current?.submitted ?? []), submission],
812
+ };
479
813
  });
814
+ return { submission: submission, earlier };
815
+ }
816
+ function forgotten(conversation, submission) {
817
+ return withDraft(conversation, (current) => {
818
+ const held = current?.submitted ?? [];
819
+ if (current === undefined || !held.includes(submission))
820
+ return undefined;
821
+ return withSubmitted(current, held.filter((one) => one !== submission));
822
+ });
823
+ }
824
+ async function dispatched(conversation, submission) {
825
+ const dispatch = { answered: false };
826
+ sending.set(submission.operation, dispatch);
827
+ try {
828
+ const result = await invoke('conversations/send', conversation, {
829
+ operation_id: submission.operation,
830
+ text: submission.text,
831
+ });
832
+ dispatch.answered = result.kind !== 'transport-uncertainty';
833
+ if (result.kind === 'revoked-authorization')
834
+ return result;
835
+ if (result.kind !== 'action' && result.retryable) {
836
+ deps.unanswered();
837
+ return result;
838
+ }
839
+ const turn = answered(result, 'message_id');
840
+ await withDraft(conversation, async (current, local) => {
841
+ const held = current?.submitted?.find((one) => one.operation === submission.operation);
842
+ if (current === undefined || held === undefined || held.turn !== null)
843
+ return undefined;
844
+ const others = current.submitted.filter((one) => one !== held);
845
+ if (result.kind !== 'action')
846
+ return returned(current, held, others);
847
+ const arrived = turn !== null && (await local.turnArrived(conversation, turn));
848
+ return withSubmitted(current, turn === null || arrived
849
+ ? others
850
+ : current.submitted.map((one) => (one === held ? { ...held, turn } : one)));
851
+ });
852
+ return result;
853
+ }
854
+ finally {
855
+ if (sending.get(submission.operation) === dispatch)
856
+ sending.delete(submission.operation);
857
+ }
480
858
  }
481
- function invoke(name, conversation, extra = {}) {
482
- return deps.call(name, { conversation, ...extra });
859
+ function invoke(name, conversation, extra = { operation_id: changeBatchIdentity() }) {
860
+ return deps.call(name, { conversation_id: conversation, ...extra });
483
861
  }
484
862
  const surface = {
485
- async list() {
486
- await agentsReady();
487
- return entries();
863
+ list() {
864
+ return new Promise((settle) => {
865
+ const answer = (listed) => {
866
+ unlisted.delete(answer);
867
+ listWatchers.delete(answer);
868
+ settle(listed);
869
+ };
870
+ unlisted.add(answer);
871
+ learned();
872
+ });
488
873
  },
489
874
  async read(conversation, options = {}) {
490
- const limit = limitOf(options);
491
- return deps.durably(() => Promise.resolve(viewOf(conversation, limit)));
875
+ const { text, ...windowed } = options;
876
+ const asked = askedOf(windowed);
877
+ const phrase = phraseOf(text);
878
+ return deps.durably(async (local) => {
879
+ const { messages, more } = await local.messageWindow(conversation, asked.limit, asked.before, phrase);
880
+ const total = asked.total ? await local.messageCount(conversation, phrase) : null;
881
+ return structuredClone(viewOf(conversation, messages, frameOf(asked, messages, more, total)));
882
+ });
492
883
  },
493
- create(input) {
494
- return deps.call('conversation.create', input);
884
+ async create(input) {
885
+ const result = await deps.call('conversations/create', input);
886
+ const conversation = answered(result, 'conversation_id');
887
+ if (conversation === null)
888
+ return result;
889
+ if (!listing.has(conversation))
890
+ creations.add(conversation);
891
+ const creation = creationOf(input);
892
+ if (creation !== null)
893
+ await moved(creation.agent, creation.operation, conversation);
894
+ return result;
495
895
  },
496
- async send(conversation) {
497
- const draft = drafts.get(conversation);
498
- const pending = draft === undefined ? undefined : { draft };
499
- if (pending !== undefined) {
500
- sending.set(conversation, pending);
501
- deps.publish(conversationChangeSet([], [conversation]));
502
- }
503
- try {
504
- const files = await Promise.all((draft?.files ?? []).map(async (file) => ({
505
- ...(await declaredFile(file)),
506
- blob: file.blob,
507
- })));
508
- const result = await invoke('conversation.send', conversation, {
509
- text: draft?.text ?? '',
510
- files,
511
- });
512
- const turn = turnOf(result);
513
- if (turn === null || draft === undefined)
514
- return result;
515
- await withDraft(conversation, (current) => {
516
- if (current !== draft)
517
- return undefined;
518
- const sealed = (messages.get(conversation) ?? []).some((message) => message.turn === turn);
519
- return sealed ? null : { ...draft, turn };
896
+ async send(conversation, operation = changeBatchIdentity(), draft) {
897
+ if (deps.full())
898
+ return DURABLE_WRITE;
899
+ if (deps.online()) {
900
+ const held = await submitted(conversation, operation, draft).catch((error) => {
901
+ if (refusedForSpace(error))
902
+ return null;
903
+ throw error;
520
904
  });
521
- return result;
905
+ if (held === null)
906
+ return DURABLE_WRITE;
907
+ const { submission, earlier } = held;
908
+ if (!earlier || !deps.expired(operation))
909
+ return dispatched(conversation, submission);
910
+ await forgotten(conversation, submission);
911
+ return EXPIRED;
522
912
  }
523
- finally {
524
- if (pending !== undefined && sending.get(conversation) === pending) {
525
- sending.delete(conversation);
526
- if (drafts.get(conversation) === draft) {
527
- deps.publish(conversationChangeSet([], [conversation]));
528
- }
529
- }
913
+ if (draft !== undefined) {
914
+ await withDraft(conversation, (current) => current?.submitted?.some((one) => one.operation === operation) === true
915
+ ? undefined
916
+ : returned(current ?? { conversation, text: '', revision: '' }, { operation, ...draft, turn: null }, current?.submitted ?? []));
530
917
  }
918
+ return OFFLINE;
531
919
  },
532
920
  answer(conversation, answer) {
533
- return invoke('conversation.answer', conversation, { answer });
921
+ return invoke('conversations/interaction/answer', conversation, {
922
+ operation_id: changeBatchIdentity(),
923
+ ...answer,
924
+ });
534
925
  },
535
926
  interrupt(conversation) {
536
- return invoke('conversation.interrupt', conversation);
927
+ return invoke('conversations/interrupt', conversation);
537
928
  },
538
929
  kill(conversation) {
539
- return invoke('conversation.kill', conversation);
540
- },
541
- async cancel(conversation, turn) {
542
- const result = await invoke('conversation.cancel', conversation, { turn });
543
- if (result.kind !== 'action')
544
- return result;
545
- await withDraft(conversation, (current) => current === undefined || current.turn !== turn ? undefined : { ...current, turn: null });
546
- return result;
930
+ return invoke('conversations/kill', conversation);
547
931
  },
548
932
  delete(conversation) {
549
- return invoke('conversation.delete', conversation);
933
+ return invoke('conversations/delete', conversation);
550
934
  },
551
935
  };
552
936
  const chatDraft = {
553
- write(conversation, draft) {
554
- return withDraft(conversation, () => ({
937
+ write(conversation, draft, revision = chatDraftRevision()) {
938
+ return withDraft(conversation, (current) => ({
555
939
  conversation,
556
940
  text: draft.text,
557
- files: [...draft.files],
558
- turn: null,
941
+ revision,
942
+ ...(current === undefined ? {} : pending(current)),
943
+ }));
944
+ },
945
+ prepend(conversation, draft) {
946
+ return withDraft(conversation, (current) => ({
947
+ conversation,
948
+ ...(current === undefined ? { text: draft.text } : prepended(current, draft)),
949
+ revision: chatDraftRevision(),
950
+ ...(current === undefined ? {} : pending(current)),
559
951
  }));
560
952
  },
561
953
  discard(conversation) {
562
- return withDraft(conversation, (current) => (current === undefined ? undefined : null));
954
+ return withDraft(conversation, (current) => current === undefined
955
+ ? undefined
956
+ : { conversation, text: '', revision: chatDraftRevision(), ...pending(current) });
563
957
  },
564
958
  };
565
959
  return {
566
960
  surface,
567
961
  chatDraft,
568
- hydrate(held, stored) {
569
- messages.clear();
962
+ hydrate(held, kept, list, cursor) {
963
+ if (list !== null && !listKnown) {
964
+ listing.clear();
965
+ for (const entry of list)
966
+ listing.set(entry.conversation, { ...entry });
967
+ listHeld = true;
968
+ listCursor = cursor;
969
+ deps.stream()?.hold(cursor);
970
+ }
971
+ stored.clear();
570
972
  reads.clear();
571
- for (const view of views.values())
572
- view.messages = null;
973
+ partials.clear();
573
974
  for (const entry of held) {
574
- messages.set(entry.conversation, [...entry.messages].sort(ordered));
975
+ stored.add(entry.conversation);
575
976
  if (entry.read !== undefined)
576
977
  reads.set(entry.conversation, entry.read);
978
+ if (entry.partial)
979
+ partials.add(entry.conversation);
577
980
  }
578
981
  drafts.clear();
579
- for (const record of stored)
982
+ for (const record of kept)
580
983
  drafts.set(record.conversation, record);
984
+ if (listHeld)
985
+ learned();
581
986
  },
582
987
  async reconcile() {
583
988
  if (!listKnown)
584
989
  return;
585
990
  const named = new Set(listing.keys());
586
991
  const leaving = [
587
- ...new Set([...replicated, ...messages.keys(), ...reads.keys(), ...drafts.keys()]),
588
- ].filter((conversation) => !named.has(conversation));
992
+ ...new Set([...replicated, ...stored, ...reads.keys(), ...drafts.keys()]),
993
+ ].filter((conversation) => !named.has(conversation) &&
994
+ !creations.has(conversation) &&
995
+ (!conversation.startsWith(NEW_CHAT) || retiredChat(conversation)));
589
996
  for (const conversation of named) {
590
- if (!replicated.has(conversation))
997
+ if (!replicated.has(conversation) && missed(conversation))
591
998
  behind.add(conversation);
592
999
  }
593
1000
  replicated = named;
@@ -596,44 +1003,101 @@ export function createConversationStore(deps) {
596
1003
  await remove(leaving);
597
1004
  },
598
1005
  async catchUp(progress) {
599
- const reading = [...behind].filter((conversation) => replicated.has(conversation)).sort();
600
- if (reading.length > 0 && !deps.closed()) {
601
- const failed = await readHistories(reading);
1006
+ if (deps.full()) {
1007
+ if (behind.size > 0)
1008
+ progress.complete = false;
1009
+ return;
1010
+ }
1011
+ const owed = (conversation) => behind.has(conversation) && replicated.has(conversation);
1012
+ const read = async (conversations) => {
1013
+ if (conversations.length === 0 || deps.closed())
1014
+ return;
1015
+ const failed = await caughtUp(conversations);
602
1016
  if (failed?.kind === 'transport-uncertainty')
603
1017
  progress.reachable = false;
604
- }
1018
+ };
1019
+ const asking = [...behind].filter(owed).sort();
1020
+ const awaited = asking.filter((conversation) => catching.has(conversation));
1021
+ await Promise.all([
1022
+ read(asking.filter((conversation) => !catching.has(conversation))),
1023
+ Promise.all(awaited.map((conversation) => catching.get(conversation))).then(() => read(awaited.filter((conversation) => owed(conversation) && !catching.has(conversation)))),
1024
+ ]);
605
1025
  if (behind.size > 0)
606
1026
  progress.complete = false;
607
1027
  },
1028
+ async resend(progress) {
1029
+ for (const [conversation, held] of [...drafts]) {
1030
+ for (const submission of held.submitted ?? []) {
1031
+ if (submission.turn !== null || sending.has(submission.operation))
1032
+ continue;
1033
+ if (deps.closed())
1034
+ return;
1035
+ if (deps.expired(submission.operation)) {
1036
+ await forgotten(conversation, submission);
1037
+ continue;
1038
+ }
1039
+ const result = await dispatched(conversation, submission);
1040
+ if (result.kind === 'revoked-authorization')
1041
+ return;
1042
+ if (result.kind === 'transport-uncertainty') {
1043
+ progress.reachable = false;
1044
+ return;
1045
+ }
1046
+ if (result.kind !== 'action' && result.retryable) {
1047
+ progress.complete = false;
1048
+ return;
1049
+ }
1050
+ }
1051
+ }
1052
+ },
608
1053
  current() {
609
1054
  return behind.size === 0;
610
1055
  },
611
- reconnected() {
612
- for (const conversation of replicated)
613
- gap(conversation);
1056
+ known() {
1057
+ return listKnown;
1058
+ },
1059
+ reopened() {
1060
+ relisting = null;
1061
+ for (const [operation, dispatch] of sending)
1062
+ if (!dispatch.answered)
1063
+ sending.delete(operation);
1064
+ },
1065
+ lost() {
1066
+ catching.clear();
614
1067
  },
615
1068
  watch(conversation, options, deliver) {
616
- const limit = limitOf(options);
617
- const key = JSON.stringify([conversation, limit ?? null]);
1069
+ const asked = askedOf(options);
1070
+ const key = JSON.stringify([conversation, asked.limit, asked.after ?? null, asked.total]);
618
1071
  const view = views.get(key) ?? {
619
1072
  conversation,
620
- limit,
1073
+ asked,
621
1074
  watchers: new Set(),
622
- messages: null,
1075
+ window: null,
1076
+ more: false,
1077
+ total: null,
623
1078
  };
624
1079
  views.set(key, view);
625
1080
  const watcher = { notify: deliver, ready: false };
626
1081
  view.watchers.add(watcher);
627
- void deps
628
- .durably(() => Promise.resolve())
1082
+ const reading = new AbortController();
1083
+ void untilAnswered(() => deps.durably(async (local) => {
1084
+ if (view.window !== null)
1085
+ return;
1086
+ const { messages, more } = await local.messageWindow(conversation, asked.limit, asked.before);
1087
+ view.window = messages;
1088
+ view.more = more;
1089
+ view.total = asked.total ? await local.messageCount(conversation) : null;
1090
+ }), reading.signal)
629
1091
  .then(() => {
630
1092
  if (deps.closed() || !view.watchers.has(watcher))
631
1093
  return;
632
1094
  watcher.ready = true;
633
- watcher.notify(viewOf(conversation, limit));
1095
+ const window = view.window ?? [];
1096
+ watcher.notify(viewOf(conversation, window, sharedFrame(view, window)));
634
1097
  })
635
1098
  .catch(() => { });
636
1099
  return () => {
1100
+ reading.abort();
637
1101
  if (!view.watchers.delete(watcher))
638
1102
  return;
639
1103
  if (view.watchers.size === 0)
@@ -641,62 +1105,88 @@ export function createConversationStore(deps) {
641
1105
  };
642
1106
  },
643
1107
  watchList(deliver) {
644
- listWatchers.add(deliver);
645
- void agentsReady().then(() => {
646
- if (!deps.closed() && listWatchers.has(deliver))
647
- deliver(entries());
648
- });
1108
+ unlisted.add(deliver);
1109
+ learned();
649
1110
  return () => {
1111
+ unlisted.delete(deliver);
650
1112
  listWatchers.delete(deliver);
651
1113
  };
652
1114
  },
1115
+ watchAgents(deliver) {
1116
+ unnamed.add(deliver);
1117
+ learned();
1118
+ readAgents();
1119
+ return () => {
1120
+ unnamed.delete(deliver);
1121
+ agentWatchers.delete(deliver);
1122
+ };
1123
+ },
1124
+ learned,
653
1125
  changed(set) {
654
1126
  for (const conversation of set.conversations)
655
1127
  emit(conversation);
656
- const declaration = deps.agents();
657
- if (declaration !== undefined &&
658
- set.documents.some((change) => change.locator === declaration.locator)) {
659
- agentsRead = readAgents().catch(() => {
660
- agentsRead = null;
661
- });
662
- }
663
1128
  },
664
1129
  receive(event) {
665
1130
  if (deps.closed())
666
1131
  return false;
667
1132
  if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted') {
668
1133
  if (event.kind === 'list') {
669
- listing.clear();
670
- for (const entry of event.entries)
671
- listing.set(entry.conversation, { ...entry });
1134
+ if (event.whole)
1135
+ relisting ??= new Map();
1136
+ for (const entry of event.entries) {
1137
+ if (relisting === null)
1138
+ listed(entry);
1139
+ else
1140
+ relisting.set(entry.conversation, { ...entry });
1141
+ }
1142
+ if (event.more)
1143
+ return false;
1144
+ if (relisting !== null) {
1145
+ listing.clear();
1146
+ for (const entry of relisting.values())
1147
+ listed(entry);
1148
+ relisting = null;
1149
+ }
1150
+ listCursor = event.cursor;
1151
+ deps.stream()?.hold(event.cursor);
672
1152
  listKnown = true;
1153
+ for (const conversation of replicated) {
1154
+ if (!listing.has(conversation))
1155
+ continue;
1156
+ if (!missed(conversation))
1157
+ behind.delete(conversation);
1158
+ else if (!behind.has(conversation))
1159
+ gap(conversation);
1160
+ }
1161
+ openingEnded();
673
1162
  }
674
1163
  else if (event.kind === 'listed') {
675
- listing.set(event.entry.conversation, { ...event.entry });
1164
+ listed(event.entry);
1165
+ relisting?.set(event.entry.conversation, { ...event.entry });
676
1166
  }
677
1167
  else {
678
1168
  listing.delete(event.conversation);
1169
+ relisting?.delete(event.conversation);
679
1170
  }
1171
+ keepList();
680
1172
  emitList();
681
- return (event.kind !== 'list' &&
1173
+ return ((event.kind !== 'list' &&
682
1174
  listKnown &&
683
1175
  (listing.size !== replicated.size ||
684
- [...listing.keys()].some((conversation) => !replicated.has(conversation))));
1176
+ [...listing.keys()].some((conversation) => !replicated.has(conversation)))) ||
1177
+ (listKnown && [...drafts.keys()].some(retiredChat)));
685
1178
  }
686
1179
  heard(event);
687
1180
  return false;
688
1181
  },
689
- messages() {
690
- return [...messages].flatMap(([conversation, held]) => held.map(({ identity, text }) => ({ conversation, identity, text })));
691
- },
692
- message(conversation, identity) {
693
- const held = messages.get(conversation)?.find((message) => message.identity === identity);
694
- return held === undefined ? null : { conversation, identity, text: held.text };
695
- },
696
1182
  close() {
697
1183
  views.clear();
698
1184
  lives.clear();
1185
+ unsent.clear();
699
1186
  listWatchers.clear();
1187
+ unlisted.clear();
1188
+ agentWatchers.clear();
1189
+ unnamed.clear();
700
1190
  },
701
1191
  };
702
1192
  }