@agentshouse/mdruntime 0.1.0-cede5a74284be → 0.1.0-cef5833f6c689

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