@agentshouse/mdruntime 0.1.0-c09bf842921ff → 0.1.0-c0b4d0492dec3

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 (108) hide show
  1. package/README.md +1 -1
  2. package/dist/adapter.d.ts +15 -16
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/bootstrap.d.ts +8 -4
  5. package/dist/bootstrap.d.ts.map +1 -1
  6. package/dist/bootstrap.js +206 -76
  7. package/dist/change.d.ts +1 -0
  8. package/dist/change.d.ts.map +1 -1
  9. package/dist/chat-types.d.ts +3 -0
  10. package/dist/chat-types.d.ts.map +1 -0
  11. package/dist/chat-types.js +2 -0
  12. package/dist/client.d.ts +5 -0
  13. package/dist/client.d.ts.map +1 -1
  14. package/dist/client.js +1 -0
  15. package/dist/compile.d.ts +5 -0
  16. package/dist/compile.d.ts.map +1 -1
  17. package/dist/compile.js +186 -55
  18. package/dist/connection.d.ts +8 -2
  19. package/dist/connection.d.ts.map +1 -1
  20. package/dist/connection.js +1386 -1040
  21. package/dist/conversation-row.d.ts +33 -0
  22. package/dist/conversation-row.d.ts.map +1 -0
  23. package/dist/conversation-row.js +47 -0
  24. package/dist/conversation.d.ts +13 -5
  25. package/dist/conversation.d.ts.map +1 -1
  26. package/dist/conversation.js +354 -192
  27. package/dist/entry.d.ts.map +1 -1
  28. package/dist/entry.js +1 -0
  29. package/dist/figures.d.ts +48 -0
  30. package/dist/figures.d.ts.map +1 -0
  31. package/dist/figures.js +197 -0
  32. package/dist/host.d.ts.map +1 -1
  33. package/dist/host.js +3 -3
  34. package/dist/index.d.ts +9 -4
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +3 -1
  37. package/dist/job.d.ts +20 -1
  38. package/dist/job.d.ts.map +1 -1
  39. package/dist/job.js +26 -0
  40. package/dist/live.d.ts +4 -4
  41. package/dist/live.d.ts.map +1 -1
  42. package/dist/live.js +53 -15
  43. package/dist/memory.d.ts +1 -0
  44. package/dist/memory.d.ts.map +1 -1
  45. package/dist/memory.js +11 -3
  46. package/dist/messages.d.ts +25 -1
  47. package/dist/messages.d.ts.map +1 -1
  48. package/dist/messages.js +82 -3
  49. package/dist/model.d.ts +0 -2
  50. package/dist/model.d.ts.map +1 -1
  51. package/dist/model.js +0 -6
  52. package/dist/outcome.d.ts +1 -0
  53. package/dist/outcome.d.ts.map +1 -1
  54. package/dist/outcome.js +3 -0
  55. package/dist/overlay.d.ts +1 -0
  56. package/dist/overlay.d.ts.map +1 -1
  57. package/dist/overlay.js +11 -0
  58. package/dist/parts.d.ts +59 -0
  59. package/dist/parts.d.ts.map +1 -0
  60. package/dist/parts.js +118 -0
  61. package/dist/query.d.ts +10 -0
  62. package/dist/query.d.ts.map +1 -1
  63. package/dist/questions.d.ts +32 -0
  64. package/dist/questions.d.ts.map +1 -0
  65. package/dist/questions.js +118 -0
  66. package/dist/reach.d.ts +5 -5
  67. package/dist/reach.d.ts.map +1 -1
  68. package/dist/reach.js +156 -99
  69. package/dist/read.d.ts +7 -0
  70. package/dist/read.d.ts.map +1 -0
  71. package/dist/read.js +17 -0
  72. package/dist/reads.d.ts +35 -0
  73. package/dist/reads.d.ts.map +1 -0
  74. package/dist/reads.js +128 -0
  75. package/dist/recovery.d.ts.map +1 -1
  76. package/dist/recovery.js +9 -1
  77. package/dist/refresh.d.ts +1 -1
  78. package/dist/refresh.d.ts.map +1 -1
  79. package/dist/refresh.js +4 -0
  80. package/dist/replica.d.ts.map +1 -1
  81. package/dist/replica.js +23 -6
  82. package/dist/save.d.ts.map +1 -1
  83. package/dist/save.js +10 -7
  84. package/dist/source.d.ts +61 -0
  85. package/dist/source.d.ts.map +1 -0
  86. package/dist/source.js +81 -0
  87. package/dist/status.d.ts +0 -2
  88. package/dist/status.d.ts.map +1 -1
  89. package/dist/status.js +0 -6
  90. package/dist/store.d.ts +85 -25
  91. package/dist/store.d.ts.map +1 -1
  92. package/dist/store.js +136 -98
  93. package/dist/stored.d.ts +55 -28
  94. package/dist/stored.d.ts.map +1 -1
  95. package/dist/stored.js +187 -74
  96. package/dist/tables.d.ts +99 -19
  97. package/dist/tables.d.ts.map +1 -1
  98. package/dist/tables.js +171 -34
  99. package/dist/tallies.d.ts +18 -0
  100. package/dist/tallies.d.ts.map +1 -0
  101. package/dist/tallies.js +129 -0
  102. package/dist/wire.d.ts +6 -0
  103. package/dist/wire.d.ts.map +1 -1
  104. package/dist/wire.js +3 -0
  105. package/package.json +2 -2
  106. package/dist/fence.d.ts +0 -6
  107. package/dist/fence.d.ts.map +0 -1
  108. package/dist/fence.js +0 -10
@@ -1,9 +1,12 @@
1
1
  import { cursorOf, decodedCursor } from '@agentshouse/mdmodel';
2
2
  import { conversationChangeSet } from './change-set.js';
3
+ import { heldFacts } from './conversation-row.js';
3
4
  import { runtimeError } from './failure.js';
4
5
  import { refusedForSpace } from './durability.js';
5
6
  import { changeBatchIdentity, chatDraftRevision } from './identity.js';
6
7
  import { OFFLINE } from './outcome.js';
8
+ import { jobsOf, runningLine } from './parts.js';
9
+ import { openQuestion } from './questions.js';
7
10
  import { untilAnswered } from './recovery.js';
8
11
  import { capped, FROM_START, messageOrder, precedes } from './windows.js';
9
12
  const QUIET = {
@@ -42,11 +45,14 @@ export function unheldView(conversation) {
42
45
  link: null,
43
46
  deliveryUnknown: [],
44
47
  chatDraft: null,
48
+ jobs: [],
45
49
  };
46
50
  }
47
51
  export const SESSION_STARTED = 'Session started';
48
52
  const NEW_CHAT = 'new:';
49
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;
50
56
  export function newChat(agent) {
51
57
  return `${NEW_CHAT}${agent}`;
52
58
  }
@@ -254,10 +260,6 @@ function returned(draft, submission, others) {
254
260
  }, others);
255
261
  }
256
262
  export function createConversationStore(deps) {
257
- const stored = new Set();
258
- const reads = new Map();
259
- const partials = new Set();
260
- const drafts = new Map();
261
263
  const sending = new Map();
262
264
  const behind = new Set();
263
265
  const catching = new Map();
@@ -265,15 +267,16 @@ export function createConversationStore(deps) {
265
267
  const lives = new Map();
266
268
  const unsent = new Map();
267
269
  const views = new Map();
268
- const listing = new Map();
270
+ const heardNewest = new Map();
269
271
  let relisting = null;
270
- let listCursor = null;
271
272
  const creations = new Set();
272
273
  const listWatchers = new Set();
273
274
  const unlisted = new Set();
274
275
  let listKnown = false;
275
- let listHeld = false;
276
- let listKept = false;
276
+ let listOwed = false;
277
+ let listTelling = false;
278
+ let rowTimer = null;
279
+ const closing = new AbortController();
277
280
  const agentWatchers = new Set();
278
281
  const unnamed = new Set();
279
282
  let agentsShown = null;
@@ -284,8 +287,7 @@ export function createConversationStore(deps) {
284
287
  function liveOf(conversation) {
285
288
  return lives.get(conversation) ?? QUIET;
286
289
  }
287
- function viewOf(conversation, held, frame) {
288
- const draft = drafts.get(conversation);
290
+ function viewOf(conversation, held, frame, draft, ended, marked) {
289
291
  const facts = liveOf(conversation);
290
292
  return {
291
293
  conversation,
@@ -299,16 +301,17 @@ export function createConversationStore(deps) {
299
301
  turn: structuredClone(facts.turn),
300
302
  questions: structuredClone(facts.questions),
301
303
  process: facts.process === null ? null : { ...facts.process },
302
- processEnded: listing.get(conversation)?.processEnded ?? null,
304
+ processEnded: ended,
303
305
  commands: structuredClone(facts.commands),
304
306
  options: structuredClone(facts.options),
305
307
  starting: facts.starting,
306
308
  subagents: structuredClone(facts.subagents),
307
309
  link: facts.link === null ? null : { ...facts.link },
308
310
  deliveryUnknown: [...facts.deliveryUnknown],
309
- chatDraft: draft === undefined || unwritten(draft)
310
- ? null
311
- : { 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),
312
315
  };
313
316
  }
314
317
  function emit(conversation) {
@@ -320,7 +323,7 @@ export function createConversationStore(deps) {
320
323
  const ready = [...view.watchers].filter((watcher) => watcher.ready);
321
324
  if (ready.length === 0)
322
325
  continue;
323
- 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);
324
327
  for (const watcher of ready)
325
328
  watcher.notify(value);
326
329
  }
@@ -330,11 +333,13 @@ export function createConversationStore(deps) {
330
333
  }
331
334
  async function slide(local, conversation, arriving) {
332
335
  let count;
336
+ let marked;
333
337
  for (const view of views.values()) {
334
338
  if (view.conversation !== conversation || view.window === null)
335
339
  continue;
336
340
  if (arriving === null) {
337
341
  view.window = [];
342
+ view.marked = [];
338
343
  view.more = false;
339
344
  if (view.total !== null)
340
345
  view.total = 0;
@@ -344,6 +349,10 @@ export function createConversationStore(deps) {
344
349
  const next = windowOf(view.window, joining, view.asked.limit);
345
350
  view.window = next.messages;
346
351
  view.more ||= next.dropped;
352
+ if (arriving.length > 0) {
353
+ marked ??= await local.jobMarkers(conversation);
354
+ view.marked = marked;
355
+ }
347
356
  if (view.total !== null && arriving.length > 0) {
348
357
  count ??= await local.messageCount(conversation);
349
358
  view.total = count;
@@ -356,8 +365,24 @@ export function createConversationStore(deps) {
356
365
  return 'asking';
357
366
  return facts.turn === null ? 'idle' : 'working';
358
367
  }
359
- function entries() {
360
- 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) => ({
361
386
  conversation: entry.conversation,
362
387
  agent: { identity: entry.agent, name: entry.agentName },
363
388
  name: entry.name,
@@ -365,73 +390,153 @@ export function createConversationStore(deps) {
365
390
  finished: entry.finished,
366
391
  outcome: entry.outcome,
367
392
  waiting: entry.waiting,
368
- state: stateOf(entry.conversation),
369
- started: lives.get(entry.conversation)?.turn?.started ?? null,
393
+ ...rowOf(entry.conversation),
394
+ ...heldFacts(rows.get(entry.conversation), entry),
370
395
  }));
371
396
  }
372
- function keepList() {
373
- if (!listKnown || listKept)
374
- return;
375
- listKept = true;
376
- queueMicrotask(() => {
377
- listKept = false;
378
- 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)
379
434
  return;
380
- const entries = [...listing.values()];
381
- const cursor = listCursor;
382
- void deps
383
- .durably(async (local) => {
384
- await local.fact('conversationList', entries);
385
- await local.fact('conversationCursor', cursor);
386
- })
387
- .catch(() => { });
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);
388
471
  });
389
472
  }
390
- function listed(entry) {
391
- const ended = listing.get(entry.conversation)?.processEnded ?? null;
392
- listing.set(entry.conversation, { ...entry });
393
- creations.delete(entry.conversation);
394
- if (entry.processEnded !== ended)
395
- emit(entry.conversation);
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);
396
511
  }
397
512
  function emitList() {
398
513
  if (deps.closed())
399
514
  return;
400
- if (listWatchers.size > 0) {
401
- const value = entries();
402
- for (const watcher of listWatchers)
403
- watcher(value);
404
- }
515
+ tellList();
405
516
  emitAgents();
406
517
  learned();
407
518
  }
408
519
  function learned() {
409
520
  if (deps.closed())
410
521
  return;
411
- if (!listKnown && !listHeld && deps.stream() !== undefined && deps.online())
522
+ if (unlisted.size > 0)
523
+ tellList();
524
+ if (unnamed.size === 0)
412
525
  return;
413
- if (unlisted.size > 0) {
414
- const value = entries();
415
- for (const watcher of [...unlisted]) {
416
- unlisted.delete(watcher);
417
- listWatchers.add(watcher);
418
- watcher(value);
419
- }
420
- }
421
- if (unnamed.size > 0) {
422
- const value = agents();
526
+ shownList((list) => {
527
+ const value = agents(list);
423
528
  for (const watcher of [...unnamed]) {
424
529
  unnamed.delete(watcher);
425
530
  agentWatchers.add(watcher);
426
531
  watcher(value);
427
532
  }
428
- }
533
+ });
429
534
  }
430
- function agents() {
535
+ function agents(list) {
431
536
  const named = new Map();
432
537
  for (const agent of listedAgents ?? [])
433
538
  named.set(agent.agent, agent);
434
- for (const entry of listing.values()) {
539
+ for (const entry of list) {
435
540
  named.set(entry.agent, { name: entry.agentName, retired: entry.agentRetired });
436
541
  }
437
542
  return [...named]
@@ -461,31 +566,33 @@ export function createConversationStore(deps) {
461
566
  function emitAgents() {
462
567
  if (deps.closed() || agentWatchers.size === 0)
463
568
  return;
464
- const value = agents();
465
- const shown = JSON.stringify(value);
466
- if (shown === agentsShown)
467
- return;
468
- agentsShown = shown;
469
- for (const watcher of agentWatchers)
470
- 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
+ });
471
578
  }
472
579
  function setLive(conversation, next) {
473
- const before = stateOf(conversation);
474
- const began = lives.get(conversation)?.turn?.started;
580
+ const before = rowOf(conversation);
475
581
  if (next === null)
476
582
  lives.delete(conversation);
477
583
  else
478
584
  lives.set(conversation, next);
479
- if (!listing.has(conversation))
480
- return;
481
- if (stateOf(conversation) !== before || next?.turn?.started !== began)
585
+ const after = rowOf(conversation);
586
+ if (after.state !== before.state || after.started !== before.started)
482
587
  emitList();
588
+ else if (after.line !== before.line || after.question !== before.question)
589
+ tellSoon();
483
590
  }
484
- function retiredChat(conversation) {
591
+ function retiredChat(conversation, list) {
485
592
  if (!conversation.startsWith(NEW_CHAT))
486
593
  return false;
487
594
  const agent = conversation.slice(NEW_CHAT.length);
488
- return [...listing.values()].some((entry) => entry.agent === agent && entry.agentRetired);
595
+ return list.some((entry) => entry.agent === agent && entry.agentRetired);
489
596
  }
490
597
  function live(conversation, change) {
491
598
  const owed = unsent.get(conversation);
@@ -543,17 +650,23 @@ export function createConversationStore(deps) {
543
650
  }
544
651
  async function remove(leaving) {
545
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
+ ]);
546
659
  await local.dropConversations(leaving);
547
660
  const changed = [];
548
661
  for (const conversation of leaving) {
549
- if (stored.has(conversation) || drafts.has(conversation) || lives.has(conversation)) {
662
+ if (kept.has(conversation) || lives.has(conversation))
550
663
  changed.push(conversation);
664
+ for (const view of views.values()) {
665
+ if (view.conversation === conversation)
666
+ view.draft = null;
551
667
  }
552
- stored.delete(conversation);
553
- reads.delete(conversation);
554
- partials.delete(conversation);
555
- drafts.delete(conversation);
556
668
  behind.delete(conversation);
669
+ heardNewest.delete(conversation);
557
670
  unsent.delete(conversation);
558
671
  setLive(conversation, null);
559
672
  await slide(local, conversation, null);
@@ -562,19 +675,68 @@ export function createConversationStore(deps) {
562
675
  deps.publish(conversationChangeSet(changed));
563
676
  });
564
677
  }
565
- function missed(conversation) {
566
- const read = reads.get(conversation);
567
- 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);
568
689
  return (read === undefined ||
569
690
  newest === undefined ||
570
- partials.has(conversation) ||
691
+ held?.partial === true ||
571
692
  (newest !== null && (read === null || newest > read)));
572
693
  }
573
- function noted(conversation, message) {
574
- const entry = listing.get(conversation);
575
- 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))
576
699
  return;
577
- 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
+ }
578
740
  }
579
741
  function gap(conversation) {
580
742
  gaps.set(conversation, (gaps.get(conversation) ?? 0) + 1);
@@ -584,7 +746,7 @@ export function createConversationStore(deps) {
584
746
  return deps.durably(async (local) => {
585
747
  if (!replicated.has(conversation))
586
748
  return;
587
- const read = reads.get(conversation);
749
+ const read = (await readsOf(local, [conversation])).get(conversation)?.read;
588
750
  const advances = read !== undefined &&
589
751
  !behind.has(conversation) &&
590
752
  (read === null || message.position > read);
@@ -594,19 +756,18 @@ export function createConversationStore(deps) {
594
756
  messages: [message],
595
757
  ...(advances ? { read: { at: message.position, partial: false } } : {}),
596
758
  });
597
- stored.add(conversation);
598
- if (advances)
599
- reads.set(conversation, message.position);
600
759
  sealTurns(conversation, new Set([message.turn]));
601
760
  await slide(local, conversation, [message]);
602
761
  deps.publish(conversationChangeSet([conversation]));
762
+ tellList();
603
763
  });
604
764
  }
605
765
  async function readHistories(conversations, ended) {
606
766
  const generations = new Map(conversations.map((one) => [one, gaps.get(one) ?? 0]));
767
+ const held = await readsOf(await deps.local(), conversations);
607
768
  let waiting = conversations.map((conversation) => ({
608
769
  conversation,
609
- after: reads.get(conversation) ?? undefined,
770
+ after: held.get(conversation)?.read ?? undefined,
610
771
  }));
611
772
  while (waiting.length > 0) {
612
773
  const asking = waiting.slice(0, HISTORY_CONVERSATIONS);
@@ -662,15 +823,10 @@ export function createConversationStore(deps) {
662
823
  return;
663
824
  const whole = next === undefined;
664
825
  const cursor = next ?? newestOf(arriving, from);
665
- 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);
666
828
  const sealing = await sealed(local, conversation, new Set(arriving.map((message) => message.turn)));
667
829
  await local.keepMessages({ conversation, messages: arriving, read: { at: cursor, partial } });
668
- stored.add(conversation);
669
- reads.set(conversation, cursor);
670
- if (partial)
671
- partials.add(conversation);
672
- else
673
- partials.delete(conversation);
674
830
  sealTurns(conversation, new Set(arriving.map((message) => message.turn)));
675
831
  if (whole && (gaps.get(conversation) ?? 0) === generation)
676
832
  behind.delete(conversation);
@@ -678,6 +834,8 @@ export function createConversationStore(deps) {
678
834
  if (arriving.length > 0 || sealing || whole) {
679
835
  deps.publish(conversationChangeSet([conversation]));
680
836
  }
837
+ if (arriving.length > 0)
838
+ tellSoon();
681
839
  });
682
840
  }
683
841
  function heard(event) {
@@ -744,17 +902,24 @@ export function createConversationStore(deps) {
744
902
  emit(conversation);
745
903
  }
746
904
  async function kept(local, conversation, following) {
747
- if (following === null || empty(following)) {
905
+ const shown = following === null || empty(following) ? null : following;
906
+ if (shown === null)
748
907
  await local.dropChatDraft(conversation);
749
- drafts.delete(conversation);
750
- }
751
- else {
752
- await local.keepChatDrafts([following]);
753
- 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;
754
916
  }
755
917
  }
918
+ async function draftAt(local, conversation) {
919
+ return (await local.chatDraft(conversation)) ?? undefined;
920
+ }
756
921
  async function sealed(local, conversation, turns) {
757
- const held = drafts.get(conversation);
922
+ const held = await draftAt(local, conversation);
758
923
  const submitted = held?.submitted ?? [];
759
924
  const left = submitted.filter((one) => one.turn === null || !turns.has(one.turn));
760
925
  if (held === undefined || left.length === submitted.length)
@@ -765,7 +930,7 @@ export function createConversationStore(deps) {
765
930
  function moved(agent, operation, to) {
766
931
  const from = newChat(agent);
767
932
  return deps.durably(async (local) => {
768
- const held = drafts.get(from);
933
+ const held = await draftAt(local, from);
769
934
  const earlier = held?.moves ?? [];
770
935
  if (earlier.includes(operation))
771
936
  return;
@@ -775,18 +940,20 @@ export function createConversationStore(deps) {
775
940
  revision: chatDraftRevision(),
776
941
  };
777
942
  const { moves: _answered, ...unsent } = held ?? blank;
778
- 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 }];
779
946
  const left = { ...(taken.length === 0 ? unsent : blank), moves: [...earlier, operation] };
780
947
  await local.keepChatDrafts([left, ...taken]);
781
- drafts.set(from, left);
948
+ shownDraft(from, left);
782
949
  for (const one of taken)
783
- drafts.set(to, one);
950
+ shownDraft(to, one);
784
951
  deps.publish(conversationChangeSet([from, to]));
785
952
  });
786
953
  }
787
954
  function withDraft(conversation, next) {
788
955
  return deps.durably(async (local) => {
789
- const following = await next(drafts.get(conversation), local);
956
+ const following = await next(await draftAt(local, conversation), local);
790
957
  if (following === undefined)
791
958
  return;
792
959
  await kept(local, conversation, following);
@@ -868,7 +1035,8 @@ export function createConversationStore(deps) {
868
1035
  return deps.durably(async (local) => {
869
1036
  const { messages, more } = await local.messageWindow(conversation, asked.limit, asked.before, phrase);
870
1037
  const total = asked.total ? await local.messageCount(conversation, phrase) : null;
871
- return structuredClone(viewOf(conversation, messages, frameOf(asked, messages, more, total)));
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));
872
1040
  });
873
1041
  },
874
1042
  async create(input) {
@@ -876,8 +1044,10 @@ export function createConversationStore(deps) {
876
1044
  const conversation = answered(result, 'conversation_id');
877
1045
  if (conversation === null)
878
1046
  return result;
879
- if (!listing.has(conversation))
1047
+ const listed = (await heldList()) ?? [];
1048
+ if (!listed.some((entry) => entry.conversation === conversation)) {
880
1049
  creations.add(conversation);
1050
+ }
881
1051
  const creation = creationOf(input);
882
1052
  if (creation !== null)
883
1053
  await moved(creation.agent, creation.operation, conversation);
@@ -945,43 +1115,40 @@ export function createConversationStore(deps) {
945
1115
  return {
946
1116
  surface,
947
1117
  chatDraft,
948
- hydrate(held, kept, list, cursor) {
949
- if (list !== null && !listKnown) {
950
- listing.clear();
951
- for (const entry of list)
952
- listing.set(entry.conversation, { ...entry });
953
- listHeld = true;
954
- listCursor = cursor;
955
- deps.stream()?.hold(cursor);
956
- }
957
- stored.clear();
958
- reads.clear();
959
- partials.clear();
960
- for (const entry of held) {
961
- stored.add(entry.conversation);
962
- if (entry.read !== undefined)
963
- reads.set(entry.conversation, entry.read);
964
- if (entry.partial)
965
- partials.add(entry.conversation);
966
- }
967
- drafts.clear();
968
- for (const record of kept)
969
- drafts.set(record.conversation, record);
970
- if (listHeld)
971
- 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();
972
1124
  },
973
1125
  async reconcile() {
974
1126
  if (!listKnown)
975
1127
  return;
976
- 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]));
977
1138
  const leaving = [
978
- ...new Set([...replicated, ...stored, ...reads.keys(), ...drafts.keys()]),
1139
+ ...new Set([
1140
+ ...replicated,
1141
+ ...reads.keys(),
1142
+ ...kept.map(({ conversation }) => conversation),
1143
+ ]),
979
1144
  ].filter((conversation) => !named.has(conversation) &&
980
1145
  !creations.has(conversation) &&
981
- (!conversation.startsWith(NEW_CHAT) || retiredChat(conversation)));
1146
+ (!conversation.startsWith(NEW_CHAT) || retiredChat(conversation, list)));
982
1147
  for (const conversation of named) {
983
- if (!replicated.has(conversation) && missed(conversation))
1148
+ if (!replicated.has(conversation) &&
1149
+ missed(conversation, reads.get(conversation), entries.get(conversation))) {
984
1150
  behind.add(conversation);
1151
+ }
985
1152
  }
986
1153
  replicated = named;
987
1154
  handReplicated();
@@ -1012,12 +1179,16 @@ export function createConversationStore(deps) {
1012
1179
  progress.complete = false;
1013
1180
  },
1014
1181
  async resend(progress) {
1015
- for (const [conversation, held] of [...drafts]) {
1016
- for (const submission of held.submitted ?? []) {
1017
- if (submission.turn !== null || sending.has(submission.operation))
1018
- continue;
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;
1019
1189
  if (deps.closed())
1020
1190
  return;
1191
+ tried.add(submission.operation);
1021
1192
  const result = await dispatched(conversation, submission);
1022
1193
  if (result.kind === 'revoked-authorization')
1023
1194
  return;
@@ -1055,6 +1226,9 @@ export function createConversationStore(deps) {
1055
1226
  asked,
1056
1227
  watchers: new Set(),
1057
1228
  window: null,
1229
+ draft: null,
1230
+ ended: null,
1231
+ marked: [],
1058
1232
  more: false,
1059
1233
  total: null,
1060
1234
  };
@@ -1066,7 +1240,10 @@ export function createConversationStore(deps) {
1066
1240
  if (view.window !== null)
1067
1241
  return;
1068
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);
1069
1245
  view.window = messages;
1246
+ view.marked = await local.jobMarkers(conversation);
1070
1247
  view.more = more;
1071
1248
  view.total = asked.total ? await local.messageCount(conversation) : null;
1072
1249
  }), reading.signal)
@@ -1075,7 +1252,7 @@ export function createConversationStore(deps) {
1075
1252
  return;
1076
1253
  watcher.ready = true;
1077
1254
  const window = view.window ?? [];
1078
- watcher.notify(viewOf(conversation, window, sharedFrame(view, window)));
1255
+ watcher.notify(viewOf(conversation, window, sharedFrame(view, window), view.draft, view.ended, view.marked));
1079
1256
  })
1080
1257
  .catch(() => { });
1081
1258
  return () => {
@@ -1110,58 +1287,43 @@ export function createConversationStore(deps) {
1110
1287
  },
1111
1288
  receive(event) {
1112
1289
  if (deps.closed())
1113
- return false;
1114
- if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted') {
1115
- if (event.kind === 'list') {
1116
- if (event.whole)
1117
- relisting ??= new Map();
1118
- for (const entry of event.entries) {
1119
- if (relisting === null)
1120
- listed(entry);
1121
- else
1122
- relisting.set(entry.conversation, { ...entry });
1123
- }
1124
- if (event.more)
1125
- return false;
1126
- if (relisting !== null) {
1127
- listing.clear();
1128
- for (const entry of relisting.values())
1129
- listed(entry);
1130
- relisting = null;
1131
- }
1132
- listCursor = event.cursor;
1133
- deps.stream()?.hold(event.cursor);
1134
- listKnown = true;
1135
- for (const conversation of replicated) {
1136
- if (!listing.has(conversation))
1137
- continue;
1138
- if (!missed(conversation))
1139
- behind.delete(conversation);
1140
- else if (!behind.has(conversation))
1141
- gap(conversation);
1142
- }
1143
- openingEnded();
1144
- }
1145
- else if (event.kind === 'listed') {
1146
- listed(event.entry);
1147
- relisting?.set(event.entry.conversation, { ...event.entry });
1148
- }
1149
- else {
1150
- listing.delete(event.conversation);
1151
- 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 });
1152
1297
  }
1153
- keepList();
1154
- emitList();
1155
- return ((event.kind !== 'list' &&
1156
- listKnown &&
1157
- (listing.size !== replicated.size ||
1158
- [...listing.keys()].some((conversation) => !replicated.has(conversation)))) ||
1159
- (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;
1160
1319
  }
1161
1320
  heard(event);
1162
- return false;
1163
1321
  },
1164
1322
  close() {
1323
+ closing.abort();
1324
+ if (rowTimer !== null)
1325
+ clearTimeout(rowTimer);
1326
+ rowTimer = null;
1165
1327
  views.clear();
1166
1328
  lives.clear();
1167
1329
  unsent.clear();