@agentshouse/mdruntime 0.1.0-c7bd7ca3faee4 → 0.1.0-c7dd6612b99d5

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