@agentshouse/mdruntime 0.1.0-c967d1ca66fee → 0.1.0-c969b8dee6cc8

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