@agentshouse/mdruntime 0.1.0-cc7bd7a53214e → 0.1.0-ccbb89b0ca151

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