@agentshouse/mdruntime 0.1.0-c69ca0e865e0b → 0.1.0-c6fd2cbbff187

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