@agentshouse/mdruntime 0.1.0-c967d1ca66fee → 0.1.0-c97456d63baba

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 (183) hide show
  1. package/AGENTS.md +257 -227
  2. package/README.md +11 -1
  3. package/dist/acquisition.d.ts +14 -0
  4. package/dist/acquisition.d.ts.map +1 -0
  5. package/dist/acquisition.js +24 -0
  6. package/dist/adapter.d.ts +69 -20
  7. package/dist/adapter.d.ts.map +1 -1
  8. package/dist/attempt.d.ts +16 -0
  9. package/dist/attempt.d.ts.map +1 -0
  10. package/dist/attempt.js +70 -0
  11. package/dist/binary.d.ts +23 -0
  12. package/dist/binary.d.ts.map +1 -0
  13. package/dist/binary.js +8 -0
  14. package/dist/bootstrap.d.ts +2 -2
  15. package/dist/bootstrap.d.ts.map +1 -1
  16. package/dist/bootstrap.js +33 -14
  17. package/dist/change-set.d.ts +2 -0
  18. package/dist/change-set.d.ts.map +1 -1
  19. package/dist/change-set.js +39 -13
  20. package/dist/change.d.ts +11 -0
  21. package/dist/change.d.ts.map +1 -1
  22. package/dist/change.js +26 -1
  23. package/dist/chat/acts.d.ts +33 -0
  24. package/dist/chat/acts.d.ts.map +1 -0
  25. package/dist/chat/acts.js +36 -0
  26. package/dist/chat/context.d.ts +3 -0
  27. package/dist/chat/context.d.ts.map +1 -0
  28. package/dist/chat/context.js +10 -0
  29. package/dist/chat/input.d.ts +9 -0
  30. package/dist/chat/input.d.ts.map +1 -0
  31. package/dist/chat/input.js +99 -0
  32. package/dist/chat/kit.d.ts +61 -0
  33. package/dist/chat/kit.d.ts.map +1 -0
  34. package/dist/chat/kit.js +123 -0
  35. package/dist/chat/lasting.d.ts +6 -0
  36. package/dist/chat/lasting.d.ts.map +1 -0
  37. package/dist/chat/lasting.js +19 -0
  38. package/dist/chat/list.d.ts +27 -0
  39. package/dist/chat/list.d.ts.map +1 -0
  40. package/dist/chat/list.js +63 -0
  41. package/dist/chat/markdown.d.ts +20 -0
  42. package/dist/chat/markdown.d.ts.map +1 -0
  43. package/dist/chat/markdown.js +188 -0
  44. package/dist/chat/message.d.ts +6 -0
  45. package/dist/chat/message.d.ts.map +1 -0
  46. package/dist/chat/message.js +192 -0
  47. package/dist/chat/messages.d.ts +22 -0
  48. package/dist/chat/messages.d.ts.map +1 -0
  49. package/dist/chat/messages.js +155 -0
  50. package/dist/chat/name.d.ts +6 -0
  51. package/dist/chat/name.d.ts.map +1 -0
  52. package/dist/chat/name.js +37 -0
  53. package/dist/chat/opened.d.ts +11 -0
  54. package/dist/chat/opened.d.ts.map +1 -0
  55. package/dist/chat/opened.js +17 -0
  56. package/dist/chat/overlay.d.ts +31 -0
  57. package/dist/chat/overlay.d.ts.map +1 -0
  58. package/dist/chat/overlay.js +97 -0
  59. package/dist/chat/parts.d.ts +57 -0
  60. package/dist/chat/parts.d.ts.map +1 -0
  61. package/dist/chat/parts.js +119 -0
  62. package/dist/chat/process.d.ts +20 -0
  63. package/dist/chat/process.d.ts.map +1 -0
  64. package/dist/chat/process.js +32 -0
  65. package/dist/chat/provider.d.ts +63 -0
  66. package/dist/chat/provider.d.ts.map +1 -0
  67. package/dist/chat/provider.js +629 -0
  68. package/dist/chat/question.d.ts +9 -0
  69. package/dist/chat/question.d.ts.map +1 -0
  70. package/dist/chat/question.js +80 -0
  71. package/dist/chat/questions.d.ts +31 -0
  72. package/dist/chat/questions.d.ts.map +1 -0
  73. package/dist/chat/questions.js +111 -0
  74. package/dist/chat/read.d.ts +5 -0
  75. package/dist/chat/read.d.ts.map +1 -0
  76. package/dist/chat/read.js +13 -0
  77. package/dist/chat/ready.d.ts +34 -0
  78. package/dist/chat/ready.d.ts.map +1 -0
  79. package/dist/chat/ready.js +91 -0
  80. package/dist/chat/style.d.ts +4 -0
  81. package/dist/chat/style.d.ts.map +1 -0
  82. package/dist/chat/style.js +302 -0
  83. package/dist/chat.d.ts +14 -0
  84. package/dist/chat.d.ts.map +1 -0
  85. package/dist/chat.js +8 -0
  86. package/dist/client.d.ts +21 -10
  87. package/dist/client.d.ts.map +1 -1
  88. package/dist/client.js +14 -2
  89. package/dist/collection.d.ts +17 -3
  90. package/dist/collection.d.ts.map +1 -1
  91. package/dist/collection.js +230 -19
  92. package/dist/connection.d.ts +32 -12
  93. package/dist/connection.d.ts.map +1 -1
  94. package/dist/connection.js +1482 -351
  95. package/dist/conversation.d.ts +23 -9
  96. package/dist/conversation.d.ts.map +1 -1
  97. package/dist/conversation.js +372 -142
  98. package/dist/durability.d.ts +7 -1
  99. package/dist/durability.d.ts.map +1 -1
  100. package/dist/entry.d.ts +17 -0
  101. package/dist/entry.d.ts.map +1 -0
  102. package/dist/entry.js +156 -0
  103. package/dist/expectation.d.ts +21 -0
  104. package/dist/expectation.d.ts.map +1 -0
  105. package/dist/expectation.js +70 -0
  106. package/dist/fence.d.ts +6 -0
  107. package/dist/fence.d.ts.map +1 -0
  108. package/dist/fence.js +10 -0
  109. package/dist/host.d.ts +13 -3
  110. package/dist/host.d.ts.map +1 -1
  111. package/dist/host.js +48 -11
  112. package/dist/identity.d.ts +4 -0
  113. package/dist/identity.d.ts.map +1 -1
  114. package/dist/identity.js +12 -0
  115. package/dist/index.d.ts +13 -7
  116. package/dist/index.d.ts.map +1 -1
  117. package/dist/index.js +4 -0
  118. package/dist/memory.d.ts +5 -3
  119. package/dist/memory.d.ts.map +1 -1
  120. package/dist/memory.js +47 -13
  121. package/dist/model.d.ts +4 -2
  122. package/dist/model.d.ts.map +1 -1
  123. package/dist/model.js +10 -1
  124. package/dist/observation.d.ts +5 -2
  125. package/dist/observation.d.ts.map +1 -1
  126. package/dist/observation.js +22 -11
  127. package/dist/outcome.d.ts +11 -1
  128. package/dist/outcome.d.ts.map +1 -1
  129. package/dist/outcome.js +7 -0
  130. package/dist/overlay.d.ts +14 -2
  131. package/dist/overlay.d.ts.map +1 -1
  132. package/dist/overlay.js +75 -62
  133. package/dist/query.d.ts +31 -3
  134. package/dist/query.d.ts.map +1 -1
  135. package/dist/reach.d.ts +8 -2
  136. package/dist/reach.d.ts.map +1 -1
  137. package/dist/reach.js +42 -8
  138. package/dist/react.d.ts +7 -4
  139. package/dist/react.d.ts.map +1 -1
  140. package/dist/read-side.d.ts +13 -6
  141. package/dist/read-side.d.ts.map +1 -1
  142. package/dist/read-side.js +291 -85
  143. package/dist/readiness.d.ts +53 -0
  144. package/dist/readiness.d.ts.map +1 -0
  145. package/dist/readiness.js +33 -0
  146. package/dist/recovery.d.ts +25 -0
  147. package/dist/recovery.d.ts.map +1 -0
  148. package/dist/recovery.js +197 -0
  149. package/dist/refresh.d.ts.map +1 -1
  150. package/dist/refresh.js +21 -25
  151. package/dist/replica.d.ts +6 -3
  152. package/dist/replica.d.ts.map +1 -1
  153. package/dist/replica.js +78 -45
  154. package/dist/room-draft.d.ts +20 -3
  155. package/dist/room-draft.d.ts.map +1 -1
  156. package/dist/room-draft.js +119 -1
  157. package/dist/rows.d.ts +6 -13
  158. package/dist/rows.d.ts.map +1 -1
  159. package/dist/rows.js +5 -13
  160. package/dist/save.d.ts +28 -0
  161. package/dist/save.d.ts.map +1 -1
  162. package/dist/save.js +343 -29
  163. package/dist/search.d.ts +28 -6
  164. package/dist/search.d.ts.map +1 -1
  165. package/dist/search.js +108 -45
  166. package/dist/shared.d.ts +11 -0
  167. package/dist/shared.d.ts.map +1 -0
  168. package/dist/shared.js +368 -0
  169. package/dist/status.d.ts +4 -0
  170. package/dist/status.d.ts.map +1 -1
  171. package/dist/store.d.ts +55 -21
  172. package/dist/store.d.ts.map +1 -1
  173. package/dist/store.js +424 -206
  174. package/dist/turn.d.ts +7 -0
  175. package/dist/turn.d.ts.map +1 -0
  176. package/dist/turn.js +45 -0
  177. package/dist/wire.d.ts +137 -30
  178. package/dist/wire.d.ts.map +1 -1
  179. package/dist/wire.js +49 -29
  180. package/package.json +26 -14
  181. package/dist/attachment.d.ts +0 -3
  182. package/dist/attachment.d.ts.map +0 -1
  183. package/dist/attachment.js +0 -8
@@ -1,6 +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 { changeBatchIdentity, chatDraftRevision } from './identity.js';
5
+ import { OFFLINE } from './outcome.js';
4
6
  const QUIET = {
5
7
  turn: null,
6
8
  questions: [],
@@ -20,6 +22,17 @@ export function unheldView(conversation) {
20
22
  chatDraft: null,
21
23
  };
22
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
+ }
23
36
  function startedTurn(turn, operation) {
24
37
  if (operation.op === 'begin')
25
38
  return { turn, started: operation.at, parts: [], context: null };
@@ -76,11 +89,15 @@ function ordered(left, right) {
76
89
  return left.identity < right.identity ? -1 : 1;
77
90
  return 0;
78
91
  }
79
- function merged(held, arriving) {
92
+ function windowOf(held, arriving, limit) {
80
93
  const byIdentity = new Map(held.map((message) => [message.identity, message]));
81
94
  for (const message of arriving)
82
95
  byIdentity.set(message.identity, message);
83
- 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);
84
101
  }
85
102
  function acceptedMessage(value) {
86
103
  if (typeof value !== 'object' || value === null)
@@ -139,14 +156,14 @@ function historyPages(result) {
139
156
  }
140
157
  return pages;
141
158
  }
142
- function turnOf(result) {
159
+ function answered(result, field) {
143
160
  if (result.kind !== 'action')
144
161
  return null;
145
162
  const answer = result.result;
146
163
  if (typeof answer !== 'object' || answer === null)
147
164
  return null;
148
- const record = answer;
149
- return typeof record.turn === 'string' ? record.turn : null;
165
+ const value = answer[field];
166
+ return typeof value === 'string' ? value : null;
150
167
  }
151
168
  function limitOf(options) {
152
169
  const { limit } = options;
@@ -157,9 +174,35 @@ function limitOf(options) {
157
174
  }
158
175
  return limit;
159
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
+ }
160
202
  export function createConversationStore(deps) {
161
- const messages = new Map();
203
+ const stored = new Set();
162
204
  const reads = new Map();
205
+ const partials = new Set();
163
206
  const drafts = new Map();
164
207
  const sending = new Map();
165
208
  const behind = new Set();
@@ -167,58 +210,63 @@ export function createConversationStore(deps) {
167
210
  const lives = new Map();
168
211
  const views = new Map();
169
212
  const listing = new Map();
213
+ const creations = new Set();
170
214
  const listWatchers = new Set();
171
215
  let listKnown = false;
216
+ const agentWatchers = new Set();
172
217
  let agentNames = new Map();
218
+ let retiredAgents = new Set();
173
219
  let agentsRead = null;
174
220
  let replicated = new Set();
175
221
  let handedReplicated;
176
222
  function liveOf(conversation) {
177
223
  return lives.get(conversation) ?? QUIET;
178
224
  }
179
- function shownMessages(conversation, limit) {
180
- const held = messages.get(conversation) ?? [];
181
- const shown = limit === undefined ? held : held.slice(Math.max(0, held.length - limit));
182
- return shown.map(({ identity, turn, text, message }) => ({
183
- identity,
184
- turn,
185
- text,
186
- message: structuredClone(message),
187
- }));
188
- }
189
- function viewOf(conversation, limit, shown = shownMessages(conversation, limit)) {
225
+ function viewOf(conversation, held) {
190
226
  const draft = drafts.get(conversation);
191
227
  const facts = liveOf(conversation);
192
228
  return {
193
229
  conversation,
194
- messages: shown,
230
+ messages: held.map(({ identity, turn, text, message }) => ({
231
+ identity,
232
+ turn,
233
+ text,
234
+ message,
235
+ })),
195
236
  turn: structuredClone(facts.turn),
196
237
  questions: structuredClone(facts.questions),
197
238
  process: facts.process === null ? null : { ...facts.process },
198
239
  commands: structuredClone(facts.commands),
199
240
  options: structuredClone(facts.options),
200
- chatDraft: draft === undefined || draft.turn !== null || sending.get(conversation)?.draft === draft
241
+ chatDraft: draft === undefined || unwritten(draft)
201
242
  ? null
202
- : { text: draft.text, files: [...draft.files] },
243
+ : { text: draft.text, files: [...draft.files], revision: draft.revision },
203
244
  };
204
245
  }
205
- function emit(conversation, messagesChanged = true) {
246
+ function emit(conversation) {
206
247
  if (deps.closed())
207
248
  return;
208
249
  for (const view of views.values()) {
209
- if (view.conversation !== conversation)
250
+ if (view.conversation !== conversation || view.window === null)
210
251
  continue;
211
- if (messagesChanged)
212
- view.messages = null;
213
252
  const ready = [...view.watchers].filter((watcher) => watcher.ready);
214
253
  if (ready.length === 0)
215
254
  continue;
216
- view.messages ??= shownMessages(conversation, view.limit);
217
- const value = viewOf(conversation, view.limit, view.messages);
255
+ const value = viewOf(conversation, view.window);
218
256
  for (const watcher of ready)
219
257
  watcher.notify(value);
220
258
  }
221
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
+ }
222
270
  function stateOf(conversation) {
223
271
  const facts = liveOf(conversation);
224
272
  if (facts.questions.length > 0)
@@ -244,6 +292,21 @@ export function createConversationStore(deps) {
244
292
  for (const watcher of listWatchers)
245
293
  watcher(value);
246
294
  }
295
+ function agents() {
296
+ return [...agentNames]
297
+ .filter(([identity]) => !retiredAgents.has(identity))
298
+ .map(([identity, name]) => ({ identity, name }))
299
+ .sort((left, right) => left.name === right.name
300
+ ? left.identity.localeCompare(right.identity)
301
+ : left.name.localeCompare(right.name));
302
+ }
303
+ function emitAgents() {
304
+ if (deps.closed() || agentWatchers.size === 0)
305
+ return;
306
+ const value = agents();
307
+ for (const watcher of agentWatchers)
308
+ watcher(value);
309
+ }
247
310
  function setLive(conversation, next) {
248
311
  const before = stateOf(conversation);
249
312
  const began = lives.get(conversation)?.turn?.started;
@@ -260,20 +323,38 @@ export function createConversationStore(deps) {
260
323
  const declaration = deps.agents();
261
324
  if (declaration === undefined)
262
325
  return;
263
- 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;
264
330
  const names = new Map();
331
+ const retired = new Set();
265
332
  for (const document of documents) {
266
333
  if (document.type !== declaration.type)
267
334
  continue;
268
335
  const identity = document.fields?.[declaration.identity];
269
336
  const name = document.fields?.[declaration.name];
270
- if (typeof identity === 'string' && typeof name === 'string')
271
- names.set(identity, name);
337
+ if (typeof identity !== 'string' || typeof name !== 'string')
338
+ continue;
339
+ names.set(identity, name);
340
+ const ended = declaration.retired === undefined ? null : document.fields?.[declaration.retired];
341
+ if (ended !== undefined && ended !== null)
342
+ retired.add(identity);
272
343
  }
273
- if (JSON.stringify([...names]) === JSON.stringify([...agentNames]))
344
+ const named = JSON.stringify([...names]) !== JSON.stringify([...agentNames]);
345
+ if (!named && JSON.stringify([...retired]) === JSON.stringify([...retiredAgents]))
274
346
  return;
275
347
  agentNames = names;
276
- emitList();
348
+ retiredAgents = retired;
349
+ if (named)
350
+ emitList();
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)));
277
358
  }
278
359
  function agentsReady() {
279
360
  agentsRead ??= readAgents().catch(() => {
@@ -283,7 +364,7 @@ export function createConversationStore(deps) {
283
364
  }
284
365
  function live(conversation, change) {
285
366
  setLive(conversation, { ...liveOf(conversation), ...change });
286
- emit(conversation, false);
367
+ emit(conversation);
287
368
  }
288
369
  function relayed(conversation, turn, operation) {
289
370
  const held = liveOf(conversation).turn;
@@ -311,21 +392,25 @@ export function createConversationStore(deps) {
311
392
  }
312
393
  async function remove(leaving) {
313
394
  await deps.durably(async (local) => {
314
- await local.dropConversations(leaving);
315
395
  const removed = [];
316
- const changed = [];
317
396
  for (const conversation of leaving) {
318
- for (const message of messages.get(conversation) ?? []) {
319
- removed.push({ conversation, identity: message.identity });
397
+ for (const identity of await local.messageIdentities(conversation)) {
398
+ removed.push({ conversation, identity, text: null });
320
399
  }
321
- 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)) {
322
405
  changed.push(conversation);
323
406
  }
324
- messages.delete(conversation);
407
+ stored.delete(conversation);
325
408
  reads.delete(conversation);
409
+ partials.delete(conversation);
326
410
  drafts.delete(conversation);
327
411
  behind.delete(conversation);
328
412
  setLive(conversation, null);
413
+ slide(conversation, null);
329
414
  }
330
415
  if (changed.length > 0)
331
416
  deps.publish(conversationChangeSet(removed, changed));
@@ -343,30 +428,25 @@ export function createConversationStore(deps) {
343
428
  const advances = read !== undefined &&
344
429
  !behind.has(conversation) &&
345
430
  (read === null || message.position > read);
346
- const sealed = drafts.get(conversation)?.turn === message.turn;
431
+ await sealed(local, conversation, new Set([message.turn]));
347
432
  await local.keepMessages({
348
433
  conversation,
349
434
  messages: [message],
350
- ...(advances ? { read: message.position } : {}),
351
- sealed,
435
+ ...(advances ? { read: { at: message.position, partial: false } } : {}),
352
436
  });
353
- messages.set(conversation, merged(messages.get(conversation) ?? [], [message]));
437
+ stored.add(conversation);
354
438
  if (advances)
355
439
  reads.set(conversation, message.position);
356
- if (sealed)
357
- drafts.delete(conversation);
358
440
  sealTurns(conversation, new Set([message.turn]));
359
- deps.publish(conversationChangeSet([{ conversation, identity: message.identity }], [conversation]));
441
+ slide(conversation, [message]);
442
+ deps.publish(conversationChangeSet(changesOf(conversation, [message]), [conversation]));
360
443
  });
361
444
  }
362
445
  async function readHistories(conversations) {
363
446
  const generations = new Map(conversations.map((one) => [one, gaps.get(one) ?? 0]));
364
- const starts = new Map(conversations.map((one) => [one, reads.get(one)]));
365
- const arriving = new Map();
366
- const complete = new Set();
367
447
  let asking = conversations.map((conversation) => ({
368
448
  conversation,
369
- after: starts.get(conversation) ?? undefined,
449
+ after: reads.get(conversation) ?? undefined,
370
450
  }));
371
451
  while (asking.length > 0) {
372
452
  const result = await deps.call('conversation.history', {
@@ -378,46 +458,41 @@ export function createConversationStore(deps) {
378
458
  const pages = historyPages(result);
379
459
  if (pages === null)
380
460
  return result;
381
- const asked = new Set(asking.map(({ conversation }) => conversation));
461
+ const asked = new Map(asking.map(({ conversation, after }) => [conversation, after]));
382
462
  const following = [];
383
463
  for (const page of pages) {
384
464
  if (!asked.has(page.conversation))
385
465
  continue;
386
- arriving.set(page.conversation, [
387
- ...(arriving.get(page.conversation) ?? []),
388
- ...page.messages,
389
- ]);
390
- if (page.next === undefined)
391
- complete.add(page.conversation);
392
- 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) {
393
468
  following.push({ conversation: page.conversation, after: page.next });
469
+ }
394
470
  }
395
471
  asking = following;
396
472
  }
397
- for (const [conversation, held] of arriving) {
398
- if (!complete.has(conversation))
399
- continue;
400
- await keepHistory(conversation, held, starts.get(conversation), generations.get(conversation) ?? 0);
401
- }
402
473
  return null;
403
474
  }
404
- function keepHistory(conversation, arriving, from, generation) {
475
+ function keepPage(conversation, arriving, from, next, generation) {
405
476
  return deps.durably(async (local) => {
406
477
  if (!replicated.has(conversation))
407
478
  return;
408
- const newest = arriving.reduce((top, message) => (top === null || message.position > top ? message.position : top), from ?? null);
409
- const turn = drafts.get(conversation)?.turn;
410
- const sealed = arriving.some((message) => message.turn === turn);
411
- await local.keepMessages({ conversation, messages: arriving, read: newest, sealed });
412
- messages.set(conversation, merged(messages.get(conversation) ?? [], arriving));
413
- reads.set(conversation, newest);
414
- if (sealed)
415
- 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);
416
490
  sealTurns(conversation, new Set(arriving.map((message) => message.turn)));
417
- if ((gaps.get(conversation) ?? 0) === generation)
491
+ if (whole && (gaps.get(conversation) ?? 0) === generation)
418
492
  behind.delete(conversation);
419
- if (arriving.length > 0 || sealed) {
420
- 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]));
421
496
  }
422
497
  });
423
498
  }
@@ -430,7 +505,7 @@ export function createConversationStore(deps) {
430
505
  if (held === undefined)
431
506
  return;
432
507
  setLive(conversation, { ...QUIET, turn: held.turn });
433
- emit(conversation, false);
508
+ emit(conversation);
434
509
  return;
435
510
  }
436
511
  if (event.kind === 'turn') {
@@ -463,22 +538,126 @@ export function createConversationStore(deps) {
463
538
  if (sealTurns(conversation, new Set([arriving.turn])))
464
539
  emit(conversation);
465
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
+ }
466
583
  function withDraft(conversation, next) {
467
584
  return deps.durably(async (local) => {
468
- const following = next(drafts.get(conversation));
585
+ const following = await next(drafts.get(conversation), local);
469
586
  if (following === undefined)
470
587
  return;
471
- if (following === null) {
472
- await local.dropChatDraft(conversation);
473
- drafts.delete(conversation);
474
- }
475
- else {
476
- await local.keepChatDraft(following);
477
- drafts.set(conversation, following);
478
- }
588
+ await kept(local, conversation, following);
479
589
  deps.publish(conversationChangeSet([], [conversation]));
480
590
  });
481
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
+ }
482
661
  function invoke(name, conversation, extra = {}) {
483
662
  return deps.call(name, { conversation, ...extra });
484
663
  }
@@ -489,46 +668,34 @@ export function createConversationStore(deps) {
489
668
  },
490
669
  async read(conversation, options = {}) {
491
670
  const limit = limitOf(options);
492
- return deps.durably(() => Promise.resolve(viewOf(conversation, limit)));
671
+ return deps.durably(async (local) => structuredClone(viewOf(conversation, await local.messageWindow(conversation, limit))));
493
672
  },
494
- create(input) {
495
- return deps.call('conversation.create', input);
496
- },
497
- async send(conversation) {
498
- const draft = drafts.get(conversation);
499
- const pending = draft === undefined ? undefined : { draft };
500
- if (pending !== undefined) {
501
- sending.set(conversation, pending);
502
- deps.publish(conversationChangeSet([], [conversation]));
503
- }
504
- try {
505
- const files = await Promise.all((draft?.files ?? []).map(async (file) => ({
506
- ...(await declaredFile(file)),
507
- blob: file.blob,
508
- })));
509
- const result = await invoke('conversation.send', conversation, {
510
- text: draft?.text ?? '',
511
- files,
512
- });
513
- const turn = turnOf(result);
514
- if (turn === null || draft === undefined)
515
- return result;
516
- await withDraft(conversation, (current) => {
517
- if (current !== draft)
518
- return undefined;
519
- const sealed = (messages.get(conversation) ?? []).some((message) => message.turn === turn);
520
- return sealed ? null : { ...draft, turn };
521
- });
673
+ async create(input) {
674
+ const result = await deps.call('conversation.create', input);
675
+ const conversation = answered(result, 'conversation');
676
+ if (conversation === null)
522
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;
523
692
  }
524
- finally {
525
- if (pending !== undefined && sending.get(conversation) === pending) {
526
- sending.delete(conversation);
527
- if (drafts.get(conversation) === draft) {
528
- deps.publish(conversationChangeSet([], [conversation]));
529
- }
530
- }
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 ?? []));
531
697
  }
698
+ return OFFLINE;
532
699
  },
533
700
  answer(conversation, answer) {
534
701
  return invoke('conversation.answer', conversation, { answer });
@@ -543,7 +710,12 @@ export function createConversationStore(deps) {
543
710
  const result = await invoke('conversation.cancel', conversation, { turn });
544
711
  if (result.kind !== 'action')
545
712
  return result;
546
- 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
+ });
547
719
  return result;
548
720
  },
549
721
  delete(conversation) {
@@ -551,33 +723,43 @@ export function createConversationStore(deps) {
551
723
  },
552
724
  };
553
725
  const chatDraft = {
554
- write(conversation, draft) {
555
- return withDraft(conversation, () => ({
726
+ write(conversation, draft, revision = chatDraftRevision()) {
727
+ return withDraft(conversation, (current) => ({
556
728
  conversation,
557
729
  text: draft.text,
558
730
  files: [...draft.files],
559
- turn: null,
731
+ revision,
732
+ ...(current === undefined ? {} : pending(current)),
560
733
  }));
561
734
  },
562
735
  discard(conversation) {
563
- 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
+ });
564
745
  },
565
746
  };
566
747
  return {
567
748
  surface,
568
749
  chatDraft,
569
- hydrate(held, stored) {
570
- messages.clear();
750
+ hydrate(held, kept) {
751
+ stored.clear();
571
752
  reads.clear();
572
- for (const view of views.values())
573
- view.messages = null;
753
+ partials.clear();
574
754
  for (const entry of held) {
575
- messages.set(entry.conversation, [...entry.messages].sort(ordered));
755
+ stored.add(entry.conversation);
576
756
  if (entry.read !== undefined)
577
757
  reads.set(entry.conversation, entry.read);
758
+ if (entry.partial)
759
+ partials.add(entry.conversation);
578
760
  }
579
761
  drafts.clear();
580
- for (const record of stored)
762
+ for (const record of kept)
581
763
  drafts.set(record.conversation, record);
582
764
  },
583
765
  async reconcile() {
@@ -585,8 +767,10 @@ export function createConversationStore(deps) {
585
767
  return;
586
768
  const named = new Set(listing.keys());
587
769
  const leaving = [
588
- ...new Set([...replicated, ...messages.keys(), ...reads.keys(), ...drafts.keys()]),
589
- ].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)));
590
774
  for (const conversation of named) {
591
775
  if (!replicated.has(conversation))
592
776
  behind.add(conversation);
@@ -606,13 +790,44 @@ export function createConversationStore(deps) {
606
790
  if (behind.size > 0)
607
791
  progress.complete = false;
608
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
+ },
609
818
  current() {
610
819
  return behind.size === 0;
611
820
  },
821
+ known() {
822
+ return listKnown;
823
+ },
612
824
  reconnected() {
613
825
  for (const conversation of replicated)
614
826
  gap(conversation);
615
827
  },
828
+ reopened() {
829
+ sending.clear();
830
+ },
616
831
  watch(conversation, options, deliver) {
617
832
  const limit = limitOf(options);
618
833
  const key = JSON.stringify([conversation, limit ?? null]);
@@ -620,18 +835,20 @@ export function createConversationStore(deps) {
620
835
  conversation,
621
836
  limit,
622
837
  watchers: new Set(),
623
- messages: null,
838
+ window: null,
624
839
  };
625
840
  views.set(key, view);
626
841
  const watcher = { notify: deliver, ready: false };
627
842
  view.watchers.add(watcher);
628
843
  void deps
629
- .durably(() => Promise.resolve())
844
+ .durably(async (local) => {
845
+ view.window ??= await local.messageWindow(conversation, limit);
846
+ })
630
847
  .then(() => {
631
848
  if (deps.closed() || !view.watchers.has(watcher))
632
849
  return;
633
850
  watcher.ready = true;
634
- watcher.notify(viewOf(conversation, limit));
851
+ watcher.notify(viewOf(conversation, view.window ?? []));
635
852
  })
636
853
  .catch(() => { });
637
854
  return () => {
@@ -651,6 +868,16 @@ export function createConversationStore(deps) {
651
868
  listWatchers.delete(deliver);
652
869
  };
653
870
  },
871
+ watchAgents(deliver) {
872
+ agentWatchers.add(deliver);
873
+ void agentsReady().then(() => {
874
+ if (!deps.closed() && agentWatchers.has(deliver))
875
+ deliver(agents());
876
+ });
877
+ return () => {
878
+ agentWatchers.delete(deliver);
879
+ };
880
+ },
654
881
  changed(set) {
655
882
  for (const conversation of set.conversations)
656
883
  emit(conversation);
@@ -668,12 +895,15 @@ export function createConversationStore(deps) {
668
895
  if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted') {
669
896
  if (event.kind === 'list') {
670
897
  listing.clear();
671
- for (const entry of event.entries)
898
+ for (const entry of event.entries) {
672
899
  listing.set(entry.conversation, { ...entry });
900
+ creations.delete(entry.conversation);
901
+ }
673
902
  listKnown = true;
674
903
  }
675
904
  else if (event.kind === 'listed') {
676
905
  listing.set(event.entry.conversation, { ...event.entry });
906
+ creations.delete(event.entry.conversation);
677
907
  }
678
908
  else {
679
909
  listing.delete(event.conversation);
@@ -687,17 +917,17 @@ export function createConversationStore(deps) {
687
917
  heard(event);
688
918
  return false;
689
919
  },
690
- messages() {
691
- return [...messages].flatMap(([conversation, held]) => held.map(({ identity, text }) => ({ conversation, identity, text })));
920
+ searchable() {
921
+ return [...(listKnown ? listing.keys() : stored)].sort();
692
922
  },
693
- message(conversation, identity) {
694
- const held = messages.get(conversation)?.find((message) => message.identity === identity);
695
- return held === undefined ? null : { conversation, identity, text: held.text };
923
+ acquired(conversation) {
924
+ return reads.has(conversation) && !partials.has(conversation);
696
925
  },
697
926
  close() {
698
927
  views.clear();
699
928
  lives.clear();
700
929
  listWatchers.clear();
930
+ agentWatchers.clear();
701
931
  },
702
932
  };
703
933
  }