@agentshouse/mdruntime 0.1.0-cf6041a06849c → 0.1.0-cf6e25bcd52f6

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 +23 -10
  2. package/dist/adapter.d.ts +144 -29
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/attached-model.d.ts +5 -1
  5. package/dist/attached-model.d.ts.map +1 -1
  6. package/dist/attached-model.js +53 -6
  7. package/dist/attempt.d.ts +16 -0
  8. package/dist/attempt.d.ts.map +1 -0
  9. package/dist/attempt.js +70 -0
  10. package/dist/binary.d.ts +18 -0
  11. package/dist/binary.d.ts.map +1 -0
  12. package/dist/binary.js +8 -0
  13. package/dist/bootstrap.d.ts +41 -6
  14. package/dist/bootstrap.d.ts.map +1 -1
  15. package/dist/bootstrap.js +224 -45
  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 +35 -14
  26. package/dist/client.d.ts.map +1 -1
  27. package/dist/client.js +156 -19
  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 +765 -0
  34. package/dist/connection.d.ts +63 -20
  35. package/dist/connection.d.ts.map +1 -1
  36. package/dist/connection.js +3889 -743
  37. package/dist/conversation.d.ts +42 -12
  38. package/dist/conversation.d.ts.map +1 -1
  39. package/dist/conversation.js +642 -188
  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 +19 -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 +368 -0
  64. package/dist/host.d.ts +15 -2
  65. package/dist/host.d.ts.map +1 -1
  66. package/dist/host.js +134 -21
  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 -5
  80. package/dist/memory.d.ts.map +1 -1
  81. package/dist/memory.js +140 -31
  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 +20 -4
  86. package/dist/model.d.ts.map +1 -1
  87. package/dist/model.js +19 -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 -1
  107. package/dist/paths.d.ts.map +1 -1
  108. package/dist/paths.js +4 -16
  109. package/dist/query.d.ts +71 -5
  110. package/dist/query.d.ts.map +1 -1
  111. package/dist/query.js +100 -26
  112. package/dist/reach.d.ts +18 -4
  113. package/dist/reach.d.ts.map +1 -1
  114. package/dist/reach.js +247 -84
  115. package/dist/react.d.ts +31 -19
  116. package/dist/react.d.ts.map +1 -1
  117. package/dist/react.js +13 -21
  118. package/dist/readiness.d.ts +64 -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 +100 -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 +322 -155
  133. package/dist/room-draft.d.ts +41 -9
  134. package/dist/room-draft.d.ts.map +1 -1
  135. package/dist/room-draft.js +186 -37
  136. package/dist/rows.d.ts +39 -34
  137. package/dist/rows.d.ts.map +1 -1
  138. package/dist/rows.js +197 -80
  139. package/dist/save.d.ts +74 -4
  140. package/dist/save.d.ts.map +1 -1
  141. package/dist/save.js +900 -178
  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 +132 -58
  155. package/dist/store.d.ts.map +1 -1
  156. package/dist/store.js +379 -344
  157. package/dist/stored.d.ts +154 -0
  158. package/dist/stored.d.ts.map +1 -0
  159. package/dist/stored.js +578 -0
  160. package/dist/tables.d.ts +362 -0
  161. package/dist/tables.d.ts.map +1 -0
  162. package/dist/tables.js +627 -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 +173 -33
  173. package/dist/wire.d.ts.map +1 -1
  174. package/dist/wire.js +69 -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 -231
  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 -377
@@ -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,160 @@ 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
+ };
227
+ }
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 };
149
234
  }
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;
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
+ const agentWatchers = new Set();
263
+ const unnamed = new Set();
264
+ let agentsKnown = false;
171
265
  let agentNames = new Map();
266
+ let retiredAgents = new Set();
172
267
  let agentsRead = null;
268
+ let agentsFrom;
173
269
  let replicated = new Set();
174
270
  let handedReplicated;
175
271
  function liveOf(conversation) {
176
272
  return lives.get(conversation) ?? QUIET;
177
273
  }
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)) {
274
+ function viewOf(conversation, held, frame) {
189
275
  const draft = drafts.get(conversation);
190
276
  const facts = liveOf(conversation);
191
277
  return {
192
278
  conversation,
193
- messages: shown,
279
+ messages: held.map(({ identity, turn, text, message }) => ({
280
+ identity,
281
+ turn,
282
+ text,
283
+ message,
284
+ })),
285
+ ...frame,
194
286
  turn: structuredClone(facts.turn),
195
287
  questions: structuredClone(facts.questions),
196
288
  process: facts.process === null ? null : { ...facts.process },
197
289
  commands: structuredClone(facts.commands),
198
290
  options: structuredClone(facts.options),
199
- chatDraft: draft === undefined || draft.turn !== null || sending.get(conversation)?.draft === draft
291
+ starting: facts.starting,
292
+ link: facts.link === null ? null : { ...facts.link },
293
+ deliveryUnknown: [...facts.deliveryUnknown],
294
+ chatDraft: draft === undefined || unwritten(draft)
200
295
  ? null
201
- : { text: draft.text, files: [...draft.files] },
296
+ : { text: draft.text, revision: draft.revision },
202
297
  };
203
298
  }
204
- function emit(conversation, messagesChanged = true) {
299
+ function emit(conversation) {
205
300
  if (deps.closed())
206
301
  return;
207
302
  for (const view of views.values()) {
208
- if (view.conversation !== conversation)
303
+ if (view.conversation !== conversation || view.window === null)
209
304
  continue;
210
- if (messagesChanged)
211
- view.messages = null;
212
305
  const ready = [...view.watchers].filter((watcher) => watcher.ready);
213
306
  if (ready.length === 0)
214
307
  continue;
215
- view.messages ??= shownMessages(conversation, view.limit);
216
- const value = viewOf(conversation, view.limit, view.messages);
308
+ const value = viewOf(conversation, view.window, sharedFrame(view, view.window));
217
309
  for (const watcher of ready)
218
310
  watcher.notify(value);
219
311
  }
220
312
  }
313
+ function sharedFrame(view, window) {
314
+ return frameOf(view.asked, window, view.more, view.total);
315
+ }
316
+ async function slide(local, conversation, arriving) {
317
+ let count;
318
+ for (const view of views.values()) {
319
+ if (view.conversation !== conversation || view.window === null)
320
+ continue;
321
+ if (arriving === null) {
322
+ view.window = [];
323
+ view.more = false;
324
+ if (view.total !== null)
325
+ view.total = 0;
326
+ continue;
327
+ }
328
+ const joining = arriving.filter((message) => precedes(message, view.asked.before));
329
+ const next = windowOf(view.window, joining, view.asked.limit);
330
+ view.window = next.messages;
331
+ view.more ||= next.dropped;
332
+ if (view.total !== null && arriving.length > 0) {
333
+ count ??= await local.messageCount(conversation);
334
+ view.total = count;
335
+ }
336
+ }
337
+ }
221
338
  function stateOf(conversation) {
222
339
  const facts = liveOf(conversation);
223
340
  if (facts.questions.length > 0)
@@ -232,15 +349,58 @@ export function createConversationStore(deps) {
232
349
  created: entry.created,
233
350
  finished: entry.finished,
234
351
  outcome: entry.outcome,
352
+ waiting: entry.waiting,
235
353
  state: stateOf(entry.conversation),
236
354
  started: lives.get(entry.conversation)?.turn?.started ?? null,
237
355
  }));
238
356
  }
239
357
  function emitList() {
240
- if (deps.closed() || listWatchers.size === 0)
358
+ if (deps.closed())
241
359
  return;
242
- const value = entries();
243
- for (const watcher of listWatchers)
360
+ if (listWatchers.size > 0) {
361
+ const value = entries();
362
+ for (const watcher of listWatchers)
363
+ watcher(value);
364
+ }
365
+ learned();
366
+ }
367
+ function unlearnable() {
368
+ return !deps.online() || (deps.listed() && deps.agents() === undefined);
369
+ }
370
+ function learned() {
371
+ if (deps.closed())
372
+ return;
373
+ const named = agentsKnown || unlearnable();
374
+ if (unlisted.size > 0 && named && (listKnown || deps.stream() === undefined || unlearnable())) {
375
+ const value = entries();
376
+ for (const watcher of [...unlisted]) {
377
+ unlisted.delete(watcher);
378
+ listWatchers.add(watcher);
379
+ watcher(value);
380
+ }
381
+ }
382
+ if (unnamed.size > 0 && named) {
383
+ const value = agents();
384
+ for (const watcher of [...unnamed]) {
385
+ unnamed.delete(watcher);
386
+ agentWatchers.add(watcher);
387
+ watcher(value);
388
+ }
389
+ }
390
+ }
391
+ function agents() {
392
+ return [...agentNames]
393
+ .filter(([identity]) => !retiredAgents.has(identity))
394
+ .map(([identity, name]) => ({ identity, name }))
395
+ .sort((left, right) => left.name === right.name
396
+ ? left.identity.localeCompare(right.identity)
397
+ : left.name.localeCompare(right.name));
398
+ }
399
+ function emitAgents() {
400
+ if (deps.closed() || agentWatchers.size === 0)
401
+ return;
402
+ const value = agents();
403
+ for (const watcher of agentWatchers)
244
404
  watcher(value);
245
405
  }
246
406
  function setLive(conversation, next) {
@@ -257,22 +417,52 @@ export function createConversationStore(deps) {
257
417
  }
258
418
  async function readAgents() {
259
419
  const declaration = deps.agents();
420
+ agentsFrom = JSON.stringify(declaration);
260
421
  if (declaration === undefined)
261
422
  return;
262
- const { documents } = await deps.documents(declaration.locator);
263
423
  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')
424
+ const retired = new Set();
425
+ let covered = null;
426
+ for (let after;;) {
427
+ const read = await deps.documents(declaration, after);
428
+ if ('pending' in read)
429
+ return;
430
+ const coverage = JSON.stringify(read.coverage);
431
+ if (covered !== null && coverage !== covered)
432
+ return readAgents();
433
+ covered = coverage;
434
+ for (const document of read.documents) {
435
+ const identity = document.fields?.[declaration.identity];
436
+ const name = document.fields?.[declaration.name];
437
+ if (typeof identity !== 'string' || typeof name !== 'string')
438
+ continue;
270
439
  names.set(identity, name);
440
+ const ended = declaration.retired === undefined ? null : document.fields?.[declaration.retired];
441
+ if (ended !== undefined && ended !== null)
442
+ retired.add(identity);
443
+ }
444
+ if (read.next === undefined)
445
+ break;
446
+ after = read.next;
271
447
  }
272
- if (JSON.stringify([...names]) === JSON.stringify([...agentNames]))
448
+ agentsKnown = true;
449
+ const named = JSON.stringify([...names]) !== JSON.stringify([...agentNames]);
450
+ if (!named && JSON.stringify([...retired]) === JSON.stringify([...retiredAgents])) {
451
+ learned();
273
452
  return;
453
+ }
274
454
  agentNames = names;
275
- emitList();
455
+ retiredAgents = retired;
456
+ emitAgents();
457
+ if (named)
458
+ emitList();
459
+ learned();
460
+ const ended = [...drafts.keys()].filter(retiredChat);
461
+ if (ended.length > 0)
462
+ await remove(ended);
463
+ }
464
+ function retiredChat(conversation) {
465
+ return (conversation.startsWith(NEW_CHAT) && retiredAgents.has(conversation.slice(NEW_CHAT.length)));
276
466
  }
277
467
  function agentsReady() {
278
468
  agentsRead ??= readAgents().catch(() => {
@@ -281,8 +471,33 @@ export function createConversationStore(deps) {
281
471
  return agentsRead;
282
472
  }
283
473
  function live(conversation, change) {
474
+ const owed = unsent.get(conversation);
475
+ for (const fact of FACTS.filter((one) => one in change))
476
+ owed?.delete(fact);
284
477
  setLive(conversation, { ...liveOf(conversation), ...change });
285
- emit(conversation, false);
478
+ emit(conversation);
479
+ }
480
+ function openingEnded() {
481
+ const owed = [...unsent];
482
+ unsent.clear();
483
+ for (const [conversation, facts] of owed) {
484
+ const held = lives.get(conversation);
485
+ if (held === undefined || facts.size === 0)
486
+ continue;
487
+ setLive(conversation, {
488
+ turn: held.turn,
489
+ questions: facts.has('questions') ? QUIET.questions : held.questions,
490
+ process: facts.has('process') ? QUIET.process : held.process,
491
+ commands: facts.has('commands') ? QUIET.commands : held.commands,
492
+ options: facts.has('options') ? QUIET.options : held.options,
493
+ starting: facts.has('starting') ? QUIET.starting : held.starting,
494
+ link: facts.has('link') ? QUIET.link : held.link,
495
+ deliveryUnknown: facts.has('deliveryUnknown')
496
+ ? QUIET.deliveryUnknown
497
+ : held.deliveryUnknown,
498
+ });
499
+ emit(conversation);
500
+ }
286
501
  }
287
502
  function relayed(conversation, turn, operation) {
288
503
  const held = liveOf(conversation).turn;
@@ -311,23 +526,22 @@ export function createConversationStore(deps) {
311
526
  async function remove(leaving) {
312
527
  await deps.durably(async (local) => {
313
528
  await local.dropConversations(leaving);
314
- const removed = [];
315
529
  const changed = [];
316
530
  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)) {
531
+ if (stored.has(conversation) || drafts.has(conversation) || lives.has(conversation)) {
321
532
  changed.push(conversation);
322
533
  }
323
- messages.delete(conversation);
534
+ stored.delete(conversation);
324
535
  reads.delete(conversation);
536
+ partials.delete(conversation);
325
537
  drafts.delete(conversation);
326
538
  behind.delete(conversation);
539
+ unsent.delete(conversation);
327
540
  setLive(conversation, null);
541
+ await slide(local, conversation, null);
328
542
  }
329
543
  if (changed.length > 0)
330
- deps.publish(conversationChangeSet(removed, changed));
544
+ deps.publish(conversationChangeSet(changed));
331
545
  });
332
546
  }
333
547
  function gap(conversation) {
@@ -342,32 +556,28 @@ export function createConversationStore(deps) {
342
556
  const advances = read !== undefined &&
343
557
  !behind.has(conversation) &&
344
558
  (read === null || message.position > read);
345
- const sealed = drafts.get(conversation)?.turn === message.turn;
559
+ await sealed(local, conversation, new Set([message.turn]));
346
560
  await local.keepMessages({
347
561
  conversation,
348
562
  messages: [message],
349
- ...(advances ? { read: message.position } : {}),
350
- sealed,
563
+ ...(advances ? { read: { at: message.position, partial: false } } : {}),
351
564
  });
352
- messages.set(conversation, merged(messages.get(conversation) ?? [], [message]));
565
+ stored.add(conversation);
353
566
  if (advances)
354
567
  reads.set(conversation, message.position);
355
- if (sealed)
356
- drafts.delete(conversation);
357
568
  sealTurns(conversation, new Set([message.turn]));
358
- deps.publish(conversationChangeSet([{ conversation, identity: message.identity }], [conversation]));
569
+ await slide(local, conversation, [message]);
570
+ deps.publish(conversationChangeSet([conversation]));
359
571
  });
360
572
  }
361
- async function readHistories(conversations) {
573
+ async function readHistories(conversations, ended) {
362
574
  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) => ({
575
+ let waiting = conversations.map((conversation) => ({
367
576
  conversation,
368
- after: starts.get(conversation) ?? undefined,
577
+ after: reads.get(conversation) ?? undefined,
369
578
  }));
370
- while (asking.length > 0) {
579
+ while (waiting.length > 0) {
580
+ const asking = waiting.slice(0, HISTORY_CONVERSATIONS);
371
581
  const result = await deps.call('conversation.history', {
372
582
  conversations: asking.map(({ conversation, after }) => ({
373
583
  conversation,
@@ -377,46 +587,64 @@ export function createConversationStore(deps) {
377
587
  const pages = historyPages(result);
378
588
  if (pages === null)
379
589
  return result;
380
- const asked = new Set(asking.map(({ conversation }) => conversation));
590
+ const asked = new Map(asking.map(({ conversation, after }) => [conversation, after]));
381
591
  const following = [];
382
592
  for (const page of pages) {
383
593
  if (!asked.has(page.conversation))
384
594
  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
595
+ await keepPage(page.conversation, page.messages, asked.get(page.conversation) ?? null, page.next, generations.get(page.conversation) ?? 0);
596
+ if (page.next !== undefined) {
392
597
  following.push({ conversation: page.conversation, after: page.next });
598
+ }
393
599
  }
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);
600
+ for (const { conversation } of asking) {
601
+ if (!following.some((next) => next.conversation === conversation))
602
+ ended(conversation);
603
+ }
604
+ waiting = [...waiting.slice(HISTORY_CONVERSATIONS), ...following];
400
605
  }
401
606
  return null;
402
607
  }
403
- function keepHistory(conversation, arriving, from, generation) {
608
+ function caughtUp(conversations) {
609
+ const ends = new Map();
610
+ for (const conversation of conversations) {
611
+ const held = new Promise((end) => ends.set(conversation, end));
612
+ catching.set(conversation, held);
613
+ void held.then(() => {
614
+ if (catching.get(conversation) === held)
615
+ catching.delete(conversation);
616
+ });
617
+ }
618
+ const ended = (conversation) => ends.get(conversation)?.();
619
+ const read = readHistories(conversations, ended);
620
+ const endAll = () => {
621
+ for (const conversation of conversations)
622
+ ended(conversation);
623
+ };
624
+ read.then(endAll, endAll);
625
+ return read;
626
+ }
627
+ function keepPage(conversation, arriving, from, next, generation) {
404
628
  return deps.durably(async (local) => {
405
629
  if (!replicated.has(conversation))
406
630
  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);
631
+ const whole = next === undefined;
632
+ const cursor = next ?? newestOf(arriving, from);
633
+ const partial = !whole && (partials.has(conversation) || !reads.has(conversation));
634
+ const sealing = await sealed(local, conversation, new Set(arriving.map((message) => message.turn)));
635
+ await local.keepMessages({ conversation, messages: arriving, read: { at: cursor, partial } });
636
+ stored.add(conversation);
637
+ reads.set(conversation, cursor);
638
+ if (partial)
639
+ partials.add(conversation);
640
+ else
641
+ partials.delete(conversation);
415
642
  sealTurns(conversation, new Set(arriving.map((message) => message.turn)));
416
- if ((gaps.get(conversation) ?? 0) === generation)
643
+ if (whole && (gaps.get(conversation) ?? 0) === generation)
417
644
  behind.delete(conversation);
418
- if (arriving.length > 0 || sealed) {
419
- deps.publish(conversationChangeSet(arriving.map((message) => ({ conversation, identity: message.identity })), [conversation]));
645
+ await slide(local, conversation, arriving);
646
+ if (arriving.length > 0 || sealing || whole) {
647
+ deps.publish(conversationChangeSet([conversation]));
420
648
  }
421
649
  });
422
650
  }
@@ -425,11 +653,8 @@ export function createConversationStore(deps) {
425
653
  if (event.kind === 'gap') {
426
654
  if (replicated.has(conversation))
427
655
  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);
656
+ if (lives.has(conversation))
657
+ unsent.set(conversation, new Set(FACTS));
433
658
  return;
434
659
  }
435
660
  if (event.kind === 'turn') {
@@ -452,82 +677,207 @@ export function createConversationStore(deps) {
452
677
  live(conversation, { options: structuredClone(event.options) });
453
678
  return;
454
679
  }
680
+ if (event.kind === 'starting') {
681
+ live(conversation, { starting: event.step });
682
+ return;
683
+ }
684
+ if (event.kind === 'link') {
685
+ live(conversation, { link: { connected: event.connected } });
686
+ return;
687
+ }
688
+ if (event.kind === 'deliveries') {
689
+ live(conversation, { deliveryUnknown: [...event.unknown] });
690
+ return;
691
+ }
455
692
  const arriving = acceptedMessage(event.message);
456
693
  if (arriving === null)
457
694
  return;
458
695
  if (replicated.has(conversation)) {
459
- void keep(conversation, arriving).catch(() => { });
696
+ if (deps.full()) {
697
+ gap(conversation);
698
+ return;
699
+ }
700
+ void keep(conversation, arriving).catch((error) => {
701
+ if (refusedForSpace(error))
702
+ gap(conversation);
703
+ });
460
704
  return;
461
705
  }
462
706
  if (sealTurns(conversation, new Set([arriving.turn])))
463
707
  emit(conversation);
464
708
  }
709
+ async function kept(local, conversation, following) {
710
+ if (following === null || empty(following)) {
711
+ await local.dropChatDraft(conversation);
712
+ drafts.delete(conversation);
713
+ }
714
+ else {
715
+ await local.keepChatDrafts([following]);
716
+ drafts.set(conversation, following);
717
+ }
718
+ }
719
+ async function sealed(local, conversation, turns) {
720
+ const held = drafts.get(conversation);
721
+ const submitted = held?.submitted ?? [];
722
+ const left = submitted.filter((one) => one.turn === null || !turns.has(one.turn));
723
+ if (held === undefined || left.length === submitted.length)
724
+ return false;
725
+ await kept(local, conversation, withSubmitted(held, left));
726
+ return true;
727
+ }
728
+ function moved(agent, operation, to) {
729
+ const from = newChat(agent);
730
+ return deps.durably(async (local) => {
731
+ const held = drafts.get(from);
732
+ const earlier = (held?.moves ?? []).filter((one) => !deps.expired(one));
733
+ if (earlier.includes(operation))
734
+ return;
735
+ const blank = {
736
+ conversation: from,
737
+ text: '',
738
+ revision: chatDraftRevision(),
739
+ };
740
+ const { moves: _answered, ...unsent } = held ?? blank;
741
+ const taken = drafts.has(to) || empty(unsent) ? [] : [{ ...unsent, conversation: to }];
742
+ const left = { ...(taken.length === 0 ? unsent : blank), moves: [...earlier, operation] };
743
+ await local.keepChatDrafts([left, ...taken]);
744
+ drafts.set(from, left);
745
+ for (const one of taken)
746
+ drafts.set(to, one);
747
+ deps.publish(conversationChangeSet([from, to]));
748
+ });
749
+ }
465
750
  function withDraft(conversation, next) {
466
751
  return deps.durably(async (local) => {
467
- const following = next(drafts.get(conversation));
752
+ const following = await next(drafts.get(conversation), local);
468
753
  if (following === undefined)
469
754
  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]));
755
+ await kept(local, conversation, following);
756
+ deps.publish(conversationChangeSet([conversation]));
757
+ });
758
+ }
759
+ async function submitted(conversation, operation, draft) {
760
+ let submission;
761
+ let earlier = false;
762
+ await withDraft(conversation, (current) => {
763
+ submission = current?.submitted?.find((one) => one.operation === operation);
764
+ earlier = submission !== undefined;
765
+ if (submission !== undefined)
766
+ return undefined;
767
+ const said = draft ?? current;
768
+ submission = { operation, text: said?.text ?? '', turn: null };
769
+ return {
770
+ conversation,
771
+ ...(draft === undefined || current === undefined
772
+ ? { text: '', revision: chatDraftRevision() }
773
+ : { text: current.text, revision: current.revision }),
774
+ submitted: [...(current?.submitted ?? []), submission],
775
+ };
776
+ });
777
+ return { submission: submission, earlier };
778
+ }
779
+ function forgotten(conversation, submission) {
780
+ return withDraft(conversation, (current) => {
781
+ const held = current?.submitted ?? [];
782
+ if (current === undefined || !held.includes(submission))
783
+ return undefined;
784
+ return withSubmitted(current, held.filter((one) => one !== submission));
479
785
  });
480
786
  }
787
+ async function dispatched(conversation, submission) {
788
+ const dispatch = { answered: false };
789
+ sending.set(submission.operation, dispatch);
790
+ try {
791
+ const result = await invoke('conversation.send', conversation, {
792
+ text: submission.text,
793
+ operation_id: submission.operation,
794
+ });
795
+ dispatch.answered = result.kind !== 'transport-uncertainty';
796
+ if (result.kind === 'revoked-authorization')
797
+ return result;
798
+ if (result.kind !== 'action' && result.retryable) {
799
+ deps.unanswered();
800
+ return result;
801
+ }
802
+ const turn = answered(result, 'turn');
803
+ await withDraft(conversation, async (current, local) => {
804
+ const held = current?.submitted?.find((one) => one.operation === submission.operation);
805
+ if (current === undefined || held === undefined || held.turn !== null)
806
+ return undefined;
807
+ const others = current.submitted.filter((one) => one !== held);
808
+ if (result.kind !== 'action')
809
+ return returned(current, held, others);
810
+ const arrived = turn !== null && (await local.turnArrived(conversation, turn));
811
+ return withSubmitted(current, turn === null || arrived
812
+ ? others
813
+ : current.submitted.map((one) => (one === held ? { ...held, turn } : one)));
814
+ });
815
+ return result;
816
+ }
817
+ finally {
818
+ if (sending.get(submission.operation) === dispatch)
819
+ sending.delete(submission.operation);
820
+ }
821
+ }
481
822
  function invoke(name, conversation, extra = {}) {
482
823
  return deps.call(name, { conversation, ...extra });
483
824
  }
484
825
  const surface = {
485
826
  async list() {
486
827
  await agentsReady();
487
- return entries();
828
+ return new Promise((settle) => {
829
+ const answer = (listed) => {
830
+ unlisted.delete(answer);
831
+ listWatchers.delete(answer);
832
+ settle(listed);
833
+ };
834
+ unlisted.add(answer);
835
+ learned();
836
+ });
488
837
  },
489
838
  async read(conversation, options = {}) {
490
- const limit = limitOf(options);
491
- return deps.durably(() => Promise.resolve(viewOf(conversation, limit)));
839
+ const asked = askedOf(options);
840
+ return deps.durably(async (local) => {
841
+ const { messages, more } = await local.messageWindow(conversation, asked.limit, asked.before);
842
+ const total = asked.total ? await local.messageCount(conversation) : null;
843
+ return structuredClone(viewOf(conversation, messages, frameOf(asked, messages, more, total)));
844
+ });
492
845
  },
493
- create(input) {
494
- return deps.call('conversation.create', input);
846
+ async create(input) {
847
+ const result = await deps.call('conversation.create', input);
848
+ const conversation = answered(result, 'conversation');
849
+ if (conversation === null)
850
+ return result;
851
+ if (!listing.has(conversation))
852
+ creations.add(conversation);
853
+ const creation = creationOf(input);
854
+ if (creation !== null)
855
+ await moved(creation.agent, creation.operation, conversation);
856
+ return result;
495
857
  },
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,
858
+ async send(conversation, operation = changeBatchIdentity(), draft) {
859
+ if (deps.full())
860
+ return DURABLE_WRITE;
861
+ if (deps.online()) {
862
+ const held = await submitted(conversation, operation, draft).catch((error) => {
863
+ if (refusedForSpace(error))
864
+ return null;
865
+ throw error;
511
866
  });
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;
867
+ if (held === null)
868
+ return DURABLE_WRITE;
869
+ const { submission, earlier } = held;
870
+ if (!earlier || !deps.expired(operation))
871
+ return dispatched(conversation, submission);
872
+ await forgotten(conversation, submission);
873
+ return EXPIRED;
522
874
  }
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
- }
875
+ if (draft !== undefined) {
876
+ await withDraft(conversation, (current) => current?.submitted?.some((one) => one.operation === operation) === true
877
+ ? undefined
878
+ : returned(current ?? { conversation, text: '', revision: '' }, { operation, ...draft, turn: null }, current?.submitted ?? []));
530
879
  }
880
+ return OFFLINE;
531
881
  },
532
882
  answer(conversation, answer) {
533
883
  return invoke('conversation.answer', conversation, { answer });
@@ -542,7 +892,12 @@ export function createConversationStore(deps) {
542
892
  const result = await invoke('conversation.cancel', conversation, { turn });
543
893
  if (result.kind !== 'action')
544
894
  return result;
545
- await withDraft(conversation, (current) => current === undefined || current.turn !== turn ? undefined : { ...current, turn: null });
895
+ await withDraft(conversation, (current) => {
896
+ const held = current?.submitted?.find((one) => one.turn === turn);
897
+ if (current === undefined || held === undefined)
898
+ return undefined;
899
+ return returned(current, held, current.submitted.filter((one) => one !== held));
900
+ });
546
901
  return result;
547
902
  },
548
903
  delete(conversation) {
@@ -550,33 +905,44 @@ export function createConversationStore(deps) {
550
905
  },
551
906
  };
552
907
  const chatDraft = {
553
- write(conversation, draft) {
554
- return withDraft(conversation, () => ({
908
+ write(conversation, draft, revision = chatDraftRevision()) {
909
+ return withDraft(conversation, (current) => ({
555
910
  conversation,
556
911
  text: draft.text,
557
- files: [...draft.files],
558
- turn: null,
912
+ revision,
913
+ ...(current === undefined ? {} : pending(current)),
914
+ }));
915
+ },
916
+ prepend(conversation, draft) {
917
+ return withDraft(conversation, (current) => ({
918
+ conversation,
919
+ ...(current === undefined ? { text: draft.text } : prepended(current, draft)),
920
+ revision: chatDraftRevision(),
921
+ ...(current === undefined ? {} : pending(current)),
559
922
  }));
560
923
  },
561
924
  discard(conversation) {
562
- return withDraft(conversation, (current) => (current === undefined ? undefined : null));
925
+ return withDraft(conversation, (current) => current === undefined
926
+ ? undefined
927
+ : { conversation, text: '', revision: chatDraftRevision(), ...pending(current) });
563
928
  },
564
929
  };
565
930
  return {
566
931
  surface,
567
932
  chatDraft,
568
- hydrate(held, stored) {
569
- messages.clear();
933
+ hydrate(held, kept) {
934
+ stored.clear();
570
935
  reads.clear();
571
- for (const view of views.values())
572
- view.messages = null;
936
+ partials.clear();
573
937
  for (const entry of held) {
574
- messages.set(entry.conversation, [...entry.messages].sort(ordered));
938
+ stored.add(entry.conversation);
575
939
  if (entry.read !== undefined)
576
940
  reads.set(entry.conversation, entry.read);
941
+ if (entry.partial)
942
+ partials.add(entry.conversation);
577
943
  }
578
944
  drafts.clear();
579
- for (const record of stored)
945
+ for (const record of kept)
580
946
  drafts.set(record.conversation, record);
581
947
  },
582
948
  async reconcile() {
@@ -584,8 +950,10 @@ export function createConversationStore(deps) {
584
950
  return;
585
951
  const named = new Set(listing.keys());
586
952
  const leaving = [
587
- ...new Set([...replicated, ...messages.keys(), ...reads.keys(), ...drafts.keys()]),
588
- ].filter((conversation) => !named.has(conversation));
953
+ ...new Set([...replicated, ...stored, ...reads.keys(), ...drafts.keys()]),
954
+ ].filter((conversation) => !named.has(conversation) &&
955
+ !creations.has(conversation) &&
956
+ (!conversation.startsWith(NEW_CHAT) || retiredChat(conversation)));
589
957
  for (const conversation of named) {
590
958
  if (!replicated.has(conversation))
591
959
  behind.add(conversation);
@@ -596,41 +964,100 @@ export function createConversationStore(deps) {
596
964
  await remove(leaving);
597
965
  },
598
966
  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);
967
+ if (deps.full()) {
968
+ if (behind.size > 0)
969
+ progress.complete = false;
970
+ return;
971
+ }
972
+ const owed = (conversation) => behind.has(conversation) && replicated.has(conversation);
973
+ const read = async (conversations) => {
974
+ if (conversations.length === 0 || deps.closed())
975
+ return;
976
+ const failed = await caughtUp(conversations);
602
977
  if (failed?.kind === 'transport-uncertainty')
603
978
  progress.reachable = false;
604
- }
979
+ };
980
+ const asking = [...behind].filter(owed).sort();
981
+ const awaited = asking.filter((conversation) => catching.has(conversation));
982
+ await Promise.all([
983
+ read(asking.filter((conversation) => !catching.has(conversation))),
984
+ Promise.all(awaited.map((conversation) => catching.get(conversation))).then(() => read(awaited.filter((conversation) => owed(conversation) && !catching.has(conversation)))),
985
+ ]);
605
986
  if (behind.size > 0)
606
987
  progress.complete = false;
607
988
  },
989
+ async resend(progress) {
990
+ for (const [conversation, held] of [...drafts]) {
991
+ for (const submission of held.submitted ?? []) {
992
+ if (submission.turn !== null || sending.has(submission.operation))
993
+ continue;
994
+ if (deps.closed())
995
+ return;
996
+ if (deps.expired(submission.operation)) {
997
+ await forgotten(conversation, submission);
998
+ continue;
999
+ }
1000
+ const result = await dispatched(conversation, submission);
1001
+ if (result.kind === 'revoked-authorization')
1002
+ return;
1003
+ if (result.kind === 'transport-uncertainty') {
1004
+ progress.reachable = false;
1005
+ return;
1006
+ }
1007
+ if (result.kind !== 'action' && result.retryable) {
1008
+ progress.complete = false;
1009
+ return;
1010
+ }
1011
+ }
1012
+ }
1013
+ },
608
1014
  current() {
609
1015
  return behind.size === 0;
610
1016
  },
1017
+ known() {
1018
+ return listKnown;
1019
+ },
611
1020
  reconnected() {
612
1021
  for (const conversation of replicated)
613
1022
  gap(conversation);
614
1023
  },
1024
+ reopened() {
1025
+ for (const [operation, dispatch] of sending)
1026
+ if (!dispatch.answered)
1027
+ sending.delete(operation);
1028
+ },
1029
+ lost() {
1030
+ catching.clear();
1031
+ },
615
1032
  watch(conversation, options, deliver) {
616
- const limit = limitOf(options);
617
- const key = JSON.stringify([conversation, limit ?? null]);
1033
+ const asked = askedOf(options);
1034
+ const key = JSON.stringify([conversation, asked.limit, asked.after ?? null, asked.total]);
618
1035
  const view = views.get(key) ?? {
619
1036
  conversation,
620
- limit,
1037
+ asked,
621
1038
  watchers: new Set(),
622
- messages: null,
1039
+ window: null,
1040
+ more: false,
1041
+ total: null,
623
1042
  };
624
1043
  views.set(key, view);
625
1044
  const watcher = { notify: deliver, ready: false };
626
1045
  view.watchers.add(watcher);
627
1046
  void deps
628
- .durably(() => Promise.resolve())
1047
+ .durably(async (local) => {
1048
+ if (view.window !== null)
1049
+ return;
1050
+ const { messages, more } = await local.messageWindow(conversation, asked.limit, asked.before);
1051
+ view.window = messages;
1052
+ view.more = more;
1053
+ view.total = asked.total ? await local.messageCount(conversation) : null;
1054
+ })
629
1055
  .then(() => {
630
1056
  if (deps.closed() || !view.watchers.has(watcher))
631
1057
  return;
632
1058
  watcher.ready = true;
633
- watcher.notify(viewOf(conversation, limit));
1059
+ const window = view.window ?? [];
1060
+ watcher.notify(viewOf(conversation, window, sharedFrame(view, window)));
634
1061
  })
635
1062
  .catch(() => { });
636
1063
  return () => {
@@ -641,15 +1068,35 @@ export function createConversationStore(deps) {
641
1068
  };
642
1069
  },
643
1070
  watchList(deliver) {
644
- listWatchers.add(deliver);
1071
+ let ended = false;
645
1072
  void agentsReady().then(() => {
646
- if (!deps.closed() && listWatchers.has(deliver))
647
- deliver(entries());
1073
+ if (ended)
1074
+ return;
1075
+ unlisted.add(deliver);
1076
+ learned();
648
1077
  });
649
1078
  return () => {
1079
+ ended = true;
1080
+ unlisted.delete(deliver);
650
1081
  listWatchers.delete(deliver);
651
1082
  };
652
1083
  },
1084
+ watchAgents(deliver) {
1085
+ unnamed.add(deliver);
1086
+ void agentsReady().then(learned);
1087
+ return () => {
1088
+ unnamed.delete(deliver);
1089
+ agentWatchers.delete(deliver);
1090
+ };
1091
+ },
1092
+ learned,
1093
+ declared() {
1094
+ if (JSON.stringify(deps.agents()) === agentsFrom)
1095
+ return;
1096
+ agentsRead = readAgents().catch(() => {
1097
+ agentsRead = null;
1098
+ });
1099
+ },
653
1100
  changed(set) {
654
1101
  for (const conversation of set.conversations)
655
1102
  emit(conversation);
@@ -667,12 +1114,16 @@ export function createConversationStore(deps) {
667
1114
  if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted') {
668
1115
  if (event.kind === 'list') {
669
1116
  listing.clear();
670
- for (const entry of event.entries)
1117
+ for (const entry of event.entries) {
671
1118
  listing.set(entry.conversation, { ...entry });
1119
+ creations.delete(entry.conversation);
1120
+ }
672
1121
  listKnown = true;
1122
+ openingEnded();
673
1123
  }
674
1124
  else if (event.kind === 'listed') {
675
1125
  listing.set(event.entry.conversation, { ...event.entry });
1126
+ creations.delete(event.entry.conversation);
676
1127
  }
677
1128
  else {
678
1129
  listing.delete(event.conversation);
@@ -686,17 +1137,20 @@ export function createConversationStore(deps) {
686
1137
  heard(event);
687
1138
  return false;
688
1139
  },
689
- messages() {
690
- return [...messages].flatMap(([conversation, held]) => held.map(({ identity, text }) => ({ conversation, identity, text })));
1140
+ searchable() {
1141
+ return [...(listKnown ? listing.keys() : stored)].sort();
691
1142
  },
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 };
1143
+ acquired(conversation) {
1144
+ return reads.has(conversation) && !partials.has(conversation);
695
1145
  },
696
1146
  close() {
697
1147
  views.clear();
698
1148
  lives.clear();
1149
+ unsent.clear();
699
1150
  listWatchers.clear();
1151
+ unlisted.clear();
1152
+ agentWatchers.clear();
1153
+ unnamed.clear();
700
1154
  },
701
1155
  };
702
1156
  }