@agentshouse/mdruntime 0.1.0-cbb8d8acc9d19 → 0.1.0-cc56db96f86ae

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 +285 -225
  2. package/README.md +21 -10
  3. package/dist/adapter.d.ts +114 -26
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/attempt.d.ts +16 -0
  6. package/dist/attempt.d.ts.map +1 -0
  7. package/dist/attempt.js +70 -0
  8. package/dist/binary.d.ts +23 -0
  9. package/dist/binary.d.ts.map +1 -0
  10. package/dist/binary.js +8 -0
  11. package/dist/bootstrap.d.ts +18 -5
  12. package/dist/bootstrap.d.ts.map +1 -1
  13. package/dist/bootstrap.js +147 -40
  14. package/dist/change-set.d.ts +7 -2
  15. package/dist/change-set.d.ts.map +1 -1
  16. package/dist/change-set.js +50 -16
  17. package/dist/change.d.ts +45 -0
  18. package/dist/change.d.ts.map +1 -1
  19. package/dist/change.js +34 -0
  20. package/dist/chat/acts.d.ts +33 -0
  21. package/dist/chat/acts.d.ts.map +1 -0
  22. package/dist/chat/acts.js +36 -0
  23. package/dist/chat/context.d.ts +3 -0
  24. package/dist/chat/context.d.ts.map +1 -0
  25. package/dist/chat/context.js +10 -0
  26. package/dist/chat/earlier.d.ts +10 -0
  27. package/dist/chat/earlier.d.ts.map +1 -0
  28. package/dist/chat/earlier.js +54 -0
  29. package/dist/chat/house.d.ts +25 -0
  30. package/dist/chat/house.d.ts.map +1 -0
  31. package/dist/chat/house.js +38 -0
  32. package/dist/chat/input.d.ts +11 -0
  33. package/dist/chat/input.d.ts.map +1 -0
  34. package/dist/chat/input.js +92 -0
  35. package/dist/chat/kit.d.ts +61 -0
  36. package/dist/chat/kit.d.ts.map +1 -0
  37. package/dist/chat/kit.js +123 -0
  38. package/dist/chat/lasting.d.ts +6 -0
  39. package/dist/chat/lasting.d.ts.map +1 -0
  40. package/dist/chat/lasting.js +19 -0
  41. package/dist/chat/list.d.ts +28 -0
  42. package/dist/chat/list.d.ts.map +1 -0
  43. package/dist/chat/list.js +103 -0
  44. package/dist/chat/markdown.d.ts +21 -0
  45. package/dist/chat/markdown.d.ts.map +1 -0
  46. package/dist/chat/markdown.js +246 -0
  47. package/dist/chat/message.d.ts +7 -0
  48. package/dist/chat/message.d.ts.map +1 -0
  49. package/dist/chat/message.js +237 -0
  50. package/dist/chat/messages.d.ts +29 -0
  51. package/dist/chat/messages.d.ts.map +1 -0
  52. package/dist/chat/messages.js +190 -0
  53. package/dist/chat/name.d.ts +7 -0
  54. package/dist/chat/name.d.ts.map +1 -0
  55. package/dist/chat/name.js +37 -0
  56. package/dist/chat/opened.d.ts +12 -0
  57. package/dist/chat/opened.d.ts.map +1 -0
  58. package/dist/chat/opened.js +19 -0
  59. package/dist/chat/overlay.d.ts +32 -0
  60. package/dist/chat/overlay.d.ts.map +1 -0
  61. package/dist/chat/overlay.js +103 -0
  62. package/dist/chat/parts.d.ts +58 -0
  63. package/dist/chat/parts.d.ts.map +1 -0
  64. package/dist/chat/parts.js +120 -0
  65. package/dist/chat/pieces.d.ts +62 -0
  66. package/dist/chat/pieces.d.ts.map +1 -0
  67. package/dist/chat/pieces.js +1110 -0
  68. package/dist/chat/process.d.ts +20 -0
  69. package/dist/chat/process.d.ts.map +1 -0
  70. package/dist/chat/process.js +32 -0
  71. package/dist/chat/provider.d.ts +68 -0
  72. package/dist/chat/provider.d.ts.map +1 -0
  73. package/dist/chat/provider.js +619 -0
  74. package/dist/chat/question.d.ts +9 -0
  75. package/dist/chat/question.d.ts.map +1 -0
  76. package/dist/chat/question.js +69 -0
  77. package/dist/chat/questions.d.ts +31 -0
  78. package/dist/chat/questions.d.ts.map +1 -0
  79. package/dist/chat/questions.js +111 -0
  80. package/dist/chat/read.d.ts +5 -0
  81. package/dist/chat/read.d.ts.map +1 -0
  82. package/dist/chat/read.js +13 -0
  83. package/dist/chat/ready.d.ts +37 -0
  84. package/dist/chat/ready.d.ts.map +1 -0
  85. package/dist/chat/ready.js +93 -0
  86. package/dist/chat/room-file.d.ts +8 -0
  87. package/dist/chat/room-file.d.ts.map +1 -0
  88. package/dist/chat/room-file.js +67 -0
  89. package/dist/chat/steady.d.ts +3 -0
  90. package/dist/chat/steady.d.ts.map +1 -0
  91. package/dist/chat/steady.js +26 -0
  92. package/dist/chat/style.d.ts +4 -0
  93. package/dist/chat/style.d.ts.map +1 -0
  94. package/dist/chat/style.js +306 -0
  95. package/dist/chat.d.ts +15 -0
  96. package/dist/chat.d.ts.map +1 -0
  97. package/dist/chat.js +8 -0
  98. package/dist/client.d.ts +33 -13
  99. package/dist/client.d.ts.map +1 -1
  100. package/dist/client.js +81 -16
  101. package/dist/collection.d.ts +23 -13
  102. package/dist/collection.d.ts.map +1 -1
  103. package/dist/collection.js +367 -38
  104. package/dist/connection.d.ts +54 -16
  105. package/dist/connection.d.ts.map +1 -1
  106. package/dist/connection.js +2582 -486
  107. package/dist/conversation.d.ts +39 -12
  108. package/dist/conversation.d.ts.map +1 -1
  109. package/dist/conversation.js +546 -171
  110. package/dist/digest.d.ts +2 -0
  111. package/dist/digest.d.ts.map +1 -0
  112. package/dist/digest.js +100 -0
  113. package/dist/durability.d.ts +7 -1
  114. package/dist/durability.d.ts.map +1 -1
  115. package/dist/entry.d.ts +17 -0
  116. package/dist/entry.d.ts.map +1 -0
  117. package/dist/entry.js +157 -0
  118. package/dist/expectation.d.ts +21 -0
  119. package/dist/expectation.d.ts.map +1 -0
  120. package/dist/expectation.js +70 -0
  121. package/dist/failure.d.ts +13 -0
  122. package/dist/failure.d.ts.map +1 -0
  123. package/dist/failure.js +19 -0
  124. package/dist/fence.d.ts +6 -0
  125. package/dist/fence.d.ts.map +1 -0
  126. package/dist/fence.js +10 -0
  127. package/dist/file.d.ts +2 -10
  128. package/dist/file.d.ts.map +1 -1
  129. package/dist/file.js +3 -120
  130. package/dist/host.d.ts +15 -3
  131. package/dist/host.d.ts.map +1 -1
  132. package/dist/host.js +107 -21
  133. package/dist/identity.d.ts +5 -0
  134. package/dist/identity.d.ts.map +1 -1
  135. package/dist/identity.js +19 -1
  136. package/dist/index.d.ts +20 -10
  137. package/dist/index.d.ts.map +1 -1
  138. package/dist/index.js +7 -0
  139. package/dist/memory.d.ts +11 -4
  140. package/dist/memory.d.ts.map +1 -1
  141. package/dist/memory.js +92 -27
  142. package/dist/model.d.ts +7 -4
  143. package/dist/model.d.ts.map +1 -1
  144. package/dist/model.js +10 -1
  145. package/dist/observation.d.ts +7 -2
  146. package/dist/observation.d.ts.map +1 -1
  147. package/dist/observation.js +29 -12
  148. package/dist/open.d.ts +2 -0
  149. package/dist/open.d.ts.map +1 -0
  150. package/dist/open.js +1 -0
  151. package/dist/outcome.d.ts +57 -3
  152. package/dist/outcome.d.ts.map +1 -1
  153. package/dist/outcome.js +67 -1
  154. package/dist/overlay.d.ts +18 -5
  155. package/dist/overlay.d.ts.map +1 -1
  156. package/dist/overlay.js +271 -165
  157. package/dist/query.d.ts +61 -4
  158. package/dist/query.d.ts.map +1 -1
  159. package/dist/query.js +40 -7
  160. package/dist/reach.d.ts +13 -4
  161. package/dist/reach.d.ts.map +1 -1
  162. package/dist/reach.js +201 -47
  163. package/dist/react.d.ts +31 -20
  164. package/dist/react.d.ts.map +1 -1
  165. package/dist/react.js +13 -21
  166. package/dist/read-side.d.ts +18 -9
  167. package/dist/read-side.d.ts.map +1 -1
  168. package/dist/read-side.js +903 -157
  169. package/dist/readiness.d.ts +53 -0
  170. package/dist/readiness.d.ts.map +1 -0
  171. package/dist/readiness.js +33 -0
  172. package/dist/reads.d.ts +11 -0
  173. package/dist/reads.d.ts.map +1 -0
  174. package/dist/reads.js +98 -0
  175. package/dist/recovery.d.ts +25 -0
  176. package/dist/recovery.d.ts.map +1 -0
  177. package/dist/recovery.js +198 -0
  178. package/dist/refresh.d.ts +13 -3
  179. package/dist/refresh.d.ts.map +1 -1
  180. package/dist/refresh.js +73 -43
  181. package/dist/replica.d.ts +16 -8
  182. package/dist/replica.d.ts.map +1 -1
  183. package/dist/replica.js +198 -91
  184. package/dist/room-draft.d.ts +40 -9
  185. package/dist/room-draft.d.ts.map +1 -1
  186. package/dist/room-draft.js +153 -37
  187. package/dist/rows.d.ts +10 -13
  188. package/dist/rows.d.ts.map +1 -1
  189. package/dist/rows.js +19 -14
  190. package/dist/save.d.ts +72 -4
  191. package/dist/save.d.ts.map +1 -1
  192. package/dist/save.js +812 -145
  193. package/dist/search.d.ts +52 -8
  194. package/dist/search.d.ts.map +1 -1
  195. package/dist/search.js +242 -46
  196. package/dist/seed.d.ts +16 -0
  197. package/dist/seed.d.ts.map +1 -0
  198. package/dist/seed.js +75 -0
  199. package/dist/shared.d.ts +19 -0
  200. package/dist/shared.d.ts.map +1 -0
  201. package/dist/shared.js +461 -0
  202. package/dist/status.d.ts +6 -2
  203. package/dist/status.d.ts.map +1 -1
  204. package/dist/status.js +0 -8
  205. package/dist/store.d.ts +90 -23
  206. package/dist/store.d.ts.map +1 -1
  207. package/dist/store.js +675 -295
  208. package/dist/tables.d.ts +181 -0
  209. package/dist/tables.d.ts.map +1 -0
  210. package/dist/tables.js +309 -0
  211. package/dist/turn.d.ts +9 -0
  212. package/dist/turn.d.ts.map +1 -0
  213. package/dist/turn.js +67 -0
  214. package/dist/windows.d.ts +21 -0
  215. package/dist/windows.d.ts.map +1 -0
  216. package/dist/windows.js +84 -0
  217. package/dist/wire.d.ts +165 -33
  218. package/dist/wire.d.ts.map +1 -1
  219. package/dist/wire.js +67 -31
  220. package/package.json +30 -14
  221. package/dist/attachment.d.ts +0 -3
  222. package/dist/attachment.d.ts.map +0 -1
  223. package/dist/attachment.js +0 -8
@@ -1,12 +1,28 @@
1
1
  import { conversationChangeSet } from './change-set.js';
2
+ import { runtimeError } from './failure.js';
2
3
  import { declaredFile } from './file.js';
4
+ import { changeBatchIdentity, chatDraftRevision } from './identity.js';
5
+ import { OFFLINE } from './outcome.js';
6
+ import { capped, cursorOf, decodedCursor, FROM_START, messageOrder, precedes } from './windows.js';
3
7
  const QUIET = {
4
8
  turn: null,
5
9
  questions: [],
6
10
  process: null,
7
11
  commands: [],
8
12
  options: [],
13
+ starting: null,
14
+ link: null,
15
+ deliveryUnknown: [],
9
16
  };
17
+ const FACTS = [
18
+ 'questions',
19
+ 'process',
20
+ 'commands',
21
+ 'options',
22
+ 'starting',
23
+ 'link',
24
+ 'deliveryUnknown',
25
+ ];
10
26
  export function unheldView(conversation) {
11
27
  return {
12
28
  conversation,
@@ -16,9 +32,23 @@ export function unheldView(conversation) {
16
32
  process: null,
17
33
  commands: [],
18
34
  options: [],
35
+ starting: null,
36
+ link: null,
37
+ deliveryUnknown: [],
19
38
  chatDraft: null,
20
39
  };
21
40
  }
41
+ export const SESSION_STARTED = 'Session started';
42
+ const NEW_CHAT = 'new:';
43
+ export function newChat(agent) {
44
+ return `${NEW_CHAT}${agent}`;
45
+ }
46
+ function creationOf(input) {
47
+ if (typeof input !== 'object' || input === null)
48
+ return null;
49
+ const { agent_id: agent, operation_id: operation } = input;
50
+ return typeof agent === 'string' && typeof operation === 'string' ? { agent, operation } : null;
51
+ }
22
52
  function startedTurn(turn, operation) {
23
53
  if (operation.op === 'begin')
24
54
  return { turn, started: operation.at, parts: [], context: null };
@@ -68,18 +98,28 @@ function appliedTurn(held, operation) {
68
98
  parts: held.parts.map((kept, at) => (at === operation.index ? changed : kept)),
69
99
  };
70
100
  }
71
- function ordered(left, right) {
72
- if (left.position !== right.position)
73
- return left.position < right.position ? -1 : 1;
74
- if (left.identity !== right.identity)
75
- return left.identity < right.identity ? -1 : 1;
76
- return 0;
77
- }
78
- function merged(held, arriving) {
101
+ function windowOf(held, arriving, limit) {
79
102
  const byIdentity = new Map(held.map((message) => [message.identity, message]));
80
103
  for (const message of arriving)
81
104
  byIdentity.set(message.identity, message);
82
- return [...byIdentity.values()].sort(ordered);
105
+ const next = [...byIdentity.values()].sort(messageOrder);
106
+ const start = Math.max(0, next.length - limit);
107
+ return { messages: next.slice(start), dropped: start > 0 };
108
+ }
109
+ function frameOf(asked, messages, more, total) {
110
+ const oldest = messages[0];
111
+ const next = !more
112
+ ? undefined
113
+ : oldest === undefined
114
+ ? (asked.after ?? FROM_START)
115
+ : cursorOf([oldest.position, oldest.identity]);
116
+ return {
117
+ ...(next === undefined ? {} : { next }),
118
+ ...(total === null ? {} : { total }),
119
+ };
120
+ }
121
+ function newestOf(arriving, from) {
122
+ return arriving.reduce((top, message) => (top === null || message.position > top ? message.position : top), from);
83
123
  }
84
124
  function acceptedMessage(value) {
85
125
  if (typeof value !== 'object' || value === null)
@@ -138,86 +178,153 @@ function historyPages(result) {
138
178
  }
139
179
  return pages;
140
180
  }
141
- function turnOf(result) {
181
+ function answered(result, field) {
142
182
  if (result.kind !== 'action')
143
183
  return null;
144
184
  const answer = result.result;
145
185
  if (typeof answer !== 'object' || answer === null)
146
186
  return null;
147
- const record = answer;
148
- return typeof record.turn === 'string' ? record.turn : null;
187
+ const value = answer[field];
188
+ return typeof value === 'string' ? value : null;
189
+ }
190
+ function beforeOf(after) {
191
+ if (after === undefined)
192
+ return null;
193
+ const decoded = typeof after === 'string' ? decodedCursor(after) : null;
194
+ if (decoded?.length === 0)
195
+ return null;
196
+ const [position, identity] = decoded ?? [];
197
+ if (decoded?.length !== 2 || typeof position !== 'string' || typeof identity !== 'string') {
198
+ throw runtimeError('invalid-after', `conversation after must be the next an earlier view returned, got ${String(after)}`);
199
+ }
200
+ return [position, identity];
201
+ }
202
+ function askedOf(options) {
203
+ const { limit, after, total } = options;
204
+ if (limit !== undefined && (!Number.isInteger(limit) || limit < 0)) {
205
+ throw runtimeError('invalid-limit', `conversation limit must be a non-negative integer, got ${String(limit)}`);
206
+ }
207
+ return { limit: capped(limit), after, before: beforeOf(after), total: total === true };
208
+ }
209
+ const EXPIRED = { kind: 'rejected', code: 'expired', fields: {}, retryable: false };
210
+ function unwritten(draft) {
211
+ return draft.text === '';
212
+ }
213
+ function pending(draft) {
214
+ return {
215
+ ...(draft.submitted === undefined ? {} : { submitted: draft.submitted }),
216
+ ...(draft.moves === undefined ? {} : { moves: draft.moves }),
217
+ };
218
+ }
219
+ function empty(draft) {
220
+ return unwritten(draft) && draft.submitted === undefined && draft.moves === undefined;
221
+ }
222
+ function withSubmitted(draft, submitted) {
223
+ const { submitted: _held, ...unsent } = draft;
224
+ return submitted.length === 0 ? unsent : { ...unsent, submitted };
149
225
  }
150
- function limitOf(options) {
151
- const { limit } = options;
152
- if (limit === undefined)
153
- return undefined;
154
- if (!Number.isInteger(limit) || limit < 0) {
155
- throw new Error(`conversation limit must be a non-negative integer, got ${String(limit)}`);
156
- }
157
- return limit;
226
+ function prepended(draft, said) {
227
+ return { text: [said.text, draft.text].filter((one) => one !== '').join('\n') };
228
+ }
229
+ function returned(draft, submission, others) {
230
+ return withSubmitted({
231
+ conversation: draft.conversation,
232
+ ...prepended(draft, submission),
233
+ revision: chatDraftRevision(),
234
+ }, others);
158
235
  }
159
236
  export function createConversationStore(deps) {
160
- const messages = new Map();
237
+ const stored = new Set();
161
238
  const reads = new Map();
239
+ const partials = new Set();
162
240
  const drafts = new Map();
163
241
  const sending = new Map();
164
242
  const behind = new Set();
243
+ const catching = new Map();
165
244
  const gaps = new Map();
166
245
  const lives = new Map();
246
+ const unsent = new Map();
167
247
  const views = new Map();
168
248
  const listing = new Map();
249
+ const creations = new Set();
169
250
  const listWatchers = new Set();
170
251
  let listKnown = false;
252
+ const agentWatchers = new Set();
171
253
  let agentNames = new Map();
254
+ let retiredAgents = new Set();
172
255
  let agentsRead = null;
173
256
  let replicated = new Set();
174
257
  let handedReplicated;
175
258
  function liveOf(conversation) {
176
259
  return lives.get(conversation) ?? QUIET;
177
260
  }
178
- function shownMessages(conversation, limit) {
179
- const held = messages.get(conversation) ?? [];
180
- const shown = limit === undefined ? held : held.slice(Math.max(0, held.length - limit));
181
- return shown.map(({ identity, turn, text, message }) => ({
182
- identity,
183
- turn,
184
- text,
185
- message: structuredClone(message),
186
- }));
187
- }
188
- function viewOf(conversation, limit, shown = shownMessages(conversation, limit)) {
261
+ function viewOf(conversation, held, frame) {
189
262
  const draft = drafts.get(conversation);
190
263
  const facts = liveOf(conversation);
191
264
  return {
192
265
  conversation,
193
- messages: shown,
266
+ messages: held.map(({ identity, turn, text, message }) => ({
267
+ identity,
268
+ turn,
269
+ text,
270
+ message,
271
+ })),
272
+ ...frame,
194
273
  turn: structuredClone(facts.turn),
195
274
  questions: structuredClone(facts.questions),
196
275
  process: facts.process === null ? null : { ...facts.process },
197
276
  commands: structuredClone(facts.commands),
198
277
  options: structuredClone(facts.options),
199
- chatDraft: draft === undefined || draft.turn !== null || sending.get(conversation)?.draft === draft
278
+ starting: facts.starting,
279
+ link: facts.link === null ? null : { ...facts.link },
280
+ deliveryUnknown: [...facts.deliveryUnknown],
281
+ chatDraft: draft === undefined || unwritten(draft)
200
282
  ? null
201
- : { text: draft.text, files: [...draft.files] },
283
+ : { text: draft.text, revision: draft.revision },
202
284
  };
203
285
  }
204
- function emit(conversation, messagesChanged = true) {
286
+ function emit(conversation) {
205
287
  if (deps.closed())
206
288
  return;
207
289
  for (const view of views.values()) {
208
- if (view.conversation !== conversation)
290
+ if (view.conversation !== conversation || view.window === null)
209
291
  continue;
210
- if (messagesChanged)
211
- view.messages = null;
212
292
  const ready = [...view.watchers].filter((watcher) => watcher.ready);
213
293
  if (ready.length === 0)
214
294
  continue;
215
- view.messages ??= shownMessages(conversation, view.limit);
216
- const value = viewOf(conversation, view.limit, view.messages);
295
+ const value = viewOf(conversation, view.window, sharedFrame(view, view.window));
217
296
  for (const watcher of ready)
218
297
  watcher.notify(value);
219
298
  }
220
299
  }
300
+ function sharedFrame(view, window) {
301
+ return frameOf(view.asked, window, view.more, view.total);
302
+ }
303
+ async function slide(local, conversation, arriving) {
304
+ let count;
305
+ for (const view of views.values()) {
306
+ if (view.conversation !== conversation || view.window === null)
307
+ continue;
308
+ if (arriving === null) {
309
+ view.window = [];
310
+ view.more = false;
311
+ if (view.total !== null)
312
+ view.total = 0;
313
+ continue;
314
+ }
315
+ const joining = arriving.filter((message) => precedes(message, view.asked.before));
316
+ const next = windowOf(view.window, joining, view.asked.limit);
317
+ view.window = next.messages;
318
+ view.more ||= next.dropped;
319
+ if (view.total !== null && arriving.length > 0) {
320
+ count ??= await local.messageCount(conversation);
321
+ view.total = count;
322
+ }
323
+ }
324
+ }
325
+ function changesOf(conversation, arriving) {
326
+ return arriving.map(({ identity, text }) => ({ conversation, identity, text }));
327
+ }
221
328
  function stateOf(conversation) {
222
329
  const facts = liveOf(conversation);
223
330
  if (facts.questions.length > 0)
@@ -232,6 +339,7 @@ export function createConversationStore(deps) {
232
339
  created: entry.created,
233
340
  finished: entry.finished,
234
341
  outcome: entry.outcome,
342
+ waiting: entry.waiting,
235
343
  state: stateOf(entry.conversation),
236
344
  started: lives.get(entry.conversation)?.turn?.started ?? null,
237
345
  }));
@@ -243,6 +351,21 @@ export function createConversationStore(deps) {
243
351
  for (const watcher of listWatchers)
244
352
  watcher(value);
245
353
  }
354
+ function agents() {
355
+ return [...agentNames]
356
+ .filter(([identity]) => !retiredAgents.has(identity))
357
+ .map(([identity, name]) => ({ identity, name }))
358
+ .sort((left, right) => left.name === right.name
359
+ ? left.identity.localeCompare(right.identity)
360
+ : left.name.localeCompare(right.name));
361
+ }
362
+ function emitAgents() {
363
+ if (deps.closed() || agentWatchers.size === 0)
364
+ return;
365
+ const value = agents();
366
+ for (const watcher of agentWatchers)
367
+ watcher(value);
368
+ }
246
369
  function setLive(conversation, next) {
247
370
  const before = stateOf(conversation);
248
371
  const began = lives.get(conversation)?.turn?.started;
@@ -259,20 +382,38 @@ export function createConversationStore(deps) {
259
382
  const declaration = deps.agents();
260
383
  if (declaration === undefined)
261
384
  return;
262
- const { documents } = await deps.documents(declaration.locator);
385
+ const held = await deps.documents(declaration);
386
+ if ('pending' in held)
387
+ return;
388
+ const { documents } = held;
263
389
  const names = new Map();
390
+ const retired = new Set();
264
391
  for (const document of documents) {
265
392
  if (document.type !== declaration.type)
266
393
  continue;
267
394
  const identity = document.fields?.[declaration.identity];
268
395
  const name = document.fields?.[declaration.name];
269
- if (typeof identity === 'string' && typeof name === 'string')
270
- names.set(identity, name);
396
+ if (typeof identity !== 'string' || typeof name !== 'string')
397
+ continue;
398
+ names.set(identity, name);
399
+ const ended = declaration.retired === undefined ? null : document.fields?.[declaration.retired];
400
+ if (ended !== undefined && ended !== null)
401
+ retired.add(identity);
271
402
  }
272
- if (JSON.stringify([...names]) === JSON.stringify([...agentNames]))
403
+ const named = JSON.stringify([...names]) !== JSON.stringify([...agentNames]);
404
+ if (!named && JSON.stringify([...retired]) === JSON.stringify([...retiredAgents]))
273
405
  return;
274
406
  agentNames = names;
275
- emitList();
407
+ retiredAgents = retired;
408
+ if (named)
409
+ emitList();
410
+ emitAgents();
411
+ const ended = [...drafts.keys()].filter(retiredChat);
412
+ if (ended.length > 0)
413
+ await remove(ended);
414
+ }
415
+ function retiredChat(conversation) {
416
+ return (conversation.startsWith(NEW_CHAT) && retiredAgents.has(conversation.slice(NEW_CHAT.length)));
276
417
  }
277
418
  function agentsReady() {
278
419
  agentsRead ??= readAgents().catch(() => {
@@ -281,8 +422,33 @@ export function createConversationStore(deps) {
281
422
  return agentsRead;
282
423
  }
283
424
  function live(conversation, change) {
425
+ const owed = unsent.get(conversation);
426
+ for (const fact of FACTS.filter((one) => one in change))
427
+ owed?.delete(fact);
284
428
  setLive(conversation, { ...liveOf(conversation), ...change });
285
- emit(conversation, false);
429
+ emit(conversation);
430
+ }
431
+ function openingEnded() {
432
+ const owed = [...unsent];
433
+ unsent.clear();
434
+ for (const [conversation, facts] of owed) {
435
+ const held = lives.get(conversation);
436
+ if (held === undefined || facts.size === 0)
437
+ continue;
438
+ setLive(conversation, {
439
+ turn: held.turn,
440
+ questions: facts.has('questions') ? QUIET.questions : held.questions,
441
+ process: facts.has('process') ? QUIET.process : held.process,
442
+ commands: facts.has('commands') ? QUIET.commands : held.commands,
443
+ options: facts.has('options') ? QUIET.options : held.options,
444
+ starting: facts.has('starting') ? QUIET.starting : held.starting,
445
+ link: facts.has('link') ? QUIET.link : held.link,
446
+ deliveryUnknown: facts.has('deliveryUnknown')
447
+ ? QUIET.deliveryUnknown
448
+ : held.deliveryUnknown,
449
+ });
450
+ emit(conversation);
451
+ }
286
452
  }
287
453
  function relayed(conversation, turn, operation) {
288
454
  const held = liveOf(conversation).turn;
@@ -310,21 +476,26 @@ export function createConversationStore(deps) {
310
476
  }
311
477
  async function remove(leaving) {
312
478
  await deps.durably(async (local) => {
313
- await local.dropConversations(leaving);
314
479
  const removed = [];
315
- const changed = [];
316
480
  for (const conversation of leaving) {
317
- for (const message of messages.get(conversation) ?? []) {
318
- removed.push({ conversation, identity: message.identity });
481
+ for (const identity of await local.messageIdentities(conversation)) {
482
+ removed.push({ conversation, identity, text: null });
319
483
  }
320
- if (messages.has(conversation) || drafts.has(conversation) || lives.has(conversation)) {
484
+ }
485
+ await local.dropConversations(leaving);
486
+ const changed = [];
487
+ for (const conversation of leaving) {
488
+ if (stored.has(conversation) || drafts.has(conversation) || lives.has(conversation)) {
321
489
  changed.push(conversation);
322
490
  }
323
- messages.delete(conversation);
491
+ stored.delete(conversation);
324
492
  reads.delete(conversation);
493
+ partials.delete(conversation);
325
494
  drafts.delete(conversation);
326
495
  behind.delete(conversation);
496
+ unsent.delete(conversation);
327
497
  setLive(conversation, null);
498
+ await slide(local, conversation, null);
328
499
  }
329
500
  if (changed.length > 0)
330
501
  deps.publish(conversationChangeSet(removed, changed));
@@ -342,30 +513,25 @@ export function createConversationStore(deps) {
342
513
  const advances = read !== undefined &&
343
514
  !behind.has(conversation) &&
344
515
  (read === null || message.position > read);
345
- const sealed = drafts.get(conversation)?.turn === message.turn;
516
+ await sealed(local, conversation, new Set([message.turn]));
346
517
  await local.keepMessages({
347
518
  conversation,
348
519
  messages: [message],
349
- ...(advances ? { read: message.position } : {}),
350
- sealed,
520
+ ...(advances ? { read: { at: message.position, partial: false } } : {}),
351
521
  });
352
- messages.set(conversation, merged(messages.get(conversation) ?? [], [message]));
522
+ stored.add(conversation);
353
523
  if (advances)
354
524
  reads.set(conversation, message.position);
355
- if (sealed)
356
- drafts.delete(conversation);
357
525
  sealTurns(conversation, new Set([message.turn]));
358
- deps.publish(conversationChangeSet([{ conversation, identity: message.identity }], [conversation]));
526
+ await slide(local, conversation, [message]);
527
+ deps.publish(conversationChangeSet(changesOf(conversation, [message]), [conversation]));
359
528
  });
360
529
  }
361
- async function readHistories(conversations) {
530
+ async function readHistories(conversations, ended) {
362
531
  const generations = new Map(conversations.map((one) => [one, gaps.get(one) ?? 0]));
363
- const starts = new Map(conversations.map((one) => [one, reads.get(one)]));
364
- const arriving = new Map();
365
- const complete = new Set();
366
532
  let asking = conversations.map((conversation) => ({
367
533
  conversation,
368
- after: starts.get(conversation) ?? undefined,
534
+ after: reads.get(conversation) ?? undefined,
369
535
  }));
370
536
  while (asking.length > 0) {
371
537
  const result = await deps.call('conversation.history', {
@@ -377,46 +543,64 @@ export function createConversationStore(deps) {
377
543
  const pages = historyPages(result);
378
544
  if (pages === null)
379
545
  return result;
380
- const asked = new Set(asking.map(({ conversation }) => conversation));
546
+ const asked = new Map(asking.map(({ conversation, after }) => [conversation, after]));
381
547
  const following = [];
382
548
  for (const page of pages) {
383
549
  if (!asked.has(page.conversation))
384
550
  continue;
385
- arriving.set(page.conversation, [
386
- ...(arriving.get(page.conversation) ?? []),
387
- ...page.messages,
388
- ]);
389
- if (page.next === undefined)
390
- complete.add(page.conversation);
391
- else
551
+ await keepPage(page.conversation, page.messages, asked.get(page.conversation) ?? null, page.next, generations.get(page.conversation) ?? 0);
552
+ if (page.next !== undefined) {
392
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);
393
559
  }
394
560
  asking = following;
395
561
  }
396
- for (const [conversation, held] of arriving) {
397
- if (!complete.has(conversation))
398
- continue;
399
- await keepHistory(conversation, held, starts.get(conversation), generations.get(conversation) ?? 0);
400
- }
401
562
  return null;
402
563
  }
403
- 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) {
404
584
  return deps.durably(async (local) => {
405
585
  if (!replicated.has(conversation))
406
586
  return;
407
- const newest = arriving.reduce((top, message) => (top === null || message.position > top ? message.position : top), from ?? null);
408
- const turn = drafts.get(conversation)?.turn;
409
- const sealed = arriving.some((message) => message.turn === turn);
410
- await local.keepMessages({ conversation, messages: arriving, read: newest, sealed });
411
- messages.set(conversation, merged(messages.get(conversation) ?? [], arriving));
412
- reads.set(conversation, newest);
413
- if (sealed)
414
- drafts.delete(conversation);
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);
415
598
  sealTurns(conversation, new Set(arriving.map((message) => message.turn)));
416
- if ((gaps.get(conversation) ?? 0) === generation)
599
+ if (whole && (gaps.get(conversation) ?? 0) === generation)
417
600
  behind.delete(conversation);
418
- if (arriving.length > 0 || sealed) {
419
- deps.publish(conversationChangeSet(arriving.map((message) => ({ conversation, identity: message.identity })), [conversation]));
601
+ await slide(local, conversation, arriving);
602
+ if (arriving.length > 0 || sealing || whole) {
603
+ deps.publish(conversationChangeSet(changesOf(conversation, arriving), [conversation]));
420
604
  }
421
605
  });
422
606
  }
@@ -425,11 +609,8 @@ export function createConversationStore(deps) {
425
609
  if (event.kind === 'gap') {
426
610
  if (replicated.has(conversation))
427
611
  gap(conversation);
428
- const held = lives.get(conversation);
429
- if (held === undefined)
430
- return;
431
- setLive(conversation, { ...QUIET, turn: held.turn });
432
- emit(conversation, false);
612
+ if (lives.has(conversation))
613
+ unsent.set(conversation, new Set(FACTS));
433
614
  return;
434
615
  }
435
616
  if (event.kind === 'turn') {
@@ -452,6 +633,18 @@ export function createConversationStore(deps) {
452
633
  live(conversation, { options: structuredClone(event.options) });
453
634
  return;
454
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
+ }
455
648
  const arriving = acceptedMessage(event.message);
456
649
  if (arriving === null)
457
650
  return;
@@ -462,22 +655,118 @@ export function createConversationStore(deps) {
462
655
  if (sealTurns(conversation, new Set([arriving.turn])))
463
656
  emit(conversation);
464
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
+ }
465
699
  function withDraft(conversation, next) {
466
700
  return deps.durably(async (local) => {
467
- const following = next(drafts.get(conversation));
701
+ const following = await next(drafts.get(conversation), local);
468
702
  if (following === undefined)
469
703
  return;
470
- if (following === null) {
471
- await local.dropChatDraft(conversation);
472
- drafts.delete(conversation);
473
- }
474
- else {
475
- await local.keepChatDraft(following);
476
- drafts.set(conversation, following);
477
- }
704
+ await kept(local, conversation, following);
478
705
  deps.publish(conversationChangeSet([], [conversation]));
479
706
  });
480
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
+ }
481
770
  function invoke(name, conversation, extra = {}) {
482
771
  return deps.call(name, { conversation, ...extra });
483
772
  }
@@ -487,47 +776,48 @@ export function createConversationStore(deps) {
487
776
  return entries();
488
777
  },
489
778
  async read(conversation, options = {}) {
490
- const limit = limitOf(options);
491
- return deps.durably(() => Promise.resolve(viewOf(conversation, limit)));
492
- },
493
- create(input) {
494
- return deps.call('conversation.create', input);
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
+ });
495
785
  },
496
- async send(conversation) {
497
- const draft = drafts.get(conversation);
498
- const pending = draft === undefined ? undefined : { draft };
499
- if (pending !== undefined) {
500
- sending.set(conversation, pending);
501
- deps.publish(conversationChangeSet([], [conversation]));
502
- }
503
- try {
504
- const files = await Promise.all((draft?.files ?? []).map(async (file) => ({
505
- ...(await declaredFile(file)),
506
- blob: file.blob,
507
- })));
508
- const result = await invoke('conversation.send', conversation, {
509
- text: draft?.text ?? '',
510
- files,
511
- });
512
- const turn = turnOf(result);
513
- if (turn === null || draft === undefined)
514
- return result;
515
- await withDraft(conversation, (current) => {
516
- if (current !== draft)
517
- return undefined;
518
- const sealed = (messages.get(conversation) ?? []).some((message) => message.turn === turn);
519
- return sealed ? null : { ...draft, turn };
520
- });
786
+ async create(input) {
787
+ const result = await deps.call('conversation.create', input);
788
+ const conversation = answered(result, 'conversation');
789
+ if (conversation === null)
521
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;
797
+ },
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;
522
805
  }
523
- finally {
524
- if (pending !== undefined && sending.get(conversation) === pending) {
525
- sending.delete(conversation);
526
- if (drafts.get(conversation) === draft) {
527
- deps.publish(conversationChangeSet([], [conversation]));
528
- }
529
- }
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 ?? []));
530
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
+ });
531
821
  },
532
822
  answer(conversation, answer) {
533
823
  return invoke('conversation.answer', conversation, { answer });
@@ -542,7 +832,12 @@ export function createConversationStore(deps) {
542
832
  const result = await invoke('conversation.cancel', conversation, { turn });
543
833
  if (result.kind !== 'action')
544
834
  return result;
545
- 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
+ });
546
841
  return result;
547
842
  },
548
843
  delete(conversation) {
@@ -550,33 +845,44 @@ export function createConversationStore(deps) {
550
845
  },
551
846
  };
552
847
  const chatDraft = {
553
- write(conversation, draft) {
554
- return withDraft(conversation, () => ({
848
+ write(conversation, draft, revision = chatDraftRevision()) {
849
+ return withDraft(conversation, (current) => ({
555
850
  conversation,
556
851
  text: draft.text,
557
- files: [...draft.files],
558
- turn: null,
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)),
559
862
  }));
560
863
  },
561
864
  discard(conversation) {
562
- return withDraft(conversation, (current) => (current === undefined ? undefined : null));
865
+ return withDraft(conversation, (current) => current === undefined
866
+ ? undefined
867
+ : { conversation, text: '', revision: chatDraftRevision(), ...pending(current) });
563
868
  },
564
869
  };
565
870
  return {
566
871
  surface,
567
872
  chatDraft,
568
- hydrate(held, stored) {
569
- messages.clear();
873
+ hydrate(held, kept) {
874
+ stored.clear();
570
875
  reads.clear();
571
- for (const view of views.values())
572
- view.messages = null;
876
+ partials.clear();
573
877
  for (const entry of held) {
574
- messages.set(entry.conversation, [...entry.messages].sort(ordered));
878
+ stored.add(entry.conversation);
575
879
  if (entry.read !== undefined)
576
880
  reads.set(entry.conversation, entry.read);
881
+ if (entry.partial)
882
+ partials.add(entry.conversation);
577
883
  }
578
884
  drafts.clear();
579
- for (const record of stored)
885
+ for (const record of kept)
580
886
  drafts.set(record.conversation, record);
581
887
  },
582
888
  async reconcile() {
@@ -584,8 +890,10 @@ export function createConversationStore(deps) {
584
890
  return;
585
891
  const named = new Set(listing.keys());
586
892
  const leaving = [
587
- ...new Set([...replicated, ...messages.keys(), ...reads.keys(), ...drafts.keys()]),
588
- ].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)));
589
897
  for (const conversation of named) {
590
898
  if (!replicated.has(conversation))
591
899
  behind.add(conversation);
@@ -596,41 +904,93 @@ export function createConversationStore(deps) {
596
904
  await remove(leaving);
597
905
  },
598
906
  async catchUp(progress) {
599
- const reading = [...behind].filter((conversation) => replicated.has(conversation)).sort();
600
- if (reading.length > 0 && !deps.closed()) {
601
- const failed = await readHistories(reading);
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);
602
912
  if (failed?.kind === 'transport-uncertainty')
603
913
  progress.reachable = false;
604
- }
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
+ ]);
605
921
  if (behind.size > 0)
606
922
  progress.complete = false;
607
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
+ },
608
949
  current() {
609
950
  return behind.size === 0;
610
951
  },
952
+ known() {
953
+ return listKnown;
954
+ },
611
955
  reconnected() {
612
956
  for (const conversation of replicated)
613
957
  gap(conversation);
614
958
  },
959
+ reopened() {
960
+ sending.clear();
961
+ },
962
+ lost() {
963
+ catching.clear();
964
+ },
615
965
  watch(conversation, options, deliver) {
616
- const limit = limitOf(options);
617
- 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]);
618
968
  const view = views.get(key) ?? {
619
969
  conversation,
620
- limit,
970
+ asked,
621
971
  watchers: new Set(),
622
- messages: null,
972
+ window: null,
973
+ more: false,
974
+ total: null,
623
975
  };
624
976
  views.set(key, view);
625
977
  const watcher = { notify: deliver, ready: false };
626
978
  view.watchers.add(watcher);
627
979
  void deps
628
- .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
+ })
629
988
  .then(() => {
630
989
  if (deps.closed() || !view.watchers.has(watcher))
631
990
  return;
632
991
  watcher.ready = true;
633
- watcher.notify(viewOf(conversation, limit));
992
+ const window = view.window ?? [];
993
+ watcher.notify(viewOf(conversation, window, sharedFrame(view, window)));
634
994
  })
635
995
  .catch(() => { });
636
996
  return () => {
@@ -650,6 +1010,16 @@ export function createConversationStore(deps) {
650
1010
  listWatchers.delete(deliver);
651
1011
  };
652
1012
  },
1013
+ watchAgents(deliver) {
1014
+ agentWatchers.add(deliver);
1015
+ void agentsReady().then(() => {
1016
+ if (!deps.closed() && agentWatchers.has(deliver))
1017
+ deliver(agents());
1018
+ });
1019
+ return () => {
1020
+ agentWatchers.delete(deliver);
1021
+ };
1022
+ },
653
1023
  changed(set) {
654
1024
  for (const conversation of set.conversations)
655
1025
  emit(conversation);
@@ -667,12 +1037,16 @@ export function createConversationStore(deps) {
667
1037
  if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted') {
668
1038
  if (event.kind === 'list') {
669
1039
  listing.clear();
670
- for (const entry of event.entries)
1040
+ for (const entry of event.entries) {
671
1041
  listing.set(entry.conversation, { ...entry });
1042
+ creations.delete(entry.conversation);
1043
+ }
672
1044
  listKnown = true;
1045
+ openingEnded();
673
1046
  }
674
1047
  else if (event.kind === 'listed') {
675
1048
  listing.set(event.entry.conversation, { ...event.entry });
1049
+ creations.delete(event.entry.conversation);
676
1050
  }
677
1051
  else {
678
1052
  listing.delete(event.conversation);
@@ -686,17 +1060,18 @@ export function createConversationStore(deps) {
686
1060
  heard(event);
687
1061
  return false;
688
1062
  },
689
- messages() {
690
- return [...messages].flatMap(([conversation, held]) => held.map(({ identity, text }) => ({ conversation, identity, text })));
1063
+ searchable() {
1064
+ return [...(listKnown ? listing.keys() : stored)].sort();
691
1065
  },
692
- message(conversation, identity) {
693
- const held = messages.get(conversation)?.find((message) => message.identity === identity);
694
- return held === undefined ? null : { conversation, identity, text: held.text };
1066
+ acquired(conversation) {
1067
+ return reads.has(conversation) && !partials.has(conversation);
695
1068
  },
696
1069
  close() {
697
1070
  views.clear();
698
1071
  lives.clear();
1072
+ unsent.clear();
699
1073
  listWatchers.clear();
1074
+ agentWatchers.clear();
700
1075
  },
701
1076
  };
702
1077
  }