@agentshouse/mdruntime 0.1.0-c7274d650bd05 → 0.1.0-c73e437bf1616

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