@agentshouse/mdruntime 0.1.0-cdf82f77fae68 → 0.1.0-ceb97106cc2fd

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