@agentshouse/mdruntime 0.1.0-cd3486bd400ed → 0.1.0-cd37f80239af0

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 (201) hide show
  1. package/README.md +21 -9
  2. package/dist/adapter.d.ts +149 -46
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/attached-model.d.ts +5 -1
  5. package/dist/attached-model.d.ts.map +1 -1
  6. package/dist/attached-model.js +38 -4
  7. package/dist/attempt.d.ts.map +1 -1
  8. package/dist/attempt.js +14 -1
  9. package/dist/binary.d.ts +18 -0
  10. package/dist/binary.d.ts.map +1 -0
  11. package/dist/binary.js +8 -0
  12. package/dist/bootstrap.d.ts +27 -11
  13. package/dist/bootstrap.d.ts.map +1 -1
  14. package/dist/bootstrap.js +184 -84
  15. package/dist/chain.d.ts +11 -0
  16. package/dist/chain.d.ts.map +1 -0
  17. package/dist/chain.js +64 -0
  18. package/dist/change-set.d.ts +9 -14
  19. package/dist/change-set.d.ts.map +1 -1
  20. package/dist/change-set.js +11 -62
  21. package/dist/change.d.ts +41 -0
  22. package/dist/change.d.ts.map +1 -1
  23. package/dist/change.js +34 -0
  24. package/dist/client.d.ts +31 -13
  25. package/dist/client.d.ts.map +1 -1
  26. package/dist/client.js +103 -7
  27. package/dist/collection.d.ts +10 -7
  28. package/dist/collection.d.ts.map +1 -1
  29. package/dist/collection.js +38 -79
  30. package/dist/compile.d.ts +16 -0
  31. package/dist/compile.d.ts.map +1 -0
  32. package/dist/compile.js +786 -0
  33. package/dist/connection.d.ts +50 -16
  34. package/dist/connection.d.ts.map +1 -1
  35. package/dist/connection.js +3786 -931
  36. package/dist/conversation.d.ts +37 -16
  37. package/dist/conversation.d.ts.map +1 -1
  38. package/dist/conversation.js +700 -257
  39. package/dist/digest.d.ts +7 -0
  40. package/dist/digest.d.ts.map +1 -0
  41. package/dist/digest.js +113 -0
  42. package/dist/durability.d.ts +4 -1
  43. package/dist/durability.d.ts.map +1 -1
  44. package/dist/durability.js +5 -0
  45. package/dist/entry.d.ts +20 -0
  46. package/dist/entry.d.ts.map +1 -0
  47. package/dist/entry.js +151 -0
  48. package/dist/fence.d.ts +6 -0
  49. package/dist/fence.d.ts.map +1 -0
  50. package/dist/fence.js +10 -0
  51. package/dist/file.d.ts +2 -19
  52. package/dist/file.d.ts.map +1 -1
  53. package/dist/file.js +7 -122
  54. package/dist/fulltext.d.ts +27 -0
  55. package/dist/fulltext.d.ts.map +1 -0
  56. package/dist/fulltext.js +370 -0
  57. package/dist/host.d.ts +13 -3
  58. package/dist/host.d.ts.map +1 -1
  59. package/dist/host.js +130 -17
  60. package/dist/identity.d.ts +2 -0
  61. package/dist/identity.d.ts.map +1 -1
  62. package/dist/identity.js +6 -0
  63. package/dist/index.d.ts +17 -9
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +7 -0
  66. package/dist/live.d.ts +22 -0
  67. package/dist/live.d.ts.map +1 -0
  68. package/dist/live.js +123 -0
  69. package/dist/media.d.ts +8 -0
  70. package/dist/media.d.ts.map +1 -0
  71. package/dist/media.js +218 -0
  72. package/dist/memory.d.ts +17 -7
  73. package/dist/memory.d.ts.map +1 -1
  74. package/dist/memory.js +231 -91
  75. package/dist/messages.d.ts +28 -0
  76. package/dist/messages.d.ts.map +1 -0
  77. package/dist/messages.js +90 -0
  78. package/dist/model.d.ts +9 -2
  79. package/dist/model.d.ts.map +1 -1
  80. package/dist/model.js +8 -2
  81. package/dist/name.d.ts +4 -0
  82. package/dist/name.d.ts.map +1 -0
  83. package/dist/name.js +122 -0
  84. package/dist/observation.d.ts +20 -2
  85. package/dist/observation.d.ts.map +1 -1
  86. package/dist/observation.js +113 -12
  87. package/dist/opened.d.ts +22 -0
  88. package/dist/opened.d.ts.map +1 -0
  89. package/dist/opened.js +107 -0
  90. package/dist/outcome.d.ts +52 -4
  91. package/dist/outcome.d.ts.map +1 -1
  92. package/dist/outcome.js +46 -0
  93. package/dist/overlay.d.ts +15 -5
  94. package/dist/overlay.d.ts.map +1 -1
  95. package/dist/overlay.js +129 -177
  96. package/dist/paths.d.ts +1 -0
  97. package/dist/paths.d.ts.map +1 -1
  98. package/dist/paths.js +4 -0
  99. package/dist/query.d.ts +68 -4
  100. package/dist/query.d.ts.map +1 -1
  101. package/dist/query.js +48 -7
  102. package/dist/reach.d.ts +18 -4
  103. package/dist/reach.d.ts.map +1 -1
  104. package/dist/reach.js +224 -55
  105. package/dist/react.d.ts +9 -6
  106. package/dist/react.d.ts.map +1 -1
  107. package/dist/react.js +7 -17
  108. package/dist/readiness.d.ts +52 -0
  109. package/dist/readiness.d.ts.map +1 -0
  110. package/dist/readiness.js +27 -0
  111. package/dist/recovery.d.ts +26 -0
  112. package/dist/recovery.d.ts.map +1 -0
  113. package/dist/recovery.js +224 -0
  114. package/dist/refresh.d.ts +17 -14
  115. package/dist/refresh.d.ts.map +1 -1
  116. package/dist/refresh.js +136 -56
  117. package/dist/replica.d.ts +58 -33
  118. package/dist/replica.d.ts.map +1 -1
  119. package/dist/replica.js +307 -159
  120. package/dist/room-draft.d.ts +51 -4
  121. package/dist/room-draft.d.ts.map +1 -1
  122. package/dist/room-draft.js +210 -18
  123. package/dist/rows.d.ts +37 -35
  124. package/dist/rows.d.ts.map +1 -1
  125. package/dist/rows.js +173 -102
  126. package/dist/save.d.ts +70 -3
  127. package/dist/save.d.ts.map +1 -1
  128. package/dist/save.js +805 -147
  129. package/dist/search.d.ts +39 -28
  130. package/dist/search.d.ts.map +1 -1
  131. package/dist/search.js +37 -105
  132. package/dist/seed.d.ts +16 -0
  133. package/dist/seed.d.ts.map +1 -0
  134. package/dist/seed.js +75 -0
  135. package/dist/shared.d.ts +15 -0
  136. package/dist/shared.d.ts.map +1 -0
  137. package/dist/shared.js +421 -0
  138. package/dist/status.d.ts +5 -0
  139. package/dist/status.d.ts.map +1 -1
  140. package/dist/status.js +3 -1
  141. package/dist/store.d.ts +107 -80
  142. package/dist/store.d.ts.map +1 -1
  143. package/dist/store.js +325 -461
  144. package/dist/stored.d.ts +130 -0
  145. package/dist/stored.d.ts.map +1 -0
  146. package/dist/stored.js +560 -0
  147. package/dist/tables.d.ts +325 -0
  148. package/dist/tables.d.ts.map +1 -0
  149. package/dist/tables.js +578 -0
  150. package/dist/transfer.d.ts +0 -5
  151. package/dist/transfer.d.ts.map +1 -1
  152. package/dist/turn.d.ts +9 -0
  153. package/dist/turn.d.ts.map +1 -0
  154. package/dist/turn.js +75 -0
  155. package/dist/validation.d.ts +29 -0
  156. package/dist/validation.d.ts.map +1 -0
  157. package/dist/validation.js +101 -0
  158. package/dist/windows.d.ts +22 -0
  159. package/dist/windows.d.ts.map +1 -0
  160. package/dist/windows.js +89 -0
  161. package/dist/wire.d.ts +165 -30
  162. package/dist/wire.d.ts.map +1 -1
  163. package/dist/wire.js +56 -29
  164. package/dist/words.d.ts +19 -0
  165. package/dist/words.d.ts.map +1 -0
  166. package/dist/words.js +156 -0
  167. package/package.json +26 -15
  168. package/AGENTS.md +0 -290
  169. package/dist/attachment.d.ts +0 -3
  170. package/dist/attachment.d.ts.map +0 -1
  171. package/dist/attachment.js +0 -8
  172. package/dist/chat/acts.d.ts +0 -34
  173. package/dist/chat/acts.d.ts.map +0 -1
  174. package/dist/chat/acts.js +0 -36
  175. package/dist/chat/messages.d.ts +0 -21
  176. package/dist/chat/messages.d.ts.map +0 -1
  177. package/dist/chat/messages.js +0 -151
  178. package/dist/chat/overlay.d.ts +0 -31
  179. package/dist/chat/overlay.d.ts.map +0 -1
  180. package/dist/chat/overlay.js +0 -97
  181. package/dist/chat/parts.d.ts +0 -57
  182. package/dist/chat/parts.d.ts.map +0 -1
  183. package/dist/chat/parts.js +0 -119
  184. package/dist/chat/process.d.ts +0 -20
  185. package/dist/chat/process.d.ts.map +0 -1
  186. package/dist/chat/process.js +0 -32
  187. package/dist/chat/provider.d.ts +0 -57
  188. package/dist/chat/provider.d.ts.map +0 -1
  189. package/dist/chat/provider.js +0 -637
  190. package/dist/chat/questions.d.ts +0 -31
  191. package/dist/chat/questions.d.ts.map +0 -1
  192. package/dist/chat/questions.js +0 -111
  193. package/dist/chat/read.d.ts +0 -5
  194. package/dist/chat/read.d.ts.map +0 -1
  195. package/dist/chat/read.js +0 -13
  196. package/dist/chat.d.ts +0 -7
  197. package/dist/chat.d.ts.map +0 -1
  198. package/dist/chat.js +0 -2
  199. package/dist/read-side.d.ts +0 -18
  200. package/dist/read-side.d.ts.map +0 -1
  201. package/dist/read-side.js +0 -433
@@ -1,14 +1,31 @@
1
1
  import { conversationChangeSet } from './change-set.js';
2
2
  import { runtimeError } from './failure.js';
3
- import { declaredFile } from './file.js';
4
- import { chatDraftRevision } from './identity.js';
3
+ import { refusedForSpace } from './durability.js';
4
+ import { changeBatchIdentity, chatDraftRevision } from './identity.js';
5
+ import { OFFLINE } from './outcome.js';
6
+ import { untilAnswered } from './recovery.js';
7
+ import { capped, cursorOf, decodedCursor, FROM_START, messageOrder, precedes } from './windows.js';
5
8
  const QUIET = {
6
9
  turn: null,
7
10
  questions: [],
8
11
  process: null,
9
12
  commands: [],
10
13
  options: [],
14
+ starting: null,
15
+ subagents: [],
16
+ link: null,
17
+ deliveryUnknown: [],
11
18
  };
19
+ const FACTS = [
20
+ 'questions',
21
+ 'process',
22
+ 'commands',
23
+ 'options',
24
+ 'starting',
25
+ 'subagents',
26
+ 'link',
27
+ 'deliveryUnknown',
28
+ ];
12
29
  export function unheldView(conversation) {
13
30
  return {
14
31
  conversation,
@@ -18,9 +35,27 @@ export function unheldView(conversation) {
18
35
  process: null,
19
36
  commands: [],
20
37
  options: [],
38
+ starting: null,
39
+ subagents: [],
40
+ link: null,
41
+ deliveryUnknown: [],
21
42
  chatDraft: null,
22
43
  };
23
44
  }
45
+ export const SESSION_STARTED = 'Session started';
46
+ const NEW_CHAT = 'new:';
47
+ export const HISTORY_CONVERSATIONS = 64;
48
+ export function newChat(agent) {
49
+ return `${NEW_CHAT}${agent}`;
50
+ }
51
+ function creationOf(input) {
52
+ if (typeof input !== 'object' || input === null)
53
+ return null;
54
+ const { agent_id: agent, operation_id: operation, replaces, } = input;
55
+ return typeof agent === 'string' && typeof operation === 'string' && replaces === undefined
56
+ ? { agent, operation }
57
+ : null;
58
+ }
24
59
  function startedTurn(turn, operation) {
25
60
  if (operation.op === 'begin')
26
61
  return { turn, started: operation.at, parts: [], context: null };
@@ -70,18 +105,28 @@ function appliedTurn(held, operation) {
70
105
  parts: held.parts.map((kept, at) => (at === operation.index ? changed : kept)),
71
106
  };
72
107
  }
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) {
108
+ function windowOf(held, arriving, limit) {
81
109
  const byIdentity = new Map(held.map((message) => [message.identity, message]));
82
110
  for (const message of arriving)
83
111
  byIdentity.set(message.identity, message);
84
- return [...byIdentity.values()].sort(ordered);
112
+ const next = [...byIdentity.values()].sort(messageOrder);
113
+ const start = Math.max(0, next.length - limit);
114
+ return { messages: next.slice(start), dropped: start > 0 };
115
+ }
116
+ function frameOf(asked, messages, more, total) {
117
+ const oldest = messages[0];
118
+ const next = !more
119
+ ? undefined
120
+ : oldest === undefined
121
+ ? (asked.after ?? FROM_START)
122
+ : cursorOf([oldest.position, oldest.identity]);
123
+ return {
124
+ ...(next === undefined ? {} : { next }),
125
+ ...(total === null ? {} : { total }),
126
+ };
127
+ }
128
+ function newestOf(arriving, from) {
129
+ return arriving.reduce((top, message) => (top === null || message.position > top ? message.position : top), from);
85
130
  }
86
131
  function acceptedMessage(value) {
87
132
  if (typeof value !== 'object' || value === null)
@@ -140,88 +185,163 @@ function historyPages(result) {
140
185
  }
141
186
  return pages;
142
187
  }
143
- function turnOf(result) {
188
+ function answered(result, field) {
144
189
  if (result.kind !== 'action')
145
190
  return null;
146
191
  const answer = result.result;
147
192
  if (typeof answer !== 'object' || answer === null)
148
193
  return null;
149
- const record = answer;
150
- return typeof record.turn === 'string' ? record.turn : null;
194
+ const value = answer[field];
195
+ return typeof value === 'string' ? value : null;
196
+ }
197
+ function beforeOf(after) {
198
+ if (after === undefined)
199
+ return null;
200
+ const decoded = typeof after === 'string' ? decodedCursor(after) : null;
201
+ if (decoded?.length === 0)
202
+ return null;
203
+ const [position, identity] = decoded ?? [];
204
+ if (decoded?.length !== 2 || typeof position !== 'string' || typeof identity !== 'string') {
205
+ throw runtimeError('invalid-after', `conversation after must be the next an earlier view returned, got ${String(after)}`);
206
+ }
207
+ return [position, identity];
151
208
  }
152
- function limitOf(options) {
153
- const { limit } = options;
154
- if (limit === undefined)
155
- return undefined;
156
- if (!Number.isInteger(limit) || limit < 0) {
209
+ function askedOf(options) {
210
+ const { limit, after, total } = options;
211
+ if (limit !== undefined && (!Number.isInteger(limit) || limit < 0)) {
157
212
  throw runtimeError('invalid-limit', `conversation limit must be a non-negative integer, got ${String(limit)}`);
158
213
  }
159
- return limit;
214
+ return { limit: capped(limit), after, before: beforeOf(after), total: total === true };
215
+ }
216
+ const EXPIRED = { kind: 'rejected', code: 'expired', fields: {}, retryable: false };
217
+ const DURABLE_WRITE = {
218
+ kind: 'rejected',
219
+ code: 'durable-write',
220
+ fields: {},
221
+ retryable: true,
222
+ };
223
+ function unwritten(draft) {
224
+ return draft.text === '';
225
+ }
226
+ function pending(draft) {
227
+ return {
228
+ ...(draft.submitted === undefined ? {} : { submitted: draft.submitted }),
229
+ ...(draft.moves === undefined ? {} : { moves: draft.moves }),
230
+ };
231
+ }
232
+ function empty(draft) {
233
+ return unwritten(draft) && draft.submitted === undefined && draft.moves === undefined;
234
+ }
235
+ function withSubmitted(draft, submitted) {
236
+ const { submitted: _held, ...unsent } = draft;
237
+ return submitted.length === 0 ? unsent : { ...unsent, submitted };
238
+ }
239
+ function prepended(draft, said) {
240
+ return { text: [said.text, draft.text].filter((one) => one !== '').join('\n') };
241
+ }
242
+ function returned(draft, submission, others) {
243
+ return withSubmitted({
244
+ conversation: draft.conversation,
245
+ ...prepended(draft, submission),
246
+ revision: chatDraftRevision(),
247
+ }, others);
160
248
  }
161
249
  export function createConversationStore(deps) {
162
- const messages = new Map();
250
+ const stored = new Set();
163
251
  const reads = new Map();
252
+ const partials = new Set();
164
253
  const drafts = new Map();
165
254
  const sending = new Map();
166
255
  const behind = new Set();
256
+ const catching = new Map();
167
257
  const gaps = new Map();
168
258
  const lives = new Map();
259
+ const unsent = new Map();
169
260
  const views = new Map();
170
261
  const listing = new Map();
262
+ let relisting = null;
263
+ let listCursor = null;
264
+ const creations = new Set();
171
265
  const listWatchers = new Set();
266
+ const unlisted = new Set();
172
267
  let listKnown = false;
268
+ let listHeld = false;
269
+ let listKept = false;
173
270
  const agentWatchers = new Set();
174
- let agentNames = new Map();
175
- let retiredAgents = new Set();
176
- let agentsRead = null;
271
+ const unnamed = new Set();
272
+ let agentsShown = null;
273
+ let listedAgents = null;
274
+ let agentsReading = false;
177
275
  let replicated = new Set();
178
276
  let handedReplicated;
179
277
  function liveOf(conversation) {
180
278
  return lives.get(conversation) ?? QUIET;
181
279
  }
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)) {
280
+ function viewOf(conversation, held, frame) {
193
281
  const draft = drafts.get(conversation);
194
282
  const facts = liveOf(conversation);
195
283
  return {
196
284
  conversation,
197
- messages: shown,
285
+ messages: held.map(({ identity, turn, text, message }) => ({
286
+ identity,
287
+ turn,
288
+ text,
289
+ message,
290
+ })),
291
+ ...frame,
198
292
  turn: structuredClone(facts.turn),
199
293
  questions: structuredClone(facts.questions),
200
294
  process: facts.process === null ? null : { ...facts.process },
201
295
  commands: structuredClone(facts.commands),
202
296
  options: structuredClone(facts.options),
203
- chatDraft: draft === undefined || draft.turn !== null || sending.get(conversation)?.draft === draft
297
+ starting: facts.starting,
298
+ subagents: structuredClone(facts.subagents),
299
+ link: facts.link === null ? null : { ...facts.link },
300
+ deliveryUnknown: [...facts.deliveryUnknown],
301
+ chatDraft: draft === undefined || unwritten(draft)
204
302
  ? null
205
- : { text: draft.text, files: [...draft.files], revision: draft.revision },
303
+ : { text: draft.text, revision: draft.revision },
206
304
  };
207
305
  }
208
- function emit(conversation, messagesChanged = true) {
306
+ function emit(conversation) {
209
307
  if (deps.closed())
210
308
  return;
211
309
  for (const view of views.values()) {
212
- if (view.conversation !== conversation)
310
+ if (view.conversation !== conversation || view.window === null)
213
311
  continue;
214
- if (messagesChanged)
215
- view.messages = null;
216
312
  const ready = [...view.watchers].filter((watcher) => watcher.ready);
217
313
  if (ready.length === 0)
218
314
  continue;
219
- view.messages ??= shownMessages(conversation, view.limit);
220
- const value = viewOf(conversation, view.limit, view.messages);
315
+ const value = viewOf(conversation, view.window, sharedFrame(view, view.window));
221
316
  for (const watcher of ready)
222
317
  watcher.notify(value);
223
318
  }
224
319
  }
320
+ function sharedFrame(view, window) {
321
+ return frameOf(view.asked, window, view.more, view.total);
322
+ }
323
+ async function slide(local, conversation, arriving) {
324
+ let count;
325
+ for (const view of views.values()) {
326
+ if (view.conversation !== conversation || view.window === null)
327
+ continue;
328
+ if (arriving === null) {
329
+ view.window = [];
330
+ view.more = false;
331
+ if (view.total !== null)
332
+ view.total = 0;
333
+ continue;
334
+ }
335
+ const joining = arriving.filter((message) => precedes(message, view.asked.before));
336
+ const next = windowOf(view.window, joining, view.asked.limit);
337
+ view.window = next.messages;
338
+ view.more ||= next.dropped;
339
+ if (view.total !== null && arriving.length > 0) {
340
+ count ??= await local.messageCount(conversation);
341
+ view.total = count;
342
+ }
343
+ }
344
+ }
225
345
  function stateOf(conversation) {
226
346
  const facts = liveOf(conversation);
227
347
  if (facts.questions.length > 0)
@@ -231,34 +351,110 @@ export function createConversationStore(deps) {
231
351
  function entries() {
232
352
  return [...listing.values()].map((entry) => ({
233
353
  conversation: entry.conversation,
234
- agent: { identity: entry.agent, name: agentNames.get(entry.agent) ?? null },
354
+ agent: { identity: entry.agent, name: entry.agentName },
235
355
  name: entry.name,
236
356
  created: entry.created,
237
357
  finished: entry.finished,
238
358
  outcome: entry.outcome,
359
+ waiting: entry.waiting,
239
360
  state: stateOf(entry.conversation),
240
361
  started: lives.get(entry.conversation)?.turn?.started ?? null,
241
362
  }));
242
363
  }
364
+ function keepList() {
365
+ if (!listKnown || listKept)
366
+ return;
367
+ listKept = true;
368
+ queueMicrotask(() => {
369
+ listKept = false;
370
+ if (deps.closed())
371
+ return;
372
+ const entries = [...listing.values()];
373
+ const cursor = listCursor;
374
+ void deps
375
+ .durably(async (local) => {
376
+ await local.fact('conversationList', entries);
377
+ await local.fact('conversationCursor', cursor);
378
+ })
379
+ .catch(() => { });
380
+ });
381
+ }
382
+ function listed(entry) {
383
+ listing.set(entry.conversation, { ...entry });
384
+ creations.delete(entry.conversation);
385
+ }
243
386
  function emitList() {
244
- if (deps.closed() || listWatchers.size === 0)
387
+ if (deps.closed())
245
388
  return;
246
- const value = entries();
247
- for (const watcher of listWatchers)
248
- watcher(value);
389
+ if (listWatchers.size > 0) {
390
+ const value = entries();
391
+ for (const watcher of listWatchers)
392
+ watcher(value);
393
+ }
394
+ emitAgents();
395
+ learned();
396
+ }
397
+ function learned() {
398
+ if (deps.closed())
399
+ return;
400
+ if (!listKnown && !listHeld && deps.stream() !== undefined && deps.online())
401
+ return;
402
+ if (unlisted.size > 0) {
403
+ const value = entries();
404
+ for (const watcher of [...unlisted]) {
405
+ unlisted.delete(watcher);
406
+ listWatchers.add(watcher);
407
+ watcher(value);
408
+ }
409
+ }
410
+ if (unnamed.size > 0) {
411
+ const value = agents();
412
+ for (const watcher of [...unnamed]) {
413
+ unnamed.delete(watcher);
414
+ agentWatchers.add(watcher);
415
+ watcher(value);
416
+ }
417
+ }
249
418
  }
250
419
  function agents() {
251
- return [...agentNames]
252
- .filter(([identity]) => !retiredAgents.has(identity))
253
- .map(([identity, name]) => ({ identity, name }))
420
+ const named = new Map();
421
+ for (const agent of listedAgents ?? [])
422
+ named.set(agent.agent, agent);
423
+ for (const entry of listing.values()) {
424
+ named.set(entry.agent, { name: entry.agentName, retired: entry.agentRetired });
425
+ }
426
+ return [...named]
427
+ .filter(([, agent]) => !agent.retired)
428
+ .map(([identity, agent]) => ({ identity, name: agent.name }))
254
429
  .sort((left, right) => left.name === right.name
255
430
  ? left.identity.localeCompare(right.identity)
256
431
  : left.name.localeCompare(right.name));
257
432
  }
433
+ function readAgents() {
434
+ const stream = deps.stream();
435
+ if (stream?.agents === undefined || !deps.online() || agentsReading)
436
+ return;
437
+ agentsReading = true;
438
+ void stream
439
+ .agents()
440
+ .then((read) => {
441
+ if (read === null)
442
+ return;
443
+ listedAgents = read;
444
+ emitAgents();
445
+ }, () => { })
446
+ .finally(() => {
447
+ agentsReading = false;
448
+ });
449
+ }
258
450
  function emitAgents() {
259
451
  if (deps.closed() || agentWatchers.size === 0)
260
452
  return;
261
453
  const value = agents();
454
+ const shown = JSON.stringify(value);
455
+ if (shown === agentsShown)
456
+ return;
457
+ agentsShown = shown;
262
458
  for (const watcher of agentWatchers)
263
459
  watcher(value);
264
460
  }
@@ -274,43 +470,41 @@ export function createConversationStore(deps) {
274
470
  if (stateOf(conversation) !== before || next?.turn?.started !== began)
275
471
  emitList();
276
472
  }
277
- async function readAgents() {
278
- const declaration = deps.agents();
279
- if (declaration === undefined)
280
- return;
281
- const { documents } = await deps.documents(declaration.locator);
282
- const names = new Map();
283
- const retired = new Set();
284
- for (const document of documents) {
285
- if (document.type !== declaration.type)
286
- continue;
287
- const identity = document.fields?.[declaration.identity];
288
- const name = document.fields?.[declaration.name];
289
- if (typeof identity !== 'string' || typeof name !== 'string')
290
- continue;
291
- names.set(identity, name);
292
- const ended = declaration.retired === undefined ? null : document.fields?.[declaration.retired];
293
- if (ended !== undefined && ended !== null)
294
- retired.add(identity);
295
- }
296
- const named = JSON.stringify([...names]) !== JSON.stringify([...agentNames]);
297
- if (!named && JSON.stringify([...retired]) === JSON.stringify([...retiredAgents]))
298
- return;
299
- agentNames = names;
300
- retiredAgents = retired;
301
- if (named)
302
- emitList();
303
- emitAgents();
304
- }
305
- function agentsReady() {
306
- agentsRead ??= readAgents().catch(() => {
307
- agentsRead = null;
308
- });
309
- return agentsRead;
473
+ function retiredChat(conversation) {
474
+ if (!conversation.startsWith(NEW_CHAT))
475
+ return false;
476
+ const agent = conversation.slice(NEW_CHAT.length);
477
+ return [...listing.values()].some((entry) => entry.agent === agent && entry.agentRetired);
310
478
  }
311
479
  function live(conversation, change) {
480
+ const owed = unsent.get(conversation);
481
+ for (const fact of FACTS.filter((one) => one in change))
482
+ owed?.delete(fact);
312
483
  setLive(conversation, { ...liveOf(conversation), ...change });
313
- emit(conversation, false);
484
+ emit(conversation);
485
+ }
486
+ function openingEnded() {
487
+ const owed = [...unsent];
488
+ unsent.clear();
489
+ for (const [conversation, facts] of owed) {
490
+ const held = lives.get(conversation);
491
+ if (held === undefined || facts.size === 0)
492
+ continue;
493
+ setLive(conversation, {
494
+ turn: held.turn,
495
+ questions: facts.has('questions') ? QUIET.questions : held.questions,
496
+ process: facts.has('process') ? QUIET.process : held.process,
497
+ commands: facts.has('commands') ? QUIET.commands : held.commands,
498
+ options: facts.has('options') ? QUIET.options : held.options,
499
+ starting: facts.has('starting') ? QUIET.starting : held.starting,
500
+ subagents: facts.has('subagents') ? QUIET.subagents : held.subagents,
501
+ link: facts.has('link') ? QUIET.link : held.link,
502
+ deliveryUnknown: facts.has('deliveryUnknown')
503
+ ? QUIET.deliveryUnknown
504
+ : held.deliveryUnknown,
505
+ });
506
+ emit(conversation);
507
+ }
314
508
  }
315
509
  function relayed(conversation, turn, operation) {
316
510
  const held = liveOf(conversation).turn;
@@ -339,25 +533,38 @@ export function createConversationStore(deps) {
339
533
  async function remove(leaving) {
340
534
  await deps.durably(async (local) => {
341
535
  await local.dropConversations(leaving);
342
- const removed = [];
343
536
  const changed = [];
344
537
  for (const conversation of leaving) {
345
- for (const message of messages.get(conversation) ?? []) {
346
- removed.push({ conversation, identity: message.identity });
347
- }
348
- if (messages.has(conversation) || drafts.has(conversation) || lives.has(conversation)) {
538
+ if (stored.has(conversation) || drafts.has(conversation) || lives.has(conversation)) {
349
539
  changed.push(conversation);
350
540
  }
351
- messages.delete(conversation);
541
+ stored.delete(conversation);
352
542
  reads.delete(conversation);
543
+ partials.delete(conversation);
353
544
  drafts.delete(conversation);
354
545
  behind.delete(conversation);
546
+ unsent.delete(conversation);
355
547
  setLive(conversation, null);
548
+ await slide(local, conversation, null);
356
549
  }
357
550
  if (changed.length > 0)
358
- deps.publish(conversationChangeSet(removed, changed));
551
+ deps.publish(conversationChangeSet(changed));
359
552
  });
360
553
  }
554
+ function missed(conversation) {
555
+ const read = reads.get(conversation);
556
+ const newest = listing.get(conversation)?.newest;
557
+ return (read === undefined ||
558
+ newest === undefined ||
559
+ partials.has(conversation) ||
560
+ (newest !== null && (read === null || newest > read)));
561
+ }
562
+ function noted(conversation, message) {
563
+ const entry = listing.get(conversation);
564
+ if (entry === undefined || (entry.newest !== null && entry.newest >= message.position))
565
+ return;
566
+ listing.set(conversation, { ...entry, newest: message.position });
567
+ }
361
568
  function gap(conversation) {
362
569
  gaps.set(conversation, (gaps.get(conversation) ?? 0) + 1);
363
570
  behind.add(conversation);
@@ -370,33 +577,29 @@ export function createConversationStore(deps) {
370
577
  const advances = read !== undefined &&
371
578
  !behind.has(conversation) &&
372
579
  (read === null || message.position > read);
373
- const sealed = drafts.get(conversation)?.turn === message.turn;
580
+ await sealed(local, conversation, new Set([message.turn]));
374
581
  await local.keepMessages({
375
582
  conversation,
376
583
  messages: [message],
377
- ...(advances ? { read: message.position } : {}),
378
- sealed,
584
+ ...(advances ? { read: { at: message.position, partial: false } } : {}),
379
585
  });
380
- messages.set(conversation, merged(messages.get(conversation) ?? [], [message]));
586
+ stored.add(conversation);
381
587
  if (advances)
382
588
  reads.set(conversation, message.position);
383
- if (sealed)
384
- drafts.delete(conversation);
385
589
  sealTurns(conversation, new Set([message.turn]));
386
- deps.publish(conversationChangeSet([{ conversation, identity: message.identity }], [conversation]));
590
+ await slide(local, conversation, [message]);
591
+ deps.publish(conversationChangeSet([conversation]));
387
592
  });
388
593
  }
389
- async function readHistories(conversations) {
594
+ async function readHistories(conversations, ended) {
390
595
  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
- let asking = conversations.map((conversation) => ({
596
+ let waiting = conversations.map((conversation) => ({
395
597
  conversation,
396
- after: starts.get(conversation) ?? undefined,
598
+ after: reads.get(conversation) ?? undefined,
397
599
  }));
398
- while (asking.length > 0) {
399
- const result = await deps.call('conversation.history', {
600
+ while (waiting.length > 0) {
601
+ const asking = waiting.slice(0, HISTORY_CONVERSATIONS);
602
+ const result = await deps.history({
400
603
  conversations: asking.map(({ conversation, after }) => ({
401
604
  conversation,
402
605
  ...(after === undefined ? {} : { after }),
@@ -405,46 +608,64 @@ export function createConversationStore(deps) {
405
608
  const pages = historyPages(result);
406
609
  if (pages === null)
407
610
  return result;
408
- const asked = new Set(asking.map(({ conversation }) => conversation));
611
+ const asked = new Map(asking.map(({ conversation, after }) => [conversation, after]));
409
612
  const following = [];
410
613
  for (const page of pages) {
411
614
  if (!asked.has(page.conversation))
412
615
  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
616
+ await keepPage(page.conversation, page.messages, asked.get(page.conversation) ?? null, page.next, generations.get(page.conversation) ?? 0);
617
+ if (page.next !== undefined) {
420
618
  following.push({ conversation: page.conversation, after: page.next });
619
+ }
421
620
  }
422
- asking = following;
423
- }
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);
621
+ for (const { conversation } of asking) {
622
+ if (!following.some((next) => next.conversation === conversation))
623
+ ended(conversation);
624
+ }
625
+ waiting = [...waiting.slice(HISTORY_CONVERSATIONS), ...following];
428
626
  }
429
627
  return null;
430
628
  }
431
- function keepHistory(conversation, arriving, from, generation) {
629
+ function caughtUp(conversations) {
630
+ const ends = new Map();
631
+ for (const conversation of conversations) {
632
+ const held = new Promise((end) => ends.set(conversation, end));
633
+ catching.set(conversation, held);
634
+ void held.then(() => {
635
+ if (catching.get(conversation) === held)
636
+ catching.delete(conversation);
637
+ });
638
+ }
639
+ const ended = (conversation) => ends.get(conversation)?.();
640
+ const read = readHistories(conversations, ended);
641
+ const endAll = () => {
642
+ for (const conversation of conversations)
643
+ ended(conversation);
644
+ };
645
+ read.then(endAll, endAll);
646
+ return read;
647
+ }
648
+ function keepPage(conversation, arriving, from, next, generation) {
432
649
  return deps.durably(async (local) => {
433
650
  if (!replicated.has(conversation))
434
651
  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);
652
+ const whole = next === undefined;
653
+ const cursor = next ?? newestOf(arriving, from);
654
+ const partial = !whole && (partials.has(conversation) || !reads.has(conversation));
655
+ const sealing = await sealed(local, conversation, new Set(arriving.map((message) => message.turn)));
656
+ await local.keepMessages({ conversation, messages: arriving, read: { at: cursor, partial } });
657
+ stored.add(conversation);
658
+ reads.set(conversation, cursor);
659
+ if (partial)
660
+ partials.add(conversation);
661
+ else
662
+ partials.delete(conversation);
443
663
  sealTurns(conversation, new Set(arriving.map((message) => message.turn)));
444
- if ((gaps.get(conversation) ?? 0) === generation)
664
+ if (whole && (gaps.get(conversation) ?? 0) === generation)
445
665
  behind.delete(conversation);
446
- if (arriving.length > 0 || sealed) {
447
- deps.publish(conversationChangeSet(arriving.map((message) => ({ conversation, identity: message.identity })), [conversation]));
666
+ await slide(local, conversation, arriving);
667
+ if (arriving.length > 0 || sealing || whole) {
668
+ deps.publish(conversationChangeSet([conversation]));
448
669
  }
449
670
  });
450
671
  }
@@ -453,11 +674,8 @@ export function createConversationStore(deps) {
453
674
  if (event.kind === 'gap') {
454
675
  if (replicated.has(conversation))
455
676
  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);
677
+ if (lives.has(conversation))
678
+ unsent.set(conversation, new Set(FACTS));
461
679
  return;
462
680
  }
463
681
  if (event.kind === 'turn') {
@@ -480,143 +698,290 @@ export function createConversationStore(deps) {
480
698
  live(conversation, { options: structuredClone(event.options) });
481
699
  return;
482
700
  }
701
+ if (event.kind === 'starting') {
702
+ live(conversation, { starting: event.step });
703
+ return;
704
+ }
705
+ if (event.kind === 'subagents') {
706
+ live(conversation, { subagents: structuredClone(event.subagents) });
707
+ return;
708
+ }
709
+ if (event.kind === 'link') {
710
+ live(conversation, { link: { connected: event.connected } });
711
+ return;
712
+ }
713
+ if (event.kind === 'deliveries') {
714
+ live(conversation, { deliveryUnknown: [...event.unknown] });
715
+ return;
716
+ }
483
717
  const arriving = acceptedMessage(event.message);
484
718
  if (arriving === null)
485
719
  return;
720
+ noted(conversation, arriving);
486
721
  if (replicated.has(conversation)) {
487
- void keep(conversation, arriving).catch(() => { });
722
+ if (deps.full()) {
723
+ gap(conversation);
724
+ return;
725
+ }
726
+ void keep(conversation, arriving).catch((error) => {
727
+ if (refusedForSpace(error))
728
+ gap(conversation);
729
+ });
488
730
  return;
489
731
  }
490
732
  if (sealTurns(conversation, new Set([arriving.turn])))
491
733
  emit(conversation);
492
734
  }
735
+ async function kept(local, conversation, following) {
736
+ if (following === null || empty(following)) {
737
+ await local.dropChatDraft(conversation);
738
+ drafts.delete(conversation);
739
+ }
740
+ else {
741
+ await local.keepChatDrafts([following]);
742
+ drafts.set(conversation, following);
743
+ }
744
+ }
745
+ async function sealed(local, conversation, turns) {
746
+ const held = drafts.get(conversation);
747
+ const submitted = held?.submitted ?? [];
748
+ const left = submitted.filter((one) => one.turn === null || !turns.has(one.turn));
749
+ if (held === undefined || left.length === submitted.length)
750
+ return false;
751
+ await kept(local, conversation, withSubmitted(held, left));
752
+ return true;
753
+ }
754
+ function moved(agent, operation, to) {
755
+ const from = newChat(agent);
756
+ return deps.durably(async (local) => {
757
+ const held = drafts.get(from);
758
+ const earlier = (held?.moves ?? []).filter((one) => !deps.expired(one));
759
+ if (earlier.includes(operation))
760
+ return;
761
+ const blank = {
762
+ conversation: from,
763
+ text: '',
764
+ revision: chatDraftRevision(),
765
+ };
766
+ const { moves: _answered, ...unsent } = held ?? blank;
767
+ const taken = drafts.has(to) || empty(unsent) ? [] : [{ ...unsent, conversation: to }];
768
+ const left = { ...(taken.length === 0 ? unsent : blank), moves: [...earlier, operation] };
769
+ await local.keepChatDrafts([left, ...taken]);
770
+ drafts.set(from, left);
771
+ for (const one of taken)
772
+ drafts.set(to, one);
773
+ deps.publish(conversationChangeSet([from, to]));
774
+ });
775
+ }
493
776
  function withDraft(conversation, next) {
494
777
  return deps.durably(async (local) => {
495
- const following = next(drafts.get(conversation));
778
+ const following = await next(drafts.get(conversation), local);
496
779
  if (following === undefined)
497
780
  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
- }
506
- deps.publish(conversationChangeSet([], [conversation]));
781
+ await kept(local, conversation, following);
782
+ deps.publish(conversationChangeSet([conversation]));
783
+ });
784
+ }
785
+ async function submitted(conversation, operation, draft) {
786
+ let submission;
787
+ let earlier = false;
788
+ await withDraft(conversation, (current) => {
789
+ submission = current?.submitted?.find((one) => one.operation === operation);
790
+ earlier = submission !== undefined;
791
+ if (submission !== undefined)
792
+ return undefined;
793
+ const said = draft ?? current;
794
+ submission = { operation, text: said?.text ?? '', turn: null };
795
+ return {
796
+ conversation,
797
+ ...(draft === undefined || current === undefined
798
+ ? { text: '', revision: chatDraftRevision() }
799
+ : { text: current.text, revision: current.revision }),
800
+ submitted: [...(current?.submitted ?? []), submission],
801
+ };
507
802
  });
803
+ return { submission: submission, earlier };
508
804
  }
509
- function invoke(name, conversation, extra = {}) {
510
- return deps.call(name, { conversation, ...extra });
805
+ function forgotten(conversation, submission) {
806
+ return withDraft(conversation, (current) => {
807
+ const held = current?.submitted ?? [];
808
+ if (current === undefined || !held.includes(submission))
809
+ return undefined;
810
+ return withSubmitted(current, held.filter((one) => one !== submission));
811
+ });
812
+ }
813
+ async function dispatched(conversation, submission) {
814
+ const dispatch = { answered: false };
815
+ sending.set(submission.operation, dispatch);
816
+ try {
817
+ const result = await invoke('conversations/send', conversation, {
818
+ operation_id: submission.operation,
819
+ text: submission.text,
820
+ });
821
+ dispatch.answered = result.kind !== 'transport-uncertainty';
822
+ if (result.kind === 'revoked-authorization')
823
+ return result;
824
+ if (result.kind !== 'action' && result.retryable) {
825
+ deps.unanswered();
826
+ return result;
827
+ }
828
+ const turn = answered(result, 'message_id');
829
+ await withDraft(conversation, async (current, local) => {
830
+ const held = current?.submitted?.find((one) => one.operation === submission.operation);
831
+ if (current === undefined || held === undefined || held.turn !== null)
832
+ return undefined;
833
+ const others = current.submitted.filter((one) => one !== held);
834
+ if (result.kind !== 'action')
835
+ return returned(current, held, others);
836
+ const arrived = turn !== null && (await local.turnArrived(conversation, turn));
837
+ return withSubmitted(current, turn === null || arrived
838
+ ? others
839
+ : current.submitted.map((one) => (one === held ? { ...held, turn } : one)));
840
+ });
841
+ return result;
842
+ }
843
+ finally {
844
+ if (sending.get(submission.operation) === dispatch)
845
+ sending.delete(submission.operation);
846
+ }
847
+ }
848
+ function invoke(name, conversation, extra = { operation_id: changeBatchIdentity() }) {
849
+ return deps.call(name, { conversation_id: conversation, ...extra });
511
850
  }
512
851
  const surface = {
513
- async list() {
514
- await agentsReady();
515
- return entries();
852
+ list() {
853
+ return new Promise((settle) => {
854
+ const answer = (listed) => {
855
+ unlisted.delete(answer);
856
+ listWatchers.delete(answer);
857
+ settle(listed);
858
+ };
859
+ unlisted.add(answer);
860
+ learned();
861
+ });
516
862
  },
517
863
  async read(conversation, options = {}) {
518
- const limit = limitOf(options);
519
- return deps.durably(() => Promise.resolve(viewOf(conversation, limit)));
864
+ const asked = askedOf(options);
865
+ return deps.durably(async (local) => {
866
+ const { messages, more } = await local.messageWindow(conversation, asked.limit, asked.before);
867
+ const total = asked.total ? await local.messageCount(conversation) : null;
868
+ return structuredClone(viewOf(conversation, messages, frameOf(asked, messages, more, total)));
869
+ });
520
870
  },
521
- create(input) {
522
- return deps.call('conversation.create', input);
871
+ async create(input) {
872
+ const result = await deps.call('conversations/create', input);
873
+ const conversation = answered(result, 'conversation_id');
874
+ if (conversation === null)
875
+ return result;
876
+ if (!listing.has(conversation))
877
+ creations.add(conversation);
878
+ const creation = creationOf(input);
879
+ if (creation !== null)
880
+ await moved(creation.agent, creation.operation, conversation);
881
+ return result;
523
882
  },
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 };
883
+ async send(conversation, operation = changeBatchIdentity(), draft) {
884
+ if (deps.full())
885
+ return DURABLE_WRITE;
886
+ if (deps.online()) {
887
+ const held = await submitted(conversation, operation, draft).catch((error) => {
888
+ if (refusedForSpace(error))
889
+ return null;
890
+ throw error;
548
891
  });
549
- return result;
892
+ if (held === null)
893
+ return DURABLE_WRITE;
894
+ const { submission, earlier } = held;
895
+ if (!earlier || !deps.expired(operation))
896
+ return dispatched(conversation, submission);
897
+ await forgotten(conversation, submission);
898
+ return EXPIRED;
550
899
  }
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
- }
900
+ if (draft !== undefined) {
901
+ await withDraft(conversation, (current) => current?.submitted?.some((one) => one.operation === operation) === true
902
+ ? undefined
903
+ : returned(current ?? { conversation, text: '', revision: '' }, { operation, ...draft, turn: null }, current?.submitted ?? []));
558
904
  }
905
+ return OFFLINE;
559
906
  },
560
907
  answer(conversation, answer) {
561
- return invoke('conversation.answer', conversation, { answer });
908
+ return invoke('conversations/interaction/answer', conversation, {
909
+ operation_id: changeBatchIdentity(),
910
+ ...answer,
911
+ });
562
912
  },
563
913
  interrupt(conversation) {
564
- return invoke('conversation.interrupt', conversation);
914
+ return invoke('conversations/interrupt', conversation);
565
915
  },
566
916
  kill(conversation) {
567
- return invoke('conversation.kill', conversation);
568
- },
569
- async cancel(conversation, turn) {
570
- const result = await invoke('conversation.cancel', conversation, { turn });
571
- if (result.kind !== 'action')
572
- return result;
573
- await withDraft(conversation, (current) => current === undefined || current.turn !== turn ? undefined : { ...current, turn: null });
574
- return result;
917
+ return invoke('conversations/kill', conversation);
575
918
  },
576
919
  delete(conversation) {
577
- return invoke('conversation.delete', conversation);
920
+ return invoke('conversations/delete', conversation);
578
921
  },
579
922
  };
580
923
  const chatDraft = {
581
924
  write(conversation, draft, revision = chatDraftRevision()) {
582
- return withDraft(conversation, () => ({
925
+ return withDraft(conversation, (current) => ({
583
926
  conversation,
584
927
  text: draft.text,
585
- files: [...draft.files],
586
- turn: null,
587
928
  revision,
929
+ ...(current === undefined ? {} : pending(current)),
930
+ }));
931
+ },
932
+ prepend(conversation, draft) {
933
+ return withDraft(conversation, (current) => ({
934
+ conversation,
935
+ ...(current === undefined ? { text: draft.text } : prepended(current, draft)),
936
+ revision: chatDraftRevision(),
937
+ ...(current === undefined ? {} : pending(current)),
588
938
  }));
589
939
  },
590
940
  discard(conversation) {
591
- return withDraft(conversation, (current) => (current === undefined ? undefined : null));
941
+ return withDraft(conversation, (current) => current === undefined
942
+ ? undefined
943
+ : { conversation, text: '', revision: chatDraftRevision(), ...pending(current) });
592
944
  },
593
945
  };
594
946
  return {
595
947
  surface,
596
948
  chatDraft,
597
- hydrate(held, stored) {
598
- messages.clear();
949
+ hydrate(held, kept, list, cursor) {
950
+ if (list !== null && !listKnown) {
951
+ listing.clear();
952
+ for (const entry of list)
953
+ listing.set(entry.conversation, { ...entry });
954
+ listHeld = true;
955
+ listCursor = cursor;
956
+ deps.stream()?.hold(cursor);
957
+ }
958
+ stored.clear();
599
959
  reads.clear();
600
- for (const view of views.values())
601
- view.messages = null;
960
+ partials.clear();
602
961
  for (const entry of held) {
603
- messages.set(entry.conversation, [...entry.messages].sort(ordered));
962
+ stored.add(entry.conversation);
604
963
  if (entry.read !== undefined)
605
964
  reads.set(entry.conversation, entry.read);
965
+ if (entry.partial)
966
+ partials.add(entry.conversation);
606
967
  }
607
968
  drafts.clear();
608
- for (const record of stored)
969
+ for (const record of kept)
609
970
  drafts.set(record.conversation, record);
971
+ if (listHeld)
972
+ learned();
610
973
  },
611
974
  async reconcile() {
612
975
  if (!listKnown)
613
976
  return;
614
977
  const named = new Set(listing.keys());
615
978
  const leaving = [
616
- ...new Set([...replicated, ...messages.keys(), ...reads.keys(), ...drafts.keys()]),
617
- ].filter((conversation) => !named.has(conversation));
979
+ ...new Set([...replicated, ...stored, ...reads.keys(), ...drafts.keys()]),
980
+ ].filter((conversation) => !named.has(conversation) &&
981
+ !creations.has(conversation) &&
982
+ (!conversation.startsWith(NEW_CHAT) || retiredChat(conversation)));
618
983
  for (const conversation of named) {
619
- if (!replicated.has(conversation))
984
+ if (!replicated.has(conversation) && missed(conversation))
620
985
  behind.add(conversation);
621
986
  }
622
987
  replicated = named;
@@ -625,44 +990,101 @@ export function createConversationStore(deps) {
625
990
  await remove(leaving);
626
991
  },
627
992
  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);
993
+ if (deps.full()) {
994
+ if (behind.size > 0)
995
+ progress.complete = false;
996
+ return;
997
+ }
998
+ const owed = (conversation) => behind.has(conversation) && replicated.has(conversation);
999
+ const read = async (conversations) => {
1000
+ if (conversations.length === 0 || deps.closed())
1001
+ return;
1002
+ const failed = await caughtUp(conversations);
631
1003
  if (failed?.kind === 'transport-uncertainty')
632
1004
  progress.reachable = false;
633
- }
1005
+ };
1006
+ const asking = [...behind].filter(owed).sort();
1007
+ const awaited = asking.filter((conversation) => catching.has(conversation));
1008
+ await Promise.all([
1009
+ read(asking.filter((conversation) => !catching.has(conversation))),
1010
+ Promise.all(awaited.map((conversation) => catching.get(conversation))).then(() => read(awaited.filter((conversation) => owed(conversation) && !catching.has(conversation)))),
1011
+ ]);
634
1012
  if (behind.size > 0)
635
1013
  progress.complete = false;
636
1014
  },
1015
+ async resend(progress) {
1016
+ for (const [conversation, held] of [...drafts]) {
1017
+ for (const submission of held.submitted ?? []) {
1018
+ if (submission.turn !== null || sending.has(submission.operation))
1019
+ continue;
1020
+ if (deps.closed())
1021
+ return;
1022
+ if (deps.expired(submission.operation)) {
1023
+ await forgotten(conversation, submission);
1024
+ continue;
1025
+ }
1026
+ const result = await dispatched(conversation, submission);
1027
+ if (result.kind === 'revoked-authorization')
1028
+ return;
1029
+ if (result.kind === 'transport-uncertainty') {
1030
+ progress.reachable = false;
1031
+ return;
1032
+ }
1033
+ if (result.kind !== 'action' && result.retryable) {
1034
+ progress.complete = false;
1035
+ return;
1036
+ }
1037
+ }
1038
+ }
1039
+ },
637
1040
  current() {
638
1041
  return behind.size === 0;
639
1042
  },
640
- reconnected() {
641
- for (const conversation of replicated)
642
- gap(conversation);
1043
+ known() {
1044
+ return listKnown;
1045
+ },
1046
+ reopened() {
1047
+ relisting = null;
1048
+ for (const [operation, dispatch] of sending)
1049
+ if (!dispatch.answered)
1050
+ sending.delete(operation);
1051
+ },
1052
+ lost() {
1053
+ catching.clear();
643
1054
  },
644
1055
  watch(conversation, options, deliver) {
645
- const limit = limitOf(options);
646
- const key = JSON.stringify([conversation, limit ?? null]);
1056
+ const asked = askedOf(options);
1057
+ const key = JSON.stringify([conversation, asked.limit, asked.after ?? null, asked.total]);
647
1058
  const view = views.get(key) ?? {
648
1059
  conversation,
649
- limit,
1060
+ asked,
650
1061
  watchers: new Set(),
651
- messages: null,
1062
+ window: null,
1063
+ more: false,
1064
+ total: null,
652
1065
  };
653
1066
  views.set(key, view);
654
1067
  const watcher = { notify: deliver, ready: false };
655
1068
  view.watchers.add(watcher);
656
- void deps
657
- .durably(() => Promise.resolve())
1069
+ const reading = new AbortController();
1070
+ void untilAnswered(() => deps.durably(async (local) => {
1071
+ if (view.window !== null)
1072
+ return;
1073
+ const { messages, more } = await local.messageWindow(conversation, asked.limit, asked.before);
1074
+ view.window = messages;
1075
+ view.more = more;
1076
+ view.total = asked.total ? await local.messageCount(conversation) : null;
1077
+ }), reading.signal)
658
1078
  .then(() => {
659
1079
  if (deps.closed() || !view.watchers.has(watcher))
660
1080
  return;
661
1081
  watcher.ready = true;
662
- watcher.notify(viewOf(conversation, limit));
1082
+ const window = view.window ?? [];
1083
+ watcher.notify(viewOf(conversation, window, sharedFrame(view, window)));
663
1084
  })
664
1085
  .catch(() => { });
665
1086
  return () => {
1087
+ reading.abort();
666
1088
  if (!view.watchers.delete(watcher))
667
1089
  return;
668
1090
  if (view.watchers.size === 0)
@@ -670,73 +1092,94 @@ export function createConversationStore(deps) {
670
1092
  };
671
1093
  },
672
1094
  watchList(deliver) {
673
- listWatchers.add(deliver);
674
- void agentsReady().then(() => {
675
- if (!deps.closed() && listWatchers.has(deliver))
676
- deliver(entries());
677
- });
1095
+ unlisted.add(deliver);
1096
+ learned();
678
1097
  return () => {
1098
+ unlisted.delete(deliver);
679
1099
  listWatchers.delete(deliver);
680
1100
  };
681
1101
  },
682
1102
  watchAgents(deliver) {
683
- agentWatchers.add(deliver);
684
- void agentsReady().then(() => {
685
- if (!deps.closed() && agentWatchers.has(deliver))
686
- deliver(agents());
687
- });
1103
+ unnamed.add(deliver);
1104
+ learned();
1105
+ readAgents();
688
1106
  return () => {
1107
+ unnamed.delete(deliver);
689
1108
  agentWatchers.delete(deliver);
690
1109
  };
691
1110
  },
1111
+ learned,
692
1112
  changed(set) {
693
1113
  for (const conversation of set.conversations)
694
1114
  emit(conversation);
695
- const declaration = deps.agents();
696
- if (declaration !== undefined &&
697
- set.documents.some((change) => change.locator === declaration.locator)) {
698
- agentsRead = readAgents().catch(() => {
699
- agentsRead = null;
700
- });
701
- }
702
1115
  },
703
1116
  receive(event) {
704
1117
  if (deps.closed())
705
1118
  return false;
706
1119
  if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted') {
707
1120
  if (event.kind === 'list') {
708
- listing.clear();
709
- for (const entry of event.entries)
710
- listing.set(entry.conversation, { ...entry });
1121
+ if (event.whole)
1122
+ relisting ??= new Map();
1123
+ for (const entry of event.entries) {
1124
+ if (relisting === null)
1125
+ listed(entry);
1126
+ else
1127
+ relisting.set(entry.conversation, { ...entry });
1128
+ }
1129
+ if (event.more)
1130
+ return false;
1131
+ if (relisting !== null) {
1132
+ listing.clear();
1133
+ for (const entry of relisting.values())
1134
+ listed(entry);
1135
+ relisting = null;
1136
+ }
1137
+ listCursor = event.cursor;
1138
+ deps.stream()?.hold(event.cursor);
711
1139
  listKnown = true;
1140
+ for (const conversation of replicated) {
1141
+ if (!listing.has(conversation))
1142
+ continue;
1143
+ if (!missed(conversation))
1144
+ behind.delete(conversation);
1145
+ else if (!behind.has(conversation))
1146
+ gap(conversation);
1147
+ }
1148
+ openingEnded();
712
1149
  }
713
1150
  else if (event.kind === 'listed') {
714
- listing.set(event.entry.conversation, { ...event.entry });
1151
+ listed(event.entry);
1152
+ relisting?.set(event.entry.conversation, { ...event.entry });
715
1153
  }
716
1154
  else {
717
1155
  listing.delete(event.conversation);
1156
+ relisting?.delete(event.conversation);
718
1157
  }
1158
+ keepList();
719
1159
  emitList();
720
- return (event.kind !== 'list' &&
1160
+ return ((event.kind !== 'list' &&
721
1161
  listKnown &&
722
1162
  (listing.size !== replicated.size ||
723
- [...listing.keys()].some((conversation) => !replicated.has(conversation))));
1163
+ [...listing.keys()].some((conversation) => !replicated.has(conversation)))) ||
1164
+ (listKnown && [...drafts.keys()].some(retiredChat)));
724
1165
  }
725
1166
  heard(event);
726
1167
  return false;
727
1168
  },
728
- messages() {
729
- return [...messages].flatMap(([conversation, held]) => held.map(({ identity, text }) => ({ conversation, identity, text })));
1169
+ searchable() {
1170
+ return [...(listKnown ? listing.keys() : stored)].sort();
730
1171
  },
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 };
1172
+ acquired(conversation) {
1173
+ return reads.has(conversation) && !partials.has(conversation);
734
1174
  },
735
1175
  close() {
736
1176
  views.clear();
737
1177
  lives.clear();
1178
+ unsent.clear();
738
1179
  listWatchers.clear();
1180
+ unlisted.clear();
739
1181
  agentWatchers.clear();
1182
+ unnamed.clear();
740
1183
  },
741
1184
  };
742
1185
  }