@agentshouse/mdruntime 0.1.0-cfbf80d8d4644 → 0.1.0-cfe148b32e3d8

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