@agentshouse/mdruntime 0.1.0-c1cc928d1d256 → 0.1.0-c1ffd2bc60355

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