@agentshouse/mdruntime 0.1.0-cede5a74284be → 0.1.0-cee9c8aa4cd86

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 (223) hide show
  1. package/AGENTS.md +294 -292
  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 -14
  17. package/dist/change-set.d.ts.map +1 -1
  18. package/dist/change-set.js +11 -63
  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 +75 -13
  46. package/dist/chat/messages.d.ts +15 -1
  47. package/dist/chat/messages.d.ts.map +1 -1
  48. package/dist/chat/messages.js +82 -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 +9 -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 +16 -0
  91. package/dist/compile.d.ts.map +1 -0
  92. package/dist/compile.js +718 -0
  93. package/dist/connection.d.ts +41 -13
  94. package/dist/connection.d.ts.map +1 -1
  95. package/dist/connection.js +3120 -702
  96. package/dist/conversation.d.ts +28 -9
  97. package/dist/conversation.d.ts.map +1 -1
  98. package/dist/conversation.js +513 -178
  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/fulltext.d.ts +27 -0
  114. package/dist/fulltext.d.ts.map +1 -0
  115. package/dist/fulltext.js +368 -0
  116. package/dist/host.d.ts +14 -3
  117. package/dist/host.d.ts.map +1 -1
  118. package/dist/host.js +102 -13
  119. package/dist/identity.d.ts +1 -0
  120. package/dist/identity.d.ts.map +1 -1
  121. package/dist/identity.js +3 -0
  122. package/dist/index.d.ts +15 -9
  123. package/dist/index.d.ts.map +1 -1
  124. package/dist/index.js +5 -0
  125. package/dist/live.d.ts +22 -0
  126. package/dist/live.d.ts.map +1 -0
  127. package/dist/live.js +123 -0
  128. package/dist/memory.d.ts +9 -2
  129. package/dist/memory.d.ts.map +1 -1
  130. package/dist/memory.js +55 -12
  131. package/dist/messages.d.ts +28 -0
  132. package/dist/messages.d.ts.map +1 -0
  133. package/dist/messages.js +90 -0
  134. package/dist/model.d.ts +4 -2
  135. package/dist/model.d.ts.map +1 -1
  136. package/dist/model.js +10 -1
  137. package/dist/name.d.ts +4 -0
  138. package/dist/name.d.ts.map +1 -0
  139. package/dist/name.js +122 -0
  140. package/dist/observation.d.ts +19 -2
  141. package/dist/observation.d.ts.map +1 -1
  142. package/dist/observation.js +113 -12
  143. package/dist/opened.d.ts +22 -0
  144. package/dist/opened.d.ts.map +1 -0
  145. package/dist/opened.js +107 -0
  146. package/dist/outcome.d.ts +39 -4
  147. package/dist/outcome.d.ts.map +1 -1
  148. package/dist/outcome.js +29 -0
  149. package/dist/overlay.d.ts +3 -4
  150. package/dist/overlay.d.ts.map +1 -1
  151. package/dist/overlay.js +99 -181
  152. package/dist/query.d.ts +57 -4
  153. package/dist/query.d.ts.map +1 -1
  154. package/dist/query.js +37 -7
  155. package/dist/reach.d.ts +11 -4
  156. package/dist/reach.d.ts.map +1 -1
  157. package/dist/reach.js +182 -51
  158. package/dist/react.d.ts +8 -6
  159. package/dist/react.d.ts.map +1 -1
  160. package/dist/react.js +7 -17
  161. package/dist/readiness.d.ts +53 -0
  162. package/dist/readiness.d.ts.map +1 -0
  163. package/dist/readiness.js +33 -0
  164. package/dist/reads.d.ts +11 -0
  165. package/dist/reads.d.ts.map +1 -0
  166. package/dist/reads.js +98 -0
  167. package/dist/recovery.d.ts +25 -0
  168. package/dist/recovery.d.ts.map +1 -0
  169. package/dist/recovery.js +198 -0
  170. package/dist/refresh.d.ts +12 -15
  171. package/dist/refresh.d.ts.map +1 -1
  172. package/dist/refresh.js +74 -44
  173. package/dist/replica.d.ts +55 -33
  174. package/dist/replica.d.ts.map +1 -1
  175. package/dist/replica.js +295 -149
  176. package/dist/room-draft.d.ts +16 -2
  177. package/dist/room-draft.d.ts.map +1 -1
  178. package/dist/room-draft.js +32 -25
  179. package/dist/rows.d.ts +28 -37
  180. package/dist/rows.d.ts.map +1 -1
  181. package/dist/rows.js +113 -72
  182. package/dist/save.d.ts +50 -9
  183. package/dist/save.d.ts.map +1 -1
  184. package/dist/save.js +528 -166
  185. package/dist/search.d.ts +39 -28
  186. package/dist/search.d.ts.map +1 -1
  187. package/dist/search.js +37 -105
  188. package/dist/seed.d.ts +16 -0
  189. package/dist/seed.d.ts.map +1 -0
  190. package/dist/seed.js +75 -0
  191. package/dist/shared.d.ts +16 -0
  192. package/dist/shared.d.ts.map +1 -0
  193. package/dist/shared.js +431 -0
  194. package/dist/status.d.ts +4 -0
  195. package/dist/status.d.ts.map +1 -1
  196. package/dist/store.d.ts +108 -75
  197. package/dist/store.d.ts.map +1 -1
  198. package/dist/store.js +345 -450
  199. package/dist/stored.d.ts +138 -0
  200. package/dist/stored.d.ts.map +1 -0
  201. package/dist/stored.js +544 -0
  202. package/dist/tables.d.ts +332 -0
  203. package/dist/tables.d.ts.map +1 -0
  204. package/dist/tables.js +570 -0
  205. package/dist/turn.d.ts +9 -0
  206. package/dist/turn.d.ts.map +1 -0
  207. package/dist/turn.js +75 -0
  208. package/dist/validation.d.ts +30 -0
  209. package/dist/validation.d.ts.map +1 -0
  210. package/dist/validation.js +162 -0
  211. package/dist/windows.d.ts +22 -0
  212. package/dist/windows.d.ts.map +1 -0
  213. package/dist/windows.js +89 -0
  214. package/dist/wire.d.ts +168 -32
  215. package/dist/wire.d.ts.map +1 -1
  216. package/dist/wire.js +56 -31
  217. package/dist/words.d.ts +19 -0
  218. package/dist/words.d.ts.map +1 -0
  219. package/dist/words.js +150 -0
  220. package/package.json +14 -4
  221. package/dist/read-side.d.ts +0 -18
  222. package/dist/read-side.d.ts.map +0 -1
  223. 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,70 @@ 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
+ }
225
327
  function stateOf(conversation) {
226
328
  const facts = liveOf(conversation);
227
329
  if (facts.questions.length > 0)
@@ -236,6 +338,7 @@ export function createConversationStore(deps) {
236
338
  created: entry.created,
237
339
  finished: entry.finished,
238
340
  outcome: entry.outcome,
341
+ waiting: entry.waiting,
239
342
  state: stateOf(entry.conversation),
240
343
  started: lives.get(entry.conversation)?.turn?.started ?? null,
241
344
  }));
@@ -278,20 +381,30 @@ export function createConversationStore(deps) {
278
381
  const declaration = deps.agents();
279
382
  if (declaration === undefined)
280
383
  return;
281
- const { documents } = await deps.documents(declaration.locator);
282
384
  const names = new Map();
283
385
  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);
386
+ let covered = null;
387
+ for (let after;;) {
388
+ const read = await deps.documents(declaration, after);
389
+ if ('pending' in read)
390
+ return;
391
+ const coverage = JSON.stringify(read.coverage);
392
+ if (covered !== null && coverage !== covered)
393
+ return readAgents();
394
+ covered = coverage;
395
+ for (const document of read.documents) {
396
+ const identity = document.fields?.[declaration.identity];
397
+ const name = document.fields?.[declaration.name];
398
+ if (typeof identity !== 'string' || typeof name !== 'string')
399
+ continue;
400
+ names.set(identity, name);
401
+ const ended = declaration.retired === undefined ? null : document.fields?.[declaration.retired];
402
+ if (ended !== undefined && ended !== null)
403
+ retired.add(identity);
404
+ }
405
+ if (read.next === undefined)
406
+ break;
407
+ after = read.next;
295
408
  }
296
409
  const named = JSON.stringify([...names]) !== JSON.stringify([...agentNames]);
297
410
  if (!named && JSON.stringify([...retired]) === JSON.stringify([...retiredAgents]))
@@ -301,6 +414,12 @@ export function createConversationStore(deps) {
301
414
  if (named)
302
415
  emitList();
303
416
  emitAgents();
417
+ const ended = [...drafts.keys()].filter(retiredChat);
418
+ if (ended.length > 0)
419
+ await remove(ended);
420
+ }
421
+ function retiredChat(conversation) {
422
+ return (conversation.startsWith(NEW_CHAT) && retiredAgents.has(conversation.slice(NEW_CHAT.length)));
304
423
  }
305
424
  function agentsReady() {
306
425
  agentsRead ??= readAgents().catch(() => {
@@ -309,8 +428,33 @@ export function createConversationStore(deps) {
309
428
  return agentsRead;
310
429
  }
311
430
  function live(conversation, change) {
431
+ const owed = unsent.get(conversation);
432
+ for (const fact of FACTS.filter((one) => one in change))
433
+ owed?.delete(fact);
312
434
  setLive(conversation, { ...liveOf(conversation), ...change });
313
- emit(conversation, false);
435
+ emit(conversation);
436
+ }
437
+ function openingEnded() {
438
+ const owed = [...unsent];
439
+ unsent.clear();
440
+ for (const [conversation, facts] of owed) {
441
+ const held = lives.get(conversation);
442
+ if (held === undefined || facts.size === 0)
443
+ continue;
444
+ setLive(conversation, {
445
+ turn: held.turn,
446
+ questions: facts.has('questions') ? QUIET.questions : held.questions,
447
+ process: facts.has('process') ? QUIET.process : held.process,
448
+ commands: facts.has('commands') ? QUIET.commands : held.commands,
449
+ options: facts.has('options') ? QUIET.options : held.options,
450
+ starting: facts.has('starting') ? QUIET.starting : held.starting,
451
+ link: facts.has('link') ? QUIET.link : held.link,
452
+ deliveryUnknown: facts.has('deliveryUnknown')
453
+ ? QUIET.deliveryUnknown
454
+ : held.deliveryUnknown,
455
+ });
456
+ emit(conversation);
457
+ }
314
458
  }
315
459
  function relayed(conversation, turn, operation) {
316
460
  const held = liveOf(conversation).turn;
@@ -339,23 +483,22 @@ export function createConversationStore(deps) {
339
483
  async function remove(leaving) {
340
484
  await deps.durably(async (local) => {
341
485
  await local.dropConversations(leaving);
342
- const removed = [];
343
486
  const changed = [];
344
487
  for (const conversation of leaving) {
345
- for (const message of messages.get(conversation) ?? []) {
346
- removed.push({ conversation, identity: message.identity });
347
- }
348
- if (messages.has(conversation) || drafts.has(conversation) || lives.has(conversation)) {
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
- deps.publish(conversationChangeSet(removed, changed));
501
+ deps.publish(conversationChangeSet(changed));
359
502
  });
360
503
  }
361
504
  function gap(conversation) {
@@ -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([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([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
- }
506
- deps.publish(conversationChangeSet([], [conversation]));
704
+ await kept(local, conversation, following);
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
  },