@agentshouse/mdruntime 0.1.0-c389df724fd2f → 0.1.0-c38dcfe165d81

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