@agentshouse/mdruntime 0.1.0-cc7bd7a53214e → 0.1.0-ccd098ff75bbf

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