@agentshouse/mdruntime 0.1.0-cebcd51f70ed1 → 0.1.0-cedb03eaa48b6

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