@agentshouse/mdruntime 0.1.0-cde2b99d2fcbc → 0.1.0-ce131cb435f8b

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