@agentshouse/mdruntime 0.1.0-c588e78cf6975 → 0.1.0-c59a5a6bd2f31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/README.md +2 -2
  2. package/dist/acceptance.d.ts +26 -0
  3. package/dist/acceptance.d.ts.map +1 -0
  4. package/dist/acceptance.js +384 -0
  5. package/dist/adapter.d.ts +38 -25
  6. package/dist/adapter.d.ts.map +1 -1
  7. package/dist/attached-model.d.ts +3 -10
  8. package/dist/attached-model.d.ts.map +1 -1
  9. package/dist/attached-model.js +21 -56
  10. package/dist/bootstrap.d.ts +8 -4
  11. package/dist/bootstrap.d.ts.map +1 -1
  12. package/dist/bootstrap.js +209 -78
  13. package/dist/chain.d.ts +2 -1
  14. package/dist/chain.d.ts.map +1 -1
  15. package/dist/change.d.ts +1 -0
  16. package/dist/change.d.ts.map +1 -1
  17. package/dist/chat-types.d.ts +3 -0
  18. package/dist/chat-types.d.ts.map +1 -0
  19. package/dist/chat-types.js +2 -0
  20. package/dist/client.d.ts +14 -4
  21. package/dist/client.d.ts.map +1 -1
  22. package/dist/client.js +3 -0
  23. package/dist/collection.d.ts +2 -1
  24. package/dist/collection.d.ts.map +1 -1
  25. package/dist/compile.d.ts +7 -7
  26. package/dist/compile.d.ts.map +1 -1
  27. package/dist/compile.js +229 -297
  28. package/dist/connection.d.ts +19 -43
  29. package/dist/connection.d.ts.map +1 -1
  30. package/dist/connection.js +1325 -3586
  31. package/dist/context.d.ts +2 -0
  32. package/dist/context.d.ts.map +1 -0
  33. package/dist/context.js +7 -0
  34. package/dist/conversation-row.d.ts +33 -0
  35. package/dist/conversation-row.d.ts.map +1 -0
  36. package/dist/conversation-row.js +47 -0
  37. package/dist/conversation.d.ts +18 -9
  38. package/dist/conversation.d.ts.map +1 -1
  39. package/dist/conversation.js +375 -224
  40. package/dist/durability.d.ts +6 -0
  41. package/dist/durability.d.ts.map +1 -1
  42. package/dist/entry.d.ts +2 -2
  43. package/dist/entry.d.ts.map +1 -1
  44. package/dist/entry.js +17 -8
  45. package/dist/figures.d.ts +48 -0
  46. package/dist/figures.d.ts.map +1 -0
  47. package/dist/figures.js +197 -0
  48. package/dist/host.d.ts +4 -5
  49. package/dist/host.d.ts.map +1 -1
  50. package/dist/host.js +22 -21
  51. package/dist/identity.d.ts +0 -1
  52. package/dist/identity.d.ts.map +1 -1
  53. package/dist/identity.js +0 -3
  54. package/dist/index.d.ts +18 -8
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +5 -1
  57. package/dist/install.d.ts +86 -0
  58. package/dist/install.d.ts.map +1 -0
  59. package/dist/install.js +537 -0
  60. package/dist/job.d.ts +33 -0
  61. package/dist/job.d.ts.map +1 -0
  62. package/dist/job.js +54 -0
  63. package/dist/live.d.ts +5 -4
  64. package/dist/live.d.ts.map +1 -1
  65. package/dist/live.js +62 -20
  66. package/dist/log.d.ts +1 -0
  67. package/dist/log.d.ts.map +1 -1
  68. package/dist/memory.d.ts +4 -1
  69. package/dist/memory.d.ts.map +1 -1
  70. package/dist/memory.js +43 -4
  71. package/dist/messages.d.ts +27 -3
  72. package/dist/messages.d.ts.map +1 -1
  73. package/dist/messages.js +97 -19
  74. package/dist/model.d.ts +2 -9
  75. package/dist/model.d.ts.map +1 -1
  76. package/dist/model.js +0 -9
  77. package/dist/observation.d.ts +2 -2
  78. package/dist/observation.d.ts.map +1 -1
  79. package/dist/observation.js +7 -2
  80. package/dist/outcome.d.ts +1 -0
  81. package/dist/outcome.d.ts.map +1 -1
  82. package/dist/outcome.js +3 -0
  83. package/dist/overlay.d.ts +1 -0
  84. package/dist/overlay.d.ts.map +1 -1
  85. package/dist/overlay.js +11 -0
  86. package/dist/parts.d.ts +59 -0
  87. package/dist/parts.d.ts.map +1 -0
  88. package/dist/parts.js +118 -0
  89. package/dist/query.d.ts +13 -210
  90. package/dist/query.d.ts.map +1 -1
  91. package/dist/query.js +3 -508
  92. package/dist/questions.d.ts +32 -0
  93. package/dist/questions.d.ts.map +1 -0
  94. package/dist/questions.js +118 -0
  95. package/dist/reach.d.ts +11 -11
  96. package/dist/reach.d.ts.map +1 -1
  97. package/dist/reach.js +166 -113
  98. package/dist/react.d.ts +4 -2
  99. package/dist/react.d.ts.map +1 -1
  100. package/dist/read.d.ts +7 -0
  101. package/dist/read.d.ts.map +1 -0
  102. package/dist/read.js +17 -0
  103. package/dist/readiness.d.ts +0 -2
  104. package/dist/readiness.d.ts.map +1 -1
  105. package/dist/reads.d.ts +35 -0
  106. package/dist/reads.d.ts.map +1 -0
  107. package/dist/reads.js +128 -0
  108. package/dist/recovery.d.ts.map +1 -1
  109. package/dist/recovery.js +9 -1
  110. package/dist/refresh.d.ts +1 -1
  111. package/dist/refresh.d.ts.map +1 -1
  112. package/dist/refresh.js +8 -20
  113. package/dist/replica.d.ts +8 -8
  114. package/dist/replica.d.ts.map +1 -1
  115. package/dist/replica.js +50 -18
  116. package/dist/room-draft.d.ts +3 -2
  117. package/dist/room-draft.d.ts.map +1 -1
  118. package/dist/room-draft.js +4 -4
  119. package/dist/room-drafts.d.ts +29 -0
  120. package/dist/room-drafts.d.ts.map +1 -0
  121. package/dist/room-drafts.js +216 -0
  122. package/dist/rows.d.ts +14 -22
  123. package/dist/rows.d.ts.map +1 -1
  124. package/dist/rows.js +48 -189
  125. package/dist/save.d.ts.map +1 -1
  126. package/dist/save.js +11 -8
  127. package/dist/saves.d.ts +96 -0
  128. package/dist/saves.d.ts.map +1 -0
  129. package/dist/saves.js +999 -0
  130. package/dist/search.d.ts +12 -63
  131. package/dist/search.d.ts.map +1 -1
  132. package/dist/search.js +9 -52
  133. package/dist/seed.d.ts +16 -2
  134. package/dist/seed.d.ts.map +1 -1
  135. package/dist/seed.js +66 -0
  136. package/dist/socket/admission.d.ts +7 -0
  137. package/dist/socket/admission.d.ts.map +1 -0
  138. package/dist/socket/admission.js +39 -0
  139. package/dist/socket/answers.d.ts +6 -0
  140. package/dist/socket/answers.d.ts.map +1 -0
  141. package/dist/socket/answers.js +20 -0
  142. package/dist/socket/reads.d.ts +10 -0
  143. package/dist/socket/reads.d.ts.map +1 -0
  144. package/dist/socket/reads.js +31 -0
  145. package/dist/socket.d.ts +43 -0
  146. package/dist/socket.d.ts.map +1 -0
  147. package/dist/socket.js +96 -0
  148. package/dist/source.d.ts +61 -0
  149. package/dist/source.d.ts.map +1 -0
  150. package/dist/source.js +81 -0
  151. package/dist/status.d.ts +0 -2
  152. package/dist/status.d.ts.map +1 -1
  153. package/dist/status.js +0 -6
  154. package/dist/store/fences.d.ts +16 -0
  155. package/dist/store/fences.d.ts.map +1 -0
  156. package/dist/store/fences.js +26 -0
  157. package/dist/store/installs.d.ts +30 -0
  158. package/dist/store/installs.d.ts.map +1 -0
  159. package/dist/store/installs.js +34 -0
  160. package/dist/store/records.d.ts +212 -0
  161. package/dist/store/records.d.ts.map +1 -0
  162. package/dist/store/records.js +408 -0
  163. package/dist/store/room-held.d.ts +8 -0
  164. package/dist/store/room-held.d.ts.map +1 -0
  165. package/dist/store/room-held.js +17 -0
  166. package/dist/store/watchers.d.ts +15 -0
  167. package/dist/store/watchers.d.ts.map +1 -0
  168. package/dist/store/watchers.js +36 -0
  169. package/dist/store/writes.d.ts +24 -0
  170. package/dist/store/writes.d.ts.map +1 -0
  171. package/dist/store/writes.js +125 -0
  172. package/dist/store.d.ts +78 -156
  173. package/dist/store.d.ts.map +1 -1
  174. package/dist/store.js +117 -401
  175. package/dist/stored.d.ts +58 -65
  176. package/dist/stored.d.ts.map +1 -1
  177. package/dist/stored.js +264 -243
  178. package/dist/tables.d.ts +107 -51
  179. package/dist/tables.d.ts.map +1 -1
  180. package/dist/tables.js +193 -59
  181. package/dist/tallies.d.ts +18 -0
  182. package/dist/tallies.d.ts.map +1 -0
  183. package/dist/tallies.js +129 -0
  184. package/dist/validation.d.ts.map +1 -1
  185. package/dist/validation.js +7 -4
  186. package/dist/windows.d.ts +1 -12
  187. package/dist/windows.d.ts.map +1 -1
  188. package/dist/windows.js +2 -75
  189. package/dist/wire.d.ts +14 -0
  190. package/dist/wire.d.ts.map +1 -1
  191. package/dist/wire.js +5 -0
  192. package/package.json +2 -2
  193. package/dist/fence.d.ts +0 -6
  194. package/dist/fence.d.ts.map +0 -1
  195. package/dist/fence.js +0 -10
  196. package/dist/fulltext.d.ts +0 -27
  197. package/dist/fulltext.d.ts.map +0 -1
  198. package/dist/fulltext.js +0 -370
  199. package/dist/words.d.ts +0 -19
  200. package/dist/words.d.ts.map +0 -1
  201. package/dist/words.js +0 -156
@@ -1,10 +1,14 @@
1
+ import { cursorOf, decodedCursor } from '@agentshouse/mdmodel';
1
2
  import { conversationChangeSet } from './change-set.js';
3
+ import { heldFacts } from './conversation-row.js';
2
4
  import { runtimeError } from './failure.js';
3
5
  import { refusedForSpace } from './durability.js';
4
6
  import { changeBatchIdentity, chatDraftRevision } from './identity.js';
5
7
  import { OFFLINE } from './outcome.js';
8
+ import { jobsOf, runningLine } from './parts.js';
9
+ import { openQuestion } from './questions.js';
6
10
  import { untilAnswered } from './recovery.js';
7
- import { capped, cursorOf, decodedCursor, FROM_START, messageOrder, precedes } from './windows.js';
11
+ import { capped, FROM_START, messageOrder, precedes } from './windows.js';
8
12
  const QUIET = {
9
13
  turn: null,
10
14
  questions: [],
@@ -33,6 +37,7 @@ export function unheldView(conversation) {
33
37
  turn: null,
34
38
  questions: [],
35
39
  process: null,
40
+ processEnded: null,
36
41
  commands: [],
37
42
  options: [],
38
43
  starting: null,
@@ -40,21 +45,22 @@ export function unheldView(conversation) {
40
45
  link: null,
41
46
  deliveryUnknown: [],
42
47
  chatDraft: null,
48
+ jobs: [],
43
49
  };
44
50
  }
45
51
  export const SESSION_STARTED = 'Session started';
46
52
  const NEW_CHAT = 'new:';
47
53
  export const HISTORY_CONVERSATIONS = 64;
54
+ // A row is read at a glance and each telling clones the list to every observing tab, while a running line changes at each Kit flush.
55
+ const ROW_INTERVAL_MS = 1_000;
48
56
  export function newChat(agent) {
49
57
  return `${NEW_CHAT}${agent}`;
50
58
  }
51
59
  function creationOf(input) {
52
60
  if (typeof input !== 'object' || input === null)
53
61
  return null;
54
- const { agent_id: agent, operation_id: operation, replaces, } = input;
55
- return typeof agent === 'string' && typeof operation === 'string' && replaces === undefined
56
- ? { agent, operation }
57
- : null;
62
+ const { agent_id: agent, operation_id: operation } = input;
63
+ return typeof agent === 'string' && typeof operation === 'string' ? { agent, operation } : null;
58
64
  }
59
65
  function startedTurn(turn, operation) {
60
66
  if (operation.op === 'begin')
@@ -213,7 +219,14 @@ function askedOf(options) {
213
219
  }
214
220
  return { limit: capped(limit), after, before: beforeOf(after), total: total === true };
215
221
  }
216
- const EXPIRED = { kind: 'rejected', code: 'expired', fields: {}, retryable: false };
222
+ function phraseOf(text) {
223
+ if (text === undefined)
224
+ return null;
225
+ if (typeof text !== 'string' || text === '') {
226
+ throw runtimeError('invalid-text', 'a conversation is found by a phrase');
227
+ }
228
+ return text;
229
+ }
217
230
  const DURABLE_WRITE = {
218
231
  kind: 'rejected',
219
232
  code: 'durable-write',
@@ -247,10 +260,6 @@ function returned(draft, submission, others) {
247
260
  }, others);
248
261
  }
249
262
  export function createConversationStore(deps) {
250
- const stored = new Set();
251
- const reads = new Map();
252
- const partials = new Set();
253
- const drafts = new Map();
254
263
  const sending = new Map();
255
264
  const behind = new Set();
256
265
  const catching = new Map();
@@ -258,15 +267,16 @@ export function createConversationStore(deps) {
258
267
  const lives = new Map();
259
268
  const unsent = new Map();
260
269
  const views = new Map();
261
- const listing = new Map();
270
+ const heardNewest = new Map();
262
271
  let relisting = null;
263
- let listCursor = null;
264
272
  const creations = new Set();
265
273
  const listWatchers = new Set();
266
274
  const unlisted = new Set();
267
275
  let listKnown = false;
268
- let listHeld = false;
269
- let listKept = false;
276
+ let listOwed = false;
277
+ let listTelling = false;
278
+ let rowTimer = null;
279
+ const closing = new AbortController();
270
280
  const agentWatchers = new Set();
271
281
  const unnamed = new Set();
272
282
  let agentsShown = null;
@@ -277,8 +287,7 @@ export function createConversationStore(deps) {
277
287
  function liveOf(conversation) {
278
288
  return lives.get(conversation) ?? QUIET;
279
289
  }
280
- function viewOf(conversation, held, frame) {
281
- const draft = drafts.get(conversation);
290
+ function viewOf(conversation, held, frame, draft, ended, marked) {
282
291
  const facts = liveOf(conversation);
283
292
  return {
284
293
  conversation,
@@ -292,15 +301,17 @@ export function createConversationStore(deps) {
292
301
  turn: structuredClone(facts.turn),
293
302
  questions: structuredClone(facts.questions),
294
303
  process: facts.process === null ? null : { ...facts.process },
304
+ processEnded: ended,
295
305
  commands: structuredClone(facts.commands),
296
306
  options: structuredClone(facts.options),
297
307
  starting: facts.starting,
298
308
  subagents: structuredClone(facts.subagents),
299
309
  link: facts.link === null ? null : { ...facts.link },
300
310
  deliveryUnknown: [...facts.deliveryUnknown],
301
- chatDraft: draft === undefined || unwritten(draft)
302
- ? null
303
- : { text: draft.text, revision: draft.revision },
311
+ chatDraft: draft === null || unwritten(draft) ? null : { text: draft.text, revision: draft.revision },
312
+ jobs: facts.process?.running === false
313
+ ? []
314
+ : jobsOf(marked.filter(({ at }) => ended === null || at > ended), facts.turn),
304
315
  };
305
316
  }
306
317
  function emit(conversation) {
@@ -312,7 +323,7 @@ export function createConversationStore(deps) {
312
323
  const ready = [...view.watchers].filter((watcher) => watcher.ready);
313
324
  if (ready.length === 0)
314
325
  continue;
315
- const value = viewOf(conversation, view.window, sharedFrame(view, view.window));
326
+ const value = viewOf(conversation, view.window, sharedFrame(view, view.window), view.draft, view.ended, view.marked);
316
327
  for (const watcher of ready)
317
328
  watcher.notify(value);
318
329
  }
@@ -322,11 +333,13 @@ export function createConversationStore(deps) {
322
333
  }
323
334
  async function slide(local, conversation, arriving) {
324
335
  let count;
336
+ let marked;
325
337
  for (const view of views.values()) {
326
338
  if (view.conversation !== conversation || view.window === null)
327
339
  continue;
328
340
  if (arriving === null) {
329
341
  view.window = [];
342
+ view.marked = [];
330
343
  view.more = false;
331
344
  if (view.total !== null)
332
345
  view.total = 0;
@@ -336,6 +349,10 @@ export function createConversationStore(deps) {
336
349
  const next = windowOf(view.window, joining, view.asked.limit);
337
350
  view.window = next.messages;
338
351
  view.more ||= next.dropped;
352
+ if (arriving.length > 0) {
353
+ marked ??= await local.jobMarkers(conversation);
354
+ view.marked = marked;
355
+ }
339
356
  if (view.total !== null && arriving.length > 0) {
340
357
  count ??= await local.messageCount(conversation);
341
358
  view.total = count;
@@ -348,8 +365,24 @@ export function createConversationStore(deps) {
348
365
  return 'asking';
349
366
  return facts.turn === null ? 'idle' : 'working';
350
367
  }
351
- function entries() {
352
- return [...listing.values()].map((entry) => ({
368
+ function rowOf(conversation) {
369
+ const turn = lives.get(conversation)?.turn ?? null;
370
+ return {
371
+ state: stateOf(conversation),
372
+ started: turn?.started ?? null,
373
+ line: turn === null ? null : runningLine(turn.parts),
374
+ question: openQuestion(liveOf(conversation).questions),
375
+ };
376
+ }
377
+ async function entries() {
378
+ const list = await heldList();
379
+ if (list === null)
380
+ return null;
381
+ const held = list.length === 0
382
+ ? []
383
+ : await deps.durably((local) => local.conversationRows(list.map(({ conversation }) => conversation)));
384
+ const rows = new Map(held.map((one) => [one.conversation, one]));
385
+ return list.map((entry) => ({
353
386
  conversation: entry.conversation,
354
387
  agent: { identity: entry.agent, name: entry.agentName },
355
388
  name: entry.name,
@@ -357,70 +390,153 @@ export function createConversationStore(deps) {
357
390
  finished: entry.finished,
358
391
  outcome: entry.outcome,
359
392
  waiting: entry.waiting,
360
- state: stateOf(entry.conversation),
361
- started: lives.get(entry.conversation)?.turn?.started ?? null,
393
+ ...rowOf(entry.conversation),
394
+ ...heldFacts(rows.get(entry.conversation), entry),
362
395
  }));
363
396
  }
364
- function keepList() {
365
- if (!listKnown || listKept)
366
- return;
367
- listKept = true;
368
- queueMicrotask(() => {
369
- listKept = false;
370
- if (deps.closed())
397
+ async function heldList() {
398
+ const stored = (await (await deps.local()).facts()).conversationList;
399
+ if (stored !== null)
400
+ return stored;
401
+ return !listKnown && deps.stream() !== undefined && deps.online() ? null : [];
402
+ }
403
+ async function endedIn(conversation) {
404
+ const list = await heldList();
405
+ return list?.find((entry) => entry.conversation === conversation)?.processEnded ?? null;
406
+ }
407
+ function listChanged(change) {
408
+ void deps
409
+ .durably(async (local) => {
410
+ const { conversationList, conversationCursor } = await local.facts();
411
+ const kept = change.replacing ? [] : (conversationList ?? []);
412
+ const held = new Map(kept.map((entry) => [entry.conversation, entry]));
413
+ for (const entry of change.set) {
414
+ held.set(entry.conversation, { ...entry });
415
+ creations.delete(entry.conversation);
416
+ }
417
+ if (change.gone !== null)
418
+ held.delete(change.gone);
419
+ const list = [...held.values()];
420
+ if (JSON.stringify(list) !== JSON.stringify(conversationList)) {
421
+ await local.fact('conversationList', list);
422
+ }
423
+ if (change.cursor !== undefined) {
424
+ if (change.cursor !== conversationCursor) {
425
+ await local.fact('conversationCursor', change.cursor);
426
+ }
427
+ deps.stream()?.hold(change.cursor);
428
+ }
429
+ return list;
430
+ })
431
+ .catch(async () => (await (await deps.local()).facts()).conversationList ?? [])
432
+ .then((list) => {
433
+ if (deps.closed() || change.paged === true)
371
434
  return;
372
- const entries = [...listing.values()];
373
- const cursor = listCursor;
374
- void deps
375
- .durably(async (local) => {
376
- await local.fact('conversationList', entries);
377
- await local.fact('conversationCursor', cursor);
378
- })
379
- .catch(() => { });
435
+ endedAnew(list);
436
+ if (change.cursor !== undefined) {
437
+ listKnown = true;
438
+ settleBehind(list);
439
+ openingEnded();
440
+ deps.settled();
441
+ }
442
+ emitList();
443
+ if (!listKnown)
444
+ return;
445
+ retiredDrafts(list);
446
+ if (change.cursor === undefined &&
447
+ (list.length !== replicated.size ||
448
+ list.some(({ conversation }) => !replicated.has(conversation)))) {
449
+ deps.again();
450
+ }
451
+ }, () => { });
452
+ }
453
+ function endedAnew(list) {
454
+ const entries = new Map(list.map((entry) => [entry.conversation, entry]));
455
+ for (const view of views.values()) {
456
+ const ended = entries.get(view.conversation)?.processEnded ?? null;
457
+ if (view.window === null || ended === view.ended)
458
+ continue;
459
+ view.ended = ended;
460
+ emit(view.conversation);
461
+ }
462
+ }
463
+ let listShown = Promise.resolve();
464
+ function shownList(work) {
465
+ const reading = heldList();
466
+ listShown = listShown.then(async () => {
467
+ const list = await reading.catch(() => null);
468
+ if (list === null || deps.closed())
469
+ return;
470
+ work(list);
380
471
  });
381
472
  }
382
- function listed(entry) {
383
- listing.set(entry.conversation, { ...entry });
384
- creations.delete(entry.conversation);
473
+ async function told() {
474
+ try {
475
+ while (listOwed && !deps.closed()) {
476
+ listOwed = false;
477
+ const value = await untilAnswered(entries, closing.signal);
478
+ if (deps.closed())
479
+ return;
480
+ if (value === null)
481
+ continue;
482
+ for (const watcher of listWatchers)
483
+ watcher(value);
484
+ for (const watcher of [...unlisted]) {
485
+ unlisted.delete(watcher);
486
+ listWatchers.add(watcher);
487
+ watcher(value);
488
+ }
489
+ }
490
+ }
491
+ finally {
492
+ listTelling = false;
493
+ }
494
+ }
495
+ function tellList() {
496
+ if (deps.closed() || (listWatchers.size === 0 && unlisted.size === 0))
497
+ return;
498
+ listOwed = true;
499
+ if (listTelling)
500
+ return;
501
+ listTelling = true;
502
+ void told().catch(() => { });
503
+ }
504
+ function tellSoon() {
505
+ if (rowTimer !== null)
506
+ return;
507
+ rowTimer = setTimeout(() => {
508
+ rowTimer = null;
509
+ tellList();
510
+ }, ROW_INTERVAL_MS);
385
511
  }
386
512
  function emitList() {
387
513
  if (deps.closed())
388
514
  return;
389
- if (listWatchers.size > 0) {
390
- const value = entries();
391
- for (const watcher of listWatchers)
392
- watcher(value);
393
- }
515
+ tellList();
394
516
  emitAgents();
395
517
  learned();
396
518
  }
397
519
  function learned() {
398
520
  if (deps.closed())
399
521
  return;
400
- if (!listKnown && !listHeld && deps.stream() !== undefined && deps.online())
522
+ if (unlisted.size > 0)
523
+ tellList();
524
+ if (unnamed.size === 0)
401
525
  return;
402
- if (unlisted.size > 0) {
403
- const value = entries();
404
- for (const watcher of [...unlisted]) {
405
- unlisted.delete(watcher);
406
- listWatchers.add(watcher);
407
- watcher(value);
408
- }
409
- }
410
- if (unnamed.size > 0) {
411
- const value = agents();
526
+ shownList((list) => {
527
+ const value = agents(list);
412
528
  for (const watcher of [...unnamed]) {
413
529
  unnamed.delete(watcher);
414
530
  agentWatchers.add(watcher);
415
531
  watcher(value);
416
532
  }
417
- }
533
+ });
418
534
  }
419
- function agents() {
535
+ function agents(list) {
420
536
  const named = new Map();
421
537
  for (const agent of listedAgents ?? [])
422
538
  named.set(agent.agent, agent);
423
- for (const entry of listing.values()) {
539
+ for (const entry of list) {
424
540
  named.set(entry.agent, { name: entry.agentName, retired: entry.agentRetired });
425
541
  }
426
542
  return [...named]
@@ -450,31 +566,33 @@ export function createConversationStore(deps) {
450
566
  function emitAgents() {
451
567
  if (deps.closed() || agentWatchers.size === 0)
452
568
  return;
453
- const value = agents();
454
- const shown = JSON.stringify(value);
455
- if (shown === agentsShown)
456
- return;
457
- agentsShown = shown;
458
- for (const watcher of agentWatchers)
459
- watcher(value);
569
+ shownList((list) => {
570
+ const value = agents(list);
571
+ const shown = JSON.stringify(value);
572
+ if (shown === agentsShown)
573
+ return;
574
+ agentsShown = shown;
575
+ for (const watcher of agentWatchers)
576
+ watcher(value);
577
+ });
460
578
  }
461
579
  function setLive(conversation, next) {
462
- const before = stateOf(conversation);
463
- const began = lives.get(conversation)?.turn?.started;
580
+ const before = rowOf(conversation);
464
581
  if (next === null)
465
582
  lives.delete(conversation);
466
583
  else
467
584
  lives.set(conversation, next);
468
- if (!listing.has(conversation))
469
- return;
470
- if (stateOf(conversation) !== before || next?.turn?.started !== began)
585
+ const after = rowOf(conversation);
586
+ if (after.state !== before.state || after.started !== before.started)
471
587
  emitList();
588
+ else if (after.line !== before.line || after.question !== before.question)
589
+ tellSoon();
472
590
  }
473
- function retiredChat(conversation) {
591
+ function retiredChat(conversation, list) {
474
592
  if (!conversation.startsWith(NEW_CHAT))
475
593
  return false;
476
594
  const agent = conversation.slice(NEW_CHAT.length);
477
- return [...listing.values()].some((entry) => entry.agent === agent && entry.agentRetired);
595
+ return list.some((entry) => entry.agent === agent && entry.agentRetired);
478
596
  }
479
597
  function live(conversation, change) {
480
598
  const owed = unsent.get(conversation);
@@ -532,17 +650,23 @@ export function createConversationStore(deps) {
532
650
  }
533
651
  async function remove(leaving) {
534
652
  await deps.durably(async (local) => {
653
+ const kept = new Set([
654
+ ...(await local.conversations(leaving)).map(({ conversation }) => conversation),
655
+ ...(await local.chatDrafts())
656
+ .filter(({ conversation }) => leaving.includes(conversation))
657
+ .map(({ conversation }) => conversation),
658
+ ]);
535
659
  await local.dropConversations(leaving);
536
660
  const changed = [];
537
661
  for (const conversation of leaving) {
538
- if (stored.has(conversation) || drafts.has(conversation) || lives.has(conversation)) {
662
+ if (kept.has(conversation) || lives.has(conversation))
539
663
  changed.push(conversation);
664
+ for (const view of views.values()) {
665
+ if (view.conversation === conversation)
666
+ view.draft = null;
540
667
  }
541
- stored.delete(conversation);
542
- reads.delete(conversation);
543
- partials.delete(conversation);
544
- drafts.delete(conversation);
545
668
  behind.delete(conversation);
669
+ heardNewest.delete(conversation);
546
670
  unsent.delete(conversation);
547
671
  setLive(conversation, null);
548
672
  await slide(local, conversation, null);
@@ -551,19 +675,68 @@ export function createConversationStore(deps) {
551
675
  deps.publish(conversationChangeSet(changed));
552
676
  });
553
677
  }
554
- function missed(conversation) {
555
- const read = reads.get(conversation);
556
- const newest = listing.get(conversation)?.newest;
678
+ function newestOfListed(entry) {
679
+ if (entry === undefined)
680
+ return undefined;
681
+ const heard = heardNewest.get(entry.conversation);
682
+ if (heard === undefined)
683
+ return entry.newest;
684
+ return entry.newest === null || heard > entry.newest ? heard : entry.newest;
685
+ }
686
+ function missed(conversation, held, entry) {
687
+ const read = held?.read;
688
+ const newest = newestOfListed(entry);
557
689
  return (read === undefined ||
558
690
  newest === undefined ||
559
- partials.has(conversation) ||
691
+ held?.partial === true ||
560
692
  (newest !== null && (read === null || newest > read)));
561
693
  }
562
- function noted(conversation, message) {
563
- const entry = listing.get(conversation);
564
- if (entry === undefined || (entry.newest !== null && entry.newest >= message.position))
694
+ async function readsOf(local, conversations) {
695
+ return new Map((await local.conversations(conversations)).map((held) => [held.conversation, held]));
696
+ }
697
+ function retiredDrafts(list) {
698
+ if (!list.some((entry) => entry.agentRetired))
565
699
  return;
566
- listing.set(conversation, { ...entry, newest: message.position });
700
+ void deps
701
+ .local()
702
+ .then((local) => local.chatDrafts())
703
+ .then((held) => {
704
+ if (!deps.closed() && held.some(({ conversation }) => retiredChat(conversation, list))) {
705
+ deps.again();
706
+ }
707
+ }, () => { });
708
+ }
709
+ function settleBehind(list) {
710
+ void deps
711
+ .durably(async (local) => {
712
+ const entries = new Map(list.map((entry) => [entry.conversation, entry]));
713
+ const named = [...replicated].filter((conversation) => entries.has(conversation));
714
+ const held = await readsOf(local, named);
715
+ let owed = false;
716
+ for (const conversation of named) {
717
+ if (!missed(conversation, held.get(conversation), entries.get(conversation))) {
718
+ behind.delete(conversation);
719
+ }
720
+ else if (!behind.has(conversation)) {
721
+ gap(conversation);
722
+ owed = true;
723
+ }
724
+ }
725
+ return owed;
726
+ })
727
+ .then((owed) => {
728
+ if (deps.closed())
729
+ return;
730
+ deps.settled();
731
+ if (owed)
732
+ deps.again();
733
+ }, () => { });
734
+ }
735
+ function noted(conversation, message) {
736
+ const heard = heardNewest.get(conversation);
737
+ if (heard === undefined || message.position > heard) {
738
+ heardNewest.set(conversation, message.position);
739
+ }
567
740
  }
568
741
  function gap(conversation) {
569
742
  gaps.set(conversation, (gaps.get(conversation) ?? 0) + 1);
@@ -573,7 +746,7 @@ export function createConversationStore(deps) {
573
746
  return deps.durably(async (local) => {
574
747
  if (!replicated.has(conversation))
575
748
  return;
576
- const read = reads.get(conversation);
749
+ const read = (await readsOf(local, [conversation])).get(conversation)?.read;
577
750
  const advances = read !== undefined &&
578
751
  !behind.has(conversation) &&
579
752
  (read === null || message.position > read);
@@ -583,19 +756,18 @@ export function createConversationStore(deps) {
583
756
  messages: [message],
584
757
  ...(advances ? { read: { at: message.position, partial: false } } : {}),
585
758
  });
586
- stored.add(conversation);
587
- if (advances)
588
- reads.set(conversation, message.position);
589
759
  sealTurns(conversation, new Set([message.turn]));
590
760
  await slide(local, conversation, [message]);
591
761
  deps.publish(conversationChangeSet([conversation]));
762
+ tellList();
592
763
  });
593
764
  }
594
765
  async function readHistories(conversations, ended) {
595
766
  const generations = new Map(conversations.map((one) => [one, gaps.get(one) ?? 0]));
767
+ const held = await readsOf(await deps.local(), conversations);
596
768
  let waiting = conversations.map((conversation) => ({
597
769
  conversation,
598
- after: reads.get(conversation) ?? undefined,
770
+ after: held.get(conversation)?.read ?? undefined,
599
771
  }));
600
772
  while (waiting.length > 0) {
601
773
  const asking = waiting.slice(0, HISTORY_CONVERSATIONS);
@@ -651,15 +823,10 @@ export function createConversationStore(deps) {
651
823
  return;
652
824
  const whole = next === undefined;
653
825
  const cursor = next ?? newestOf(arriving, from);
654
- const partial = !whole && (partials.has(conversation) || !reads.has(conversation));
826
+ const held = (await readsOf(local, [conversation])).get(conversation);
827
+ const partial = !whole && (held?.partial === true || held?.read === undefined);
655
828
  const sealing = await sealed(local, conversation, new Set(arriving.map((message) => message.turn)));
656
829
  await local.keepMessages({ conversation, messages: arriving, read: { at: cursor, partial } });
657
- stored.add(conversation);
658
- reads.set(conversation, cursor);
659
- if (partial)
660
- partials.add(conversation);
661
- else
662
- partials.delete(conversation);
663
830
  sealTurns(conversation, new Set(arriving.map((message) => message.turn)));
664
831
  if (whole && (gaps.get(conversation) ?? 0) === generation)
665
832
  behind.delete(conversation);
@@ -667,6 +834,8 @@ export function createConversationStore(deps) {
667
834
  if (arriving.length > 0 || sealing || whole) {
668
835
  deps.publish(conversationChangeSet([conversation]));
669
836
  }
837
+ if (arriving.length > 0)
838
+ tellSoon();
670
839
  });
671
840
  }
672
841
  function heard(event) {
@@ -733,17 +902,24 @@ export function createConversationStore(deps) {
733
902
  emit(conversation);
734
903
  }
735
904
  async function kept(local, conversation, following) {
736
- if (following === null || empty(following)) {
905
+ const shown = following === null || empty(following) ? null : following;
906
+ if (shown === null)
737
907
  await local.dropChatDraft(conversation);
738
- drafts.delete(conversation);
739
- }
740
- else {
741
- await local.keepChatDrafts([following]);
742
- drafts.set(conversation, following);
908
+ else
909
+ await local.keepChatDrafts([shown]);
910
+ shownDraft(conversation, shown);
911
+ }
912
+ function shownDraft(conversation, draft) {
913
+ for (const view of views.values()) {
914
+ if (view.conversation === conversation)
915
+ view.draft = draft;
743
916
  }
744
917
  }
918
+ async function draftAt(local, conversation) {
919
+ return (await local.chatDraft(conversation)) ?? undefined;
920
+ }
745
921
  async function sealed(local, conversation, turns) {
746
- const held = drafts.get(conversation);
922
+ const held = await draftAt(local, conversation);
747
923
  const submitted = held?.submitted ?? [];
748
924
  const left = submitted.filter((one) => one.turn === null || !turns.has(one.turn));
749
925
  if (held === undefined || left.length === submitted.length)
@@ -754,8 +930,8 @@ export function createConversationStore(deps) {
754
930
  function moved(agent, operation, to) {
755
931
  const from = newChat(agent);
756
932
  return deps.durably(async (local) => {
757
- const held = drafts.get(from);
758
- const earlier = (held?.moves ?? []).filter((one) => !deps.expired(one));
933
+ const held = await draftAt(local, from);
934
+ const earlier = held?.moves ?? [];
759
935
  if (earlier.includes(operation))
760
936
  return;
761
937
  const blank = {
@@ -764,18 +940,20 @@ export function createConversationStore(deps) {
764
940
  revision: chatDraftRevision(),
765
941
  };
766
942
  const { moves: _answered, ...unsent } = held ?? blank;
767
- const taken = drafts.has(to) || empty(unsent) ? [] : [{ ...unsent, conversation: to }];
943
+ const taken = (await draftAt(local, to)) !== undefined || empty(unsent)
944
+ ? []
945
+ : [{ ...unsent, conversation: to }];
768
946
  const left = { ...(taken.length === 0 ? unsent : blank), moves: [...earlier, operation] };
769
947
  await local.keepChatDrafts([left, ...taken]);
770
- drafts.set(from, left);
948
+ shownDraft(from, left);
771
949
  for (const one of taken)
772
- drafts.set(to, one);
950
+ shownDraft(to, one);
773
951
  deps.publish(conversationChangeSet([from, to]));
774
952
  });
775
953
  }
776
954
  function withDraft(conversation, next) {
777
955
  return deps.durably(async (local) => {
778
- const following = await next(drafts.get(conversation), local);
956
+ const following = await next(await draftAt(local, conversation), local);
779
957
  if (following === undefined)
780
958
  return;
781
959
  await kept(local, conversation, following);
@@ -784,10 +962,8 @@ export function createConversationStore(deps) {
784
962
  }
785
963
  async function submitted(conversation, operation, draft) {
786
964
  let submission;
787
- let earlier = false;
788
965
  await withDraft(conversation, (current) => {
789
966
  submission = current?.submitted?.find((one) => one.operation === operation);
790
- earlier = submission !== undefined;
791
967
  if (submission !== undefined)
792
968
  return undefined;
793
969
  const said = draft ?? current;
@@ -800,15 +976,7 @@ export function createConversationStore(deps) {
800
976
  submitted: [...(current?.submitted ?? []), submission],
801
977
  };
802
978
  });
803
- return { submission: submission, earlier };
804
- }
805
- function forgotten(conversation, submission) {
806
- return withDraft(conversation, (current) => {
807
- const held = current?.submitted ?? [];
808
- if (current === undefined || !held.includes(submission))
809
- return undefined;
810
- return withSubmitted(current, held.filter((one) => one !== submission));
811
- });
979
+ return submission;
812
980
  }
813
981
  async function dispatched(conversation, submission) {
814
982
  const dispatch = { answered: false };
@@ -861,11 +1029,14 @@ export function createConversationStore(deps) {
861
1029
  });
862
1030
  },
863
1031
  async read(conversation, options = {}) {
864
- const asked = askedOf(options);
1032
+ const { text, ...windowed } = options;
1033
+ const asked = askedOf(windowed);
1034
+ const phrase = phraseOf(text);
865
1035
  return deps.durably(async (local) => {
866
- const { messages, more } = await local.messageWindow(conversation, asked.limit, asked.before);
867
- const total = asked.total ? await local.messageCount(conversation) : null;
868
- return structuredClone(viewOf(conversation, messages, frameOf(asked, messages, more, total)));
1036
+ const { messages, more } = await local.messageWindow(conversation, asked.limit, asked.before, phrase);
1037
+ const total = asked.total ? await local.messageCount(conversation, phrase) : null;
1038
+ const marked = await local.jobMarkers(conversation);
1039
+ return structuredClone(viewOf(conversation, messages, frameOf(asked, messages, more, total), (await local.chatDraft(conversation)) ?? null, await endedIn(conversation), marked));
869
1040
  });
870
1041
  },
871
1042
  async create(input) {
@@ -873,8 +1044,10 @@ export function createConversationStore(deps) {
873
1044
  const conversation = answered(result, 'conversation_id');
874
1045
  if (conversation === null)
875
1046
  return result;
876
- if (!listing.has(conversation))
1047
+ const listed = (await heldList()) ?? [];
1048
+ if (!listed.some((entry) => entry.conversation === conversation)) {
877
1049
  creations.add(conversation);
1050
+ }
878
1051
  const creation = creationOf(input);
879
1052
  if (creation !== null)
880
1053
  await moved(creation.agent, creation.operation, conversation);
@@ -891,11 +1064,7 @@ export function createConversationStore(deps) {
891
1064
  });
892
1065
  if (held === null)
893
1066
  return DURABLE_WRITE;
894
- const { submission, earlier } = held;
895
- if (!earlier || !deps.expired(operation))
896
- return dispatched(conversation, submission);
897
- await forgotten(conversation, submission);
898
- return EXPIRED;
1067
+ return dispatched(conversation, held);
899
1068
  }
900
1069
  if (draft !== undefined) {
901
1070
  await withDraft(conversation, (current) => current?.submitted?.some((one) => one.operation === operation) === true
@@ -946,43 +1115,40 @@ export function createConversationStore(deps) {
946
1115
  return {
947
1116
  surface,
948
1117
  chatDraft,
949
- hydrate(held, kept, list, cursor) {
950
- if (list !== null && !listKnown) {
951
- listing.clear();
952
- for (const entry of list)
953
- listing.set(entry.conversation, { ...entry });
954
- listHeld = true;
955
- listCursor = cursor;
956
- deps.stream()?.hold(cursor);
957
- }
958
- stored.clear();
959
- reads.clear();
960
- partials.clear();
961
- for (const entry of held) {
962
- stored.add(entry.conversation);
963
- if (entry.read !== undefined)
964
- reads.set(entry.conversation, entry.read);
965
- if (entry.partial)
966
- partials.add(entry.conversation);
967
- }
968
- drafts.clear();
969
- for (const record of kept)
970
- drafts.set(record.conversation, record);
971
- if (listHeld)
972
- learned();
1118
+ async opened(local) {
1119
+ const { conversationList, conversationCursor } = await local.facts();
1120
+ if (conversationList === null || listKnown)
1121
+ return;
1122
+ deps.stream()?.hold(conversationCursor);
1123
+ learned();
973
1124
  },
974
1125
  async reconcile() {
975
1126
  if (!listKnown)
976
1127
  return;
977
- const named = new Set(listing.keys());
1128
+ const local = await deps.local();
1129
+ const [held, kept, { conversationList }] = await Promise.all([
1130
+ local.conversations(),
1131
+ local.chatDrafts(),
1132
+ local.facts(),
1133
+ ]);
1134
+ const list = conversationList ?? [];
1135
+ const entries = new Map(list.map((entry) => [entry.conversation, entry]));
1136
+ const named = new Set(entries.keys());
1137
+ const reads = new Map(held.map((one) => [one.conversation, one]));
978
1138
  const leaving = [
979
- ...new Set([...replicated, ...stored, ...reads.keys(), ...drafts.keys()]),
1139
+ ...new Set([
1140
+ ...replicated,
1141
+ ...reads.keys(),
1142
+ ...kept.map(({ conversation }) => conversation),
1143
+ ]),
980
1144
  ].filter((conversation) => !named.has(conversation) &&
981
1145
  !creations.has(conversation) &&
982
- (!conversation.startsWith(NEW_CHAT) || retiredChat(conversation)));
1146
+ (!conversation.startsWith(NEW_CHAT) || retiredChat(conversation, list)));
983
1147
  for (const conversation of named) {
984
- if (!replicated.has(conversation) && missed(conversation))
1148
+ if (!replicated.has(conversation) &&
1149
+ missed(conversation, reads.get(conversation), entries.get(conversation))) {
985
1150
  behind.add(conversation);
1151
+ }
986
1152
  }
987
1153
  replicated = named;
988
1154
  handReplicated();
@@ -1013,16 +1179,16 @@ export function createConversationStore(deps) {
1013
1179
  progress.complete = false;
1014
1180
  },
1015
1181
  async resend(progress) {
1016
- for (const [conversation, held] of [...drafts]) {
1017
- for (const submission of held.submitted ?? []) {
1018
- if (submission.turn !== null || sending.has(submission.operation))
1019
- continue;
1182
+ const local = await deps.local();
1183
+ const tried = new Set();
1184
+ for (const { conversation } of await local.chatDrafts()) {
1185
+ for (;;) {
1186
+ const submission = (await local.chatDraft(conversation))?.submitted?.find((one) => one.turn === null && !sending.has(one.operation) && !tried.has(one.operation));
1187
+ if (submission === undefined)
1188
+ break;
1020
1189
  if (deps.closed())
1021
1190
  return;
1022
- if (deps.expired(submission.operation)) {
1023
- await forgotten(conversation, submission);
1024
- continue;
1025
- }
1191
+ tried.add(submission.operation);
1026
1192
  const result = await dispatched(conversation, submission);
1027
1193
  if (result.kind === 'revoked-authorization')
1028
1194
  return;
@@ -1060,6 +1226,9 @@ export function createConversationStore(deps) {
1060
1226
  asked,
1061
1227
  watchers: new Set(),
1062
1228
  window: null,
1229
+ draft: null,
1230
+ ended: null,
1231
+ marked: [],
1063
1232
  more: false,
1064
1233
  total: null,
1065
1234
  };
@@ -1071,7 +1240,10 @@ export function createConversationStore(deps) {
1071
1240
  if (view.window !== null)
1072
1241
  return;
1073
1242
  const { messages, more } = await local.messageWindow(conversation, asked.limit, asked.before);
1243
+ view.draft = (await local.chatDraft(conversation)) ?? null;
1244
+ view.ended = await endedIn(conversation);
1074
1245
  view.window = messages;
1246
+ view.marked = await local.jobMarkers(conversation);
1075
1247
  view.more = more;
1076
1248
  view.total = asked.total ? await local.messageCount(conversation) : null;
1077
1249
  }), reading.signal)
@@ -1080,7 +1252,7 @@ export function createConversationStore(deps) {
1080
1252
  return;
1081
1253
  watcher.ready = true;
1082
1254
  const window = view.window ?? [];
1083
- watcher.notify(viewOf(conversation, window, sharedFrame(view, window)));
1255
+ watcher.notify(viewOf(conversation, window, sharedFrame(view, window), view.draft, view.ended, view.marked));
1084
1256
  })
1085
1257
  .catch(() => { });
1086
1258
  return () => {
@@ -1115,64 +1287,43 @@ export function createConversationStore(deps) {
1115
1287
  },
1116
1288
  receive(event) {
1117
1289
  if (deps.closed())
1118
- return false;
1119
- if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted') {
1120
- if (event.kind === 'list') {
1121
- if (event.whole)
1122
- relisting ??= new Map();
1123
- for (const entry of event.entries) {
1124
- if (relisting === null)
1125
- listed(entry);
1126
- else
1127
- relisting.set(entry.conversation, { ...entry });
1128
- }
1129
- if (event.more)
1130
- return false;
1131
- if (relisting !== null) {
1132
- listing.clear();
1133
- for (const entry of relisting.values())
1134
- listed(entry);
1135
- relisting = null;
1136
- }
1137
- listCursor = event.cursor;
1138
- deps.stream()?.hold(event.cursor);
1139
- listKnown = true;
1140
- for (const conversation of replicated) {
1141
- if (!listing.has(conversation))
1142
- continue;
1143
- if (!missed(conversation))
1144
- behind.delete(conversation);
1145
- else if (!behind.has(conversation))
1146
- gap(conversation);
1147
- }
1148
- openingEnded();
1149
- }
1150
- else if (event.kind === 'listed') {
1151
- listed(event.entry);
1152
- relisting?.set(event.entry.conversation, { ...event.entry });
1153
- }
1154
- else {
1155
- listing.delete(event.conversation);
1156
- relisting?.delete(event.conversation);
1290
+ return;
1291
+ if (event.kind === 'list') {
1292
+ if (event.whole)
1293
+ relisting ??= new Map();
1294
+ if (relisting !== null) {
1295
+ for (const entry of event.entries)
1296
+ relisting.set(entry.conversation, { ...entry });
1157
1297
  }
1158
- keepList();
1159
- emitList();
1160
- return ((event.kind !== 'list' &&
1161
- listKnown &&
1162
- (listing.size !== replicated.size ||
1163
- [...listing.keys()].some((conversation) => !replicated.has(conversation)))) ||
1164
- (listKnown && [...drafts.keys()].some(retiredChat)));
1298
+ if (event.more && relisting !== null)
1299
+ return;
1300
+ const whole = relisting;
1301
+ relisting = null;
1302
+ listChanged({
1303
+ replacing: whole !== null,
1304
+ set: whole === null ? event.entries : [...whole.values()],
1305
+ gone: null,
1306
+ ...(event.more ? { paged: true } : { cursor: event.cursor }),
1307
+ });
1308
+ return;
1309
+ }
1310
+ if (event.kind === 'listed') {
1311
+ relisting?.set(event.entry.conversation, { ...event.entry });
1312
+ listChanged({ replacing: false, set: [event.entry], gone: null });
1313
+ return;
1314
+ }
1315
+ if (event.kind === 'unlisted') {
1316
+ relisting?.delete(event.conversation);
1317
+ listChanged({ replacing: false, set: [], gone: event.conversation });
1318
+ return;
1165
1319
  }
1166
1320
  heard(event);
1167
- return false;
1168
- },
1169
- searchable() {
1170
- return [...(listKnown ? listing.keys() : stored)].sort();
1171
- },
1172
- acquired(conversation) {
1173
- return reads.has(conversation) && !partials.has(conversation);
1174
1321
  },
1175
1322
  close() {
1323
+ closing.abort();
1324
+ if (rowTimer !== null)
1325
+ clearTimeout(rowTimer);
1326
+ rowTimer = null;
1176
1327
  views.clear();
1177
1328
  lives.clear();
1178
1329
  unsent.clear();