@agentshouse/mdruntime 0.1.0-cf96c797c5fa8 → 0.1.0-cfac557170cf3

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