@agentshouse/mdruntime 0.1.0-c2aaa9c0e3ca4 → 0.1.0-c2c52158b7381

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