@agentshouse/mdruntime 0.1.0-c99e5271060cd → 0.1.0-c99ee6ff87175

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