@agentshouse/mdruntime 0.1.0-c6e4c19c13638 → 0.1.0-c6f02ec9468d4

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