@agentshouse/mdruntime 0.1.0-c967d1ca66fee → 0.1.0-ca14c49c06d90

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 (150) hide show
  1. package/AGENTS.md +142 -112
  2. package/README.md +11 -1
  3. package/dist/adapter.d.ts +59 -13
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/attempt.d.ts +16 -0
  6. package/dist/attempt.d.ts.map +1 -0
  7. package/dist/attempt.js +70 -0
  8. package/dist/binary.d.ts +23 -0
  9. package/dist/binary.d.ts.map +1 -0
  10. package/dist/binary.js +8 -0
  11. package/dist/bootstrap.d.ts +2 -2
  12. package/dist/bootstrap.d.ts.map +1 -1
  13. package/dist/bootstrap.js +33 -14
  14. package/dist/change-set.d.ts.map +1 -1
  15. package/dist/change-set.js +8 -7
  16. package/dist/change.d.ts +11 -0
  17. package/dist/change.d.ts.map +1 -1
  18. package/dist/change.js +26 -1
  19. package/dist/chat/acts.d.ts +33 -0
  20. package/dist/chat/acts.d.ts.map +1 -0
  21. package/dist/chat/acts.js +36 -0
  22. package/dist/chat/context.d.ts +3 -0
  23. package/dist/chat/context.d.ts.map +1 -0
  24. package/dist/chat/context.js +10 -0
  25. package/dist/chat/input.d.ts +9 -0
  26. package/dist/chat/input.d.ts.map +1 -0
  27. package/dist/chat/input.js +99 -0
  28. package/dist/chat/kit.d.ts +60 -0
  29. package/dist/chat/kit.d.ts.map +1 -0
  30. package/dist/chat/kit.js +123 -0
  31. package/dist/chat/lasting.d.ts +6 -0
  32. package/dist/chat/lasting.d.ts.map +1 -0
  33. package/dist/chat/lasting.js +19 -0
  34. package/dist/chat/list.d.ts +26 -0
  35. package/dist/chat/list.d.ts.map +1 -0
  36. package/dist/chat/list.js +62 -0
  37. package/dist/chat/markdown.d.ts +20 -0
  38. package/dist/chat/markdown.d.ts.map +1 -0
  39. package/dist/chat/markdown.js +188 -0
  40. package/dist/chat/message.d.ts +6 -0
  41. package/dist/chat/message.d.ts.map +1 -0
  42. package/dist/chat/message.js +192 -0
  43. package/dist/chat/messages.d.ts +21 -0
  44. package/dist/chat/messages.d.ts.map +1 -0
  45. package/dist/chat/messages.js +151 -0
  46. package/dist/chat/name.d.ts +6 -0
  47. package/dist/chat/name.d.ts.map +1 -0
  48. package/dist/chat/name.js +37 -0
  49. package/dist/chat/opened.d.ts +11 -0
  50. package/dist/chat/opened.d.ts.map +1 -0
  51. package/dist/chat/opened.js +17 -0
  52. package/dist/chat/overlay.d.ts +31 -0
  53. package/dist/chat/overlay.d.ts.map +1 -0
  54. package/dist/chat/overlay.js +97 -0
  55. package/dist/chat/parts.d.ts +57 -0
  56. package/dist/chat/parts.d.ts.map +1 -0
  57. package/dist/chat/parts.js +119 -0
  58. package/dist/chat/process.d.ts +20 -0
  59. package/dist/chat/process.d.ts.map +1 -0
  60. package/dist/chat/process.js +32 -0
  61. package/dist/chat/provider.d.ts +63 -0
  62. package/dist/chat/provider.d.ts.map +1 -0
  63. package/dist/chat/provider.js +620 -0
  64. package/dist/chat/question.d.ts +9 -0
  65. package/dist/chat/question.d.ts.map +1 -0
  66. package/dist/chat/question.js +80 -0
  67. package/dist/chat/questions.d.ts +31 -0
  68. package/dist/chat/questions.d.ts.map +1 -0
  69. package/dist/chat/questions.js +111 -0
  70. package/dist/chat/read.d.ts +5 -0
  71. package/dist/chat/read.d.ts.map +1 -0
  72. package/dist/chat/read.js +13 -0
  73. package/dist/chat/ready.d.ts +33 -0
  74. package/dist/chat/ready.d.ts.map +1 -0
  75. package/dist/chat/ready.js +91 -0
  76. package/dist/chat/style.d.ts +4 -0
  77. package/dist/chat/style.d.ts.map +1 -0
  78. package/dist/chat/style.js +302 -0
  79. package/dist/chat.d.ts +14 -0
  80. package/dist/chat.d.ts.map +1 -0
  81. package/dist/chat.js +8 -0
  82. package/dist/client.d.ts +11 -4
  83. package/dist/client.d.ts.map +1 -1
  84. package/dist/client.js +12 -2
  85. package/dist/connection.d.ts +13 -6
  86. package/dist/connection.d.ts.map +1 -1
  87. package/dist/connection.js +573 -193
  88. package/dist/conversation.d.ts +14 -4
  89. package/dist/conversation.d.ts.map +1 -1
  90. package/dist/conversation.js +188 -63
  91. package/dist/entry.d.ts +17 -0
  92. package/dist/entry.d.ts.map +1 -0
  93. package/dist/entry.js +156 -0
  94. package/dist/expectation.d.ts +21 -0
  95. package/dist/expectation.d.ts.map +1 -0
  96. package/dist/expectation.js +70 -0
  97. package/dist/fence.d.ts +6 -0
  98. package/dist/fence.d.ts.map +1 -0
  99. package/dist/fence.js +10 -0
  100. package/dist/host.d.ts +3 -2
  101. package/dist/host.d.ts.map +1 -1
  102. package/dist/host.js +13 -6
  103. package/dist/identity.d.ts +2 -0
  104. package/dist/identity.d.ts.map +1 -1
  105. package/dist/identity.js +6 -0
  106. package/dist/index.d.ts +9 -4
  107. package/dist/index.d.ts.map +1 -1
  108. package/dist/index.js +4 -0
  109. package/dist/memory.d.ts +4 -3
  110. package/dist/memory.d.ts.map +1 -1
  111. package/dist/memory.js +40 -11
  112. package/dist/outcome.d.ts +11 -1
  113. package/dist/outcome.d.ts.map +1 -1
  114. package/dist/outcome.js +7 -0
  115. package/dist/overlay.d.ts +13 -2
  116. package/dist/overlay.d.ts.map +1 -1
  117. package/dist/overlay.js +64 -22
  118. package/dist/query.d.ts +2 -0
  119. package/dist/query.d.ts.map +1 -1
  120. package/dist/reach.d.ts +2 -0
  121. package/dist/reach.d.ts.map +1 -1
  122. package/dist/reach.js +26 -4
  123. package/dist/react.d.ts +2 -1
  124. package/dist/react.d.ts.map +1 -1
  125. package/dist/read-side.d.ts.map +1 -1
  126. package/dist/read-side.js +9 -2
  127. package/dist/refresh.d.ts.map +1 -1
  128. package/dist/refresh.js +12 -17
  129. package/dist/replica.d.ts +5 -2
  130. package/dist/replica.d.ts.map +1 -1
  131. package/dist/replica.js +33 -28
  132. package/dist/room-draft.d.ts +19 -2
  133. package/dist/room-draft.d.ts.map +1 -1
  134. package/dist/room-draft.js +119 -1
  135. package/dist/rows.d.ts +3 -1
  136. package/dist/rows.d.ts.map +1 -1
  137. package/dist/rows.js +3 -1
  138. package/dist/save.d.ts +23 -0
  139. package/dist/save.d.ts.map +1 -1
  140. package/dist/save.js +309 -18
  141. package/dist/store.d.ts +36 -18
  142. package/dist/store.d.ts.map +1 -1
  143. package/dist/store.js +291 -172
  144. package/dist/wire.d.ts +3 -0
  145. package/dist/wire.d.ts.map +1 -1
  146. package/dist/wire.js +3 -0
  147. package/package.json +26 -14
  148. package/dist/attachment.d.ts +0 -3
  149. package/dist/attachment.d.ts.map +0 -1
  150. package/dist/attachment.js +0 -8
@@ -8,16 +8,19 @@ import { createSearchIndex, searchPlanOf, } from './search.js';
8
8
  import { bootstrapRoom } from './bootstrap.js';
9
9
  import { changeSet, mergeChangeSets } from './change-set.js';
10
10
  import { createConversationStore, } from './conversation.js';
11
+ import { queuedFile } from './change.js';
11
12
  import { validRoot } from './collection.js';
12
13
  import { runtimeError } from './failure.js';
13
14
  import { createLog } from './log.js';
14
- import { calledResult, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
15
+ import { binaryRecord } from './binary.js';
16
+ import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
15
17
  import { refreshRoom } from './refresh.js';
16
- import { buildBatch } from './save.js';
18
+ import { buildBatch, joined, limitedIn, requeued } from './save.js';
17
19
  import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
18
- import { changedParts, changesOf, held, readParts, returnedBy, returnedInto, staleWrite, underneath, } from './room-draft.js';
20
+ import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
19
21
  import { same, saving, STARTING, transferring, } from './status.js';
20
22
  import { detectStorageLoss, nextGeneration } from './durability.js';
23
+ import { FencedMap } from './fence.js';
21
24
  import { roomDraftRevision } from './identity.js';
22
25
  import { beneath, normalize } from './paths.js';
23
26
  import { openRuntimeStore, } from './store.js';
@@ -36,22 +39,36 @@ function declaredSelection(room) {
36
39
  root !== '' &&
37
40
  selection.roots.every((other, at) => at === index || !within(other, root)));
38
41
  }
42
+ const OPENING_LANE = 'opening';
43
+ const CONVERSATION_LANE = 'conversations';
44
+ const ROOM_READS = 256;
45
+ function roomLane(locator) {
46
+ return `room ${locator}`;
47
+ }
39
48
  export function composeRuntime(options) {
40
49
  const localSaves = options.adapter.localSaves === true;
41
50
  let authorization = options.authorization;
42
51
  let opening;
43
52
  let store;
44
- const replicas = new Map();
53
+ const fences = new Map();
54
+ const isFenced = (locator) => fences.has(locator);
55
+ let fencings = 0;
56
+ const lastFenced = new Map();
57
+ const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
58
+ let discoveries = 0;
59
+ const replicas = new FencedMap(isFenced);
60
+ const queuesWithoutReplica = new FencedMap(isFenced);
45
61
  const pendings = new Map();
46
- const roomDrafts = new Map();
62
+ const roomDrafts = new FencedMap(isFenced);
47
63
  const presentations = new Map();
48
64
  let limits = [];
49
65
  let capabilities = [];
50
- const rooms = new Map();
66
+ const rooms = new FencedMap(isFenced);
51
67
  const counted = new Map();
52
68
  const unreadable = new Set();
53
- const models = new Map();
54
- const liveModels = new Map();
69
+ const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
70
+ const models = new FencedMap(isModelFenced);
71
+ const liveModels = new FencedMap(isModelFenced);
55
72
  const invalidModels = new Set();
56
73
  const discovered = new Map();
57
74
  let roomsListed = false;
@@ -94,7 +111,13 @@ export function composeRuntime(options) {
94
111
  let complete = false;
95
112
  let recorded = false;
96
113
  const stale = new Set();
97
- let chain = Promise.resolve();
114
+ let generation = 0;
115
+ let discovering = null;
116
+ let opened = null;
117
+ const owed = new Set();
118
+ const lanes = new Map();
119
+ let roomReads = { generation, free: ROOM_READS, waiting: [] };
120
+ const working = new Set();
98
121
  let mutations = Promise.resolve();
99
122
  const roomListeners = new Set();
100
123
  const refusalListeners = new Set();
@@ -105,7 +128,7 @@ export function composeRuntime(options) {
105
128
  const presentationListeners = new Set();
106
129
  const readSide = createReadSide();
107
130
  let withdrawConversations;
108
- let currencyQueued = false;
131
+ let currencyQueued = null;
109
132
  let socketOpen = false;
110
133
  let serviceMode = 'open';
111
134
  let haltedRefusal = false;
@@ -115,7 +138,7 @@ export function composeRuntime(options) {
115
138
  const memberListeners = new Set();
116
139
  const materialized = new Set();
117
140
  let everyRoom = false;
118
- const transfers = new Map();
141
+ const transfers = new FencedMap(isFenced);
119
142
  const conversations = createConversationStore({
120
143
  agents: declaredAgents,
121
144
  documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
@@ -123,6 +146,7 @@ export function composeRuntime(options) {
123
146
  durably,
124
147
  stream: () => options.adapter.conversations,
125
148
  publish: publishChangeSet,
149
+ online: () => !status.providerUnreachable && refusal === null,
126
150
  closed: () => closed,
127
151
  });
128
152
  const searchIndex = createSearchIndex({
@@ -194,7 +218,9 @@ export function composeRuntime(options) {
194
218
  }
195
219
  }
196
220
  function pending() {
197
- return !localSaves && [...replicas.values()].some((replica) => replica.queue.length > 0);
221
+ return (!localSaves &&
222
+ (queuesWithoutReplica.size > 0 ||
223
+ [...replicas.values()].some((replica) => replica.queue.length > 0)));
198
224
  }
199
225
  function attention() {
200
226
  const attentionRequired = [];
@@ -228,12 +254,11 @@ export function composeRuntime(options) {
228
254
  if (replica === undefined)
229
255
  return 0;
230
256
  const roots = replica.confirmed.record.typedSelection?.roots ?? [];
231
- let count = 0;
232
- for (const path of Object.keys(replica.visible.documents)) {
233
- if (markdownPath(path) && !roots.some((root) => beneath(root, path)))
234
- count += 1;
235
- }
236
- return count;
257
+ const paths = [
258
+ ...Object.keys(replica.visible.documents).filter(markdownPath),
259
+ ...Object.keys(replica.visible.binaries),
260
+ ];
261
+ return paths.filter((path) => !roots.some((root) => beneath(root, path))).length;
237
262
  }
238
263
  function listed() {
239
264
  return structuredClone([...rooms.values()].map((room) => {
@@ -304,10 +329,7 @@ export function composeRuntime(options) {
304
329
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
305
330
  }
306
331
  function entriesOf(locator) {
307
- const replica = replicas.get(locator);
308
- if (replica === undefined)
309
- return [];
310
- return replica.queue.map(entryOf);
332
+ return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
311
333
  }
312
334
  function emitQueue(locator) {
313
335
  const entries = entriesOf(locator);
@@ -404,10 +426,7 @@ export function composeRuntime(options) {
404
426
  const revoked = { status: 'unavailable', reason: 'revoked' };
405
427
  try {
406
428
  const local = await openLocal();
407
- await local.revokeModels([...replicas.values()].map((replica) => ({
408
- record: replica.confirmed.record,
409
- queue: replica.queue,
410
- })));
429
+ await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
411
430
  }
412
431
  catch {
413
432
  journal.record('model revocation persistence failed');
@@ -474,19 +493,28 @@ export function composeRuntime(options) {
474
493
  },
475
494
  providerOwnedPaths: room.providerOwnedPaths,
476
495
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
477
- attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
478
496
  synchronizationAvailable,
479
497
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
480
498
  });
481
499
  }
482
- async function applyDiscovery(listed, local) {
500
+ async function applyDiscovery(found, started) {
501
+ for (const room of found) {
502
+ const fence = fences.get(room.locator);
503
+ if (fence !== undefined && fence.erased && fence.at < started)
504
+ fences.delete(room.locator);
505
+ }
506
+ const listed = found.filter((room) => !fences.has(room.locator));
483
507
  const listedLocators = new Set(listed.map((room) => room.locator));
484
508
  for (const locator of listedLocators)
485
509
  scopedRefusals.delete(locator);
486
510
  discovered.clear();
487
511
  for (const room of listed)
488
512
  discovered.set(room.locator, room);
489
- await dropUnlisted(listedLocators, local);
513
+ const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
514
+ if (options.adapter.exhaustiveDiscovery === true)
515
+ await loseRooms(unlisted);
516
+ else
517
+ unlistedKept(unlisted);
490
518
  for (const room of listed) {
491
519
  const held = replicas.get(room.locator)?.confirmed.record;
492
520
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -497,44 +525,79 @@ export function composeRuntime(options) {
497
525
  }
498
526
  roomsListed = true;
499
527
  emitRooms();
528
+ return listed;
500
529
  }
501
530
  function unlistedHeld(listed) {
502
- return [...new Set([...rooms.keys(), ...roomDrafts.keys(), ...transfers.keys()])].filter((locator) => !listed.has(locator));
503
- }
504
- async function dropUnlisted(listed, local) {
505
- if (unlistedHeld(listed).length === 0)
506
- return;
507
- await exclusive(async () => {
508
- const unlisted = unlistedHeld(listed);
509
- await local.dropRooms(unlisted);
510
- const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
511
- room: true,
512
- queue: true,
513
- drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
514
- }));
515
- for (const locator of unlisted) {
516
- for (const batch of replicas.get(locator)?.queue ?? [])
517
- pendings.delete(batch.id);
518
- replicas.delete(locator);
531
+ return [
532
+ ...new Set([
533
+ ...rooms.keys(),
534
+ ...roomDrafts.keys(),
535
+ ...transfers.keys(),
536
+ ...queuesWithoutReplica.keys(),
537
+ ]),
538
+ ].filter((locator) => !listed.has(locator));
539
+ }
540
+ function unlistedKept(unlisted) {
541
+ for (const locator of unlisted) {
542
+ const held = replicas.get(locator)?.confirmed.record;
543
+ if (held !== undefined)
544
+ list(held, false);
545
+ else
519
546
  rooms.delete(locator);
520
- roomDrafts.delete(locator);
521
- transfers.delete(locator);
522
- unreadable.delete(locator);
523
- stale.delete(locator);
524
- scopedRefusals.delete(locator);
525
- sourceRefusals.delete(locator);
526
- declarationRefusals.delete(locator);
527
- for (const attached of ['collection', 'typed-selection']) {
528
- const key = modelKey(locator, attached);
529
- models.delete(key);
530
- liveModels.delete(key);
531
- invalidModels.delete(key);
532
- }
533
- journal.record(`room unlisted ${locator}`);
547
+ }
548
+ }
549
+ function fence(locators) {
550
+ const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
551
+ const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
552
+ room: true,
553
+ queue: true,
554
+ drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
555
+ }));
556
+ for (const locator of fresh) {
557
+ fences.set(locator, { at: discoveries, erased: false });
558
+ lastFenced.set(locator, ++fencings);
559
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
560
+ pendings.delete(batch.id);
561
+ replicas.delete(locator);
562
+ queuesWithoutReplica.delete(locator);
563
+ rooms.delete(locator);
564
+ roomDrafts.delete(locator);
565
+ transfers.delete(locator);
566
+ discovered.delete(locator);
567
+ announced.delete(locator);
568
+ unreadable.delete(locator);
569
+ stale.delete(locator);
570
+ scopedRefusals.delete(locator);
571
+ sourceRefusals.delete(locator);
572
+ declarationRefusals.delete(locator);
573
+ for (const attached of ['collection', 'typed-selection']) {
574
+ const key = modelKey(locator, attached);
575
+ models.delete(key);
576
+ liveModels.delete(key);
577
+ invalidModels.delete(key);
534
578
  }
535
- publishChangeSet(mergeChangeSets(removed));
536
- moveTo({ ...status, ...attention() });
537
- });
579
+ journal.record(`room lost ${locator}`);
580
+ }
581
+ if (fresh.length === 0)
582
+ return fresh;
583
+ publishChangeSet(mergeChangeSets(removed));
584
+ for (const locator of fresh)
585
+ materialized.delete(locator);
586
+ moveTo({ ...status, ...attention() });
587
+ return fresh;
588
+ }
589
+ async function erased(local, locators) {
590
+ await local.erase(locators);
591
+ for (const locator of locators) {
592
+ const held = fences.get(locator);
593
+ if (held !== undefined)
594
+ held.erased = true;
595
+ }
596
+ }
597
+ async function loseRooms(locators) {
598
+ const fresh = fence(locators);
599
+ if (fresh.length > 0)
600
+ await durably((local) => erased(local, fresh));
538
601
  }
539
602
  function restored(key, persisted) {
540
603
  const prior = models.get(key);
@@ -589,6 +652,12 @@ export function composeRuntime(options) {
589
652
  }
590
653
  const previousReplicas = new Map(replicas);
591
654
  replicas.clear();
655
+ queuesWithoutReplica.clear();
656
+ const replicated = new Set(records.map((record) => record.locator));
657
+ for (const [locator, queue] of queuedBatches) {
658
+ if (!replicated.has(locator))
659
+ queuesWithoutReplica.set(locator, queue);
660
+ }
592
661
  for (const record of records) {
593
662
  const model = restored(modelKey(record.locator, 'collection'), record.model);
594
663
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
@@ -602,7 +671,6 @@ export function composeRuntime(options) {
602
671
  rooms.set(record.locator, {
603
672
  ...existing,
604
673
  roomVersion: record.roomVersion,
605
- attachments: record.attachments,
606
674
  operations: record.operations,
607
675
  replica: record.replica,
608
676
  model: modelTypes(model),
@@ -634,8 +702,15 @@ export function composeRuntime(options) {
634
702
  return lastCompleted;
635
703
  }
636
704
  async function open() {
637
- const opened = await openRuntimeStore();
705
+ if (closed)
706
+ throw new Error('runtime closed');
707
+ const opened = await openRuntimeStore(isFenced);
638
708
  store = opened;
709
+ const interrupted = await opened.fences();
710
+ for (const locator of interrupted) {
711
+ fences.set(locator, { at: discoveries, erased: false });
712
+ lastFenced.set(locator, ++fencings);
713
+ }
639
714
  const kept = options.shell.generation;
640
715
  const storedGeneration = await opened.generation();
641
716
  const loss = detectStorageLoss(storedGeneration, kept);
@@ -649,6 +724,8 @@ export function composeRuntime(options) {
649
724
  emitStorageLoss();
650
725
  }
651
726
  const lastCompleted = await readState(opened);
727
+ if (interrupted.length > 0)
728
+ await erased(opened, interrupted);
652
729
  moveTo({ ...status, lastCompleted, ...attention() });
653
730
  return opened;
654
731
  }
@@ -675,6 +752,7 @@ export function composeRuntime(options) {
675
752
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
676
753
  }
677
754
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
755
+ const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
678
756
  const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
679
757
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
680
758
  const unqueued = withdrawn.map((batch) => ({
@@ -700,20 +778,36 @@ export function composeRuntime(options) {
700
778
  });
701
779
  }
702
780
  }
703
- await local.write({
704
- next: confirmed.record,
705
- previous: before?.confirmed.record ?? null,
706
- ...(settling.stored === undefined ? {} : { stored: settling.stored }),
707
- queue: kept,
708
- previousQueue: before?.queue ?? [],
709
- roomDrafts: [...written.values()],
710
- });
711
- for (const batch of before?.queue ?? []) {
781
+ await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
782
+ for (const batch of previousQueue) {
712
783
  if (!kept.includes(batch))
713
784
  pendings.delete(batch.id);
714
785
  }
715
786
  for (const record of written.values())
716
787
  rememberRoomDraft(record);
788
+ try {
789
+ await local.write({
790
+ next: confirmed.record,
791
+ previous: before?.confirmed.record ?? null,
792
+ ...(settling.stored === undefined ? {} : { stored: settling.stored }),
793
+ });
794
+ }
795
+ catch (error) {
796
+ const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
797
+ if (held !== null)
798
+ replicas.set(locator, held);
799
+ else if (kept.length > 0)
800
+ queuesWithoutReplica.set(locator, kept);
801
+ else
802
+ queuesWithoutReplica.delete(locator);
803
+ publishChangeSet(changeSet(locator, before ?? null, held, {
804
+ queue: kept.length !== previousQueue.length,
805
+ drafts: [...written.keys()],
806
+ }));
807
+ moveTo({ ...status, ...attention() });
808
+ throw error;
809
+ }
810
+ queuesWithoutReplica.delete(locator);
717
811
  for (const { batch, returned } of unqueued) {
718
812
  const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
719
813
  journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
@@ -721,7 +815,7 @@ export function composeRuntime(options) {
721
815
  replicas.set(locator, replicaOf(confirmed, kept, before));
722
816
  return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
723
817
  room: settling.room,
724
- queue: kept.length !== (before?.queue.length ?? 0),
818
+ queue: kept.length !== previousQueue.length,
725
819
  drafts: [...written.keys()],
726
820
  });
727
821
  }
@@ -756,10 +850,10 @@ export function composeRuntime(options) {
756
850
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
757
851
  };
758
852
  }
759
- function publish(local, room, confirmed, landing = {}) {
853
+ function publish(local, room, confirmed, landing, live) {
760
854
  const { base, stored } = landing;
761
855
  return exclusive(async () => {
762
- if (!continuing())
856
+ if (!live())
763
857
  return true;
764
858
  const prior = replicas.get(room.locator);
765
859
  if (base !== undefined &&
@@ -768,7 +862,7 @@ export function composeRuntime(options) {
768
862
  return false;
769
863
  }
770
864
  adoptModels(room.locator, confirmed.record);
771
- const queue = prior?.queue ?? [];
865
+ const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
772
866
  const roomVersion = confirmed.record.roomVersion;
773
867
  const accepted = queue.filter((batch) => {
774
868
  const accepted = pendings.get(batch.id)?.accepted;
@@ -787,6 +881,7 @@ export function composeRuntime(options) {
787
881
  list(room, true);
788
882
  publishChangeSet(set);
789
883
  moveTo({ ...status, ...attention() });
884
+ options.adapter.roomStream?.published(room.locator, roomVersion);
790
885
  return true;
791
886
  });
792
887
  }
@@ -810,7 +905,24 @@ export function composeRuntime(options) {
810
905
  if (changed.length > 0)
811
906
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
812
907
  const parts = saved.map(({ path, parts }) => changesOf(path, parts));
813
- const built = buildBatch({
908
+ const changes = parts.flatMap(({ changes }) => changes);
909
+ const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
910
+ if (requeue.kind === 'refused')
911
+ return { outcome: 'refused', ...requeue.refusal };
912
+ const foreign = [...requeue.withdrawn.keys()].some((origin) => {
913
+ const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
914
+ return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
915
+ });
916
+ if (foreign) {
917
+ return {
918
+ outcome: 'refused',
919
+ code: 'unknown-revision',
920
+ retryable: false,
921
+ file: [...requeue.queued].at(-1),
922
+ };
923
+ }
924
+ const withdrawing = requeue.withdrawn.size > 0;
925
+ const input = {
814
926
  local: localSaves,
815
927
  baseRoomVersion: replica.confirmed.record.roomVersion,
816
928
  composition: compositionFor(replica.confirmed.record),
@@ -819,28 +931,86 @@ export function composeRuntime(options) {
819
931
  providerLimits: limits,
820
932
  handle: room.handle,
821
933
  visible: replica.visible,
822
- changes: parts.flatMap(({ changes }) => changes),
934
+ changes,
823
935
  bases: parts.flatMap(({ bases }) => bases),
824
936
  writtenBy,
937
+ };
938
+ const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
939
+ if (checked?.kind === 'refused')
940
+ return { outcome: 'refused', ...checked.refusal };
941
+ const owners = [...requeue.owners].flatMap((origin) => {
942
+ const batch = requeue.withdrawn.get(origin);
943
+ return batch === undefined || batch === null ? [] : [{ origin, batch }];
825
944
  });
826
- if (built.kind === 'refused')
945
+ if (owners.length > 1) {
946
+ return {
947
+ outcome: 'refused',
948
+ code: 'unknown-revision',
949
+ retryable: false,
950
+ file: [...requeue.queued].at(-1),
951
+ };
952
+ }
953
+ const built = !withdrawing
954
+ ? buildBatch(input)
955
+ : requeue.changes.length === 0
956
+ ? null
957
+ : buildBatch({
958
+ ...input,
959
+ visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
960
+ changes: requeue.changes,
961
+ });
962
+ if (built?.kind === 'refused')
827
963
  return { outcome: 'refused', ...built.refusal };
828
- const batches = [...replica.queue, built.batch];
964
+ const [owner] = owners;
965
+ const join = built === null || owner === undefined
966
+ ? null
967
+ : joined(requeue.queue, owner.batch, built.batch);
968
+ if (join?.kind === 'refused')
969
+ return { outcome: 'refused', ...join.refusal };
970
+ const withdrawn = new Map(requeue.withdrawn);
971
+ if (join !== null && owner !== undefined)
972
+ withdrawn.set(owner.origin, join.batch);
973
+ const drafted = new Map();
974
+ for (const [origin, replaced] of withdrawn) {
975
+ if (replaced === null)
976
+ continue;
977
+ const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
978
+ drafted.set(replaced.id, replaced === join?.batch
979
+ ? [
980
+ ...carried.map((held) => joinedDraft(held, saved)),
981
+ ...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
982
+ ]
983
+ : carried);
984
+ }
985
+ if (built !== null && join === null)
986
+ drafted.set(built.batch.id, saved);
987
+ const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
988
+ for (const replaced of withdrawn.values()) {
989
+ if (replaced === null || !batches.includes(replaced))
990
+ continue;
991
+ const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
992
+ if (limited !== null)
993
+ return { outcome: 'refused', ...limited };
994
+ }
829
995
  const consumed = saved.map(({ path }) => path);
830
996
  try {
831
- await local.write({
832
- next: replica.confirmed.record,
833
- previous: replica.confirmed.record,
997
+ await local.keepAuthored({
998
+ locator,
834
999
  queue: batches,
835
- previousQueue: replica.queue,
836
- saved,
1000
+ saved: drafted,
837
1001
  consumedRoomDrafts: consumed,
838
1002
  });
839
1003
  }
840
1004
  catch {
841
1005
  return { outcome: 'refused', code: 'durable-write', retryable: true };
842
1006
  }
843
- pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
1007
+ if (fences.has(locator))
1008
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1009
+ for (const origin of withdrawn.keys())
1010
+ pendings.delete(origin);
1011
+ for (const [id, drafts] of drafted) {
1012
+ pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
1013
+ }
844
1014
  if (!localSaves)
845
1015
  recorded = false;
846
1016
  for (const path of consumed)
@@ -852,7 +1022,10 @@ export function composeRuntime(options) {
852
1022
  }));
853
1023
  moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
854
1024
  submitLater(locator);
855
- return { outcome: 'saved', identifier: built.batch.id };
1025
+ const identifier = join?.batch.id ??
1026
+ built?.batch.id ??
1027
+ [...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
1028
+ return { outcome: 'saved', identifier };
856
1029
  }
857
1030
  async function mark(local, locator, identifier, submission, lost) {
858
1031
  const state = pendings.get(identifier);
@@ -862,6 +1035,8 @@ export function composeRuntime(options) {
862
1035
  return;
863
1036
  }
864
1037
  await local.submit(locator, identifier, submission, lost);
1038
+ if (fences.has(locator))
1039
+ return;
865
1040
  pendings.set(identifier, { ...state, submission, lost });
866
1041
  const replica = replicas.get(locator);
867
1042
  if (replica !== undefined)
@@ -872,6 +1047,8 @@ export function composeRuntime(options) {
872
1047
  if (!continuing())
873
1048
  throw new Error('connection refused');
874
1049
  await local.keepUpload(locator, identifier, upload);
1050
+ if (fences.has(locator))
1051
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
875
1052
  pendings.set(identifier, { ...pendings.get(identifier), upload });
876
1053
  if (!continuing())
877
1054
  throw new Error('connection refused');
@@ -887,6 +1064,7 @@ export function composeRuntime(options) {
887
1064
  }
888
1065
  if (closed || authorization === undefined || refusal !== null)
889
1066
  return UNCERTAIN;
1067
+ const since = fencings;
890
1068
  const transfer = ['transfer', 'history', locator, path];
891
1069
  if (query.revision !== undefined)
892
1070
  transfer.push(query.revision);
@@ -901,6 +1079,8 @@ export function composeRuntime(options) {
901
1079
  ...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
902
1080
  });
903
1081
  journal.record(`outcome history ${result.kind}`);
1082
+ if (fencedSince(locator, since))
1083
+ return failure({ kind: 'room-not-readable', locator });
904
1084
  if (result.kind === 'scoped-refusal') {
905
1085
  const room = replicas.get(result.locator)?.confirmed.record;
906
1086
  if (room !== undefined)
@@ -915,6 +1095,100 @@ export function composeRuntime(options) {
915
1095
  return UNCERTAIN;
916
1096
  }
917
1097
  }
1098
+ function queuedBlob(locator, path, localId) {
1099
+ for (const batch of replicas.get(locator)?.queue ?? []) {
1100
+ const change = batch.changes[queuedFile(batch, path, localId)];
1101
+ if (change?.kind === 'binary')
1102
+ return change.blob;
1103
+ }
1104
+ return null;
1105
+ }
1106
+ function binaryRoom(request) {
1107
+ if (request.kind === 'path')
1108
+ return request.locator;
1109
+ const scope = options.adapter.referrerScope(request.referrer);
1110
+ return scope?.kind === 'room' ? scope.locator : null;
1111
+ }
1112
+ async function readBinary(request) {
1113
+ if (closed || refusal !== null)
1114
+ return UNCERTAIN;
1115
+ try {
1116
+ await ensureAuthorization();
1117
+ }
1118
+ catch {
1119
+ return UNCERTAIN;
1120
+ }
1121
+ if (closed || authorization === undefined || refusal !== null)
1122
+ return UNCERTAIN;
1123
+ const since = fencings;
1124
+ journal.record(request.kind === 'path'
1125
+ ? `transfer binary ${request.locator} ${request.path}`
1126
+ : `transfer binary ${request.fileRef}`);
1127
+ try {
1128
+ const result = await options.adapter.binary(authorization, request);
1129
+ journal.record(`outcome binary ${result.kind}`);
1130
+ const locator = binaryRoom(request);
1131
+ if (locator !== null && fencedSince(locator, since))
1132
+ return failure({ kind: 'room-not-readable', locator });
1133
+ if (result.kind === 'scoped-refusal') {
1134
+ const room = replicas.get(result.locator)?.confirmed.record;
1135
+ if (room !== undefined)
1136
+ await scoped(result, room, await openLocal());
1137
+ }
1138
+ if (result.kind === 'revoked-authorization')
1139
+ await refuse('revoked-authorization');
1140
+ return binaryAnswer(result);
1141
+ }
1142
+ catch {
1143
+ journal.record('outcome binary transport-uncertainty');
1144
+ return UNCERTAIN;
1145
+ }
1146
+ }
1147
+ async function readBinaryPath(locator, path, revision) {
1148
+ await ready;
1149
+ if (revision !== undefined)
1150
+ return readBinary({ kind: 'path', locator, path, revision });
1151
+ const visible = replicas.get(locator)?.visible;
1152
+ const held = visible?.binaries[path];
1153
+ if (visible === undefined || held === undefined)
1154
+ return absentBinary(path);
1155
+ if (held.state === 'pending') {
1156
+ const blob = queuedBlob(locator, path, held.localId);
1157
+ return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1158
+ }
1159
+ const record = binaryRecord(path, held, visible.revisions[path]);
1160
+ if (record.state !== 'accepted')
1161
+ return absentBinary(path);
1162
+ const replica = replicas.get(locator);
1163
+ const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
1164
+ const holds = (at) => {
1165
+ const kept = replica?.confirmed.record.binaries[at];
1166
+ return (kept?.state === 'accepted' &&
1167
+ kept.fileRef === record.fileRef &&
1168
+ replica?.confirmed.record.revisions[at] === record.revision);
1169
+ };
1170
+ let confirmed = path;
1171
+ for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
1172
+ const change = queued[index];
1173
+ if (change.kind === 'move' && change.to === confirmed)
1174
+ confirmed = change.path;
1175
+ }
1176
+ return readBinary({
1177
+ kind: 'path',
1178
+ locator,
1179
+ path: confirmed,
1180
+ revision: record.revision,
1181
+ fileRef: record.fileRef,
1182
+ });
1183
+ }
1184
+ async function readBinaryReference(reference) {
1185
+ await ready;
1186
+ return readBinary({
1187
+ kind: 'reference',
1188
+ fileRef: reference.fileRef,
1189
+ referrer: reference.referrer,
1190
+ });
1191
+ }
918
1192
  function declaredAgents() {
919
1193
  for (const capability of capabilities) {
920
1194
  if (capability.conversationAgents !== undefined)
@@ -979,11 +1253,11 @@ export function composeRuntime(options) {
979
1253
  options.adapter.reached(locator, written, roomVersion) &&
980
1254
  !options.adapter.reached(locator, roomVersion, written));
981
1255
  }
982
- async function pullRefresh(authorized, confirmed) {
983
- if (!continuing())
1256
+ async function pullRefresh(authorized, confirmed, live) {
1257
+ if (!live())
984
1258
  return unconfirmed;
985
1259
  try {
986
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
1260
+ const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
987
1261
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
988
1262
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
989
1263
  return result;
@@ -993,33 +1267,39 @@ export function composeRuntime(options) {
993
1267
  throw error;
994
1268
  }
995
1269
  }
996
- function staging(local) {
1270
+ function staging(local, live) {
997
1271
  return {
998
1272
  held: (locator) => transfers.get(locator) ?? null,
999
1273
  staged: (record) => local.staged(record),
1000
1274
  stage: (record, page) => exclusive(async () => {
1001
- if (!continuing())
1275
+ if (!live())
1002
1276
  return false;
1003
1277
  await local.stage(record, page);
1004
1278
  transfers.set(record.locator, record);
1005
1279
  return true;
1006
1280
  }),
1007
1281
  abandon: (locator) => exclusive(async () => {
1008
- if (!continuing())
1282
+ if (!live())
1009
1283
  return;
1010
1284
  await local.abandon(locator);
1011
1285
  transfers.delete(locator);
1012
1286
  }),
1013
1287
  };
1014
1288
  }
1015
- async function pullBootstrap(authorized, room, local) {
1016
- if (!continuing())
1289
+ function reusable(room) {
1290
+ const record = replicas.get(room.locator)?.confirmed.record;
1291
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1292
+ return null;
1293
+ return { documents: record.documents, revisions: record.revisions };
1294
+ }
1295
+ async function pullBootstrap(authorized, room, local, live) {
1296
+ if (!live())
1017
1297
  return unconfirmed;
1018
1298
  try {
1019
1299
  const result = await bootstrapRoom(options.adapter, authorized, room, {
1020
1300
  collection: modelOf(room.locator),
1021
1301
  selection: modelOf(room.locator, 'typed-selection'),
1022
- }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
1302
+ }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
1023
1303
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
1024
1304
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1025
1305
  return result;
@@ -1029,11 +1309,11 @@ export function composeRuntime(options) {
1029
1309
  throw error;
1030
1310
  }
1031
1311
  }
1032
- async function advance(room, local, authorized) {
1312
+ async function advance(room, local, authorized, live) {
1033
1313
  const held = replicas.get(room.locator);
1034
1314
  if (held !== undefined) {
1035
- const refreshed = await pullRefresh(authorized, held.confirmed);
1036
- if (!continuing())
1315
+ const refreshed = await pullRefresh(authorized, held.confirmed, live);
1316
+ if (!live())
1037
1317
  return unconfirmed;
1038
1318
  if (refreshed.kind === 'still-current') {
1039
1319
  stale.delete(room.locator);
@@ -1047,24 +1327,28 @@ export function composeRuntime(options) {
1047
1327
  return null;
1048
1328
  }
1049
1329
  if (refreshed.kind === 'advanced') {
1050
- if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
1330
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
1051
1331
  const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
1052
1332
  const reached = now !== undefined &&
1053
1333
  options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
1054
- return reached ? null : advance(room, local, authorized);
1334
+ return reached ? null : advance(room, local, authorized, live);
1055
1335
  }
1056
1336
  await retainModel(local, room, 'collection', modelOf(room.locator));
1057
1337
  return null;
1058
1338
  }
1059
1339
  if (refreshed.kind !== 'beyond-horizon')
1060
1340
  return refreshed;
1341
+ if (opened !== null) {
1342
+ owed.add(room.locator);
1343
+ return unconfirmed;
1344
+ }
1061
1345
  }
1062
- const bootstrapped = await pullBootstrap(authorized, room, local);
1063
- if (!continuing())
1346
+ const bootstrapped = await pullBootstrap(authorized, room, local, live);
1347
+ if (!live())
1064
1348
  return unconfirmed;
1065
1349
  if (bootstrapped.kind !== 'staged')
1066
1350
  return bootstrapped;
1067
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1351
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
1068
1352
  return null;
1069
1353
  }
1070
1354
  async function withhold(withheld, room, local, progress) {
@@ -1138,13 +1422,13 @@ export function composeRuntime(options) {
1138
1422
  logAccepted(room.locator, submitted, accepted);
1139
1423
  const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1140
1424
  ? { kind: 'staged', confirmed: current.confirmed }
1141
- : applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.changes, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
1425
+ : applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
1142
1426
  ? accepted.roomVersion
1143
1427
  : held, accepted.roomVersion);
1144
1428
  if (applied.kind !== 'staged') {
1145
1429
  const state = pendings.get(submitted.id);
1146
1430
  await local.accept(room.locator, submitted.id, accepted.roomVersion);
1147
- if (state !== undefined)
1431
+ if (state !== undefined && !fences.has(room.locator))
1148
1432
  pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1149
1433
  emitQueue(room.locator);
1150
1434
  return applied;
@@ -1193,7 +1477,7 @@ export function composeRuntime(options) {
1193
1477
  let result;
1194
1478
  journal.record(`submission ${room.locator} ${head.id}`);
1195
1479
  await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1196
- if (discoveryPaused)
1480
+ if (discoveryPaused || fences.has(room.locator))
1197
1481
  return;
1198
1482
  if (writesHeld()) {
1199
1483
  await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
@@ -1256,7 +1540,7 @@ export function composeRuntime(options) {
1256
1540
  result.kind === 'expired' ||
1257
1541
  result.kind === 'retryable-refusal') {
1258
1542
  if (result.kind === 'expired') {
1259
- const withheld = await advance(room, local, authorized);
1543
+ const withheld = await advance(room, local, authorized, continuing);
1260
1544
  if (withheld !== null)
1261
1545
  return withhold(withheld, room, local, progress);
1262
1546
  }
@@ -1294,7 +1578,7 @@ export function composeRuntime(options) {
1294
1578
  });
1295
1579
  if (result.kind === 'expired')
1296
1580
  continue;
1297
- const withheld = await advance(room, local, authorized);
1581
+ const withheld = await advance(room, local, authorized, continuing);
1298
1582
  if (withheld !== null)
1299
1583
  return withhold(withheld, room, local, progress);
1300
1584
  continue;
@@ -1311,9 +1595,9 @@ export function composeRuntime(options) {
1311
1595
  const awaited = new Promise((resolve) => {
1312
1596
  release = resolve;
1313
1597
  });
1314
- answering.set(locator, awaited);
1598
+ answering.set(locator, { generation, answered: awaited });
1315
1599
  return () => {
1316
- if (answering.get(locator) === awaited)
1600
+ if (answering.get(locator)?.answered === awaited)
1317
1601
  answering.delete(locator);
1318
1602
  release();
1319
1603
  };
@@ -1357,11 +1641,7 @@ export function composeRuntime(options) {
1357
1641
  const read = reclassified.kind === 'deferred'
1358
1642
  ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
1359
1643
  : reclassified;
1360
- await local.write({
1361
- next: record,
1362
- previous: held.confirmed.record,
1363
- queue: held.queue,
1364
- });
1644
+ await local.write({ next: record, previous: held.confirmed.record });
1365
1645
  if (read.kind === 'staged') {
1366
1646
  replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1367
1647
  if (declarationRefusals.delete(room.locator)) {
@@ -1429,13 +1709,14 @@ export function composeRuntime(options) {
1429
1709
  roomVersion: record.roomVersion,
1430
1710
  documents: record.documents,
1431
1711
  revisions: record.revisions,
1432
- attachments: record.attachments,
1433
1712
  },
1434
1713
  ];
1435
1714
  });
1436
1715
  }
1437
- async function synchronize(listed, local, progress, attempt) {
1716
+ async function synchronize(listed, local, progress, attempt, live) {
1438
1717
  const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1718
+ if (!live())
1719
+ return;
1439
1720
  const ready = listed.filter((_, index) => {
1440
1721
  const outcome = prepared[index];
1441
1722
  return outcome?.status === 'fulfilled' && outcome.value;
@@ -1444,9 +1725,10 @@ export function composeRuntime(options) {
1444
1725
  options.adapter.roomStream?.hold(heldRooms(ready));
1445
1726
  handed = true;
1446
1727
  }
1728
+ discovering?.release();
1447
1729
  const loaded = await Promise.allSettled(ready
1448
1730
  .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1449
- .map((room) => loadRoom(room, local, progress)));
1731
+ .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
1450
1732
  const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1451
1733
  if (failed !== undefined)
1452
1734
  throw failed.reason;
@@ -1475,14 +1757,14 @@ export function composeRuntime(options) {
1475
1757
  }
1476
1758
  return true;
1477
1759
  }
1478
- async function caughtUp(room, local, progress) {
1760
+ async function caughtUp(room, local, progress, live) {
1479
1761
  const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1480
- if (held === undefined || authorization === undefined)
1762
+ if (held === undefined || authorization === undefined || !live())
1481
1763
  return;
1482
1764
  const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1483
1765
  if (!behind)
1484
1766
  return;
1485
- const withheld = await advance(room, local, authorization);
1767
+ const withheld = await advance(room, local, authorization, live);
1486
1768
  if (withheld !== null)
1487
1769
  await withhold(withheld, room, local, progress);
1488
1770
  }
@@ -1492,39 +1774,41 @@ export function composeRuntime(options) {
1492
1774
  return model.status === 'unavailable' && model.reason === 'revoked';
1493
1775
  });
1494
1776
  }
1495
- async function loadRoom(room, local, progress) {
1496
- if (discoveryPaused || authorization === undefined)
1777
+ async function loadRoom(room, local, progress, live) {
1778
+ if (!live() || authorization === undefined)
1497
1779
  return;
1498
1780
  if (replicas.has(room.locator) && revoked(room.locator)) {
1499
- await reload(room, local, progress);
1781
+ await reload(room, local, progress, live);
1500
1782
  return;
1501
1783
  }
1502
- const withheld = await advance(room, local, authorization);
1784
+ const withheld = await advance(room, local, authorization, live);
1503
1785
  if (withheld !== null) {
1504
1786
  await withhold(withheld, room, local, progress);
1505
1787
  return;
1506
1788
  }
1507
- await caughtUp(room, local, progress);
1789
+ await caughtUp(room, local, progress, live);
1508
1790
  }
1509
- async function reload(room, local, progress) {
1510
- if (discoveryPaused || authorization === undefined)
1791
+ async function reload(room, local, progress, live) {
1792
+ if (!live() || authorization === undefined)
1511
1793
  return;
1512
- const bootstrapped = await pullBootstrap(authorization, room, local);
1513
- if (!continuing())
1794
+ const bootstrapped = await pullBootstrap(authorization, room, local, live);
1795
+ if (!live())
1514
1796
  return;
1515
1797
  if (bootstrapped.kind !== 'staged') {
1516
1798
  await withhold(bootstrapped, room, local, progress);
1517
1799
  return;
1518
1800
  }
1519
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1520
- await caughtUp(room, local, progress);
1801
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
1802
+ await caughtUp(room, local, progress, live);
1521
1803
  }
1522
- async function discoverRooms(local, progress, attempt) {
1804
+ async function discoverRooms(local, progress, attempt, live) {
1523
1805
  if (closed || authorization === undefined) {
1524
1806
  notifyReady();
1525
1807
  return;
1526
1808
  }
1527
1809
  moveTo(transferring(status));
1810
+ discoveries += 1;
1811
+ const started = discoveries;
1528
1812
  let result;
1529
1813
  try {
1530
1814
  result = await options.adapter.discover(authorization);
@@ -1532,7 +1816,8 @@ export function composeRuntime(options) {
1532
1816
  catch {
1533
1817
  result = { kind: 'transport-uncertainty' };
1534
1818
  }
1535
- if (closed) {
1819
+ if (!live()) {
1820
+ progress.complete = false;
1536
1821
  notifyReady();
1537
1822
  return;
1538
1823
  }
@@ -1602,8 +1887,7 @@ export function composeRuntime(options) {
1602
1887
  member = signedIn;
1603
1888
  emitMember();
1604
1889
  }
1605
- await applyDiscovery(result.rooms, local);
1606
- await synchronize(result.rooms, local, progress, attempt);
1890
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
1607
1891
  }
1608
1892
  async function ensureAuthorization() {
1609
1893
  if (authorization !== undefined)
@@ -1617,20 +1901,50 @@ export function composeRuntime(options) {
1617
1901
  throw error;
1618
1902
  }
1619
1903
  }
1620
- function task(work) {
1904
+ async function admittedRead(work) {
1905
+ if (roomReads.generation !== generation) {
1906
+ roomReads = { generation, free: ROOM_READS, waiting: [] };
1907
+ }
1908
+ const reads = roomReads;
1909
+ while (reads.free === 0)
1910
+ await new Promise((admit) => reads.waiting.push(admit));
1911
+ reads.free -= 1;
1912
+ try {
1913
+ await work();
1914
+ }
1915
+ finally {
1916
+ reads.free += 1;
1917
+ reads.waiting.shift()?.();
1918
+ }
1919
+ }
1920
+ function roomTask(locator, work) {
1921
+ return task(roomLane(locator), (live) => admittedRead(() => work(live)));
1922
+ }
1923
+ function task(lane, work) {
1621
1924
  if (stopping)
1622
- return chain;
1623
- chain = chain.then(work, work).catch(() => undefined);
1624
- return chain;
1925
+ return Promise.resolve();
1926
+ const held = lanes.get(lane);
1927
+ const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
1928
+ const turn = current.generation;
1929
+ const live = () => turn === generation && continuing();
1930
+ const run = current.chain.then(() => work(live)).catch(() => undefined);
1931
+ current.chain = run;
1932
+ lanes.set(lane, current);
1933
+ working.add(run);
1934
+ void run.finally(() => working.delete(run));
1935
+ return run;
1625
1936
  }
1626
1937
  function sync(attempt) {
1627
- return task(async () => {
1628
- if (closed || stopping || discoveryPaused) {
1938
+ return task(OPENING_LANE, async (live) => {
1939
+ if (!live()) {
1629
1940
  notifyReady();
1630
1941
  return;
1631
1942
  }
1632
1943
  const progress = { complete: true, reachable: true };
1633
- underway = attempt === 'opened' ? progress : null;
1944
+ const gate = discover();
1945
+ if (attempt === 'opened')
1946
+ opened = generation;
1947
+ underway = attempt === 'opened' ? { generation, progress } : null;
1634
1948
  if (attempt === 'opened')
1635
1949
  streamed = progress;
1636
1950
  complete = false;
@@ -1639,12 +1953,12 @@ export function composeRuntime(options) {
1639
1953
  handed = false;
1640
1954
  try {
1641
1955
  const local = await openLocal();
1642
- if (closed)
1956
+ if (!live())
1643
1957
  return;
1644
1958
  await ensureAuthorization();
1645
- if (closed)
1959
+ if (!live())
1646
1960
  return;
1647
- await discoverRooms(local, progress, attempt);
1961
+ await discoverRooms(local, progress, attempt, live);
1648
1962
  }
1649
1963
  catch {
1650
1964
  progress.complete = false;
@@ -1652,17 +1966,21 @@ export function composeRuntime(options) {
1652
1966
  progress.reachable = false;
1653
1967
  }
1654
1968
  finally {
1969
+ gate.release();
1655
1970
  notifyReady();
1656
1971
  }
1972
+ if (!live())
1973
+ return;
1657
1974
  if (attempt !== 'opened') {
1658
- await finishOpening(progress);
1975
+ await finishOpening(progress, live);
1659
1976
  return;
1660
1977
  }
1661
1978
  if (handed || closed || progress.reachable)
1662
1979
  return;
1663
1980
  underway = null;
1981
+ opened = null;
1664
1982
  journal.record('connection reopen');
1665
- await rest(progress);
1983
+ await rest(progress, live);
1666
1984
  options.adapter.roomStream?.reopen();
1667
1985
  });
1668
1986
  }
@@ -1683,24 +2001,38 @@ export function composeRuntime(options) {
1683
2001
  });
1684
2002
  }
1685
2003
  function applyOpening(expired) {
1686
- return task(async () => {
1687
- const progress = underway;
2004
+ const progress = underway?.generation === generation ? underway.progress : null;
2005
+ const cycle = progress ?? { complete: true, reachable: true };
2006
+ const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
2007
+ return task(OPENING_LANE, async (live) => {
1688
2008
  underway = null;
1689
- if (closed || stopping)
2009
+ if (!live())
1690
2010
  return;
1691
2011
  if (progress === null) {
1692
- await reloadExpired(expired);
2012
+ await replacing;
2013
+ await rest(cycle, live);
1693
2014
  return;
1694
2015
  }
2016
+ await roomsCarried(expired);
2017
+ if (!live())
2018
+ return;
2019
+ if (opened === generation)
2020
+ opened = null;
2021
+ const replaced = new Set(owed);
2022
+ owed.clear();
1695
2023
  try {
1696
2024
  const local = await openLocal();
1697
2025
  const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1698
- const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1699
- const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
2026
+ const reloading = held.filter((room) => !expired.includes(room.locator) &&
2027
+ (replaced.has(room.locator) || revoked(room.locator)));
2028
+ const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
2029
+ const reloaded = [...(await reloads), ...(await replacing)];
1700
2030
  if (progress.reachable) {
1701
- for (const room of held)
1702
- if (!reloading.includes(room))
2031
+ for (const room of held) {
2032
+ if (!expired.includes(room.locator) && !reloading.includes(room)) {
1703
2033
  await confirmModels(room, local);
2034
+ }
2035
+ }
1704
2036
  }
1705
2037
  const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1706
2038
  if (failed !== undefined)
@@ -1710,21 +2042,21 @@ export function composeRuntime(options) {
1710
2042
  progress.complete = false;
1711
2043
  progress.reachable = false;
1712
2044
  }
1713
- await finishOpening(progress);
2045
+ if (live())
2046
+ await finishOpening(progress, live);
1714
2047
  });
1715
2048
  }
1716
- async function reloadExpired(expired) {
1717
- const progress = { complete: true, reachable: true };
2049
+ async function reloadNow(replaced, progress) {
1718
2050
  try {
1719
2051
  const local = await openLocal();
1720
- await Promise.allSettled([...discovered.values()]
1721
- .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1722
- .map((room) => reload(room, local, progress)));
2052
+ return await Promise.allSettled([...discovered.values()]
2053
+ .filter((room) => replicas.has(room.locator) && replaced(room))
2054
+ .map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
1723
2055
  }
1724
2056
  catch {
1725
2057
  progress.reachable = false;
2058
+ return [];
1726
2059
  }
1727
- await rest(progress);
1728
2060
  }
1729
2061
  async function confirmModels(room, local) {
1730
2062
  for (const attached of ['collection', 'typed-selection']) {
@@ -1776,9 +2108,9 @@ export function composeRuntime(options) {
1776
2108
  submitLater(locator);
1777
2109
  }
1778
2110
  }
1779
- async function finishOpening(progress) {
2111
+ async function finishOpening(progress, live) {
1780
2112
  try {
1781
- if (closed || discoveryPaused) {
2113
+ if (!live()) {
1782
2114
  progress.complete = false;
1783
2115
  return;
1784
2116
  }
@@ -1788,6 +2120,8 @@ export function composeRuntime(options) {
1788
2120
  conversations.reconnected();
1789
2121
  const reading = { complete: true, reachable: true };
1790
2122
  await conversations.catchUp(reading);
2123
+ if (progress.reachable && reading.reachable)
2124
+ await conversations.resend(reading);
1791
2125
  if (!reading.reachable)
1792
2126
  progress.reachable = false;
1793
2127
  }
@@ -1795,12 +2129,14 @@ export function composeRuntime(options) {
1795
2129
  progress.complete = false;
1796
2130
  }
1797
2131
  finally {
1798
- complete = progress.complete;
1799
- await rest(progress);
2132
+ if (live()) {
2133
+ complete = progress.complete;
2134
+ await rest(progress, live);
2135
+ }
1800
2136
  }
1801
2137
  }
1802
- async function rest(progress) {
1803
- if (closed)
2138
+ async function rest(progress, live = () => !closed) {
2139
+ if (!live())
1804
2140
  return;
1805
2141
  let lastCompleted = status.lastCompleted;
1806
2142
  let synced = complete && stale.size === 0 && !pending();
@@ -1808,6 +2144,8 @@ export function composeRuntime(options) {
1808
2144
  const completedAt = new Date().toISOString();
1809
2145
  try {
1810
2146
  await (await openLocal()).complete(completedAt);
2147
+ if (!live())
2148
+ return;
1811
2149
  recorded = true;
1812
2150
  lastCompleted = completedAt;
1813
2151
  }
@@ -1822,11 +2160,23 @@ export function composeRuntime(options) {
1822
2160
  ...attention(),
1823
2161
  });
1824
2162
  }
2163
+ function discover() {
2164
+ let release = () => { };
2165
+ const done = new Promise((resolve) => {
2166
+ release = resolve;
2167
+ });
2168
+ discovering = { generation, done, release };
2169
+ return discovering;
2170
+ }
1825
2171
  function applyRoom(locator, roomVersion) {
1826
2172
  announced.set(locator, roomVersion);
1827
- return task(async () => {
1828
- await answering.get(locator);
1829
- if (closed || stopping || discoveryPaused || authorization === undefined)
2173
+ return task(roomLane(locator), async (live) => {
2174
+ if (discovering?.generation === generation)
2175
+ await discovering.done;
2176
+ const saving = answering.get(locator);
2177
+ if (saving?.generation === generation)
2178
+ await saving.answered;
2179
+ if (!live() || authorization === undefined)
1830
2180
  return;
1831
2181
  const room = discovered.get(locator);
1832
2182
  const held = replicas.get(locator);
@@ -1837,25 +2187,41 @@ export function composeRuntime(options) {
1837
2187
  options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
1838
2188
  return;
1839
2189
  }
1840
- const during = underway;
2190
+ const during = underway?.generation === generation ? underway.progress : null;
1841
2191
  const progress = during ?? { complete: true, reachable: true };
1842
2192
  if (during === null)
1843
2193
  moveTo(transferring(status));
1844
2194
  try {
1845
2195
  const local = await openLocal();
1846
- const withheld = await advance(room, local, authorization);
1847
- if (withheld !== null)
1848
- await withhold(withheld, room, local, progress);
2196
+ await admittedRead(async () => {
2197
+ if (!live() || authorization === undefined)
2198
+ return;
2199
+ const withheld = await advance(room, local, authorization, live);
2200
+ if (withheld !== null)
2201
+ await withhold(withheld, room, local, progress);
2202
+ });
1849
2203
  }
1850
2204
  catch {
1851
2205
  stale.add(locator);
1852
2206
  }
1853
2207
  finally {
1854
2208
  if (during === null)
1855
- await rest(progress);
2209
+ await rest(progress, live);
1856
2210
  }
1857
2211
  });
1858
2212
  }
2213
+ function roomsCarried(replacing) {
2214
+ const replaced = new Set(replacing.map(roomLane));
2215
+ return Promise.all([...lanes]
2216
+ .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
2217
+ .map(([, held]) => held.chain));
2218
+ }
2219
+ const receipts = {
2220
+ heads: async () => (await openLocal()).receivedHeads(),
2221
+ parts: async (key, count) => (await openLocal()).receivedParts(key, count),
2222
+ keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
2223
+ drop: async (keys) => (await openLocal()).dropReceived(keys),
2224
+ };
1859
2225
  function observeStream() {
1860
2226
  const stream = options.adapter.roomStream;
1861
2227
  if (stream === undefined || withdrawStream !== undefined)
@@ -1865,6 +2231,7 @@ export function composeRuntime(options) {
1865
2231
  return;
1866
2232
  if (event.kind === 'opened') {
1867
2233
  socketOpen = true;
2234
+ generation += 1;
1868
2235
  void sync('opened');
1869
2236
  }
1870
2237
  else if (event.kind === 'closed')
@@ -1873,7 +2240,7 @@ export function composeRuntime(options) {
1873
2240
  void applyOpening(event.expired);
1874
2241
  else
1875
2242
  void applyRoom(event.locator, event.roomVersion);
1876
- });
2243
+ }, receipts);
1877
2244
  }
1878
2245
  function writesHeld() {
1879
2246
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -1911,14 +2278,15 @@ export function composeRuntime(options) {
1911
2278
  });
1912
2279
  }
1913
2280
  function scheduleCurrency() {
1914
- if (currencyQueued || closed || stopping)
2281
+ if (currencyQueued === generation || closed || stopping)
1915
2282
  return;
1916
- currencyQueued = true;
1917
- void task(runCurrency);
2283
+ currencyQueued = generation;
2284
+ void task(CONVERSATION_LANE, runCurrency);
1918
2285
  }
1919
- async function runCurrency() {
1920
- currencyQueued = false;
1921
- if (closed || stopping || discoveryPaused)
2286
+ async function runCurrency(live) {
2287
+ if (currencyQueued === generation)
2288
+ currencyQueued = null;
2289
+ if (!live())
1922
2290
  return;
1923
2291
  await conversations.reconcile();
1924
2292
  if (conversations.current()) {
@@ -1933,7 +2301,7 @@ export function composeRuntime(options) {
1933
2301
  catch {
1934
2302
  progress.reachable = false;
1935
2303
  }
1936
- await rest(progress);
2304
+ await rest(progress, live);
1937
2305
  }
1938
2306
  function close() {
1939
2307
  if (closed)
@@ -1964,7 +2332,7 @@ export function composeRuntime(options) {
1964
2332
  async function stop() {
1965
2333
  stopping = true;
1966
2334
  stopTimer();
1967
- await chain.catch(() => undefined);
2335
+ await Promise.all(working);
1968
2336
  await Promise.all([...submitting.values()].map((run) => run.done));
1969
2337
  await mutations;
1970
2338
  close();
@@ -2022,6 +2390,8 @@ export function composeRuntime(options) {
2022
2390
  }
2023
2391
  if (topic === 'conversations')
2024
2392
  return conversations.watchList(deliver);
2393
+ if (topic === 'agents')
2394
+ return conversations.watchAgents(deliver);
2025
2395
  if (topic === 'presentation') {
2026
2396
  const listener = {
2027
2397
  app: input[0],
@@ -2135,11 +2505,14 @@ export function composeRuntime(options) {
2135
2505
  if ((current?.revision ?? null) !== revision) {
2136
2506
  return staleWrite(current === undefined ? null : roomDraftOf(current));
2137
2507
  }
2508
+ const file = parts.file === undefined
2509
+ ? undefined
2510
+ : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
2138
2511
  const record = {
2139
2512
  locator: room,
2140
2513
  path,
2141
2514
  revision: roomDraftRevision(),
2142
- parts: held(current?.parts, parts),
2515
+ parts: held(current?.parts, parts, file),
2143
2516
  returned: current?.returned ?? null,
2144
2517
  };
2145
2518
  await local.keepRoomDraft(record);
@@ -2188,6 +2561,10 @@ export function composeRuntime(options) {
2188
2561
  history(locator, path, query) {
2189
2562
  return readHistory(locator, path, query);
2190
2563
  },
2564
+ binaryFile: {
2565
+ read: readBinaryPath,
2566
+ readReference: readBinaryReference,
2567
+ },
2191
2568
  call(name, input) {
2192
2569
  return invokeAction(name, input);
2193
2570
  },
@@ -2207,23 +2584,26 @@ export function composeRuntime(options) {
2207
2584
  if (refusal === null || !refusal.reauthorizationOffered)
2208
2585
  return;
2209
2586
  journal.record('connection reauthorize');
2210
- await chain.catch(() => undefined);
2587
+ await Promise.all([...lanes.values()]
2588
+ .filter((held) => held.generation === generation)
2589
+ .map((held) => held.chain));
2211
2590
  await mutations;
2212
2591
  authorization = undefined;
2213
2592
  discoveryPaused = false;
2214
2593
  if (options.adapter.roomStream === undefined)
2215
2594
  await sync('unstreamed');
2216
2595
  else
2217
- await task(ensureAuthorization);
2596
+ await task(OPENING_LANE, ensureAuthorization);
2218
2597
  },
2219
2598
  };
2220
2599
  const reachable = {
2221
2600
  md,
2222
2601
  watch,
2602
+ referrerScope: (referrer) => options.adapter.referrerScope(referrer),
2223
2603
  plan: planned,
2224
2604
  query: (plan) => rowsQuery(plan),
2225
2605
  watchPlan,
2226
2606
  search: searched,
2227
2607
  };
2228
- return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2608
+ return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
2229
2609
  }