@agentshouse/mdruntime 0.1.0-ce531b51ff088 → 0.1.0-ce5c5bb29be5a

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