@agentshouse/mdruntime 0.1.0-c95464765b116 → 0.1.0-c969b8dee6cc8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/AGENTS.md +238 -233
  2. package/README.md +8 -8
  3. package/dist/adapter.d.ts +20 -6
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/bootstrap.d.ts +2 -1
  6. package/dist/bootstrap.d.ts.map +1 -1
  7. package/dist/bootstrap.js +4 -0
  8. package/dist/change-set.d.ts.map +1 -1
  9. package/dist/change-set.js +2 -1
  10. package/dist/chat/earlier.d.ts +10 -0
  11. package/dist/chat/earlier.d.ts.map +1 -0
  12. package/dist/chat/earlier.js +54 -0
  13. package/dist/chat/kit.js +1 -1
  14. package/dist/chat/list.d.ts.map +1 -1
  15. package/dist/chat/list.js +39 -3
  16. package/dist/chat/provider.d.ts +1 -1
  17. package/dist/chat/provider.d.ts.map +1 -1
  18. package/dist/chat/provider.js +35 -27
  19. package/dist/chat/question.d.ts +1 -1
  20. package/dist/chat/question.d.ts.map +1 -1
  21. package/dist/chat/question.js +7 -18
  22. package/dist/chat/style.js +1 -1
  23. package/dist/client.d.ts +7 -2
  24. package/dist/client.d.ts.map +1 -1
  25. package/dist/client.js +16 -5
  26. package/dist/connection.d.ts +13 -3
  27. package/dist/connection.d.ts.map +1 -1
  28. package/dist/connection.js +417 -107
  29. package/dist/conversation.d.ts +7 -0
  30. package/dist/conversation.d.ts.map +1 -1
  31. package/dist/conversation.js +206 -48
  32. package/dist/entry.d.ts.map +1 -1
  33. package/dist/entry.js +1 -1
  34. package/dist/host.d.ts +8 -3
  35. package/dist/host.d.ts.map +1 -1
  36. package/dist/host.js +58 -6
  37. package/dist/index.d.ts +6 -5
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +1 -0
  40. package/dist/memory.d.ts +6 -1
  41. package/dist/memory.d.ts.map +1 -1
  42. package/dist/memory.js +20 -5
  43. package/dist/model.d.ts +3 -2
  44. package/dist/model.d.ts.map +1 -1
  45. package/dist/model.js +1 -1
  46. package/dist/observation.d.ts +2 -0
  47. package/dist/observation.d.ts.map +1 -1
  48. package/dist/observation.js +9 -3
  49. package/dist/outcome.d.ts +8 -2
  50. package/dist/outcome.d.ts.map +1 -1
  51. package/dist/outcome.js +3 -0
  52. package/dist/overlay.d.ts +1 -0
  53. package/dist/overlay.d.ts.map +1 -1
  54. package/dist/overlay.js +27 -3
  55. package/dist/query.d.ts +29 -2
  56. package/dist/query.d.ts.map +1 -1
  57. package/dist/query.js +36 -7
  58. package/dist/reach.d.ts +1 -2
  59. package/dist/reach.d.ts.map +1 -1
  60. package/dist/reach.js +17 -4
  61. package/dist/react.d.ts +3 -3
  62. package/dist/react.d.ts.map +1 -1
  63. package/dist/react.js +7 -17
  64. package/dist/read-side.d.ts.map +1 -1
  65. package/dist/read-side.js +176 -48
  66. package/dist/reads.d.ts +11 -0
  67. package/dist/reads.d.ts.map +1 -0
  68. package/dist/reads.js +98 -0
  69. package/dist/refresh.d.ts +4 -4
  70. package/dist/refresh.d.ts.map +1 -1
  71. package/dist/refresh.js +6 -0
  72. package/dist/replica.d.ts +5 -1
  73. package/dist/replica.d.ts.map +1 -1
  74. package/dist/replica.js +30 -5
  75. package/dist/room-draft.d.ts +13 -1
  76. package/dist/room-draft.d.ts.map +1 -1
  77. package/dist/room-draft.js +2 -2
  78. package/dist/rows.d.ts +3 -1
  79. package/dist/rows.d.ts.map +1 -1
  80. package/dist/rows.js +8 -1
  81. package/dist/save.d.ts +1 -0
  82. package/dist/save.d.ts.map +1 -1
  83. package/dist/save.js +17 -1
  84. package/dist/search.d.ts +28 -7
  85. package/dist/search.d.ts.map +1 -1
  86. package/dist/search.js +89 -7
  87. package/dist/seed.d.ts +16 -0
  88. package/dist/seed.d.ts.map +1 -0
  89. package/dist/seed.js +75 -0
  90. package/dist/store.d.ts +20 -5
  91. package/dist/store.d.ts.map +1 -1
  92. package/dist/store.js +58 -18
  93. package/dist/windows.d.ts +26 -0
  94. package/dist/windows.d.ts.map +1 -0
  95. package/dist/windows.js +93 -0
  96. package/dist/wire.d.ts +16 -0
  97. package/dist/wire.d.ts.map +1 -1
  98. package/dist/wire.js +4 -0
  99. package/package.json +2 -2
@@ -1,10 +1,12 @@
1
1
  import { attachedModel } from './attached-model.js';
2
2
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
3
- import { observing } from './observation.js';
4
- import { filterOf, identityOf, planOf, } from './query.js';
3
+ import { observing, whole } from './observation.js';
4
+ import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
5
5
  import { createReadSide } from './read-side.js';
6
+ import { seededResult } from './seed.js';
6
7
  import { markdownPath, roomOfKey } from './rows.js';
7
8
  import { createSearchIndex, searchPlanOf, } from './search.js';
9
+ import { capped, MAXIMUM_WINDOW } from './windows.js';
8
10
  import { bootstrapRoom } from './bootstrap.js';
9
11
  import { changeSet, entering, mergeChangeSets } from './change-set.js';
10
12
  import { createConversationStore, } from './conversation.js';
@@ -13,12 +15,13 @@ import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
13
15
  import { runtimeError } from './failure.js';
14
16
  import { createLog } from './log.js';
15
17
  import { binaryRecord } from './binary.js';
16
- import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
18
+ import { absentBinary, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
17
19
  import { createRecovery } from './recovery.js';
20
+ import { doorReads } from './reads.js';
18
21
  import { refreshRoom } from './refresh.js';
19
- import { buildBatch, joined, limitedIn, requeued } from './save.js';
20
- import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
21
- import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
22
+ import { buildBatch, joined, limitedIn, queuedDocuments, requeued, } from './save.js';
23
+ import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, standing, } from './replica.js';
24
+ import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
22
25
  import { same, saving, STARTING, transferring, } from './status.js';
23
26
  import { detectStorageLoss, nextGeneration } from './durability.js';
24
27
  import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
@@ -31,6 +34,15 @@ import { beneath, normalize } from './paths.js';
31
34
  import { openRuntimeStore, } from './store.js';
32
35
  import { unconfirmed } from './transfer.js';
33
36
  import { governs, holds, narrowed, planReach, } from './reach.js';
37
+ function roomsAlone(plan) {
38
+ return (plan.reach === null &&
39
+ plan.root.types === null &&
40
+ plan.root.where === null &&
41
+ plan.root.pathPrefix === null &&
42
+ plan.paths === null &&
43
+ plan.hop === null &&
44
+ plan.backlinks === null);
45
+ }
34
46
  function within(root, path) {
35
47
  return root === '' || beneath(root, path);
36
48
  }
@@ -48,6 +60,10 @@ const OPENING_LANE = 'opening';
48
60
  const CONVERSATION_LANE = 'conversations';
49
61
  const ROOM_READS = 256;
50
62
  const ROOM_ATTEMPTS = 16;
63
+ function operationOf(input) {
64
+ const operation = input?.operation_id;
65
+ return typeof operation === 'string' ? operation : null;
66
+ }
51
67
  const outside = new Map();
52
68
  function outsideOf(roots) {
53
69
  const key = JSON.stringify(roots);
@@ -87,6 +103,8 @@ export function composeRuntime(options) {
87
103
  const replicas = new FencedMap(isFenced);
88
104
  const queuesWithoutReplica = new FencedMap(isFenced);
89
105
  const pendings = new Map();
106
+ const seeds = new Map();
107
+ const seedWatchers = new Map();
90
108
  const roomDrafts = new FencedMap(isFenced);
91
109
  const presentations = new Map();
92
110
  let limits = [];
@@ -99,6 +117,7 @@ export function composeRuntime(options) {
99
117
  const liveModels = new FencedMap(isModelFenced);
100
118
  const invalidModels = new Set();
101
119
  const discovered = new Map();
120
+ const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
102
121
  let roomsListed = false;
103
122
  const scopedRefusals = new Map();
104
123
  const sourceRefusals = new Map();
@@ -135,6 +154,7 @@ export function composeRuntime(options) {
135
154
  let handed = false;
136
155
  const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
137
156
  const repairing = new Set();
157
+ const calling = new Set();
138
158
  const repairable = new Map();
139
159
  let outcomeRetention = null;
140
160
  const answering = new Map();
@@ -255,6 +275,15 @@ export function composeRuntime(options) {
255
275
  const value = presentations.get(app);
256
276
  return value === undefined ? null : structuredClone(value);
257
277
  }
278
+ function keepPresentation(app, next) {
279
+ return exclusive(async () => {
280
+ const local = await openLocal();
281
+ const stored = next(presentations.get(app));
282
+ await local.present({ app, value: stored });
283
+ presentations.set(app, stored);
284
+ publishChangeSet(changeSet('', null, null, { presentation: app }));
285
+ });
286
+ }
258
287
  function emitPresentation(app) {
259
288
  if (closed)
260
289
  return;
@@ -484,7 +513,8 @@ export function composeRuntime(options) {
484
513
  return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
485
514
  }
486
515
  function awaited(locator) {
487
- return rooms.has(locator) || !roomsListed;
516
+ return (!roomsListed ||
517
+ (rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
488
518
  }
489
519
  function covered(plan) {
490
520
  const locators = reachedRooms(plan);
@@ -535,20 +565,28 @@ export function composeRuntime(options) {
535
565
  .map((room) => room.locator)
536
566
  .sort();
537
567
  }
568
+ function heldLocators(acquired) {
569
+ return demand.whole ? acquired : [...new Set([...acquired, ...demand.rooms])].sort();
570
+ }
538
571
  function heldDemand(acquired) {
539
572
  if (demand.whole)
540
573
  return null;
541
- return { rooms: acquired.map((room) => room.locator), conversations: demand.conversations };
574
+ return {
575
+ rooms: heldLocators(acquired.map((room) => room.locator)),
576
+ conversations: demand.conversations,
577
+ };
542
578
  }
543
579
  function applyDemand(next) {
544
- const before = acquiredLocators();
545
- const conversed = demand.conversations;
580
+ const before = heldLocators(acquiredLocators());
581
+ const { whole, conversations: conversed } = demand;
546
582
  demand = next;
547
- const after = acquiredLocators();
583
+ const after = heldLocators(acquiredLocators());
548
584
  queueReadiness();
549
585
  if (!conversed && next.conversations)
550
586
  scheduleCurrency();
551
- if (JSON.stringify(before) === JSON.stringify(after) && conversed === next.conversations)
587
+ if (JSON.stringify(before) === JSON.stringify(after) &&
588
+ whole === next.whole &&
589
+ conversed === next.conversations)
552
590
  return;
553
591
  journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
554
592
  if (options.adapter.roomStream === undefined)
@@ -643,8 +681,7 @@ export function composeRuntime(options) {
643
681
  }
644
682
  function readinessFor(port, reach) {
645
683
  const required = requiredRooms(reach);
646
- const target = targets[port.target];
647
- const search = target?.search === true;
684
+ const search = port.search === 'required';
648
685
  const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
649
686
  const building = (state) => coverage
650
687
  .filter((room) => room.state === state && !unindexable.has(room.room))
@@ -672,15 +709,11 @@ export function composeRuntime(options) {
672
709
  : port.capacity,
673
710
  refused: refusedIn(required.rooms),
674
711
  lost: storageLoss !== null,
675
- established: target?.established === true,
712
+ established: targets[port.target]?.established === true,
676
713
  });
677
714
  }
678
- function keepTarget(name, change) {
679
- const held = targets[name] ?? { established: false, search: false };
680
- const next = { ...held, ...change };
681
- if (next.established === held.established && next.search === held.search)
682
- return;
683
- targets = { ...targets, [name]: next };
715
+ function establish(name) {
716
+ targets = { ...targets, [name]: { established: true } };
684
717
  queueReadiness();
685
718
  void durably((local) => local.keepTargets(targets)).catch(() => {
686
719
  journal.record(`target record failed ${name}`);
@@ -691,7 +724,7 @@ export function composeRuntime(options) {
691
724
  return;
692
725
  const readiness = readinessFor(watch.port, watch.reach);
693
726
  if (readiness.ready && targets[watch.port.target]?.established !== true) {
694
- keepTarget(watch.port.target, { established: true });
727
+ establish(watch.port.target);
695
728
  }
696
729
  const shown = JSON.stringify(readiness);
697
730
  if (shown === watch.last)
@@ -719,10 +752,6 @@ export function composeRuntime(options) {
719
752
  readinessWatches.delete(watch);
720
753
  };
721
754
  }
722
- function searching(target) {
723
- if (targets[target]?.search !== true)
724
- keepTarget(target, { search: true });
725
- }
726
755
  function layerBytes(layer) {
727
756
  let bytes = 0;
728
757
  for (const [path, text] of Object.entries(layer.documents))
@@ -810,18 +839,54 @@ export function composeRuntime(options) {
810
839
  const available = await estimated().catch(() => null);
811
840
  pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
812
841
  }
842
+ function seedRoomHeld(room, version) {
843
+ return prepared.has(room) && reachedBy(room, version);
844
+ }
845
+ function seedHeld(at) {
846
+ return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
847
+ }
848
+ function seedApplies(plan) {
849
+ const seed = seeds.get(plan.key);
850
+ if (seed === undefined)
851
+ return null;
852
+ if (!seedHeld(seed.at))
853
+ return seed;
854
+ seeds.delete(plan.key);
855
+ return null;
856
+ }
857
+ function seedBehind(plan, cover) {
858
+ const seed = seedApplies(plan);
859
+ if (seed === null || 'pending' in cover)
860
+ return cover;
861
+ return {
862
+ pending: Object.entries(seed.at)
863
+ .filter(([room, version]) => !seedRoomHeld(room, version))
864
+ .map(([room]) => ({ room, awaits: 'acquisition' })),
865
+ };
866
+ }
867
+ function seed(entry) {
868
+ if (seedHeld(entry.at))
869
+ return;
870
+ for (const { query, rows } of entry.queries) {
871
+ const key = queryKey(query);
872
+ seeds.set(key, { rows, at: entry.at });
873
+ for (const shown of seedWatchers.get(key) ?? [])
874
+ shown();
875
+ }
876
+ }
813
877
  async function rowsQuery(plan) {
814
878
  const before = covered(plan);
815
879
  awaitPreparation(before);
880
+ const seeded = seedApplies(plan);
881
+ if (seeded !== null)
882
+ return seededResult(seeded).result;
816
883
  if ('pending' in before)
817
884
  return before;
818
885
  const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
819
886
  awaitPreparation(cover);
820
887
  if ('pending' in cover)
821
888
  return cover;
822
- return 'groups' in rows
823
- ? { groups: rows.groups, coverage: cover.coverage }
824
- : { documents: rows.documents, coverage: cover.coverage };
889
+ return { ...rows, coverage: cover.coverage };
825
890
  }
826
891
  async function indexConversation(conversation) {
827
892
  searchIndex.indexMessages(conversation, [], false);
@@ -971,6 +1036,9 @@ export function composeRuntime(options) {
971
1036
  if (fence !== undefined && fence.erased && fence.at < started)
972
1037
  fences.delete(room.locator);
973
1038
  }
1039
+ await loseRooms(found
1040
+ .filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
1041
+ .map((room) => room.locator));
974
1042
  const listed = found.filter((room) => !fences.has(room.locator));
975
1043
  const listedLocators = new Set(listed.map((room) => room.locator));
976
1044
  for (const locator of listedLocators)
@@ -995,6 +1063,12 @@ export function composeRuntime(options) {
995
1063
  emitRooms();
996
1064
  return listed;
997
1065
  }
1066
+ function heldLocally(locator) {
1067
+ return (replicas.has(locator) ||
1068
+ roomDrafts.has(locator) ||
1069
+ transfers.has(locator) ||
1070
+ queuesWithoutReplica.has(locator));
1071
+ }
998
1072
  function unlistedHeld(listed) {
999
1073
  return [
1000
1074
  ...new Set([
@@ -1230,8 +1304,9 @@ export function composeRuntime(options) {
1230
1304
  }
1231
1305
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
1232
1306
  const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
1233
- const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
1234
- const kept = queue.filter((batch) => !withdrawn.includes(batch));
1307
+ const { withdrawn, kept } = standing(confirmed, queue, withdrawable, localSaves);
1308
+ const founded = kept.filter((batch) => !queue.includes(batch));
1309
+ const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
1235
1310
  const unqueued = withdrawn.map((batch) => ({
1236
1311
  batch,
1237
1312
  returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
@@ -1255,9 +1330,10 @@ export function composeRuntime(options) {
1255
1330
  });
1256
1331
  }
1257
1332
  }
1258
- await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
1333
+ await local.keepAuthored({ locator, queue: kept, founded, roomDrafts: [...written.values()] });
1334
+ const staying = new Set(kept.map((batch) => batch.id));
1259
1335
  for (const batch of previousQueue) {
1260
- if (!kept.includes(batch))
1336
+ if (!staying.has(batch.id))
1261
1337
  pendings.delete(batch.id);
1262
1338
  }
1263
1339
  for (const record of written.values())
@@ -1278,7 +1354,7 @@ export function composeRuntime(options) {
1278
1354
  else
1279
1355
  queuesWithoutReplica.delete(locator);
1280
1356
  publishChangeSet(changeSet(locator, before ?? null, held, {
1281
- queue: kept.length !== previousQueue.length,
1357
+ queue: queueChanged,
1282
1358
  drafts: [...written.keys()],
1283
1359
  }));
1284
1360
  moveTo({ ...status, ...attention() });
@@ -1293,7 +1369,7 @@ export function composeRuntime(options) {
1293
1369
  replicas.set(locator, replicaOf(confirmed, kept, before));
1294
1370
  return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
1295
1371
  room: settling.room,
1296
- queue: kept.length !== previousQueue.length,
1372
+ queue: queueChanged,
1297
1373
  drafts: [...written.keys()],
1298
1374
  });
1299
1375
  };
@@ -1385,7 +1461,7 @@ export function composeRuntime(options) {
1385
1461
  return true;
1386
1462
  });
1387
1463
  }
1388
- async function saveRoomDrafts(locator, named, writtenBy) {
1464
+ async function savedRoom(locator) {
1389
1465
  const local = await openLocal();
1390
1466
  await referenced(locator);
1391
1467
  const replica = replicas.get(locator);
@@ -1393,6 +1469,10 @@ export function composeRuntime(options) {
1393
1469
  if (replica === undefined || room === undefined) {
1394
1470
  throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1395
1471
  }
1472
+ return { local, replica, room };
1473
+ }
1474
+ async function saveRoomDrafts(locator, named, writtenBy) {
1475
+ const target = await savedRoom(locator);
1396
1476
  const saved = [];
1397
1477
  for (const { path, revision } of named) {
1398
1478
  const record = roomDrafts.get(locator)?.get(path);
@@ -1402,6 +1482,38 @@ export function composeRuntime(options) {
1402
1482
  if (!saved.some((draft) => draft.path === path))
1403
1483
  saved.push({ path, parts: record.parts });
1404
1484
  }
1485
+ return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
1486
+ }
1487
+ async function saveParts(locator, named, writtenBy) {
1488
+ const read = named.map(({ path, parts }) => ({ path, parts: readParts(parts) }));
1489
+ if (read.some(({ path }, at) => path === '' ||
1490
+ normalize(path) !== path ||
1491
+ read.findIndex((one) => one.path === path) !== at)) {
1492
+ throw runtimeError('room-draft-malformed');
1493
+ }
1494
+ const target = await savedRoom(locator);
1495
+ const saved = [];
1496
+ for (const { path, parts } of read) {
1497
+ const file = parts.file === undefined
1498
+ ? undefined
1499
+ : await heldFile(parts.file.blob, binaryBase(target.replica.visible, path));
1500
+ saved.push({ path, parts: held(undefined, parts, file) });
1501
+ }
1502
+ return committed(locator, target, saved, new Set(), writtenBy);
1503
+ }
1504
+ function carriedDrafts(locator, saved, consumed) {
1505
+ const holding = roomDrafts.get(locator);
1506
+ return new Map(saved.flatMap(({ path, parts }) => {
1507
+ const record = holding?.get(path);
1508
+ if (parts.move === undefined || record === undefined || consumed.has(path))
1509
+ return [];
1510
+ const to = moveTarget(parts.move);
1511
+ if (to === path || holding?.has(to) === true)
1512
+ return [];
1513
+ return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
1514
+ }));
1515
+ }
1516
+ async function committed(locator, { local, replica, room }, saved, consumed, writtenBy) {
1405
1517
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
1406
1518
  if (changed.length > 0)
1407
1519
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
@@ -1434,6 +1546,7 @@ export function composeRuntime(options) {
1434
1546
  visible: replica.visible,
1435
1547
  changes,
1436
1548
  bases: parts.flatMap(({ bases }) => bases),
1549
+ queued: queuedDocuments(replica.queue),
1437
1550
  writtenBy,
1438
1551
  clean: {
1439
1552
  text: replica.confirmed.record.documents,
@@ -1441,7 +1554,9 @@ export function composeRuntime(options) {
1441
1554
  composition: compositionOf(replica.confirmed.record),
1442
1555
  },
1443
1556
  };
1444
- const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
1557
+ const checked = withdrawing
1558
+ ? buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
1559
+ : null;
1445
1560
  if (checked?.kind === 'refused')
1446
1561
  return { outcome: 'refused', ...checked.refusal };
1447
1562
  const owners = [...requeue.owners].flatMap((origin) => {
@@ -1464,6 +1579,7 @@ export function composeRuntime(options) {
1464
1579
  ...input,
1465
1580
  visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
1466
1581
  changes: requeue.changes,
1582
+ queued: queuedDocuments(requeue.queue),
1467
1583
  });
1468
1584
  if (built?.kind === 'refused')
1469
1585
  return { outcome: 'refused', ...built.refusal };
@@ -1498,13 +1614,16 @@ export function composeRuntime(options) {
1498
1614
  if (limited !== null)
1499
1615
  return { outcome: 'refused', ...limited };
1500
1616
  }
1501
- const consumed = saved.map(({ path }) => path);
1617
+ const carried = carriedDrafts(locator, saved, consumed);
1618
+ const left = [...consumed, ...carried.keys()];
1619
+ const arrived = [...carried.values()];
1502
1620
  try {
1503
1621
  await local.keepAuthored({
1504
1622
  locator,
1505
1623
  queue: batches,
1506
1624
  saved: drafted,
1507
- consumedRoomDrafts: consumed,
1625
+ consumedRoomDrafts: left,
1626
+ roomDrafts: arrived,
1508
1627
  });
1509
1628
  }
1510
1629
  catch {
@@ -1519,12 +1638,14 @@ export function composeRuntime(options) {
1519
1638
  }
1520
1639
  if (!localSaves)
1521
1640
  recorded = false;
1522
- for (const path of consumed)
1641
+ for (const path of left)
1523
1642
  forgetRoomDraft(locator, path);
1643
+ for (const record of arrived)
1644
+ rememberRoomDraft(record);
1524
1645
  replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
1525
1646
  publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
1526
1647
  queue: true,
1527
- drafts: consumed,
1648
+ drafts: [...left, ...arrived.map(({ path }) => path)],
1528
1649
  }));
1529
1650
  moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
1530
1651
  submitLater(locator);
@@ -1615,7 +1736,7 @@ export function composeRuntime(options) {
1615
1736
  const scope = options.adapter.referrerScope(request.referrer);
1616
1737
  return scope?.kind === 'room' ? scope.locator : null;
1617
1738
  }
1618
- async function readBinary(request) {
1739
+ async function transferBinary(request, transfer, answer) {
1619
1740
  if (closed || refusal !== null)
1620
1741
  return UNCERTAIN;
1621
1742
  try {
@@ -1631,40 +1752,41 @@ export function composeRuntime(options) {
1631
1752
  ? `transfer binary ${request.locator} ${request.path}`
1632
1753
  : `transfer binary ${request.fileRef}`);
1633
1754
  try {
1634
- const result = await options.adapter.binary(authorization, request);
1635
- journal.record(`outcome binary ${result.kind}`);
1755
+ const result = await transfer(authorization);
1756
+ const settled = result;
1757
+ journal.record(`outcome binary ${settled.kind}`);
1636
1758
  const locator = binaryRoom(request);
1637
1759
  if (locator !== null && fencedSince(locator, since))
1638
1760
  return failure({ kind: 'room-not-readable', locator });
1639
- if (result.kind === 'scoped-refusal') {
1640
- const room = replicas.get(result.locator)?.confirmed.record;
1761
+ if (settled.kind === 'scoped-refusal') {
1762
+ const room = replicas.get(settled.locator)?.confirmed.record;
1641
1763
  if (room !== undefined)
1642
- await scoped(result, room, await openLocal());
1764
+ await scoped(settled, room, await openLocal());
1643
1765
  }
1644
- if (result.kind === 'revoked-authorization')
1766
+ if (settled.kind === 'revoked-authorization')
1645
1767
  await refuse('revoked-authorization');
1646
- return binaryAnswer(result);
1768
+ return answer(result);
1647
1769
  }
1648
1770
  catch {
1649
1771
  journal.record('outcome binary transport-uncertainty');
1650
1772
  return UNCERTAIN;
1651
1773
  }
1652
1774
  }
1653
- async function readBinaryPath(locator, path, revision) {
1654
- await ready;
1775
+ function readBinary(request) {
1776
+ return transferBinary(request, (held) => options.adapter.binary(held, request), binaryAnswer);
1777
+ }
1778
+ function binaryPath(locator, path, revision) {
1655
1779
  if (revision !== undefined)
1656
- return readBinary({ kind: 'path', locator, path, revision });
1780
+ return { kind: 'path', locator, path, revision };
1657
1781
  const visible = replicas.get(locator)?.visible;
1658
1782
  const held = visible?.binaries[path];
1659
1783
  if (visible === undefined || held === undefined)
1660
- return absentBinary(path);
1661
- if (held.state === 'pending') {
1662
- const blob = queuedBlob(locator, path, held.localId);
1663
- return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1664
- }
1784
+ return null;
1785
+ if (held.state === 'pending')
1786
+ return { kind: 'pending', localId: held.localId };
1665
1787
  const record = binaryRecord(path, held, visible.revisions[path]);
1666
1788
  if (record.state !== 'accepted')
1667
- return absentBinary(path);
1789
+ return null;
1668
1790
  const replica = replicas.get(locator);
1669
1791
  const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
1670
1792
  const holds = (at) => {
@@ -1679,13 +1801,31 @@ export function composeRuntime(options) {
1679
1801
  if (change.kind === 'move' && change.to === confirmed)
1680
1802
  confirmed = change.path;
1681
1803
  }
1682
- return readBinary({
1804
+ return {
1683
1805
  kind: 'path',
1684
1806
  locator,
1685
1807
  path: confirmed,
1686
1808
  revision: record.revision,
1687
1809
  fileRef: record.fileRef,
1688
- });
1810
+ };
1811
+ }
1812
+ async function readBinaryPath(locator, path, revision) {
1813
+ await ready;
1814
+ const asked = binaryPath(locator, path, revision);
1815
+ if (asked === null)
1816
+ return absentBinary(path);
1817
+ if (asked.kind === 'pending') {
1818
+ const blob = queuedBlob(locator, path, asked.localId);
1819
+ return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1820
+ }
1821
+ return readBinary(asked);
1822
+ }
1823
+ async function linkBinaryPath(locator, path, revision) {
1824
+ await ready;
1825
+ const asked = binaryPath(locator, path, revision);
1826
+ if (asked === null || asked.kind === 'pending')
1827
+ return absentBinary(path);
1828
+ return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
1689
1829
  }
1690
1830
  async function readBinaryReference(reference) {
1691
1831
  await ready;
@@ -1711,7 +1851,7 @@ export function composeRuntime(options) {
1711
1851
  }
1712
1852
  return undefined;
1713
1853
  }
1714
- async function invokeAction(name, input) {
1854
+ async function invokeAction(name, input, dispatching) {
1715
1855
  await ready;
1716
1856
  if (closed || refusal !== null)
1717
1857
  return UNCERTAIN;
@@ -1725,6 +1865,7 @@ export function composeRuntime(options) {
1725
1865
  }
1726
1866
  if (closed || authorization === undefined || refusal !== null)
1727
1867
  return UNCERTAIN;
1868
+ await dispatching?.();
1728
1869
  journal.record(`transfer action ${name}`);
1729
1870
  try {
1730
1871
  const result = await options.adapter.action(authorization, { name, input });
@@ -1745,6 +1886,60 @@ export function composeRuntime(options) {
1745
1886
  return UNCERTAIN;
1746
1887
  }
1747
1888
  }
1889
+ function unsettled(result) {
1890
+ return result.kind !== 'action' && result.retryable;
1891
+ }
1892
+ async function recoveredAction(name, input) {
1893
+ const operation = operationOf(input);
1894
+ if (operation === null)
1895
+ return invokeAction(name, input);
1896
+ const kept = async () => (await openLocal()).keepCall({ operation, name, input });
1897
+ calling.add(operation);
1898
+ try {
1899
+ let result = await invokeAction(name, input, kept);
1900
+ if (unsettled(result)) {
1901
+ await recovery.attempt(`action ${operation}`, async () => {
1902
+ if (closed || refusal !== null)
1903
+ return 'done';
1904
+ if (!socketOpen)
1905
+ return 'again';
1906
+ result = await invokeAction(name, input, kept);
1907
+ return unsettled(result) ? 'again' : 'done';
1908
+ }, { again: true });
1909
+ }
1910
+ if (!unsettled(result))
1911
+ await (await openLocal()).dropCall(operation);
1912
+ return result;
1913
+ }
1914
+ finally {
1915
+ calling.delete(operation);
1916
+ }
1917
+ }
1918
+ async function resendCalls(signal) {
1919
+ if (!socketOpen || closed || stopping)
1920
+ return 'done';
1921
+ const local = await openLocal();
1922
+ for (const held of await local.calls()) {
1923
+ if (calling.has(held.operation))
1924
+ continue;
1925
+ if (closed || signal.aborted)
1926
+ return 'done';
1927
+ if (pastHorizon(held.operation)) {
1928
+ await local.dropCall(held.operation);
1929
+ continue;
1930
+ }
1931
+ calling.add(held.operation);
1932
+ try {
1933
+ if (unsettled(await invokeAction(held.name, held.input)))
1934
+ return 'again';
1935
+ await local.dropCall(held.operation);
1936
+ }
1937
+ finally {
1938
+ calling.delete(held.operation);
1939
+ }
1940
+ }
1941
+ return 'done';
1942
+ }
1748
1943
  function continuing() {
1749
1944
  return !closed && !discoveryPaused;
1750
1945
  }
@@ -1762,8 +1957,9 @@ export function composeRuntime(options) {
1762
1957
  async function pullRefresh(authorized, confirmed, live) {
1763
1958
  if (!live())
1764
1959
  return unconfirmed;
1960
+ const turn = generation;
1765
1961
  try {
1766
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1962
+ const result = await refreshRoom(reads, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1767
1963
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
1768
1964
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
1769
1965
  return result;
@@ -1772,6 +1968,9 @@ export function composeRuntime(options) {
1772
1968
  journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
1773
1969
  throw error;
1774
1970
  }
1971
+ finally {
1972
+ reads.finished(confirmed.record.locator, turn);
1973
+ }
1775
1974
  }
1776
1975
  function staging(local, live) {
1777
1976
  return {
@@ -1814,12 +2013,13 @@ export function composeRuntime(options) {
1814
2013
  async function pullBootstrap(authorized, room, local, live) {
1815
2014
  if (!live())
1816
2015
  return unconfirmed;
2016
+ const turn = generation;
1817
2017
  if (replicas.has(room.locator)) {
1818
2018
  catching.add(room.locator);
1819
2019
  readSide.recover();
1820
2020
  }
1821
2021
  try {
1822
- const result = await bootstrapRoom(options.adapter, authorized, room, {
2022
+ const result = await bootstrapRoom(reads, authorized, room, {
1823
2023
  collection: modelOf(room.locator),
1824
2024
  selection: modelOf(room.locator, 'typed-selection'),
1825
2025
  }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
@@ -1834,12 +2034,28 @@ export function composeRuntime(options) {
1834
2034
  caughtUpTo(room.locator);
1835
2035
  throw error;
1836
2036
  }
2037
+ finally {
2038
+ reads.finished(room.locator, turn);
2039
+ }
1837
2040
  }
1838
2041
  function caughtUpTo(locator) {
1839
2042
  if (catching.delete(locator))
1840
2043
  readSide.recover();
1841
2044
  }
2045
+ function behind(room) {
2046
+ const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
2047
+ return (held !== undefined &&
2048
+ [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
2049
+ }
1842
2050
  async function advance(room, local, authorized, live) {
2051
+ const carried = reads.carried(room.locator);
2052
+ const withheld = await advanceOnce(room, local, authorized, live);
2053
+ if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
2054
+ return withheld;
2055
+ }
2056
+ return advanceOnce(room, local, authorized, live);
2057
+ }
2058
+ async function advanceOnce(room, local, authorized, live) {
1843
2059
  await referenced(room.locator);
1844
2060
  if (!live() || paused.has(room.locator))
1845
2061
  return unconfirmed;
@@ -2276,6 +2492,11 @@ export function composeRuntime(options) {
2276
2492
  });
2277
2493
  }
2278
2494
  async function scoped(result, room, local) {
2495
+ if ((result.scope === 'content' || result.scope === 'sync') && !result.retryable) {
2496
+ journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
2497
+ await loseRooms([room.locator]);
2498
+ return;
2499
+ }
2279
2500
  scopedRefusals.set(room.locator, {
2280
2501
  code: result.code,
2281
2502
  fields: result.fields,
@@ -2359,19 +2580,10 @@ export function composeRuntime(options) {
2359
2580
  await retainModel(local, room, 'collection', modelOf(room.locator));
2360
2581
  if (discoveryPaused || authorization === undefined)
2361
2582
  return false;
2362
- if (room.replica.admission !== 'admitted') {
2363
- if (replicas.has(room.locator))
2364
- progress.complete = false;
2365
- return false;
2366
- }
2367
- return true;
2583
+ return room.replica.admission === 'admitted';
2368
2584
  }
2369
2585
  async function caughtUp(room, local, progress, live) {
2370
- const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
2371
- if (held === undefined || authorization === undefined || !live())
2372
- return;
2373
- const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
2374
- if (!behind)
2586
+ if (authorization === undefined || !live() || !behind(room))
2375
2587
  return;
2376
2588
  const withheld = await advance(room, local, authorization, live);
2377
2589
  if (withheld !== null)
@@ -2599,9 +2811,43 @@ export function composeRuntime(options) {
2599
2811
  options.adapter.roomStream?.reopen();
2600
2812
  });
2601
2813
  }
2602
- function closedSocket(revoked) {
2814
+ function rediscover() {
2815
+ return task(OPENING_LANE, async (live) => {
2816
+ if (!live() || !socketOpen)
2817
+ return;
2818
+ const progress = { complete: true, reachable: true };
2819
+ const gate = discover();
2820
+ moveTo(transferring(status));
2821
+ try {
2822
+ const local = await openLocal();
2823
+ if (!live())
2824
+ return;
2825
+ await ensureAuthorization();
2826
+ if (!live())
2827
+ return;
2828
+ await discoverRooms(local, progress, 'authorized', live);
2829
+ }
2830
+ catch {
2831
+ progress.complete = false;
2832
+ }
2833
+ finally {
2834
+ gate.release();
2835
+ notifyReady();
2836
+ }
2837
+ if (!live())
2838
+ return;
2839
+ if (!progress.complete)
2840
+ complete = false;
2841
+ await rest(progress, live);
2842
+ });
2843
+ }
2844
+ function closedSocket(revoked, lost) {
2603
2845
  const opened = socketOpen;
2604
2846
  socketOpen = false;
2847
+ if (lost) {
2848
+ reads.lost();
2849
+ conversations.lost();
2850
+ }
2605
2851
  if (revoked) {
2606
2852
  void refuse('revoked-authorization');
2607
2853
  return;
@@ -2788,6 +3034,7 @@ export function composeRuntime(options) {
2788
3034
  progress.complete = false;
2789
3035
  return;
2790
3036
  }
3037
+ reads.applied();
2791
3038
  submitAll();
2792
3039
  for (const locator of stale)
2793
3040
  if (repairable.has(locator))
@@ -2800,6 +3047,8 @@ export function composeRuntime(options) {
2800
3047
  await conversations.catchUp(reading);
2801
3048
  if (progress.reachable && reading.reachable)
2802
3049
  await conversations.resend(reading);
3050
+ if (progress.reachable)
3051
+ void recovery.attempt('calls', resendCalls);
2803
3052
  if (!reading.reachable) {
2804
3053
  progress.reachable = false;
2805
3054
  resendLater();
@@ -2912,14 +3161,17 @@ export function composeRuntime(options) {
2912
3161
  if (event.kind === 'opened') {
2913
3162
  socketOpen = true;
2914
3163
  generation += 1;
3164
+ reads.opened();
2915
3165
  conversations.reopened();
2916
3166
  recovery.resume();
2917
3167
  void sync('opened');
2918
3168
  }
2919
3169
  else if (event.kind === 'closed')
2920
- closedSocket(event.revoked);
3170
+ closedSocket(event.revoked, event.lost);
2921
3171
  else if (event.kind === 'opening')
2922
3172
  void applyOpening(event.expired);
3173
+ else if (event.kind === 'authority-changed')
3174
+ void rediscover();
2923
3175
  else
2924
3176
  void applyRoom(event.locator, event.roomVersion);
2925
3177
  }, receipts);
@@ -3041,56 +3293,110 @@ export function composeRuntime(options) {
3041
3293
  if (options.adapter.roomStream === undefined)
3042
3294
  void sync('unstreamed');
3043
3295
  function planned(input, reach) {
3044
- return planOf(input, roomFacts(reach), planReach(reach));
3296
+ const plan = planOf(input, roomFacts(reach), planReach(reach));
3297
+ const named = plan.paths !== null &&
3298
+ plan.rooms !== null &&
3299
+ plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
3300
+ return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
3045
3301
  }
3046
3302
  function watchPlan(plan, deliver) {
3047
- return readSide.watch(plan, identityOf(plan), () => {
3303
+ let seeded = false;
3304
+ let pending = true;
3305
+ const shown = () => {
3306
+ const seed = seeds.get(plan.key);
3307
+ if (seed === undefined || seeded || !pending)
3308
+ return;
3309
+ seeded = true;
3310
+ deliver(seededResult(seed));
3311
+ };
3312
+ const watchers = seedWatchers.get(plan.key) ?? new Set();
3313
+ seedWatchers.set(plan.key, watchers);
3314
+ watchers.add(shown);
3315
+ const stop = readSide.watch(plan, identityOf(plan), () => {
3048
3316
  const cover = covered(plan);
3049
3317
  awaitPreparation(cover);
3050
- return cover;
3051
- }, deliver);
3318
+ return seedBehind(plan, cover);
3319
+ }, (message) => {
3320
+ const answered = message;
3321
+ pending = whole(answered) && 'pending' in answered.result;
3322
+ if (pending && seeds.has(plan.key)) {
3323
+ shown();
3324
+ return;
3325
+ }
3326
+ seeded = false;
3327
+ deliver(message);
3328
+ });
3329
+ return () => {
3330
+ watchers.delete(shown);
3331
+ if (watchers.size === 0)
3332
+ seedWatchers.delete(plan.key);
3333
+ stop();
3334
+ };
3052
3335
  }
3053
3336
  async function searched(input, reach) {
3054
- const plan = searchPlanOf(input);
3337
+ const asked = searchPlanOf(input);
3338
+ const plan = { ...asked, limit: capped(asked.limit) };
3055
3339
  const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
3056
3340
  for (;;) {
3057
3341
  const cover = covered(filter);
3058
3342
  awaitPreparation(cover);
3059
3343
  const pending = 'pending' in cover ? [...cover.pending] : [];
3060
- const covering = 'pending' in cover ? [] : cover.coverage.rooms;
3344
+ const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3345
+ const held = covered({
3346
+ ...filter,
3347
+ rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
3348
+ });
3349
+ const covering = 'pending' in held ? [] : held.coverage.rooms;
3061
3350
  const unindexed = covering
3062
3351
  .filter(({ room }) => !searchIndex.rooms().has(room))
3063
3352
  .map(({ room }) => room);
3064
3353
  for (const room of unindexed)
3065
3354
  pending.push({ room, awaits: 'index' });
3066
- const held = conversations.searchable();
3355
+ const listed = conversations.searchable();
3067
3356
  const named = reach !== null && !reach.conversations
3068
3357
  ? []
3069
- : (plan.conversations ?? held).filter((one) => held.includes(one) && conversations.acquired(one));
3358
+ : (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
3070
3359
  const waiting = named.filter((one) => !searchIndex.conversations().has(one));
3071
3360
  for (const conversation of waiting)
3072
3361
  pending.push({ conversation, awaits: 'index' });
3073
3362
  if (unindexed.length > 0 || waiting.length > 0)
3074
3363
  index(unindexed, waiting);
3075
- if (pending.length > 0 || 'pending' in cover)
3076
- return { pending };
3364
+ const complete = pending.length === 0 && !('pending' in cover);
3365
+ const answering = complete
3366
+ ? filter
3367
+ : {
3368
+ ...filter,
3369
+ rooms: covering
3370
+ .filter(({ room }) => searchIndex.rooms().has(room))
3371
+ .map(({ room }) => room),
3372
+ };
3373
+ const answered = covered(answering);
3374
+ if ('pending' in answered)
3375
+ continue;
3376
+ const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
3077
3377
  const seen = searchIndex.changes();
3078
3378
  await searchIndex.caughtUp();
3079
- const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(filter);
3379
+ const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
3080
3380
  if (selected !== null && 'pending' in selected)
3081
- return selected;
3381
+ continue;
3082
3382
  if (searchIndex.changes() !== seen ||
3083
- JSON.stringify(covered(filter)) !== JSON.stringify(cover))
3383
+ JSON.stringify(covered(answering)) !== JSON.stringify(answered))
3084
3384
  continue;
3085
- return {
3086
- ...searchIndex.search(plan, selected === null ? null : selected.documents, named),
3087
- coverage: {
3088
- rooms: covering,
3089
- conversations: named,
3090
- index: searchIndex.generation(),
3091
- overlay: cover.coverage.overlay,
3092
- },
3385
+ const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms === null ? null : new Set(answering.rooms), plan.collection
3386
+ ? new Map(answered.coverage.rooms.map(({ room }) => [
3387
+ room,
3388
+ replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
3389
+ ]))
3390
+ : null);
3391
+ const coverage = {
3392
+ rooms: answered.coverage.rooms,
3393
+ conversations: searchedConversations,
3394
+ index: searchIndex.generation(),
3395
+ overlay: answered.coverage.overlay,
3093
3396
  };
3397
+ return complete
3398
+ ? { ...hits, coverage }
3399
+ : { pending, documents: hits.documents, messages: hits.messages, coverage };
3094
3400
  }
3095
3401
  }
3096
3402
  const watch = (topic, input, deliver) => {
@@ -3138,7 +3444,7 @@ export function composeRuntime(options) {
3138
3444
  };
3139
3445
  }
3140
3446
  if (topic === 'readiness') {
3141
- return watchReadiness({ target: '', code: 'stored', capacity: null }, null, deliver);
3447
+ return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
3142
3448
  }
3143
3449
  if (topic === 'rooms') {
3144
3450
  roomListeners.add(deliver);
@@ -3275,27 +3581,31 @@ export function composeRuntime(options) {
3275
3581
  });
3276
3582
  },
3277
3583
  write(app, value) {
3278
- return exclusive(async () => {
3279
- const local = await openLocal();
3280
- const stored = structuredClone(value);
3281
- await local.present({ app, value: stored });
3282
- presentations.set(app, stored);
3283
- publishChangeSet(changeSet('', null, null, { presentation: app }));
3284
- });
3584
+ return keepPresentation(app, () => structuredClone(value));
3585
+ },
3586
+ edit(app, members) {
3587
+ return keepPresentation(app, (held) => Object.fromEntries([
3588
+ ...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(members, member)),
3589
+ ...Object.entries(structuredClone(members)).filter(([, value]) => value !== null),
3590
+ ]));
3285
3591
  },
3286
3592
  },
3287
3593
  save(room, named, writtenBy) {
3288
3594
  return exclusive(() => saveRoomDrafts(room, named, writtenBy));
3289
3595
  },
3596
+ saveParts(room, parts, writtenBy) {
3597
+ return exclusive(() => saveParts(room, parts, writtenBy));
3598
+ },
3290
3599
  history(locator, path, query) {
3291
3600
  return readHistory(locator, path, query);
3292
3601
  },
3293
3602
  binaryFile: {
3294
3603
  read: readBinaryPath,
3604
+ link: linkBinaryPath,
3295
3605
  readReference: readBinaryReference,
3296
3606
  },
3297
3607
  call(name, input) {
3298
- return invokeAction(name, input);
3608
+ return recoveredAction(name, input);
3299
3609
  },
3300
3610
  reached(locator, roomVersion, path) {
3301
3611
  return path === undefined
@@ -3339,11 +3649,11 @@ export function composeRuntime(options) {
3339
3649
  query: (plan) => rowsQuery(plan),
3340
3650
  watchPlan,
3341
3651
  search: searched,
3342
- searching,
3343
3652
  watchReadiness,
3344
3653
  };
3345
3654
  return {
3346
3655
  md,
3656
+ seed,
3347
3657
  stop,
3348
3658
  lose: loseRooms,
3349
3659
  watch,