@agentshouse/mdruntime 0.1.0-cb95ca2c884cf → 0.1.0-cba6f2439bb25

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