@agentshouse/mdruntime 0.1.0-c967d1ca66fee → 0.1.0-c96e80d38e5f0

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