@agentshouse/mdruntime 0.1.0-c7bd7ca3faee4 → 0.1.0-c89fa07b05d4b

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/AGENTS.md +259 -216
  2. package/README.md +11 -1
  3. package/dist/adapter.d.ts +66 -16
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/attempt.d.ts +16 -0
  6. package/dist/attempt.d.ts.map +1 -0
  7. package/dist/attempt.js +70 -0
  8. package/dist/binary.d.ts +23 -0
  9. package/dist/binary.d.ts.map +1 -0
  10. package/dist/binary.js +8 -0
  11. package/dist/bootstrap.d.ts +17 -5
  12. package/dist/bootstrap.d.ts.map +1 -1
  13. package/dist/bootstrap.js +109 -37
  14. package/dist/change-set.d.ts +2 -0
  15. package/dist/change-set.d.ts.map +1 -1
  16. package/dist/change-set.js +48 -16
  17. package/dist/change.d.ts +15 -0
  18. package/dist/change.d.ts.map +1 -1
  19. package/dist/change.js +26 -1
  20. package/dist/chat/acts.d.ts +33 -0
  21. package/dist/chat/acts.d.ts.map +1 -0
  22. package/dist/chat/acts.js +36 -0
  23. package/dist/chat/context.d.ts +3 -0
  24. package/dist/chat/context.d.ts.map +1 -0
  25. package/dist/chat/context.js +10 -0
  26. package/dist/chat/input.d.ts +9 -0
  27. package/dist/chat/input.d.ts.map +1 -0
  28. package/dist/chat/input.js +99 -0
  29. package/dist/chat/kit.d.ts +60 -0
  30. package/dist/chat/kit.d.ts.map +1 -0
  31. package/dist/chat/kit.js +123 -0
  32. package/dist/chat/lasting.d.ts +6 -0
  33. package/dist/chat/lasting.d.ts.map +1 -0
  34. package/dist/chat/lasting.js +19 -0
  35. package/dist/chat/list.d.ts +26 -0
  36. package/dist/chat/list.d.ts.map +1 -0
  37. package/dist/chat/list.js +62 -0
  38. package/dist/chat/markdown.d.ts +20 -0
  39. package/dist/chat/markdown.d.ts.map +1 -0
  40. package/dist/chat/markdown.js +188 -0
  41. package/dist/chat/message.d.ts +6 -0
  42. package/dist/chat/message.d.ts.map +1 -0
  43. package/dist/chat/message.js +192 -0
  44. package/dist/chat/messages.d.ts +21 -0
  45. package/dist/chat/messages.d.ts.map +1 -0
  46. package/dist/chat/messages.js +151 -0
  47. package/dist/chat/name.d.ts +6 -0
  48. package/dist/chat/name.d.ts.map +1 -0
  49. package/dist/chat/name.js +37 -0
  50. package/dist/chat/opened.d.ts +11 -0
  51. package/dist/chat/opened.d.ts.map +1 -0
  52. package/dist/chat/opened.js +17 -0
  53. package/dist/chat/overlay.d.ts +31 -0
  54. package/dist/chat/overlay.d.ts.map +1 -0
  55. package/dist/chat/overlay.js +97 -0
  56. package/dist/chat/parts.d.ts +57 -0
  57. package/dist/chat/parts.d.ts.map +1 -0
  58. package/dist/chat/parts.js +119 -0
  59. package/dist/chat/process.d.ts +20 -0
  60. package/dist/chat/process.d.ts.map +1 -0
  61. package/dist/chat/process.js +32 -0
  62. package/dist/chat/provider.d.ts +63 -0
  63. package/dist/chat/provider.d.ts.map +1 -0
  64. package/dist/chat/provider.js +620 -0
  65. package/dist/chat/question.d.ts +9 -0
  66. package/dist/chat/question.d.ts.map +1 -0
  67. package/dist/chat/question.js +80 -0
  68. package/dist/chat/questions.d.ts +31 -0
  69. package/dist/chat/questions.d.ts.map +1 -0
  70. package/dist/chat/questions.js +111 -0
  71. package/dist/chat/read.d.ts +5 -0
  72. package/dist/chat/read.d.ts.map +1 -0
  73. package/dist/chat/read.js +13 -0
  74. package/dist/chat/ready.d.ts +33 -0
  75. package/dist/chat/ready.d.ts.map +1 -0
  76. package/dist/chat/ready.js +91 -0
  77. package/dist/chat/style.d.ts +4 -0
  78. package/dist/chat/style.d.ts.map +1 -0
  79. package/dist/chat/style.js +302 -0
  80. package/dist/chat.d.ts +14 -0
  81. package/dist/chat.d.ts.map +1 -0
  82. package/dist/chat.js +8 -0
  83. package/dist/client.d.ts +18 -11
  84. package/dist/client.d.ts.map +1 -1
  85. package/dist/client.js +31 -16
  86. package/dist/collection.d.ts +21 -12
  87. package/dist/collection.d.ts.map +1 -1
  88. package/dist/collection.js +271 -23
  89. package/dist/connection.d.ts +28 -14
  90. package/dist/connection.d.ts.map +1 -1
  91. package/dist/connection.js +1218 -370
  92. package/dist/conversation.d.ts +23 -10
  93. package/dist/conversation.d.ts.map +1 -1
  94. package/dist/conversation.js +304 -136
  95. package/dist/entry.d.ts +17 -0
  96. package/dist/entry.d.ts.map +1 -0
  97. package/dist/entry.js +156 -0
  98. package/dist/expectation.d.ts +21 -0
  99. package/dist/expectation.d.ts.map +1 -0
  100. package/dist/expectation.js +70 -0
  101. package/dist/failure.d.ts +13 -0
  102. package/dist/failure.d.ts.map +1 -0
  103. package/dist/failure.js +19 -0
  104. package/dist/fence.d.ts +6 -0
  105. package/dist/fence.d.ts.map +1 -0
  106. package/dist/fence.js +10 -0
  107. package/dist/host.d.ts +4 -2
  108. package/dist/host.d.ts.map +1 -1
  109. package/dist/host.js +17 -13
  110. package/dist/identity.d.ts +5 -0
  111. package/dist/identity.d.ts.map +1 -1
  112. package/dist/identity.js +19 -1
  113. package/dist/index.d.ts +12 -6
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js +4 -0
  116. package/dist/memory.d.ts +4 -3
  117. package/dist/memory.d.ts.map +1 -1
  118. package/dist/memory.js +63 -21
  119. package/dist/model.d.ts +4 -2
  120. package/dist/model.d.ts.map +1 -1
  121. package/dist/model.js +9 -0
  122. package/dist/observation.d.ts +5 -2
  123. package/dist/observation.d.ts.map +1 -1
  124. package/dist/observation.js +22 -11
  125. package/dist/open.d.ts +2 -0
  126. package/dist/open.d.ts.map +1 -0
  127. package/dist/open.js +1 -0
  128. package/dist/outcome.d.ts +32 -3
  129. package/dist/outcome.d.ts.map +1 -1
  130. package/dist/outcome.js +42 -1
  131. package/dist/overlay.d.ts +15 -3
  132. package/dist/overlay.d.ts.map +1 -1
  133. package/dist/overlay.js +94 -77
  134. package/dist/query.d.ts +32 -3
  135. package/dist/query.d.ts.map +1 -1
  136. package/dist/query.js +3 -0
  137. package/dist/reach.d.ts +2 -0
  138. package/dist/reach.d.ts.map +1 -1
  139. package/dist/reach.js +45 -21
  140. package/dist/react.d.ts +7 -5
  141. package/dist/react.d.ts.map +1 -1
  142. package/dist/react.js +3 -2
  143. package/dist/read-side.d.ts +13 -6
  144. package/dist/read-side.d.ts.map +1 -1
  145. package/dist/read-side.js +291 -85
  146. package/dist/recovery.d.ts +25 -0
  147. package/dist/recovery.d.ts.map +1 -0
  148. package/dist/recovery.js +197 -0
  149. package/dist/refresh.d.ts +10 -0
  150. package/dist/refresh.d.ts.map +1 -1
  151. package/dist/refresh.js +43 -38
  152. package/dist/replica.d.ts +8 -5
  153. package/dist/replica.d.ts.map +1 -1
  154. package/dist/replica.js +114 -67
  155. package/dist/room-draft.d.ts +26 -9
  156. package/dist/room-draft.d.ts.map +1 -1
  157. package/dist/room-draft.js +132 -23
  158. package/dist/rows.d.ts +8 -13
  159. package/dist/rows.d.ts.map +1 -1
  160. package/dist/rows.js +12 -14
  161. package/dist/save.d.ts +31 -1
  162. package/dist/save.d.ts.map +1 -1
  163. package/dist/save.js +376 -45
  164. package/dist/search.d.ts +31 -8
  165. package/dist/search.d.ts.map +1 -1
  166. package/dist/search.js +109 -44
  167. package/dist/shared.d.ts +11 -0
  168. package/dist/shared.d.ts.map +1 -0
  169. package/dist/shared.js +368 -0
  170. package/dist/status.d.ts +2 -2
  171. package/dist/status.d.ts.map +1 -1
  172. package/dist/status.js +0 -8
  173. package/dist/store.d.ts +64 -18
  174. package/dist/store.d.ts.map +1 -1
  175. package/dist/store.js +494 -195
  176. package/dist/turn.d.ts +7 -0
  177. package/dist/turn.d.ts.map +1 -0
  178. package/dist/turn.js +45 -0
  179. package/dist/wire.d.ts +6 -3
  180. package/dist/wire.d.ts.map +1 -1
  181. package/dist/wire.js +14 -2
  182. package/package.json +26 -14
  183. package/dist/attachment.d.ts +0 -3
  184. package/dist/attachment.d.ts.map +0 -1
  185. package/dist/attachment.js +0 -8
@@ -1,5 +1,8 @@
1
1
  import { conversationChangeSet } from './change-set.js';
2
+ import { runtimeError } from './failure.js';
2
3
  import { declaredFile } from './file.js';
4
+ import { changeBatchIdentity, chatDraftRevision } from './identity.js';
5
+ import { OFFLINE } from './outcome.js';
3
6
  const QUIET = {
4
7
  turn: null,
5
8
  questions: [],
@@ -19,6 +22,7 @@ export function unheldView(conversation) {
19
22
  chatDraft: null,
20
23
  };
21
24
  }
25
+ export const SESSION_STARTED = 'Session started';
22
26
  function startedTurn(turn, operation) {
23
27
  if (operation.op === 'begin')
24
28
  return { turn, started: operation.at, parts: [], context: null };
@@ -75,11 +79,15 @@ function ordered(left, right) {
75
79
  return left.identity < right.identity ? -1 : 1;
76
80
  return 0;
77
81
  }
78
- function merged(held, arriving) {
82
+ function windowOf(held, arriving, limit) {
79
83
  const byIdentity = new Map(held.map((message) => [message.identity, message]));
80
84
  for (const message of arriving)
81
85
  byIdentity.set(message.identity, message);
82
- return [...byIdentity.values()].sort(ordered);
86
+ const next = [...byIdentity.values()].sort(ordered);
87
+ return limit === undefined ? next : next.slice(Math.max(0, next.length - limit));
88
+ }
89
+ function newestOf(arriving, from) {
90
+ return arriving.reduce((top, message) => (top === null || message.position > top ? message.position : top), from);
83
91
  }
84
92
  function acceptedMessage(value) {
85
93
  if (typeof value !== 'object' || value === null)
@@ -152,13 +160,33 @@ function limitOf(options) {
152
160
  if (limit === undefined)
153
161
  return undefined;
154
162
  if (!Number.isInteger(limit) || limit < 0) {
155
- throw new Error(`conversation limit must be a non-negative integer, got ${String(limit)}`);
163
+ throw runtimeError('invalid-limit', `conversation limit must be a non-negative integer, got ${String(limit)}`);
156
164
  }
157
165
  return limit;
158
166
  }
167
+ const EXPIRED = { kind: 'rejected', code: 'expired', fields: {}, retryable: false };
168
+ function unwritten(draft) {
169
+ return draft.text === '' && draft.files.length === 0;
170
+ }
171
+ function pending(draft) {
172
+ return draft.submitted === undefined ? {} : { submitted: draft.submitted };
173
+ }
174
+ function withSubmitted(draft, submitted) {
175
+ const { submitted: _held, ...unsent } = draft;
176
+ return submitted.length === 0 ? unsent : { ...unsent, submitted };
177
+ }
178
+ function returned(draft, submission, others) {
179
+ return withSubmitted({
180
+ conversation: draft.conversation,
181
+ text: [submission.text, draft.text].filter((one) => one !== '').join('\n'),
182
+ files: [...submission.files, ...draft.files],
183
+ revision: chatDraftRevision(),
184
+ }, others);
185
+ }
159
186
  export function createConversationStore(deps) {
160
- const messages = new Map();
187
+ const stored = new Set();
161
188
  const reads = new Map();
189
+ const partials = new Set();
162
190
  const drafts = new Map();
163
191
  const sending = new Map();
164
192
  const behind = new Set();
@@ -168,56 +196,60 @@ export function createConversationStore(deps) {
168
196
  const listing = new Map();
169
197
  const listWatchers = new Set();
170
198
  let listKnown = false;
199
+ const agentWatchers = new Set();
171
200
  let agentNames = new Map();
201
+ let retiredAgents = new Set();
172
202
  let agentsRead = null;
173
203
  let replicated = new Set();
174
204
  let handedReplicated;
175
205
  function liveOf(conversation) {
176
206
  return lives.get(conversation) ?? QUIET;
177
207
  }
178
- function shownMessages(conversation, limit) {
179
- const held = messages.get(conversation) ?? [];
180
- const shown = limit === undefined ? held : held.slice(Math.max(0, held.length - limit));
181
- return shown.map(({ identity, turn, text, message }) => ({
182
- identity,
183
- turn,
184
- text,
185
- message: structuredClone(message),
186
- }));
187
- }
188
- function viewOf(conversation, limit, shown = shownMessages(conversation, limit)) {
208
+ function viewOf(conversation, held) {
189
209
  const draft = drafts.get(conversation);
190
210
  const facts = liveOf(conversation);
191
211
  return {
192
212
  conversation,
193
- messages: shown,
213
+ messages: held.map(({ identity, turn, text, message }) => ({
214
+ identity,
215
+ turn,
216
+ text,
217
+ message,
218
+ })),
194
219
  turn: structuredClone(facts.turn),
195
220
  questions: structuredClone(facts.questions),
196
221
  process: facts.process === null ? null : { ...facts.process },
197
222
  commands: structuredClone(facts.commands),
198
223
  options: structuredClone(facts.options),
199
- chatDraft: draft === undefined || draft.turn !== null || sending.get(conversation)?.draft === draft
224
+ chatDraft: draft === undefined || unwritten(draft)
200
225
  ? null
201
- : { text: draft.text, files: [...draft.files] },
226
+ : { text: draft.text, files: [...draft.files], revision: draft.revision },
202
227
  };
203
228
  }
204
- function emit(conversation, messagesChanged = true) {
229
+ function emit(conversation) {
205
230
  if (deps.closed())
206
231
  return;
207
232
  for (const view of views.values()) {
208
- if (view.conversation !== conversation)
233
+ if (view.conversation !== conversation || view.window === null)
209
234
  continue;
210
- if (messagesChanged)
211
- view.messages = null;
212
235
  const ready = [...view.watchers].filter((watcher) => watcher.ready);
213
236
  if (ready.length === 0)
214
237
  continue;
215
- view.messages ??= shownMessages(conversation, view.limit);
216
- const value = viewOf(conversation, view.limit, view.messages);
238
+ const value = viewOf(conversation, view.window);
217
239
  for (const watcher of ready)
218
240
  watcher.notify(value);
219
241
  }
220
242
  }
243
+ function slide(conversation, arriving) {
244
+ for (const view of views.values()) {
245
+ if (view.conversation !== conversation || view.window === null)
246
+ continue;
247
+ view.window = arriving === null ? [] : windowOf(view.window, arriving, view.limit);
248
+ }
249
+ }
250
+ function changesOf(conversation, arriving) {
251
+ return arriving.map(({ identity, text }) => ({ conversation, identity, text }));
252
+ }
221
253
  function stateOf(conversation) {
222
254
  const facts = liveOf(conversation);
223
255
  if (facts.questions.length > 0)
@@ -243,6 +275,21 @@ export function createConversationStore(deps) {
243
275
  for (const watcher of listWatchers)
244
276
  watcher(value);
245
277
  }
278
+ function agents() {
279
+ return [...agentNames]
280
+ .filter(([identity]) => !retiredAgents.has(identity))
281
+ .map(([identity, name]) => ({ identity, name }))
282
+ .sort((left, right) => left.name === right.name
283
+ ? left.identity.localeCompare(right.identity)
284
+ : left.name.localeCompare(right.name));
285
+ }
286
+ function emitAgents() {
287
+ if (deps.closed() || agentWatchers.size === 0)
288
+ return;
289
+ const value = agents();
290
+ for (const watcher of agentWatchers)
291
+ watcher(value);
292
+ }
246
293
  function setLive(conversation, next) {
247
294
  const before = stateOf(conversation);
248
295
  const began = lives.get(conversation)?.turn?.started;
@@ -259,20 +306,32 @@ export function createConversationStore(deps) {
259
306
  const declaration = deps.agents();
260
307
  if (declaration === undefined)
261
308
  return;
262
- const { documents } = await deps.documents(declaration.locator);
309
+ const held = await deps.documents(declaration);
310
+ if ('pending' in held)
311
+ return;
312
+ const { documents } = held;
263
313
  const names = new Map();
314
+ const retired = new Set();
264
315
  for (const document of documents) {
265
316
  if (document.type !== declaration.type)
266
317
  continue;
267
318
  const identity = document.fields?.[declaration.identity];
268
319
  const name = document.fields?.[declaration.name];
269
- if (typeof identity === 'string' && typeof name === 'string')
270
- names.set(identity, name);
320
+ if (typeof identity !== 'string' || typeof name !== 'string')
321
+ continue;
322
+ names.set(identity, name);
323
+ const ended = declaration.retired === undefined ? null : document.fields?.[declaration.retired];
324
+ if (ended !== undefined && ended !== null)
325
+ retired.add(identity);
271
326
  }
272
- if (JSON.stringify([...names]) === JSON.stringify([...agentNames]))
327
+ const named = JSON.stringify([...names]) !== JSON.stringify([...agentNames]);
328
+ if (!named && JSON.stringify([...retired]) === JSON.stringify([...retiredAgents]))
273
329
  return;
274
330
  agentNames = names;
275
- emitList();
331
+ retiredAgents = retired;
332
+ if (named)
333
+ emitList();
334
+ emitAgents();
276
335
  }
277
336
  function agentsReady() {
278
337
  agentsRead ??= readAgents().catch(() => {
@@ -282,7 +341,7 @@ export function createConversationStore(deps) {
282
341
  }
283
342
  function live(conversation, change) {
284
343
  setLive(conversation, { ...liveOf(conversation), ...change });
285
- emit(conversation, false);
344
+ emit(conversation);
286
345
  }
287
346
  function relayed(conversation, turn, operation) {
288
347
  const held = liveOf(conversation).turn;
@@ -310,21 +369,25 @@ export function createConversationStore(deps) {
310
369
  }
311
370
  async function remove(leaving) {
312
371
  await deps.durably(async (local) => {
313
- await local.dropConversations(leaving);
314
372
  const removed = [];
315
- const changed = [];
316
373
  for (const conversation of leaving) {
317
- for (const message of messages.get(conversation) ?? []) {
318
- removed.push({ conversation, identity: message.identity });
374
+ for (const identity of await local.messageIdentities(conversation)) {
375
+ removed.push({ conversation, identity, text: null });
319
376
  }
320
- if (messages.has(conversation) || drafts.has(conversation) || lives.has(conversation)) {
377
+ }
378
+ await local.dropConversations(leaving);
379
+ const changed = [];
380
+ for (const conversation of leaving) {
381
+ if (stored.has(conversation) || drafts.has(conversation) || lives.has(conversation)) {
321
382
  changed.push(conversation);
322
383
  }
323
- messages.delete(conversation);
384
+ stored.delete(conversation);
324
385
  reads.delete(conversation);
386
+ partials.delete(conversation);
325
387
  drafts.delete(conversation);
326
388
  behind.delete(conversation);
327
389
  setLive(conversation, null);
390
+ slide(conversation, null);
328
391
  }
329
392
  if (changed.length > 0)
330
393
  deps.publish(conversationChangeSet(removed, changed));
@@ -342,30 +405,25 @@ export function createConversationStore(deps) {
342
405
  const advances = read !== undefined &&
343
406
  !behind.has(conversation) &&
344
407
  (read === null || message.position > read);
345
- const sealed = drafts.get(conversation)?.turn === message.turn;
408
+ await sealed(local, conversation, new Set([message.turn]));
346
409
  await local.keepMessages({
347
410
  conversation,
348
411
  messages: [message],
349
- ...(advances ? { read: message.position } : {}),
350
- sealed,
412
+ ...(advances ? { read: { at: message.position, partial: false } } : {}),
351
413
  });
352
- messages.set(conversation, merged(messages.get(conversation) ?? [], [message]));
414
+ stored.add(conversation);
353
415
  if (advances)
354
416
  reads.set(conversation, message.position);
355
- if (sealed)
356
- drafts.delete(conversation);
357
417
  sealTurns(conversation, new Set([message.turn]));
358
- deps.publish(conversationChangeSet([{ conversation, identity: message.identity }], [conversation]));
418
+ slide(conversation, [message]);
419
+ deps.publish(conversationChangeSet(changesOf(conversation, [message]), [conversation]));
359
420
  });
360
421
  }
361
422
  async function readHistories(conversations) {
362
423
  const generations = new Map(conversations.map((one) => [one, gaps.get(one) ?? 0]));
363
- const starts = new Map(conversations.map((one) => [one, reads.get(one)]));
364
- const arriving = new Map();
365
- const complete = new Set();
366
424
  let asking = conversations.map((conversation) => ({
367
425
  conversation,
368
- after: starts.get(conversation) ?? undefined,
426
+ after: reads.get(conversation) ?? undefined,
369
427
  }));
370
428
  while (asking.length > 0) {
371
429
  const result = await deps.call('conversation.history', {
@@ -377,46 +435,41 @@ export function createConversationStore(deps) {
377
435
  const pages = historyPages(result);
378
436
  if (pages === null)
379
437
  return result;
380
- const asked = new Set(asking.map(({ conversation }) => conversation));
438
+ const asked = new Map(asking.map(({ conversation, after }) => [conversation, after]));
381
439
  const following = [];
382
440
  for (const page of pages) {
383
441
  if (!asked.has(page.conversation))
384
442
  continue;
385
- arriving.set(page.conversation, [
386
- ...(arriving.get(page.conversation) ?? []),
387
- ...page.messages,
388
- ]);
389
- if (page.next === undefined)
390
- complete.add(page.conversation);
391
- else
443
+ await keepPage(page.conversation, page.messages, asked.get(page.conversation) ?? null, page.next, generations.get(page.conversation) ?? 0);
444
+ if (page.next !== undefined) {
392
445
  following.push({ conversation: page.conversation, after: page.next });
446
+ }
393
447
  }
394
448
  asking = following;
395
449
  }
396
- for (const [conversation, held] of arriving) {
397
- if (!complete.has(conversation))
398
- continue;
399
- await keepHistory(conversation, held, starts.get(conversation), generations.get(conversation) ?? 0);
400
- }
401
450
  return null;
402
451
  }
403
- function keepHistory(conversation, arriving, from, generation) {
452
+ function keepPage(conversation, arriving, from, next, generation) {
404
453
  return deps.durably(async (local) => {
405
454
  if (!replicated.has(conversation))
406
455
  return;
407
- const newest = arriving.reduce((top, message) => (top === null || message.position > top ? message.position : top), from ?? null);
408
- const turn = drafts.get(conversation)?.turn;
409
- const sealed = arriving.some((message) => message.turn === turn);
410
- await local.keepMessages({ conversation, messages: arriving, read: newest, sealed });
411
- messages.set(conversation, merged(messages.get(conversation) ?? [], arriving));
412
- reads.set(conversation, newest);
413
- if (sealed)
414
- drafts.delete(conversation);
456
+ const whole = next === undefined;
457
+ const cursor = next ?? newestOf(arriving, from);
458
+ const partial = !whole && (partials.has(conversation) || !reads.has(conversation));
459
+ const sealing = await sealed(local, conversation, new Set(arriving.map((message) => message.turn)));
460
+ await local.keepMessages({ conversation, messages: arriving, read: { at: cursor, partial } });
461
+ stored.add(conversation);
462
+ reads.set(conversation, cursor);
463
+ if (partial)
464
+ partials.add(conversation);
465
+ else
466
+ partials.delete(conversation);
415
467
  sealTurns(conversation, new Set(arriving.map((message) => message.turn)));
416
- if ((gaps.get(conversation) ?? 0) === generation)
468
+ if (whole && (gaps.get(conversation) ?? 0) === generation)
417
469
  behind.delete(conversation);
418
- if (arriving.length > 0 || sealed) {
419
- deps.publish(conversationChangeSet(arriving.map((message) => ({ conversation, identity: message.identity })), [conversation]));
470
+ slide(conversation, arriving);
471
+ if (arriving.length > 0 || sealing || whole) {
472
+ deps.publish(conversationChangeSet(changesOf(conversation, arriving), [conversation]));
420
473
  }
421
474
  });
422
475
  }
@@ -429,7 +482,7 @@ export function createConversationStore(deps) {
429
482
  if (held === undefined)
430
483
  return;
431
484
  setLive(conversation, { ...QUIET, turn: held.turn });
432
- emit(conversation, false);
485
+ emit(conversation);
433
486
  return;
434
487
  }
435
488
  if (event.kind === 'turn') {
@@ -462,22 +515,103 @@ export function createConversationStore(deps) {
462
515
  if (sealTurns(conversation, new Set([arriving.turn])))
463
516
  emit(conversation);
464
517
  }
518
+ async function kept(local, conversation, following) {
519
+ if (following === null || (unwritten(following) && following.submitted === undefined)) {
520
+ await local.dropChatDraft(conversation);
521
+ drafts.delete(conversation);
522
+ }
523
+ else {
524
+ await local.keepChatDraft(following);
525
+ drafts.set(conversation, following);
526
+ }
527
+ }
528
+ async function sealed(local, conversation, turns) {
529
+ const held = drafts.get(conversation);
530
+ const submitted = held?.submitted ?? [];
531
+ const left = submitted.filter((one) => one.turn === null || !turns.has(one.turn));
532
+ if (held === undefined || left.length === submitted.length)
533
+ return false;
534
+ await kept(local, conversation, withSubmitted(held, left));
535
+ return true;
536
+ }
465
537
  function withDraft(conversation, next) {
466
538
  return deps.durably(async (local) => {
467
- const following = next(drafts.get(conversation));
539
+ const following = await next(drafts.get(conversation), local);
468
540
  if (following === undefined)
469
541
  return;
470
- if (following === null) {
471
- await local.dropChatDraft(conversation);
472
- drafts.delete(conversation);
473
- }
474
- else {
475
- await local.keepChatDraft(following);
476
- drafts.set(conversation, following);
477
- }
542
+ await kept(local, conversation, following);
478
543
  deps.publish(conversationChangeSet([], [conversation]));
479
544
  });
480
545
  }
546
+ async function submitted(conversation, operation, draft) {
547
+ let submission;
548
+ let earlier = false;
549
+ await withDraft(conversation, (current) => {
550
+ submission = current?.submitted?.find((one) => one.operation === operation);
551
+ earlier = submission !== undefined;
552
+ if (submission !== undefined)
553
+ return undefined;
554
+ const said = draft ?? current;
555
+ submission = {
556
+ operation,
557
+ text: said?.text ?? '',
558
+ files: [...(said?.files ?? [])],
559
+ turn: null,
560
+ };
561
+ return {
562
+ conversation,
563
+ ...(draft === undefined || current === undefined
564
+ ? { text: '', files: [], revision: chatDraftRevision() }
565
+ : { text: current.text, files: current.files, revision: current.revision }),
566
+ submitted: [...(current?.submitted ?? []), submission],
567
+ };
568
+ });
569
+ return { submission: submission, earlier };
570
+ }
571
+ function forgotten(conversation, submission) {
572
+ return withDraft(conversation, (current) => {
573
+ const held = current?.submitted ?? [];
574
+ if (current === undefined || !held.includes(submission))
575
+ return undefined;
576
+ return withSubmitted(current, held.filter((one) => one !== submission));
577
+ });
578
+ }
579
+ async function dispatched(conversation, submission) {
580
+ const dispatch = {};
581
+ sending.set(submission.operation, dispatch);
582
+ try {
583
+ const files = await Promise.all(submission.files.map(async (file) => ({ ...(await declaredFile(file)), blob: file.blob })));
584
+ const result = await invoke('conversation.send', conversation, {
585
+ text: submission.text,
586
+ files,
587
+ operation_id: submission.operation,
588
+ });
589
+ if (result.kind === 'revoked-authorization')
590
+ return result;
591
+ if (result.kind !== 'action' && result.retryable) {
592
+ deps.unanswered();
593
+ return result;
594
+ }
595
+ const turn = turnOf(result);
596
+ await withDraft(conversation, async (current, local) => {
597
+ const held = current?.submitted?.find((one) => one.operation === submission.operation);
598
+ if (current === undefined || held === undefined || held.turn !== null)
599
+ return undefined;
600
+ const others = current.submitted.filter((one) => one !== held);
601
+ if (result.kind !== 'action')
602
+ return returned(current, held, others);
603
+ const arrived = turn !== null && (await local.turnArrived(conversation, turn));
604
+ return withSubmitted(current, turn === null || arrived
605
+ ? others
606
+ : current.submitted.map((one) => (one === held ? { ...held, turn } : one)));
607
+ });
608
+ return result;
609
+ }
610
+ finally {
611
+ if (sending.get(submission.operation) === dispatch)
612
+ sending.delete(submission.operation);
613
+ }
614
+ }
481
615
  function invoke(name, conversation, extra = {}) {
482
616
  return deps.call(name, { conversation, ...extra });
483
617
  }
@@ -488,46 +622,25 @@ export function createConversationStore(deps) {
488
622
  },
489
623
  async read(conversation, options = {}) {
490
624
  const limit = limitOf(options);
491
- return deps.durably(() => Promise.resolve(viewOf(conversation, limit)));
625
+ return deps.durably(async (local) => structuredClone(viewOf(conversation, await local.messageWindow(conversation, limit))));
492
626
  },
493
627
  create(input) {
494
628
  return deps.call('conversation.create', input);
495
629
  },
496
- async send(conversation) {
497
- const draft = drafts.get(conversation);
498
- const pending = draft === undefined ? undefined : { draft };
499
- if (pending !== undefined) {
500
- sending.set(conversation, pending);
501
- deps.publish(conversationChangeSet([], [conversation]));
502
- }
503
- try {
504
- const files = await Promise.all((draft?.files ?? []).map(async (file) => ({
505
- ...(await declaredFile(file)),
506
- blob: file.blob,
507
- })));
508
- const result = await invoke('conversation.send', conversation, {
509
- text: draft?.text ?? '',
510
- files,
511
- });
512
- const turn = turnOf(result);
513
- if (turn === null || draft === undefined)
514
- return result;
515
- await withDraft(conversation, (current) => {
516
- if (current !== draft)
517
- return undefined;
518
- const sealed = (messages.get(conversation) ?? []).some((message) => message.turn === turn);
519
- return sealed ? null : { ...draft, turn };
520
- });
521
- return result;
630
+ async send(conversation, operation = changeBatchIdentity(), draft) {
631
+ if (deps.online()) {
632
+ const { submission, earlier } = await submitted(conversation, operation, draft);
633
+ if (!earlier || !deps.expired(operation))
634
+ return dispatched(conversation, submission);
635
+ await forgotten(conversation, submission);
636
+ return EXPIRED;
522
637
  }
523
- finally {
524
- if (pending !== undefined && sending.get(conversation) === pending) {
525
- sending.delete(conversation);
526
- if (drafts.get(conversation) === draft) {
527
- deps.publish(conversationChangeSet([], [conversation]));
528
- }
529
- }
638
+ if (draft !== undefined) {
639
+ await withDraft(conversation, (current) => current?.submitted?.some((one) => one.operation === operation) === true
640
+ ? undefined
641
+ : returned(current ?? { conversation, text: '', files: [], revision: '' }, { operation, ...draft, turn: null }, current?.submitted ?? []));
530
642
  }
643
+ return OFFLINE;
531
644
  },
532
645
  answer(conversation, answer) {
533
646
  return invoke('conversation.answer', conversation, { answer });
@@ -542,7 +655,12 @@ export function createConversationStore(deps) {
542
655
  const result = await invoke('conversation.cancel', conversation, { turn });
543
656
  if (result.kind !== 'action')
544
657
  return result;
545
- await withDraft(conversation, (current) => current === undefined || current.turn !== turn ? undefined : { ...current, turn: null });
658
+ await withDraft(conversation, (current) => {
659
+ const held = current?.submitted?.find((one) => one.turn === turn);
660
+ if (current === undefined || held === undefined)
661
+ return undefined;
662
+ return returned(current, held, current.submitted.filter((one) => one !== held));
663
+ });
546
664
  return result;
547
665
  },
548
666
  delete(conversation) {
@@ -550,33 +668,43 @@ export function createConversationStore(deps) {
550
668
  },
551
669
  };
552
670
  const chatDraft = {
553
- write(conversation, draft) {
554
- return withDraft(conversation, () => ({
671
+ write(conversation, draft, revision = chatDraftRevision()) {
672
+ return withDraft(conversation, (current) => ({
555
673
  conversation,
556
674
  text: draft.text,
557
675
  files: [...draft.files],
558
- turn: null,
676
+ revision,
677
+ ...(current?.submitted === undefined ? {} : { submitted: current.submitted }),
559
678
  }));
560
679
  },
561
680
  discard(conversation) {
562
- return withDraft(conversation, (current) => (current === undefined ? undefined : null));
681
+ return withDraft(conversation, (current) => current === undefined
682
+ ? undefined
683
+ : {
684
+ conversation,
685
+ text: '',
686
+ files: [],
687
+ revision: chatDraftRevision(),
688
+ ...pending(current),
689
+ });
563
690
  },
564
691
  };
565
692
  return {
566
693
  surface,
567
694
  chatDraft,
568
- hydrate(held, stored) {
569
- messages.clear();
695
+ hydrate(held, kept) {
696
+ stored.clear();
570
697
  reads.clear();
571
- for (const view of views.values())
572
- view.messages = null;
698
+ partials.clear();
573
699
  for (const entry of held) {
574
- messages.set(entry.conversation, [...entry.messages].sort(ordered));
700
+ stored.add(entry.conversation);
575
701
  if (entry.read !== undefined)
576
702
  reads.set(entry.conversation, entry.read);
703
+ if (entry.partial)
704
+ partials.add(entry.conversation);
577
705
  }
578
706
  drafts.clear();
579
- for (const record of stored)
707
+ for (const record of kept)
580
708
  drafts.set(record.conversation, record);
581
709
  },
582
710
  async reconcile() {
@@ -584,7 +712,7 @@ export function createConversationStore(deps) {
584
712
  return;
585
713
  const named = new Set(listing.keys());
586
714
  const leaving = [
587
- ...new Set([...replicated, ...messages.keys(), ...reads.keys(), ...drafts.keys()]),
715
+ ...new Set([...replicated, ...stored, ...reads.keys(), ...drafts.keys()]),
588
716
  ].filter((conversation) => !named.has(conversation));
589
717
  for (const conversation of named) {
590
718
  if (!replicated.has(conversation))
@@ -605,6 +733,31 @@ export function createConversationStore(deps) {
605
733
  if (behind.size > 0)
606
734
  progress.complete = false;
607
735
  },
736
+ async resend(progress) {
737
+ for (const [conversation, held] of [...drafts]) {
738
+ for (const submission of held.submitted ?? []) {
739
+ if (submission.turn !== null || sending.has(submission.operation))
740
+ continue;
741
+ if (deps.closed())
742
+ return;
743
+ if (deps.expired(submission.operation)) {
744
+ await forgotten(conversation, submission);
745
+ continue;
746
+ }
747
+ const result = await dispatched(conversation, submission);
748
+ if (result.kind === 'revoked-authorization')
749
+ return;
750
+ if (result.kind === 'transport-uncertainty') {
751
+ progress.reachable = false;
752
+ return;
753
+ }
754
+ if (result.kind !== 'action' && result.retryable) {
755
+ progress.complete = false;
756
+ return;
757
+ }
758
+ }
759
+ }
760
+ },
608
761
  current() {
609
762
  return behind.size === 0;
610
763
  },
@@ -612,6 +765,9 @@ export function createConversationStore(deps) {
612
765
  for (const conversation of replicated)
613
766
  gap(conversation);
614
767
  },
768
+ reopened() {
769
+ sending.clear();
770
+ },
615
771
  watch(conversation, options, deliver) {
616
772
  const limit = limitOf(options);
617
773
  const key = JSON.stringify([conversation, limit ?? null]);
@@ -619,18 +775,20 @@ export function createConversationStore(deps) {
619
775
  conversation,
620
776
  limit,
621
777
  watchers: new Set(),
622
- messages: null,
778
+ window: null,
623
779
  };
624
780
  views.set(key, view);
625
781
  const watcher = { notify: deliver, ready: false };
626
782
  view.watchers.add(watcher);
627
783
  void deps
628
- .durably(() => Promise.resolve())
784
+ .durably(async (local) => {
785
+ view.window ??= await local.messageWindow(conversation, limit);
786
+ })
629
787
  .then(() => {
630
788
  if (deps.closed() || !view.watchers.has(watcher))
631
789
  return;
632
790
  watcher.ready = true;
633
- watcher.notify(viewOf(conversation, limit));
791
+ watcher.notify(viewOf(conversation, view.window ?? []));
634
792
  })
635
793
  .catch(() => { });
636
794
  return () => {
@@ -650,6 +808,16 @@ export function createConversationStore(deps) {
650
808
  listWatchers.delete(deliver);
651
809
  };
652
810
  },
811
+ watchAgents(deliver) {
812
+ agentWatchers.add(deliver);
813
+ void agentsReady().then(() => {
814
+ if (!deps.closed() && agentWatchers.has(deliver))
815
+ deliver(agents());
816
+ });
817
+ return () => {
818
+ agentWatchers.delete(deliver);
819
+ };
820
+ },
653
821
  changed(set) {
654
822
  for (const conversation of set.conversations)
655
823
  emit(conversation);
@@ -686,17 +854,17 @@ export function createConversationStore(deps) {
686
854
  heard(event);
687
855
  return false;
688
856
  },
689
- messages() {
690
- return [...messages].flatMap(([conversation, held]) => held.map(({ identity, text }) => ({ conversation, identity, text })));
857
+ searchable() {
858
+ return [...(listKnown ? listing.keys() : stored)].sort();
691
859
  },
692
- message(conversation, identity) {
693
- const held = messages.get(conversation)?.find((message) => message.identity === identity);
694
- return held === undefined ? null : { conversation, identity, text: held.text };
860
+ acquired(conversation) {
861
+ return reads.has(conversation) && !partials.has(conversation);
695
862
  },
696
863
  close() {
697
864
  views.clear();
698
865
  lives.clear();
699
866
  listWatchers.clear();
867
+ agentWatchers.clear();
700
868
  },
701
869
  };
702
870
  }