@agentshouse/mdruntime 0.1.0-cf28918c2bf91 → 0.1.0-cf53a3b60b194

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