@agentshouse/mdruntime 0.1.0-c2aaa9c0e3ca4 → 0.1.0-c2e8a6f99866d

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