@agentshouse/mdruntime 0.1.0-c5b50992a4193 → 0.1.0-c5d3fa6fb67bc

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