@agentshouse/mdruntime 0.1.0-c05b32a1b6587 → 0.1.0-c069136dcb4e8

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