@agentshouse/mdruntime 0.1.0-alpha.21 → 0.1.0-alpha.22

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.
@@ -1,10 +1,33 @@
1
+ import { conversationChangeSet } from './change-set.js';
1
2
  import { declaredFile } from './file.js';
2
- const NO_TURNS = new Set();
3
- function carriedMessage(value) {
3
+ function ordered(left, right) {
4
+ if (left.position !== right.position) return left.position < right.position ? -1 : 1;
5
+ if (left.identity !== right.identity) return left.identity < right.identity ? -1 : 1;
6
+ return 0;
7
+ }
8
+ function merged(held, arriving) {
9
+ const byIdentity = new Map(held.map((message)=>[
10
+ message.identity,
11
+ message
12
+ ]));
13
+ for (const message of arriving)byIdentity.set(message.identity, message);
14
+ return [
15
+ ...byIdentity.values()
16
+ ].sort(ordered);
17
+ }
18
+ function acceptedMessage(value) {
4
19
  if (typeof value !== 'object' || value === null) return null;
5
20
  const record = value;
6
- if (typeof record.turn !== 'string') return null;
7
- return structuredClone(value);
21
+ if (typeof record.identity !== 'string' || typeof record.turn !== 'string' || typeof record.position !== 'string' || typeof record.text !== 'string') {
22
+ return null;
23
+ }
24
+ return {
25
+ identity: record.identity,
26
+ turn: record.turn,
27
+ position: record.position,
28
+ text: record.text,
29
+ message: structuredClone(record.message)
30
+ };
8
31
  }
9
32
  function historyPage(result) {
10
33
  if (result.kind !== 'action') return null;
@@ -12,14 +35,18 @@ function historyPage(result) {
12
35
  if (typeof answer !== 'object' || answer === null) return null;
13
36
  const record = answer;
14
37
  if (!Array.isArray(record.messages)) return null;
38
+ if (record.next !== undefined && typeof record.next !== 'string') return null;
15
39
  const messages = [];
16
40
  for (const entry of record.messages){
17
- const carried = carriedMessage(entry);
18
- if (carried !== null) messages.push(carried);
41
+ const message = acceptedMessage(entry);
42
+ if (message === null) return null;
43
+ messages.push(message);
19
44
  }
20
45
  return {
21
46
  messages,
22
- cursor: typeof record.cursor === 'string' ? record.cursor : null
47
+ ...record.next === undefined ? {} : {
48
+ next: record.next
49
+ }
23
50
  };
24
51
  }
25
52
  function turnOf(result) {
@@ -29,182 +56,224 @@ function turnOf(result) {
29
56
  const record = answer;
30
57
  return typeof record.turn === 'string' ? record.turn : null;
31
58
  }
32
- function newest(held, page, arrived) {
33
- const arriving = new Set(page.messages.map((entry)=>entry.turn));
34
- const overlap = held.messages.findIndex((entry)=>arriving.has(entry.turn));
35
- const carried = overlap === -1 ? [] : held.messages.slice(0, overlap);
36
- const placed = new Set([
37
- ...carried,
38
- ...page.messages
39
- ].map((entry)=>entry.turn));
40
- const live = held.messages.filter((entry)=>arrived.has(entry.turn) && !placed.has(entry.turn));
41
- return {
42
- ...held,
43
- messages: [
44
- ...carried,
45
- ...page.messages,
46
- ...live
47
- ],
48
- cursor: overlap === -1 ? page.cursor : held.cursor
49
- };
50
- }
51
- function older(held, page) {
52
- const present = new Set(held.messages.map((entry)=>entry.turn));
53
- return {
54
- ...held,
55
- messages: [
56
- ...page.messages.filter((entry)=>!present.has(entry.turn)),
57
- ...held.messages
58
- ],
59
- cursor: page.cursor
60
- };
61
- }
62
- function sealed(held, arriving) {
63
- const at = held.messages.findIndex((entry)=>entry.turn === arriving.turn);
64
- if (at === -1) return {
65
- ...held,
66
- messages: [
67
- ...held.messages,
68
- arriving
69
- ]
70
- };
71
- const messages = [
72
- ...held.messages
73
- ];
74
- messages[at] = arriving;
75
- return {
76
- ...held,
77
- messages
78
- };
59
+ function limitOf(options) {
60
+ const { limit } = options;
61
+ if (limit === undefined) return undefined;
62
+ if (!Number.isInteger(limit) || limit < 0) {
63
+ throw new Error(`conversation limit must be a non-negative integer, got ${String(limit)}`);
64
+ }
65
+ return limit;
79
66
  }
80
67
  export function createConversationStore(deps) {
81
- const held = new Map();
82
- const composed = new Map();
68
+ const messages = new Map();
69
+ const reads = new Map();
70
+ const drafts = new Map();
71
+ const sending = new Map();
72
+ const behind = new Set();
73
+ const gaps = new Map();
83
74
  const growing = new Map();
84
- const viewers = new Set();
85
- const issued = new Map();
86
- const live = new Map();
87
- let handed;
88
- function heldOf(conversation) {
89
- return held.get(conversation) ?? {
90
- messages: [],
91
- cursor: null,
92
- summary: null
93
- };
94
- }
95
- function instructionOf(conversation) {
96
- return composed.get(conversation) ?? {
97
- text: '',
98
- files: [],
99
- turn: null
100
- };
101
- }
102
- function viewOf(conversation) {
103
- const carried = heldOf(conversation);
104
- const unsent = composed.get(conversation);
75
+ const summaries = new Map();
76
+ const views = new Map();
77
+ let replicated = new Set();
78
+ let handedReplicated;
79
+ let handedViewed;
80
+ function viewOf(conversation, limit) {
81
+ const held = messages.get(conversation) ?? [];
82
+ const shown = limit === undefined ? held : held.slice(Math.max(0, held.length - limit));
83
+ const draft = drafts.get(conversation);
105
84
  return {
106
85
  conversation,
107
- messages: structuredClone(carried.messages),
108
- olderPages: carried.cursor !== null,
86
+ messages: shown.map(({ identity, turn, text, message })=>({
87
+ identity,
88
+ turn,
89
+ text,
90
+ message: structuredClone(message)
91
+ })),
109
92
  growingText: growing.get(conversation) ?? null,
110
- summary: structuredClone(carried.summary),
111
- instruction: unsent === undefined || unsent.turn !== null ? null : {
112
- text: unsent.text,
93
+ summary: structuredClone(summaries.get(conversation) ?? null),
94
+ chatDraft: draft === undefined || draft.turn !== null || sending.get(conversation)?.draft === draft ? null : {
95
+ text: draft.text,
113
96
  files: [
114
- ...unsent.files
97
+ ...draft.files
115
98
  ]
116
99
  }
117
100
  };
118
101
  }
119
102
  function emit(conversation) {
120
103
  if (deps.closed()) return;
121
- for (const viewer of viewers){
122
- if (viewer.conversation === conversation && viewer.ready) viewer.notify(viewOf(conversation));
104
+ for (const view of views.values()){
105
+ if (view.conversation !== conversation) continue;
106
+ const ready = [
107
+ ...view.watchers
108
+ ].filter((watcher)=>watcher.ready);
109
+ if (ready.length === 0) continue;
110
+ const value = viewOf(conversation, view.limit);
111
+ for (const watcher of ready)watcher.notify(value);
123
112
  }
124
113
  }
125
114
  function viewed() {
126
115
  return [
127
116
  ...new Set([
128
- ...viewers
129
- ].map((viewer)=>viewer.conversation))
117
+ ...views.values()
118
+ ].map((view)=>view.conversation))
130
119
  ].sort();
131
120
  }
132
- function hand() {
133
- const path = deps.path();
134
- if (path === undefined) return;
121
+ function handViewed() {
122
+ const stream = deps.stream();
123
+ if (stream === undefined) return;
135
124
  const open = viewed();
136
125
  const named = JSON.stringify(open);
137
- if (named === handed) return;
138
- handed = named;
139
- path.view(open);
126
+ if (named === handedViewed) return;
127
+ handedViewed = named;
128
+ stream.view(open);
140
129
  }
141
- function keptRecord(conversation) {
142
- const carried = heldOf(conversation);
143
- return {
144
- conversation,
145
- messages: carried.messages,
146
- cursor: carried.cursor,
147
- summary: carried.summary
148
- };
130
+ function handReplicated() {
131
+ const stream = deps.stream();
132
+ if (stream === undefined) return;
133
+ const held = [
134
+ ...replicated
135
+ ].sort();
136
+ const named = JSON.stringify(held);
137
+ if (named === handedReplicated) return;
138
+ handedReplicated = named;
139
+ stream.replicate(held);
149
140
  }
150
- async function settle(local, conversation) {
151
- const sent = instructionOf(conversation).turn;
152
- if (sent === null) return;
153
- if (!heldOf(conversation).messages.some((entry)=>entry.turn === sent)) return;
154
- composed.delete(conversation);
155
- await local.dropInstruction(conversation);
141
+ function carded(documents) {
142
+ const declaration = deps.cards();
143
+ if (declaration === undefined) return [];
144
+ const cards = [];
145
+ for (const card of documents){
146
+ if (card.room !== declaration.locator || card.type !== declaration.type) continue;
147
+ const identity = card.fields[declaration.identity];
148
+ if (typeof identity === 'string') cards.push({
149
+ conversation: identity,
150
+ card
151
+ });
152
+ }
153
+ return cards;
156
154
  }
157
- async function keep(conversation, next) {
158
- const kept = await deps.durably(async (local)=>{
159
- const following = next(heldOf(conversation));
160
- if (following === null) return false;
161
- held.set(conversation, following);
162
- await local.keepConversation(keptRecord(conversation));
163
- await settle(local, conversation);
164
- return true;
155
+ async function remove(leaving) {
156
+ await deps.durably(async (local)=>{
157
+ await local.dropConversations(leaving);
158
+ const removed = [];
159
+ const changed = [];
160
+ for (const conversation of leaving){
161
+ for (const message of messages.get(conversation) ?? []){
162
+ removed.push({
163
+ conversation,
164
+ identity: message.identity
165
+ });
166
+ }
167
+ if (messages.has(conversation) || drafts.has(conversation) || growing.has(conversation) || summaries.has(conversation)) {
168
+ changed.push(conversation);
169
+ }
170
+ messages.delete(conversation);
171
+ reads.delete(conversation);
172
+ drafts.delete(conversation);
173
+ behind.delete(conversation);
174
+ growing.delete(conversation);
175
+ summaries.delete(conversation);
176
+ }
177
+ if (changed.length > 0) deps.publish(conversationChangeSet(removed, changed));
165
178
  });
166
- if (kept) emit(conversation);
167
- return kept;
168
179
  }
169
- async function keepInstruction(conversation, next) {
170
- await deps.durably(async (local)=>{
171
- const stored = next(instructionOf(conversation));
172
- composed.set(conversation, stored);
173
- await local.keepInstruction({
180
+ function gap(conversation) {
181
+ gaps.set(conversation, (gaps.get(conversation) ?? 0) + 1);
182
+ behind.add(conversation);
183
+ }
184
+ function keep(conversation, message) {
185
+ return deps.durably(async (local)=>{
186
+ if (!replicated.has(conversation)) return;
187
+ const read = reads.get(conversation);
188
+ const advances = read !== undefined && !behind.has(conversation) && (read === null || message.position > read);
189
+ const sealed = drafts.get(conversation)?.turn === message.turn;
190
+ await local.keepMessages({
174
191
  conversation,
175
- ...stored
192
+ messages: [
193
+ message
194
+ ],
195
+ ...advances ? {
196
+ read: message.position
197
+ } : {},
198
+ sealed
176
199
  });
177
- await settle(local, conversation);
200
+ messages.set(conversation, merged(messages.get(conversation) ?? [], [
201
+ message
202
+ ]));
203
+ if (advances) reads.set(conversation, message.position);
204
+ if (sealed) drafts.delete(conversation);
205
+ if (growing.get(conversation)?.turn === message.turn) growing.delete(conversation);
206
+ deps.publish(conversationChangeSet([
207
+ {
208
+ conversation,
209
+ identity: message.identity
210
+ }
211
+ ], [
212
+ conversation
213
+ ]));
178
214
  });
179
- emit(conversation);
180
215
  }
181
- async function dropInstruction(conversation) {
216
+ async function readHistory(conversation) {
217
+ const generation = gaps.get(conversation) ?? 0;
218
+ const from = reads.get(conversation);
219
+ const arriving = [];
220
+ let after = from ?? undefined;
221
+ for(;;){
222
+ const result = await deps.call('conversation.history', {
223
+ conversation,
224
+ ...after === undefined ? {} : {
225
+ after
226
+ }
227
+ });
228
+ const page = historyPage(result);
229
+ if (page === null) return result;
230
+ arriving.push(...page.messages);
231
+ if (page.next === undefined) break;
232
+ after = page.next;
233
+ }
182
234
  await deps.durably(async (local)=>{
183
- composed.delete(conversation);
184
- await local.dropInstruction(conversation);
235
+ if (!replicated.has(conversation)) return;
236
+ const newest = arriving.reduce((top, message)=>top === null || message.position > top ? message.position : top, from ?? null);
237
+ const turn = drafts.get(conversation)?.turn;
238
+ const sealed = arriving.some((message)=>message.turn === turn);
239
+ await local.keepMessages({
240
+ conversation,
241
+ messages: arriving,
242
+ read: newest,
243
+ sealed
244
+ });
245
+ messages.set(conversation, merged(messages.get(conversation) ?? [], arriving));
246
+ reads.set(conversation, newest);
247
+ if (sealed) drafts.delete(conversation);
248
+ const growingTurn = growing.get(conversation)?.turn;
249
+ if (arriving.some((message)=>message.turn === growingTurn)) growing.delete(conversation);
250
+ if ((gaps.get(conversation) ?? 0) === generation) behind.delete(conversation);
251
+ if (arriving.length > 0 || sealed) {
252
+ deps.publish(conversationChangeSet(arriving.map((message)=>({
253
+ conversation,
254
+ identity: message.identity
255
+ })), [
256
+ conversation
257
+ ]));
258
+ }
185
259
  });
186
- emit(conversation);
187
- }
188
- function supersede(conversation) {
189
- issued.set(conversation, (issued.get(conversation) ?? 0) + 1);
260
+ return null;
190
261
  }
191
- async function fill(conversation, cursor) {
192
- const generation = issued.get(conversation);
193
- const result = await deps.call('conversation.history', {
194
- conversation,
195
- ...cursor === null ? {} : {
196
- cursor
262
+ function withDraft(conversation, next) {
263
+ return deps.durably(async (local)=>{
264
+ const following = next(drafts.get(conversation));
265
+ if (following === undefined) return;
266
+ if (following === null) {
267
+ await local.dropChatDraft(conversation);
268
+ drafts.delete(conversation);
269
+ } else {
270
+ await local.keepChatDraft(following);
271
+ drafts.set(conversation, following);
197
272
  }
273
+ deps.publish(conversationChangeSet([], [
274
+ conversation
275
+ ]));
198
276
  });
199
- const page = historyPage(result);
200
- if (page === null) return result;
201
- const landed = await keep(conversation, (carried)=>{
202
- if (issued.get(conversation) !== generation) return null;
203
- if (cursor !== null) return carried.cursor === cursor ? older(carried, page) : null;
204
- return newest(carried, page, live.get(conversation) ?? NO_TURNS);
205
- });
206
- if (landed && cursor === null) live.delete(conversation);
207
- return result;
208
277
  }
209
278
  function invoke(name, conversation, extra = {}) {
210
279
  return deps.call(name, {
@@ -216,62 +285,63 @@ export function createConversationStore(deps) {
216
285
  list () {
217
286
  const declaration = deps.cards();
218
287
  if (declaration === undefined) return Promise.resolve([]);
219
- const documents = deps.documents().documents.filter((document)=>document.room === declaration.locator);
220
- const entries = [];
221
- for (const card of documents){
222
- if (card.type !== declaration.type) continue;
223
- const identity = card.fields[declaration.identity];
224
- if (typeof identity !== 'string') continue;
225
- const agent = documents.find((document)=>document.backlinks.some((backlink)=>backlink.path === card.path && backlink.field === declaration.agent));
226
- entries.push({
227
- conversation: identity,
288
+ const documents = deps.documents().filter((document)=>document.room === declaration.locator);
289
+ const entries = carded(documents).map(({ conversation, card })=>({
290
+ conversation,
228
291
  card,
229
- agent: agent ?? null
230
- });
231
- }
292
+ agent: documents.find((document)=>document.backlinks.some((backlink)=>backlink.path === card.path && backlink.field === declaration.agent)) ?? null
293
+ }));
232
294
  entries.sort((left, right)=>left.conversation < right.conversation ? -1 : 1);
233
295
  return Promise.resolve(entries);
234
296
  },
235
- read (conversation) {
236
- return deps.durably(()=>Promise.resolve(viewOf(conversation)));
237
- },
238
- older (conversation) {
239
- const cursor = heldOf(conversation).cursor;
240
- if (cursor === null) return Promise.resolve(null);
241
- return fill(conversation, cursor);
242
- },
243
- compose (conversation, supplied) {
244
- return keepInstruction(conversation, ()=>({
245
- text: supplied.text,
246
- files: [
247
- ...supplied.files
248
- ],
249
- turn: null
250
- }));
251
- },
252
- discard (conversation) {
253
- return dropInstruction(conversation);
297
+ async read (conversation, options = {}) {
298
+ const limit = limitOf(options);
299
+ return deps.durably(()=>Promise.resolve(viewOf(conversation, limit)));
254
300
  },
255
301
  create (input) {
256
302
  return deps.call('conversation.create', input);
257
303
  },
258
304
  async send (conversation) {
259
- const unsent = instructionOf(conversation);
260
- const files = await Promise.all(unsent.files.map(async (file)=>({
261
- ...await declaredFile(file),
262
- blob: file.blob
263
- })));
264
- const result = await invoke('conversation.send', conversation, {
265
- text: unsent.text,
266
- files
267
- });
268
- const turn = turnOf(result);
269
- if (turn === null) return result;
270
- await keepInstruction(conversation, (stored)=>({
271
- ...stored,
272
- turn
273
- }));
274
- return result;
305
+ const draft = drafts.get(conversation);
306
+ const pending = draft === undefined ? undefined : {
307
+ draft
308
+ };
309
+ if (pending !== undefined) {
310
+ sending.set(conversation, pending);
311
+ deps.publish(conversationChangeSet([], [
312
+ conversation
313
+ ]));
314
+ }
315
+ try {
316
+ const files = await Promise.all((draft?.files ?? []).map(async (file)=>({
317
+ ...await declaredFile(file),
318
+ blob: file.blob
319
+ })));
320
+ const result = await invoke('conversation.send', conversation, {
321
+ text: draft?.text ?? '',
322
+ files
323
+ });
324
+ const turn = turnOf(result);
325
+ if (turn === null || draft === undefined) return result;
326
+ await withDraft(conversation, (current)=>{
327
+ if (current !== draft) return undefined;
328
+ const sealed = (messages.get(conversation) ?? []).some((message)=>message.turn === turn);
329
+ return sealed ? null : {
330
+ ...draft,
331
+ turn
332
+ };
333
+ });
334
+ return result;
335
+ } finally{
336
+ if (pending !== undefined && sending.get(conversation) === pending) {
337
+ sending.delete(conversation);
338
+ if (drafts.get(conversation) === draft) {
339
+ deps.publish(conversationChangeSet([], [
340
+ conversation
341
+ ]));
342
+ }
343
+ }
344
+ }
275
345
  },
276
346
  answer (conversation, answer) {
277
347
  return invoke('conversation.answer', conversation, {
@@ -289,114 +359,146 @@ export function createConversationStore(deps) {
289
359
  turn
290
360
  });
291
361
  if (result.kind !== 'action') return result;
292
- if (instructionOf(conversation).turn === turn) {
293
- await keepInstruction(conversation, (stored)=>({
294
- ...stored,
295
- turn: null
296
- }));
297
- }
362
+ await withDraft(conversation, (current)=>current === undefined || current.turn !== turn ? undefined : {
363
+ ...current,
364
+ turn: null
365
+ });
298
366
  return result;
299
367
  },
300
- async delete (conversation) {
301
- const result = await invoke('conversation.delete', conversation);
302
- if (result.kind !== 'action') return result;
303
- growing.delete(conversation);
304
- live.delete(conversation);
305
- supersede(conversation);
306
- await deps.durably(async (local)=>{
307
- held.delete(conversation);
308
- composed.delete(conversation);
309
- await local.dropConversation(conversation);
310
- await local.dropInstruction(conversation);
311
- });
312
- emit(conversation);
313
- return result;
368
+ delete (conversation) {
369
+ return invoke('conversation.delete', conversation);
370
+ }
371
+ };
372
+ const chatDraft = {
373
+ write (conversation, draft) {
374
+ return withDraft(conversation, ()=>({
375
+ conversation,
376
+ text: draft.text,
377
+ files: [
378
+ ...draft.files
379
+ ],
380
+ turn: null
381
+ }));
382
+ },
383
+ discard (conversation) {
384
+ return withDraft(conversation, (current)=>current === undefined ? undefined : null);
314
385
  }
315
386
  };
316
387
  return {
317
388
  surface,
318
- hydrate (conversations, instructions) {
319
- held.clear();
320
- for (const stored of conversations){
321
- held.set(stored.conversation, {
322
- messages: stored.messages,
323
- cursor: stored.cursor,
324
- summary: stored.summary
325
- });
389
+ chatDraft,
390
+ hydrate (held, stored) {
391
+ messages.clear();
392
+ reads.clear();
393
+ for (const entry of held){
394
+ messages.set(entry.conversation, [
395
+ ...entry.messages
396
+ ].sort(ordered));
397
+ if (entry.read !== undefined) reads.set(entry.conversation, entry.read);
326
398
  }
327
- composed.clear();
328
- for (const stored of instructions){
329
- composed.set(stored.conversation, {
330
- text: stored.text,
331
- files: stored.files,
332
- turn: stored.turn
333
- });
399
+ drafts.clear();
400
+ for (const record of stored)drafts.set(record.conversation, record);
401
+ },
402
+ async reconcile () {
403
+ const named = new Set(carded(deps.documents()).map((entry)=>entry.conversation));
404
+ const leaving = [
405
+ ...new Set([
406
+ ...replicated,
407
+ ...messages.keys(),
408
+ ...reads.keys(),
409
+ ...drafts.keys()
410
+ ])
411
+ ].filter((conversation)=>!named.has(conversation));
412
+ for (const conversation of named){
413
+ if (!replicated.has(conversation)) behind.add(conversation);
334
414
  }
415
+ replicated = named;
416
+ handReplicated();
417
+ if (leaving.length > 0) await remove(leaving);
335
418
  },
336
- observe (conversation, notify) {
337
- const first = !viewed().includes(conversation);
338
- const viewer = {
419
+ async catchUp (progress) {
420
+ for (const conversation of [
421
+ ...behind
422
+ ].sort()){
423
+ if (deps.closed()) return;
424
+ if (!replicated.has(conversation)) continue;
425
+ const failed = await readHistory(conversation);
426
+ if (failed?.kind === 'transport-uncertainty') progress.reachable = false;
427
+ }
428
+ if (behind.size > 0) progress.complete = false;
429
+ },
430
+ current () {
431
+ return behind.size === 0;
432
+ },
433
+ reconnected () {
434
+ for (const conversation of replicated)gap(conversation);
435
+ },
436
+ observe (conversation, options, notify) {
437
+ const limit = limitOf(options);
438
+ const key = JSON.stringify([
339
439
  conversation,
440
+ limit ?? null
441
+ ]);
442
+ const view = views.get(key) ?? {
443
+ conversation,
444
+ limit,
445
+ watchers: new Set()
446
+ };
447
+ views.set(key, view);
448
+ const watcher = {
340
449
  notify,
341
450
  ready: false
342
451
  };
343
- viewers.add(viewer);
344
- if (first) {
345
- live.set(conversation, new Set());
346
- supersede(conversation);
347
- }
348
- hand();
349
- void deps.durably(()=>Promise.resolve()).then(async ()=>{
350
- if (deps.closed() || !viewers.has(viewer)) return;
351
- viewer.ready = true;
352
- viewer.notify(viewOf(conversation));
353
- if (first) await fill(conversation, null);
452
+ view.watchers.add(watcher);
453
+ handViewed();
454
+ void deps.durably(()=>Promise.resolve()).then(()=>{
455
+ if (deps.closed() || !view.watchers.has(watcher)) return;
456
+ watcher.ready = true;
457
+ watcher.notify(viewOf(conversation, limit));
354
458
  }).catch(()=>{});
355
459
  return ()=>{
356
- if (!viewers.delete(viewer)) return;
460
+ if (!view.watchers.delete(watcher)) return;
461
+ if (view.watchers.size === 0) views.delete(key);
357
462
  if (!viewed().includes(conversation)) {
358
463
  growing.delete(conversation);
359
- live.delete(conversation);
464
+ summaries.delete(conversation);
360
465
  }
361
- hand();
466
+ handViewed();
362
467
  };
363
468
  },
469
+ changed (set) {
470
+ for (const conversation of set.conversations)emit(conversation);
471
+ },
364
472
  receive (event) {
365
- if (deps.closed() || !viewed().includes(event.conversation)) return;
473
+ if (deps.closed()) return;
474
+ const { conversation } = event;
475
+ if (event.kind === 'gap') {
476
+ if (replicated.has(conversation)) gap(conversation);
477
+ return;
478
+ }
366
479
  if (event.kind === 'growing-text') {
367
- growing.set(event.conversation, {
480
+ if (!viewed().includes(conversation)) return;
481
+ growing.set(conversation, {
368
482
  turn: event.turn,
369
483
  text: event.text
370
484
  });
371
- emit(event.conversation);
485
+ emit(conversation);
372
486
  return;
373
487
  }
374
488
  if (event.kind === 'session-summary') {
375
- const summary = structuredClone(event.summary);
376
- void keep(event.conversation, (carried)=>({
377
- ...carried,
378
- summary
379
- })).catch(()=>{});
489
+ if (!viewed().includes(conversation)) return;
490
+ summaries.set(conversation, structuredClone(event.summary));
491
+ emit(conversation);
380
492
  return;
381
493
  }
382
- const arriving = carriedMessage(event.message);
383
- if (arriving === null) return;
384
- live.get(event.conversation)?.add(arriving.turn);
385
- if (growing.get(event.conversation)?.turn === arriving.turn) {
386
- growing.delete(event.conversation);
387
- }
388
- void deps.durably(async (local)=>{
389
- held.set(event.conversation, sealed(heldOf(event.conversation), arriving));
390
- await local.keepConversation(keptRecord(event.conversation));
391
- await settle(local, event.conversation);
392
- }).then(()=>{
393
- emit(event.conversation);
394
- }).catch(()=>{});
494
+ const arriving = acceptedMessage(event.message);
495
+ if (arriving === null || !replicated.has(conversation)) return;
496
+ void keep(conversation, arriving).catch(()=>{});
395
497
  },
396
498
  close () {
397
- viewers.clear();
499
+ views.clear();
398
500
  growing.clear();
399
- live.clear();
501
+ summaries.clear();
400
502
  }
401
503
  };
402
504
  }