@agentshouse/mdruntime 0.1.0-c069136dcb4e8 → 0.1.0-c09bf842921ff

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 (111) hide show
  1. package/README.md +1 -1
  2. package/dist/adapter.d.ts +51 -22
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/attached-model.d.ts +3 -10
  5. package/dist/attached-model.d.ts.map +1 -1
  6. package/dist/attached-model.js +21 -56
  7. package/dist/bootstrap.d.ts +2 -2
  8. package/dist/bootstrap.d.ts.map +1 -1
  9. package/dist/bootstrap.js +9 -8
  10. package/dist/chain.d.ts +2 -1
  11. package/dist/chain.d.ts.map +1 -1
  12. package/dist/client.d.ts +9 -5
  13. package/dist/client.d.ts.map +1 -1
  14. package/dist/client.js +2 -1
  15. package/dist/collection.d.ts +2 -1
  16. package/dist/collection.d.ts.map +1 -1
  17. package/dist/compile.d.ts +1 -6
  18. package/dist/compile.d.ts.map +1 -1
  19. package/dist/compile.js +90 -277
  20. package/dist/connection.d.ts +8 -7
  21. package/dist/connection.d.ts.map +1 -1
  22. package/dist/connection.js +362 -313
  23. package/dist/context.d.ts +2 -0
  24. package/dist/context.d.ts.map +1 -0
  25. package/dist/context.js +7 -0
  26. package/dist/conversation.d.ts +14 -8
  27. package/dist/conversation.d.ts.map +1 -1
  28. package/dist/conversation.js +85 -67
  29. package/dist/durability.d.ts +6 -0
  30. package/dist/durability.d.ts.map +1 -1
  31. package/dist/entry.d.ts +2 -2
  32. package/dist/entry.d.ts.map +1 -1
  33. package/dist/entry.js +16 -7
  34. package/dist/host.d.ts +3 -6
  35. package/dist/host.d.ts.map +1 -1
  36. package/dist/host.js +16 -23
  37. package/dist/identity.d.ts +0 -1
  38. package/dist/identity.d.ts.map +1 -1
  39. package/dist/identity.js +0 -3
  40. package/dist/index.d.ts +10 -7
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +3 -1
  43. package/dist/job.d.ts +14 -0
  44. package/dist/job.d.ts.map +1 -0
  45. package/dist/job.js +28 -0
  46. package/dist/live.d.ts +2 -1
  47. package/dist/live.d.ts.map +1 -1
  48. package/dist/live.js +10 -7
  49. package/dist/memory.d.ts +8 -4
  50. package/dist/memory.d.ts.map +1 -1
  51. package/dist/memory.js +116 -66
  52. package/dist/messages.d.ts +2 -2
  53. package/dist/messages.d.ts.map +1 -1
  54. package/dist/messages.js +15 -16
  55. package/dist/model.d.ts +2 -7
  56. package/dist/model.d.ts.map +1 -1
  57. package/dist/model.js +0 -3
  58. package/dist/observation.d.ts +2 -2
  59. package/dist/observation.d.ts.map +1 -1
  60. package/dist/observation.js +7 -2
  61. package/dist/query.d.ts +4 -209
  62. package/dist/query.d.ts.map +1 -1
  63. package/dist/query.js +3 -505
  64. package/dist/reach.d.ts +7 -7
  65. package/dist/reach.d.ts.map +1 -1
  66. package/dist/reach.js +33 -44
  67. package/dist/react.d.ts +3 -1
  68. package/dist/react.d.ts.map +1 -1
  69. package/dist/readiness.d.ts +0 -2
  70. package/dist/readiness.d.ts.map +1 -1
  71. package/dist/refresh.d.ts +2 -1
  72. package/dist/refresh.d.ts.map +1 -1
  73. package/dist/refresh.js +25 -1
  74. package/dist/replica.d.ts +8 -5
  75. package/dist/replica.d.ts.map +1 -1
  76. package/dist/replica.js +27 -9
  77. package/dist/room-draft.d.ts +4 -3
  78. package/dist/room-draft.d.ts.map +1 -1
  79. package/dist/room-draft.js +65 -52
  80. package/dist/rows.d.ts +14 -21
  81. package/dist/rows.d.ts.map +1 -1
  82. package/dist/rows.js +48 -184
  83. package/dist/save.d.ts.map +1 -1
  84. package/dist/save.js +1 -1
  85. package/dist/search.d.ts +12 -63
  86. package/dist/search.d.ts.map +1 -1
  87. package/dist/search.js +9 -52
  88. package/dist/store.d.ts +5 -7
  89. package/dist/store.d.ts.map +1 -1
  90. package/dist/store.js +11 -15
  91. package/dist/stored.d.ts +3 -4
  92. package/dist/stored.d.ts.map +1 -1
  93. package/dist/stored.js +90 -126
  94. package/dist/tables.d.ts +8 -6
  95. package/dist/tables.d.ts.map +1 -1
  96. package/dist/tables.js +22 -13
  97. package/dist/validation.d.ts.map +1 -1
  98. package/dist/validation.js +7 -4
  99. package/dist/windows.d.ts +1 -12
  100. package/dist/windows.d.ts.map +1 -1
  101. package/dist/windows.js +2 -75
  102. package/dist/wire.d.ts +9 -4
  103. package/dist/wire.d.ts.map +1 -1
  104. package/dist/wire.js +3 -1
  105. package/package.json +2 -2
  106. package/dist/fulltext.d.ts +0 -27
  107. package/dist/fulltext.d.ts.map +0 -1
  108. package/dist/fulltext.js +0 -370
  109. package/dist/words.d.ts +0 -19
  110. package/dist/words.d.ts.map +0 -1
  111. package/dist/words.js +0 -156
@@ -1,8 +1,8 @@
1
- import { parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
2
- import { attachedModel, builtInModel, roomTypesOf } from './attached-model.js';
1
+ import { identityOf, parseFrontmatter, plain, queryKey, roomTarget, target, } from '@agentshouse/mdmodel';
2
+ import { attachedModel, builtInModel, roomTypesOf, schemaTypes } from './attached-model.js';
3
3
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
4
4
  import { observing, whole } from './observation.js';
5
- import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
5
+ import { planWithin, } from './query.js';
6
6
  import { createLiveQueries } from './live.js';
7
7
  import { seededResult } from './seed.js';
8
8
  import { documentKey, roomOfKey } from './rows.js';
@@ -30,7 +30,7 @@ import { readinessOf, } from './readiness.js';
30
30
  import { FencedMap } from './fence.js';
31
31
  import { sameBinary } from './overlay.js';
32
32
  import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
33
- import { identityTime, roomDraftRevision } from './identity.js';
33
+ import { roomDraftRevision } from './identity.js';
34
34
  import { ancestors, beneath, normalize } from './paths.js';
35
35
  import { openRuntimeStore, } from './store.js';
36
36
  import { validatedNeighbourhood } from './validation.js';
@@ -104,6 +104,7 @@ export function composeRuntime(options) {
104
104
  const usableWhenHeld = options.usableWhenHeld ?? [];
105
105
  const queuesWithoutReplica = new FencedMap(isFenced);
106
106
  const pendings = new Map();
107
+ const landedAhead = new Map();
107
108
  const seeds = new Map();
108
109
  const seedWatchers = new Map();
109
110
  const roomDrafts = new FencedMap(isFenced);
@@ -115,6 +116,7 @@ export function composeRuntime(options) {
115
116
  const counted = new Map();
116
117
  const unreadable = new Set();
117
118
  const builtIn = builtInModel(roomTypes);
119
+ const typedPaths = options.adapter.typedPaths ?? [];
118
120
  const models = new FencedMap(isFenced);
119
121
  const liveModels = new FencedMap(isFenced);
120
122
  const invalidModels = new Set();
@@ -141,7 +143,7 @@ export function composeRuntime(options) {
141
143
  : modelOf(locator);
142
144
  }
143
145
  function composed(record) {
144
- return compositionOf(record, builtIn);
146
+ return compositionOf(record, builtIn, typedPaths);
145
147
  }
146
148
  function compositionFor(record) {
147
149
  return composed({ ...record, model: savedModel(record.locator) });
@@ -162,14 +164,14 @@ export function composeRuntime(options) {
162
164
  const repairing = new Set();
163
165
  const calling = new Set();
164
166
  const repairable = new Map();
165
- let outcomeRetention = null;
166
167
  const answering = new Map();
167
168
  let complete = false;
168
169
  let recorded = false;
169
170
  const stale = new Set();
170
171
  let generation = 0;
171
- let discovering = null;
172
172
  const reloadsDue = new Set();
173
+ const highest = new Map();
174
+ const beyondHorizon = new Set();
173
175
  const lanes = new Map();
174
176
  const roomLanes = new Map();
175
177
  let roomWork = 0;
@@ -189,6 +191,8 @@ export function composeRuntime(options) {
189
191
  let withdrawConversations;
190
192
  let currencyQueued = null;
191
193
  let socketOpen = false;
194
+ let announced = false;
195
+ let missedChange = false;
192
196
  const confirmed = new Set();
193
197
  let lockGrace = null;
194
198
  const lockListeners = new Set();
@@ -198,6 +202,8 @@ export function composeRuntime(options) {
198
202
  const serviceModeListeners = new Set();
199
203
  let member = null;
200
204
  const memberListeners = new Set();
205
+ let authorityChanges = 0;
206
+ const authorityChangeListeners = new Set();
201
207
  const unpreparedDemand = new Set();
202
208
  const unpreparable = new Set();
203
209
  let preparing = false;
@@ -247,6 +253,7 @@ export function composeRuntime(options) {
247
253
  const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
248
254
  const conversations = createConversationStore({
249
255
  call: (name, input) => invokeAction(name, input),
256
+ history: (request) => readConversationHistory(request),
250
257
  durably,
251
258
  stream: () => options.adapter.conversations,
252
259
  publish: publishChangeSet,
@@ -254,7 +261,6 @@ export function composeRuntime(options) {
254
261
  full: () => full,
255
262
  closed: () => closed,
256
263
  unanswered: resendLater,
257
- expired: (operation) => pastHorizon(operation),
258
264
  });
259
265
  const journal = createLog();
260
266
  const keptFiles = openedFiles(database, isFenced);
@@ -347,9 +353,9 @@ export function composeRuntime(options) {
347
353
  return value === undefined ? null : structuredClone(value);
348
354
  }
349
355
  async function writtenRoomDraft(room, path, revision, input, next) {
350
- const parts = readParts(input);
356
+ const parts = readParts(input, path);
351
357
  if (path === '' || normalize(path) !== path)
352
- throw runtimeError('room-draft-malformed');
358
+ throw runtimeError('room-draft-malformed', `room-draft-malformed at ${path}: a path inside the Room, such as notes/a.md`);
353
359
  const local = await openLocal();
354
360
  const current = roomDrafts.get(room)?.get(path);
355
361
  if ((current?.revision ?? null) !== revision) {
@@ -649,11 +655,7 @@ export function composeRuntime(options) {
649
655
  })();
650
656
  }
651
657
  function roomsOfReach(reach) {
652
- return new Set([
653
- ...reach.rooms,
654
- ...reach.documents.map(roomOfKey),
655
- ...reach.areas.flatMap((area) => (area.types.length === 0 ? [] : [area.room])),
656
- ]);
658
+ return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
657
659
  }
658
660
  function reachedRooms(plan) {
659
661
  const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
@@ -792,7 +794,6 @@ export function composeRuntime(options) {
792
794
  }
793
795
  function readinessFor(port, reach) {
794
796
  const required = requiredRooms(reach);
795
- const search = port.search === 'required';
796
797
  const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
797
798
  const preparing = coverage
798
799
  .filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
@@ -805,12 +806,11 @@ export function composeRuntime(options) {
805
806
  listed: required.listed,
806
807
  rooms: coverage,
807
808
  conversations: conversationCoverage(reach),
808
- search: search ? 'required' : 'unused',
809
809
  },
810
810
  persistence,
811
811
  connection: connectionOf(),
812
812
  house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
813
- space: full ? 'full' : port.space,
813
+ space: full ? 'full' : null,
814
814
  installation: installationOf(coverage),
815
815
  lost: storageLoss !== null,
816
816
  established: targets[port.target]?.established === true ||
@@ -899,8 +899,12 @@ export function composeRuntime(options) {
899
899
  unpreparable.clear();
900
900
  journal.record('space freed');
901
901
  queueReadiness();
902
- if (!closed && authorization !== undefined)
903
- void sync('unstreamed');
902
+ if (closed || authorization === undefined)
903
+ return;
904
+ if (options.adapter.roomStream === undefined)
905
+ void sync('unstreamed', null);
906
+ else
907
+ void resynchronize();
904
908
  }
905
909
  function seedRoomHeld(room, version) {
906
910
  return indexed(room) && reachedBy(room, version);
@@ -1137,7 +1141,7 @@ export function composeRuntime(options) {
1137
1141
  refusal: scopedRefusals.get(room.locator) ?? null,
1138
1142
  });
1139
1143
  }
1140
- async function applyDiscovery(found, started, covered) {
1144
+ async function applyDiscovery(found, started, left) {
1141
1145
  const lockedBefore = lockedRooms();
1142
1146
  for (const room of found) {
1143
1147
  const fence = fences.get(room.locator);
@@ -1151,18 +1155,11 @@ export function composeRuntime(options) {
1151
1155
  const listedLocators = new Set(listed.map((room) => room.locator));
1152
1156
  for (const locator of listedLocators)
1153
1157
  scopedRefusals.delete(locator);
1154
- if (covered === undefined)
1158
+ if (left === undefined)
1155
1159
  discovered.clear();
1156
- else
1157
- for (const locator of covered)
1158
- discovered.delete(locator);
1159
1160
  for (const room of listed)
1160
1161
  discovered.set(room.locator, room);
1161
- const unlisted = unlistedHeld(new Set(found.map((room) => room.locator))).filter((locator) => covered === undefined || covered.includes(locator));
1162
- if (options.adapter.exhaustiveDiscovery === true)
1163
- await loseRooms(unlisted);
1164
- else
1165
- unlistedKept(unlisted);
1162
+ await loseRooms(left ?? unlistedHeld(new Set(found.map((room) => room.locator))));
1166
1163
  for (const room of listed) {
1167
1164
  const held = replicas.get(room.locator)?.record;
1168
1165
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -1193,15 +1190,6 @@ export function composeRuntime(options) {
1193
1190
  ]),
1194
1191
  ].filter((locator) => !listed.has(locator));
1195
1192
  }
1196
- function unlistedKept(unlisted) {
1197
- for (const locator of unlisted) {
1198
- const held = replicas.get(locator)?.record;
1199
- if (held !== undefined)
1200
- list(held, false);
1201
- else
1202
- rooms.delete(locator);
1203
- }
1204
- }
1205
1193
  function fence(locators) {
1206
1194
  const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1207
1195
  const removed = fresh.map((locator) => ({
@@ -1212,9 +1200,13 @@ export function composeRuntime(options) {
1212
1200
  fences.set(locator, { at: discoveries, erased: false });
1213
1201
  lastFenced.set(locator, ++fencings);
1214
1202
  confirmed.delete(locator);
1215
- for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1203
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []) {
1216
1204
  pendings.delete(pendingKey(locator, batch.id));
1205
+ landedAhead.delete(pendingKey(locator, batch.id));
1206
+ }
1217
1207
  replicas.delete(locator);
1208
+ highest.delete(locator);
1209
+ beyondHorizon.delete(locator);
1218
1210
  queuesWithoutReplica.delete(locator);
1219
1211
  rooms.delete(locator);
1220
1212
  roomDrafts.delete(locator);
@@ -1281,8 +1273,7 @@ export function composeRuntime(options) {
1281
1273
  function retyped(persisted) {
1282
1274
  if (persisted.status === 'unavailable')
1283
1275
  return persisted;
1284
- const { state } = attachedModel({ kind: 'model', snapshot: persisted.snapshot }, NO_MODEL, roomTypes);
1285
- return state.status === 'unavailable' ? state : { ...persisted, types: state.types };
1276
+ return { ...persisted, types: schemaTypes(persisted.snapshot.schema, roomTypes) };
1286
1277
  }
1287
1278
  function restored(key, held) {
1288
1279
  const persisted = retyped(held);
@@ -1328,8 +1319,9 @@ export function composeRuntime(options) {
1328
1319
  presentations.clear();
1329
1320
  for (const record of held.presentation)
1330
1321
  presentations.set(record.app, record.value);
1331
- conversations.hydrate(heldConversations, chatDrafts, held.conversationList);
1322
+ conversations.hydrate(heldConversations, chatDrafts, held.conversationList, held.conversationCursor);
1332
1323
  pendings.clear();
1324
+ landedAhead.clear();
1333
1325
  for (const queue of queues) {
1334
1326
  for (const { batch, state } of queue.pending) {
1335
1327
  pendings.set(pendingKey(queue.locator, batch.id), state);
@@ -1478,14 +1470,12 @@ export function composeRuntime(options) {
1478
1470
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
1479
1471
  };
1480
1472
  }
1481
- const REWRITE_LINKS = ['field', 'body'];
1482
- const CHECKED_LINKS = ['field', 'section'];
1483
1473
  async function sliceAt(record, view, queue, extra = [], moved = []) {
1484
1474
  const local = await openLocal();
1485
1475
  const needed = new Set([...touchedBy(queue), ...extra]);
1486
1476
  const movedPaths = [...new Set([...movedBy(queue, record.locator), ...moved])];
1487
1477
  if (movedPaths.length > 0) {
1488
- for (const { path } of await local.referrers(record.key, view, movedPaths, REWRITE_LINKS)) {
1478
+ for (const { path } of await local.referrers(record.key, view, movedPaths)) {
1489
1479
  needed.add(path);
1490
1480
  }
1491
1481
  }
@@ -1622,10 +1612,16 @@ export function composeRuntime(options) {
1622
1612
  ? read
1623
1613
  : await stepped(slicingWith(read, overrides.set, overrides.removed));
1624
1614
  const composition = composed(record);
1625
- const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
1626
- const founded = kept.filter((batch) => !queue.includes(batch));
1615
+ const shown = queue.filter((batch) => {
1616
+ const held = landedAhead.get(pendingKey(locator, batch.id));
1617
+ return (held !== undefined &&
1618
+ Object.entries(held).some(([path, revision]) => confirmedLayer.revisions[path] !== revision));
1619
+ });
1620
+ const unshown = queue.filter((batch) => !shown.includes(batch));
1621
+ const { withdrawn, kept } = await stepped(standing(confirmedLayer, unshown, withdrawable(locator), localSaves, locator, composition));
1622
+ const founded = kept.filter((batch) => !unshown.includes(batch));
1627
1623
  const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
1628
- const unqueued = [];
1624
+ const unqueued = shown.map((batch) => ({ batch, landed: true }));
1629
1625
  for (const batch of withdrawn) {
1630
1626
  unqueued.push({ batch, landed: await landedWhole(locator, batch, confirmedLayer) });
1631
1627
  }
@@ -1690,8 +1686,10 @@ export function composeRuntime(options) {
1690
1686
  commit,
1691
1687
  apply: (answer) => {
1692
1688
  for (const batch of previousQueue) {
1693
- if (!staying.has(batch.id))
1694
- pendings.delete(pendingKey(locator, batch.id));
1689
+ if (staying.has(batch.id))
1690
+ continue;
1691
+ pendings.delete(pendingKey(locator, batch.id));
1692
+ landedAhead.delete(pendingKey(locator, batch.id));
1695
1693
  }
1696
1694
  for (const draft of written.values())
1697
1695
  rememberRoomDraft(draft);
@@ -1740,11 +1738,7 @@ export function composeRuntime(options) {
1740
1738
  const answer = answers?.collection;
1741
1739
  if (answer === undefined)
1742
1740
  return held;
1743
- const { state, refused } = attachedModel(answer, held.collection, roomTypes);
1744
- if (refused !== null) {
1745
- const where = refused.location === undefined ? '' : ` ${refused.location}`;
1746
- journal.record(`model invalid ${locator} collection ${refused.identity} ${refused.rule}${where}`);
1747
- }
1741
+ const state = attachedModel(answer, roomTypes);
1748
1742
  models.set(locator, state);
1749
1743
  return { collection: state };
1750
1744
  }
@@ -1797,6 +1791,8 @@ export function composeRuntime(options) {
1797
1791
  return true;
1798
1792
  }
1799
1793
  if (landing.kind === 'bootstrapped') {
1794
+ highest.delete(room.locator);
1795
+ beyondHorizon.delete(room.locator);
1800
1796
  untransferred(room.locator);
1801
1797
  startedOver.delete(room.locator);
1802
1798
  catching.delete(room.locator);
@@ -1824,7 +1820,7 @@ export function composeRuntime(options) {
1824
1820
  const replica = replicas.get(locator);
1825
1821
  const room = rooms.get(locator);
1826
1822
  if (replica === undefined || room === undefined) {
1827
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1823
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
1828
1824
  }
1829
1825
  return { local, replica, room };
1830
1826
  }
@@ -1885,7 +1881,7 @@ export function composeRuntime(options) {
1885
1881
  const removed = changes.flatMap((change) => (change.kind === 'remove' ? [change.path] : []));
1886
1882
  const referring = removed.length === 0
1887
1883
  ? []
1888
- : (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
1884
+ : (await local.referrers(record.key, 'confirmed', removed)).map(({ path }) => path);
1889
1885
  const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
1890
1886
  const { visible } = await replayed(record, confirmedLayer, replica.queue);
1891
1887
  const composition = composed(record);
@@ -1978,7 +1974,7 @@ export function composeRuntime(options) {
1978
1974
  }
1979
1975
  function referrersOf(local, key, view) {
1980
1976
  return async (targets) => [
1981
- ...new Set((await local.referrers(key, view, targets, CHECKED_LINKS)).map(({ path }) => path)),
1977
+ ...new Set((await local.referrers(key, view, targets)).map(({ path }) => path)),
1982
1978
  ];
1983
1979
  }
1984
1980
  async function sideOf(locator, replica, room, saving) {
@@ -2050,7 +2046,7 @@ export function composeRuntime(options) {
2050
2046
  const target = replicas.get(destination);
2051
2047
  const listedRoom = rooms.get(destination);
2052
2048
  if (target === undefined || listedRoom === undefined) {
2053
- throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
2049
+ throw runtimeError('unknown-room', `Unknown Room ${destination}`);
2054
2050
  }
2055
2051
  const arriving = await savingIn(target, [], moves.map(({ to }) => to));
2056
2052
  const built = await buildMove({
@@ -2113,10 +2109,10 @@ export function composeRuntime(options) {
2113
2109
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2114
2110
  const { answer, touched } = committedBoth;
2115
2111
  if (answer.fenced !== null) {
2116
- throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2112
+ throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2117
2113
  }
2118
2114
  if (fences.has(locator) || fences.has(destination)) {
2119
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2115
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2120
2116
  }
2121
2117
  pendings.set(pendingKey(locator, id), {
2122
2118
  drafts: saved,
@@ -2287,10 +2283,10 @@ export function composeRuntime(options) {
2287
2283
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2288
2284
  const { answer, touched } = done;
2289
2285
  if (answer.fenced !== null) {
2290
- throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2286
+ throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2291
2287
  }
2292
2288
  if (fences.has(locator))
2293
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2289
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2294
2290
  for (const origin of withdrawn.keys())
2295
2291
  pendings.delete(pendingKey(locator, origin));
2296
2292
  for (const [id, drafts] of drafted) {
@@ -2339,7 +2335,7 @@ export function composeRuntime(options) {
2339
2335
  throw new Error('connection refused');
2340
2336
  await local.keepUpload(locator, identifier, upload);
2341
2337
  if (fences.has(locator))
2342
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2338
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2343
2339
  const key = pendingKey(locator, identifier);
2344
2340
  pendings.set(key, { ...pendings.get(key), upload });
2345
2341
  if (!continuing())
@@ -2664,6 +2660,24 @@ export function composeRuntime(options) {
2664
2660
  return UNCERTAIN;
2665
2661
  }
2666
2662
  }
2663
+ async function readConversationHistory(request) {
2664
+ await ready;
2665
+ const stream = options.adapter.conversations;
2666
+ if (closed || refusal !== null || stream?.history === undefined)
2667
+ return UNCERTAIN;
2668
+ journal.record('transfer conversation history');
2669
+ try {
2670
+ const result = await stream.history(request);
2671
+ if (result.kind === 'revoked-authorization')
2672
+ await refuse('revoked-authorization');
2673
+ journal.record(`outcome conversation history ${result.kind}`);
2674
+ return calledResult(result);
2675
+ }
2676
+ catch {
2677
+ journal.record('outcome conversation history transport-uncertainty');
2678
+ return UNCERTAIN;
2679
+ }
2680
+ }
2667
2681
  function unsettled(result) {
2668
2682
  return result.kind !== 'action' && result.retryable;
2669
2683
  }
@@ -2702,10 +2716,6 @@ export function composeRuntime(options) {
2702
2716
  continue;
2703
2717
  if (closed || signal.aborted)
2704
2718
  return 'done';
2705
- if (pastHorizon(held.operation)) {
2706
- await local.dropCall(held.operation);
2707
- continue;
2708
- }
2709
2719
  calling.add(held.operation);
2710
2720
  try {
2711
2721
  if (unsettled(await invokeAction(held.name, held.input)))
@@ -2725,6 +2735,33 @@ export function composeRuntime(options) {
2725
2735
  const held = replicas.get(locator);
2726
2736
  return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
2727
2737
  }
2738
+ function shownAt(locator) {
2739
+ const held = replicas.get(locator)?.record.roomVersion;
2740
+ const raised = highest.get(locator);
2741
+ if (held === undefined || raised === undefined)
2742
+ return held ?? raised;
2743
+ return options.adapter.reached(locator, held, raised) ? held : raised;
2744
+ }
2745
+ function showing(locator, roomVersion) {
2746
+ const at = shownAt(locator);
2747
+ if (at === undefined || !options.adapter.reached(locator, at, roomVersion)) {
2748
+ highest.set(locator, roomVersion);
2749
+ }
2750
+ }
2751
+ function older(locator, roomVersion) {
2752
+ if (beyondHorizon.has(locator))
2753
+ return false;
2754
+ const at = shownAt(locator);
2755
+ return (at !== undefined &&
2756
+ options.adapter.reached(locator, at, roomVersion) &&
2757
+ !options.adapter.reached(locator, roomVersion, at));
2758
+ }
2759
+ function replaced(locators) {
2760
+ for (const locator of locators) {
2761
+ highest.delete(locator);
2762
+ beyondHorizon.add(locator);
2763
+ }
2764
+ }
2728
2765
  async function writtenAfter(locator, roomVersion, path) {
2729
2766
  const held = replicas.get(locator);
2730
2767
  if (held === undefined)
@@ -2735,14 +2772,25 @@ export function composeRuntime(options) {
2735
2772
  options.adapter.reached(locator, written, roomVersion) &&
2736
2773
  !options.adapter.reached(locator, roomVersion, written));
2737
2774
  }
2738
- function refreshingOf(local, key, composition, liveness, replaying = null) {
2739
- const write = replaying === null ? exclusive : finishingWrite;
2775
+ async function writtenLater(local, locator, key, paths, roomVersion) {
2776
+ if (paths.length === 0 || !older(locator, roomVersion))
2777
+ return new Set();
2778
+ const entries = await local.entries(key, 'confirmed', paths);
2779
+ return new Set(entries.flatMap(({ path, written }) => written !== null &&
2780
+ options.adapter.reached(locator, written, roomVersion) &&
2781
+ !options.adapter.reached(locator, roomVersion, written)
2782
+ ? [path]
2783
+ : []));
2784
+ }
2785
+ function refreshingOf(local, locator, key, composition, liveness, replaying = false) {
2786
+ const write = replaying ? finishingWrite : exclusive;
2740
2787
  return {
2741
2788
  turn() {
2742
- if (replaying !== null)
2743
- installing(replaying);
2789
+ if (replaying)
2790
+ installing(locator);
2744
2791
  },
2745
2792
  prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
2793
+ later: (paths, roomVersion) => writtenLater(local, locator, key, paths, roomVersion),
2746
2794
  async stage(page) {
2747
2795
  for (let written = 0;;) {
2748
2796
  const end = batchEnd(page.entries, written);
@@ -2932,7 +2980,7 @@ export function composeRuntime(options) {
2932
2980
  finally {
2933
2981
  hold.take();
2934
2982
  }
2935
- }, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, composed(record), liveness));
2983
+ }, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.locator, record.key, composed(record), liveness));
2936
2984
  if (result.kind !== 'advanced') {
2937
2985
  journal.record(`transfer refresh ${record.locator} ${result.kind}`);
2938
2986
  return result;
@@ -3059,7 +3107,7 @@ export function composeRuntime(options) {
3059
3107
  derive: derived,
3060
3108
  async refreshing(locator, key, composition) {
3061
3109
  await exclusive(() => local.unrefresh(key));
3062
- return refreshingOf(local, key, composition, liveness, locator);
3110
+ return refreshingOf(local, locator, key, composition, liveness, true);
3063
3111
  },
3064
3112
  abandon: (locator) => exclusive(async () => {
3065
3113
  if (liveness())
@@ -3093,9 +3141,16 @@ export function composeRuntime(options) {
3093
3141
  liveQueries.recover();
3094
3142
  }
3095
3143
  try {
3096
- const result = await bootstrapRoom(reads, authorized, room, { collection: modelOf(room.locator) }, builtIn, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
3144
+ const result = await bootstrapRoom(reads, authorized, room, { collection: modelOf(room.locator) }, builtIn, typedPaths, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
3097
3145
  const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
3098
3146
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
3147
+ if (result.kind === 'staged' && older(room.locator, result.record.roomVersion)) {
3148
+ caughtUpTo(room.locator);
3149
+ installed(room.locator);
3150
+ await abandoned(local, room.locator);
3151
+ journal.record(`transfer bootstrap ${room.locator} older than shown`);
3152
+ return { kind: 'transport-uncertainty' };
3153
+ }
3099
3154
  if (result.kind !== 'staged') {
3100
3155
  caughtUpTo(room.locator);
3101
3156
  installed(room.locator);
@@ -3146,6 +3201,7 @@ export function composeRuntime(options) {
3146
3201
  return refreshed;
3147
3202
  if (reloadsDue.has(room.locator))
3148
3203
  return unconfirmed;
3204
+ replaced([room.locator]);
3149
3205
  }
3150
3206
  const from = standingOf(room.locator);
3151
3207
  const bootstrapped = await pullBootstrap(authorized, room, local, live);
@@ -3190,7 +3246,7 @@ export function composeRuntime(options) {
3190
3246
  return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
3191
3247
  }
3192
3248
  async function withhold(withheld, room, local, progress) {
3193
- if (withheld.kind === 'unconfirmed')
3249
+ if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
3194
3250
  return;
3195
3251
  stale.add(room.locator);
3196
3252
  if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
@@ -3219,8 +3275,6 @@ export function composeRuntime(options) {
3219
3275
  }
3220
3276
  }
3221
3277
  function leaving(batch, result) {
3222
- if (result.kind === 'expired')
3223
- return { batch, returned: returnedBy('expired') };
3224
3278
  return {
3225
3279
  batch,
3226
3280
  returned: {
@@ -3253,10 +3307,11 @@ export function composeRuntime(options) {
3253
3307
  logAccepted(room.locator, submitted, accepted);
3254
3308
  let record = current.record;
3255
3309
  let changes;
3256
- if (!options.adapter.reached(room.locator, held, accepted.roomVersion)) {
3310
+ if (!options.adapter.reached(room.locator, held, accepted.roomVersion) &&
3311
+ !older(room.locator, accepted.roomVersion)) {
3257
3312
  const paths = [...returnedPaths(submitted, accepted)];
3258
3313
  const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
3259
- const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
3314
+ const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion, composed(record)));
3260
3315
  record = {
3261
3316
  ...record,
3262
3317
  roomVersion: options.adapter.follows(room.locator, held, accepted.roomVersion)
@@ -3270,6 +3325,7 @@ export function composeRuntime(options) {
3270
3325
  ...settled,
3271
3326
  apply(answer) {
3272
3327
  const set = settled.apply(answer);
3328
+ showing(room.locator, accepted.roomVersion);
3273
3329
  pendings.delete(pendingKey(room.locator, submitted.id));
3274
3330
  unreadable.delete(room.locator);
3275
3331
  list(room, true);
@@ -3419,15 +3475,10 @@ export function composeRuntime(options) {
3419
3475
  return;
3420
3476
  moveTo({ ...status, ...attention() });
3421
3477
  }
3422
- async function discardHeld(head, room, local, signal) {
3423
- const partner = partnerOf(head, room.locator);
3424
- const other = partner === null ? undefined : discovered.get(partner);
3425
- await discardPending(other === undefined ? [room] : [room, other], local, signal);
3426
- }
3427
3478
  async function submitRoom(room, local, authorized, progress, signal) {
3428
3479
  while (continuing() && authorization !== undefined && !signal.aborted) {
3429
3480
  const replica = replicas.get(room.locator);
3430
- const head = replica?.queue[0];
3481
+ const head = replica?.queue.find((batch) => !landedAhead.has(pendingKey(room.locator, batch.id)));
3431
3482
  if (replica === undefined || head === undefined)
3432
3483
  return 'done';
3433
3484
  const state = pendings.get(pendingKey(room.locator, head.id));
@@ -3454,10 +3505,6 @@ export function composeRuntime(options) {
3454
3505
  return 'done';
3455
3506
  }
3456
3507
  const lost = earlier || submission.state === 'submitted';
3457
- if (lost && pastHorizon(head.id)) {
3458
- await discardHeld(head, room, local, signal);
3459
- return 'done';
3460
- }
3461
3508
  let result;
3462
3509
  journal.record(`submission ${room.locator} ${head.id}`);
3463
3510
  await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
@@ -3535,32 +3582,12 @@ export function composeRuntime(options) {
3535
3582
  const other = partner === null ? undefined : discovered.get(partner);
3536
3583
  if (result.kind === 'conflicted' ||
3537
3584
  result.kind === 'rejected' ||
3538
- result.kind === 'expired' ||
3539
3585
  result.kind === 'retryable-refusal') {
3540
- if (result.kind === 'expired' && lost && result.restored !== true) {
3541
- journal.record(`outcome ${room.locator} ${head.id} expired`);
3542
- await discardHeld(head, room, local, signal);
3543
- return 'done';
3544
- }
3545
- if (result.kind === 'expired') {
3546
- await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
3547
- const there = other === undefined ? null : await advanced(other, local, authorized);
3548
- if (other !== undefined && there !== null) {
3549
- await withhold(there, other, local, progress);
3550
- return 'done';
3551
- }
3552
- const withheld = await advanced(room, local, authorized);
3553
- if (withheld !== null) {
3554
- await withhold(withheld, room, local, progress);
3555
- return 'done';
3556
- }
3557
- }
3558
3586
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
3559
3587
  (result.kind === 'conflicted' && result.roomVersion !== undefined
3560
3588
  ? ` ${result.roomVersion}`
3561
3589
  : ''));
3562
- if (result.kind !== 'expired')
3563
- journal.record(`refusal ${result.kind} ${result.code}`);
3590
+ journal.record(`refusal ${result.kind} ${result.code}`);
3564
3591
  }
3565
3592
  if (result.kind === 'retryable-refusal') {
3566
3593
  const reset = result.reset;
@@ -3568,7 +3595,7 @@ export function composeRuntime(options) {
3568
3595
  await exclusive(() => mark(local, room.locator, head, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
3569
3596
  return reset === undefined ? 'again' : { at: Date.parse(reset) };
3570
3597
  }
3571
- if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
3598
+ if (result.kind === 'conflicted' || result.kind === 'rejected') {
3572
3599
  const left = leaving(head, result);
3573
3600
  const refreshed = async () => ({
3574
3601
  there: other === undefined ? null : await advanced(other, local, authorized),
@@ -3592,16 +3619,19 @@ export function composeRuntime(options) {
3592
3619
  return;
3593
3620
  }
3594
3621
  const confirmedLayer = await sliceAt(current.record, 'confirmed', [head]);
3595
- const held = (result.kind === 'expired' &&
3596
- (await landedWhole(room.locator, head, confirmedLayer))) ||
3597
- (result.kind === 'conflicted' &&
3598
- (await landedWhole(room.locator, head, confirmedLayer, result.affected)));
3622
+ const held = result.kind === 'conflicted' &&
3623
+ (await landedWhole(room.locator, head, confirmedLayer, result.affected));
3624
+ if (held &&
3625
+ result.kind === 'conflicted' &&
3626
+ partner === null &&
3627
+ !landed(head, confirmedLayer)) {
3628
+ landedAhead.set(pendingKey(room.locator, head.id), Object.fromEntries([...touchedBy([head])].map((path) => [path, confirmedLayer.revisions[path]])));
3629
+ return;
3630
+ }
3599
3631
  publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
3600
3632
  await unpair(local);
3601
3633
  moveTo({ ...status, ...attention() });
3602
3634
  });
3603
- if (result.kind === 'expired')
3604
- continue;
3605
3635
  const { there, here } = judged ?? (await refreshed());
3606
3636
  if (other !== undefined && there !== null)
3607
3637
  await withhold(there, other, local, progress);
@@ -3621,67 +3651,6 @@ export function composeRuntime(options) {
3621
3651
  }
3622
3652
  return 'done';
3623
3653
  }
3624
- function pastHorizon(identity) {
3625
- return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
3626
- }
3627
- async function discardPending(rooms, local, signal) {
3628
- const discarded = await exclusive(async () => {
3629
- const held = rooms
3630
- .filter(({ locator }) => !fences.has(locator))
3631
- .map((room) => ({
3632
- room,
3633
- before: replicas.get(room.locator),
3634
- queue: queueIn(room.locator),
3635
- }));
3636
- if (held.length === 0)
3637
- return [];
3638
- const answer = await local.commit({
3639
- rooms: held.flatMap(({ room, before }) => before === undefined
3640
- ? []
3641
- : [
3642
- {
3643
- locator: room.locator,
3644
- key: before.record.key,
3645
- roots: before.record.typedSelection?.roots ?? [],
3646
- composed: composedOf(composed(before.record)),
3647
- overlay: { set: [], cleared: [...before.overlaid] },
3648
- },
3649
- ]),
3650
- discards: held.map(({ room }) => room.locator),
3651
- });
3652
- for (const { room, before, queue } of held) {
3653
- const { locator } = room;
3654
- lastFenced.set(locator, ++fencings);
3655
- for (const batch of queue)
3656
- pendings.delete(pendingKey(locator, batch.id));
3657
- queuesWithoutReplica.delete(locator);
3658
- if (before !== undefined) {
3659
- replicas.set(locator, {
3660
- ...before,
3661
- queue: [],
3662
- overlaid: new Set(),
3663
- count: answer.counts[locator] ?? before.count,
3664
- });
3665
- }
3666
- journal.record(`room discarded ${locator}`);
3667
- }
3668
- publishChangeSet(mergeChangeSets(held.map(({ room }) => changeSet(room.locator, {
3669
- documents: answer.changed[room.locator] ?? [],
3670
- queue: true,
3671
- }))));
3672
- moveTo({ ...status, ...attention() });
3673
- return held.map(({ room }) => room);
3674
- });
3675
- for (const room of discarded) {
3676
- if (signal.aborted)
3677
- return;
3678
- const progress = { complete: true, reachable: true };
3679
- await roomTask(room.locator, (live) => reload(room, local, progress, live));
3680
- if (signal.aborted)
3681
- return;
3682
- await rest(progress);
3683
- }
3684
- }
3685
3654
  function awaitingAnswer(locator) {
3686
3655
  answering.get(locator)?.release();
3687
3656
  let release = () => { };
@@ -3703,6 +3672,30 @@ export function composeRuntime(options) {
3703
3672
  JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
3704
3673
  JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
3705
3674
  }
3675
+ async function keepNaming(local, room) {
3676
+ const held = replicas.get(room.locator)?.record;
3677
+ if (held === undefined || (held.name === room.name && held.handle === room.handle))
3678
+ return;
3679
+ await exclusive(async () => {
3680
+ if (!continuing())
3681
+ return;
3682
+ const replica = replicas.get(room.locator);
3683
+ if (replica === undefined)
3684
+ return;
3685
+ const record = { ...replica.record, name: room.name, handle: room.handle };
3686
+ await local.commit({
3687
+ rooms: [
3688
+ {
3689
+ locator: room.locator,
3690
+ key: record.key,
3691
+ record: storedRecord(record),
3692
+ composed: composedOf(composed(record)),
3693
+ },
3694
+ ],
3695
+ });
3696
+ replicas.set(room.locator, { ...replica, record });
3697
+ });
3698
+ }
3706
3699
  async function retainModel(local, room, next) {
3707
3700
  const key = room.locator;
3708
3701
  const previous = modelOf(room.locator);
@@ -3720,6 +3713,8 @@ export function composeRuntime(options) {
3720
3713
  if (held !== undefined) {
3721
3714
  const record = {
3722
3715
  ...held.record,
3716
+ name: room.name,
3717
+ handle: room.handle,
3723
3718
  operations: room.operations,
3724
3719
  replica: room.replica,
3725
3720
  typedSelection: room.typedSelection ?? null,
@@ -3822,7 +3817,6 @@ export function composeRuntime(options) {
3822
3817
  options.adapter.roomStream?.hold(heldRooms(ready));
3823
3818
  handed = true;
3824
3819
  }
3825
- discovering?.release();
3826
3820
  for (const room of ready) {
3827
3821
  if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
3828
3822
  if (attempt === 'authorized')
@@ -3850,6 +3844,8 @@ export function composeRuntime(options) {
3850
3844
  }
3851
3845
  if (prior !== undefined)
3852
3846
  await retainModel(local, room, modelOf(room.locator));
3847
+ if (prior !== undefined)
3848
+ await keepNaming(local, room);
3853
3849
  if (discoveryPaused || authorization === undefined)
3854
3850
  return false;
3855
3851
  return room.replica.admission === 'admitted';
@@ -3903,7 +3899,39 @@ export function composeRuntime(options) {
3903
3899
  caughtUpTo(room.locator);
3904
3900
  }
3905
3901
  }
3906
- async function discoverRooms(local, progress, attempt, live, scope) {
3902
+ async function asked(authorized) {
3903
+ try {
3904
+ return (await options.adapter.discover?.(authorized)) ?? { kind: 'transport-uncertainty' };
3905
+ }
3906
+ catch {
3907
+ return { kind: 'transport-uncertainty' };
3908
+ }
3909
+ }
3910
+ function malformed(listed) {
3911
+ for (const room of listed) {
3912
+ if (room.operations === undefined ||
3913
+ !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
3914
+ room.operations[operation].every((scope) => typeof scope === 'string')) ||
3915
+ room.replica === undefined ||
3916
+ typeof room.replica.selection !== 'string' ||
3917
+ room.replica.selection.length === 0 ||
3918
+ !['admitted', 'unavailable'].includes(room.replica.admission) ||
3919
+ !['locked', 'open'].includes(room.replica.offline)) {
3920
+ return `invalid model or replica declaration ${room.locator}`;
3921
+ }
3922
+ if (!declaredSelection(room))
3923
+ return `invalid typed selection declaration ${room.locator}`;
3924
+ }
3925
+ return null;
3926
+ }
3927
+ async function signIn(local, signedIn) {
3928
+ if (JSON.stringify(signedIn) === JSON.stringify(member))
3929
+ return;
3930
+ await local.fact('member', signedIn);
3931
+ member = signedIn;
3932
+ emitMember();
3933
+ }
3934
+ async function discoverRooms(local, progress, attempt, live, found) {
3907
3935
  if (closed || authorization === undefined) {
3908
3936
  notifyReady();
3909
3937
  return;
@@ -3911,13 +3939,7 @@ export function composeRuntime(options) {
3911
3939
  moveTo(transferring(status));
3912
3940
  discoveries += 1;
3913
3941
  const started = discoveries;
3914
- let result;
3915
- try {
3916
- result = await options.adapter.discover(authorization, scope);
3917
- }
3918
- catch {
3919
- result = { kind: 'transport-uncertainty' };
3920
- }
3942
+ const result = found ?? (await asked(authorization));
3921
3943
  if (result.kind === 'discovery')
3922
3944
  roomsNamed = true;
3923
3945
  if (!live()) {
@@ -3927,7 +3949,7 @@ export function composeRuntime(options) {
3927
3949
  }
3928
3950
  journal.record(`connection discover ${result.kind}`);
3929
3951
  if (result.kind === 'transport-uncertainty') {
3930
- await settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
3952
+ await cachedModels(local);
3931
3953
  progress.reachable = false;
3932
3954
  progress.complete = false;
3933
3955
  notifyReady();
@@ -3948,26 +3970,12 @@ export function composeRuntime(options) {
3948
3970
  notifyReady();
3949
3971
  return;
3950
3972
  }
3951
- for (const room of result.rooms) {
3952
- if (room.operations === undefined ||
3953
- !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
3954
- room.operations[operation].every((scope) => typeof scope === 'string')) ||
3955
- room.replica === undefined ||
3956
- typeof room.replica.selection !== 'string' ||
3957
- room.replica.selection.length === 0 ||
3958
- !['admitted', 'unavailable'].includes(room.replica.admission) ||
3959
- !['locked', 'open'].includes(room.replica.offline)) {
3960
- journal.record(`invalid model or replica declaration ${room.locator}`);
3961
- await refuse('missing-required-guarantee');
3962
- notifyReady();
3963
- return;
3964
- }
3965
- if (!declaredSelection(room)) {
3966
- journal.record(`invalid typed selection declaration ${room.locator}`);
3967
- await refuse('missing-required-guarantee');
3968
- notifyReady();
3969
- return;
3970
- }
3973
+ const invalid = malformed(result.rooms);
3974
+ if (invalid !== null) {
3975
+ journal.record(invalid);
3976
+ await refuse('missing-required-guarantee');
3977
+ notifyReady();
3978
+ return;
3971
3979
  }
3972
3980
  if (refusal !== null) {
3973
3981
  refusal = null;
@@ -3975,7 +3983,6 @@ export function composeRuntime(options) {
3975
3983
  }
3976
3984
  const previousCapabilities = capabilities;
3977
3985
  capabilities = result.capabilities;
3978
- outcomeRetention = result.retention.outcome;
3979
3986
  notifyReady();
3980
3987
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
3981
3988
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
@@ -3983,13 +3990,8 @@ export function composeRuntime(options) {
3983
3990
  await local.fact('providerLimits', limits);
3984
3991
  await local.fact('capabilities', capabilities);
3985
3992
  }
3986
- const signedIn = result.member ?? null;
3987
- if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
3988
- await local.fact('member', signedIn);
3989
- member = signedIn;
3990
- emitMember();
3991
- }
3992
- await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
3993
+ await signIn(local, result.member ?? null);
3994
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
3993
3995
  }
3994
3996
  async function ensureAuthorization() {
3995
3997
  if (authorization !== undefined)
@@ -4058,14 +4060,13 @@ export function composeRuntime(options) {
4058
4060
  void run.finally(() => working.delete(run));
4059
4061
  return run;
4060
4062
  }
4061
- function sync(attempt) {
4063
+ function sync(attempt, found) {
4062
4064
  return task(OPENING_LANE, async (live) => {
4063
4065
  if (!live()) {
4064
4066
  readyFromStore();
4065
4067
  return;
4066
4068
  }
4067
4069
  const progress = { complete: true, reachable: true };
4068
- const gate = discover();
4069
4070
  underway = attempt === 'opened' ? progress : null;
4070
4071
  if (attempt === 'opened')
4071
4072
  streamed = progress;
@@ -4079,7 +4080,7 @@ export function composeRuntime(options) {
4079
4080
  await ensureAuthorization();
4080
4081
  if (!live())
4081
4082
  return;
4082
- await discoverRooms(local, progress, attempt, live);
4083
+ await discoverRooms(local, progress, attempt, live, found);
4083
4084
  }
4084
4085
  catch {
4085
4086
  progress.complete = false;
@@ -4087,7 +4088,6 @@ export function composeRuntime(options) {
4087
4088
  progress.reachable = false;
4088
4089
  }
4089
4090
  finally {
4090
- gate.release();
4091
4091
  notifyReady();
4092
4092
  }
4093
4093
  if (!live())
@@ -4099,34 +4099,59 @@ export function composeRuntime(options) {
4099
4099
  if (handed || closed || progress.reachable)
4100
4100
  return;
4101
4101
  underway = null;
4102
- journal.record('connection reopen');
4103
4102
  await rest(progress, live);
4104
- options.adapter.roomStream?.reopen();
4105
4103
  });
4106
4104
  }
4107
- function rediscover(scope) {
4105
+ function resynchronize() {
4108
4106
  return task(OPENING_LANE, async (live) => {
4109
- if (!live() || !socketOpen)
4107
+ if (!live() || !announced)
4110
4108
  return;
4111
4109
  const progress = { complete: true, reachable: true };
4112
- const gate = discover();
4113
4110
  moveTo(transferring(status));
4114
4111
  try {
4115
4112
  const local = await openLocal();
4116
4113
  if (!live())
4117
4114
  return;
4118
- await ensureAuthorization();
4115
+ await synchronize([...discovered.values()], local, progress, 'unstreamed', live);
4116
+ }
4117
+ catch {
4118
+ progress.complete = false;
4119
+ }
4120
+ if (live())
4121
+ await finishOpening(progress, live);
4122
+ });
4123
+ }
4124
+ function countAuthorityChange() {
4125
+ authorityChanges += 1;
4126
+ for (const listener of authorityChangeListeners)
4127
+ listener(authorityChanges);
4128
+ }
4129
+ function changeAuthority(change) {
4130
+ return task(OPENING_LANE, async (live) => {
4131
+ if (!live() || !announced)
4132
+ return;
4133
+ countAuthorityChange();
4134
+ const progress = { complete: true, reachable: true };
4135
+ moveTo(transferring(status));
4136
+ try {
4137
+ const local = await openLocal();
4119
4138
  if (!live())
4120
4139
  return;
4121
- await discoverRooms(local, progress, 'authorized', live, scope);
4140
+ const invalid = malformed(change.rooms);
4141
+ if (invalid !== null) {
4142
+ journal.record(invalid);
4143
+ await refuse('missing-required-guarantee');
4144
+ return;
4145
+ }
4146
+ if (change.member !== undefined)
4147
+ await signIn(local, change.member);
4148
+ discoveries += 1;
4149
+ const listed = await applyDiscovery(change.rooms, discoveries, change.left);
4150
+ await synchronize(listed, local, progress, 'authorized', live);
4122
4151
  }
4123
4152
  catch {
4124
4153
  progress.complete = false;
4125
4154
  }
4126
- finally {
4127
- gate.release();
4128
- notifyReady();
4129
- }
4130
4155
  if (!live())
4131
4156
  return;
4132
4157
  if (!progress.complete)
@@ -4134,10 +4159,12 @@ export function composeRuntime(options) {
4134
4159
  await rest(progress, live);
4135
4160
  });
4136
4161
  }
4137
- function closedSocket(revoked, lost) {
4138
- notifyDiscovered();
4139
- const opened = socketOpen;
4162
+ function closedSocket(revoked, lost, authorityChanged) {
4163
+ notifyReady();
4164
+ const opened = announced;
4140
4165
  socketOpen = false;
4166
+ announced = false;
4167
+ missedChange ||= authorityChanged;
4141
4168
  handed = false;
4142
4169
  naming = 0;
4143
4170
  connectionLost();
@@ -4153,14 +4180,29 @@ export function composeRuntime(options) {
4153
4180
  moveTo({
4154
4181
  ...status,
4155
4182
  state: status.state === 'synced' ? 'syncing' : status.state,
4156
- providerUnreachable: status.providerUnreachable || !opened,
4183
+ providerUnreachable: status.providerUnreachable || (!opened && lost),
4157
4184
  });
4185
+ void closing(opened);
4158
4186
  }
4159
- function applyOpening(expired) {
4187
+ function closing(opened) {
4188
+ return task(OPENING_LANE, async (live) => {
4189
+ if (!live())
4190
+ return;
4191
+ const local = await openLocal();
4192
+ if (!opened && !socketOpen)
4193
+ await cachedModels(local);
4194
+ await rest(null, live);
4195
+ });
4196
+ }
4197
+ function cachedModels(local) {
4198
+ return settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
4199
+ }
4200
+ function caughtUpRooms(expired) {
4160
4201
  const progress = underway;
4161
4202
  underway = null;
4162
4203
  naming = Math.max(0, naming - 1);
4163
4204
  const cycle = progress ?? { complete: true, reachable: true };
4205
+ replaced(expired);
4164
4206
  const reloading = progress === null ? [] : owedReloads(expired);
4165
4207
  const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
4166
4208
  return task(OPENING_LANE, async (live) => {
@@ -4394,21 +4436,11 @@ export function composeRuntime(options) {
4394
4436
  ...attention(),
4395
4437
  });
4396
4438
  }
4397
- function discover() {
4398
- discovering?.release();
4399
- let release = () => { };
4400
- const done = new Promise((resolve) => {
4401
- release = resolve;
4402
- });
4403
- discovering = { done, release };
4404
- return discovering;
4405
- }
4406
4439
  function applyRoom(change) {
4407
4440
  const { locator } = change;
4408
4441
  if (stopping)
4409
4442
  return Promise.resolve();
4410
4443
  return inRoom(locator, async (live) => {
4411
- await discovering?.done;
4412
4444
  await answering.get(locator)?.answered;
4413
4445
  const progress = underway ?? { complete: true, reachable: true };
4414
4446
  try {
@@ -4466,20 +4498,30 @@ export function composeRuntime(options) {
4466
4498
  return;
4467
4499
  if (event.kind === 'opened') {
4468
4500
  socketOpen = true;
4501
+ announced = false;
4469
4502
  handed = false;
4470
4503
  naming = 0;
4471
4504
  connectionLive();
4472
4505
  generation += 1;
4473
4506
  conversations.reopened();
4474
4507
  recovery.resume();
4475
- void sync('opened');
4508
+ }
4509
+ else if (event.kind === 'discovered') {
4510
+ if (!socketOpen)
4511
+ return;
4512
+ announced = true;
4513
+ if (missedChange) {
4514
+ missedChange = false;
4515
+ countAuthorityChange();
4516
+ }
4517
+ void sync('opened', event.discovery);
4476
4518
  }
4477
4519
  else if (event.kind === 'closed')
4478
- closedSocket(event.revoked, event.lost);
4479
- else if (event.kind === 'opening')
4480
- void applyOpening(event.expired);
4520
+ closedSocket(event.revoked, event.lost, event.authorityChanged);
4521
+ else if (event.kind === 'caught-up')
4522
+ void caughtUpRooms(event.expired);
4481
4523
  else if (event.kind === 'authority-changed')
4482
- void rediscover(event.scope);
4524
+ void changeAuthority(event);
4483
4525
  else
4484
4526
  void applyRoom(event);
4485
4527
  });
@@ -4572,6 +4614,7 @@ export function composeRuntime(options) {
4572
4614
  statusListeners.clear();
4573
4615
  serviceModeListeners.clear();
4574
4616
  memberListeners.clear();
4617
+ authorityChangeListeners.clear();
4575
4618
  liveQueries.close();
4576
4619
  queueSubscriptions.clear();
4577
4620
  roomDraftListeners.clear();
@@ -4598,15 +4641,15 @@ export function composeRuntime(options) {
4598
4641
  readyFromStore();
4599
4642
  });
4600
4643
  if (options.adapter.roomStream === undefined)
4601
- void sync('unstreamed');
4644
+ void sync('unstreamed', null);
4602
4645
  function planned(input, reach) {
4603
- const plan = planOf(input, roomFacts(reach), planReach(reach));
4646
+ const plan = planWithin(input, roomFacts(reach), planReach(reach));
4604
4647
  const named = plan.paths !== null &&
4605
4648
  plan.rooms !== null &&
4606
4649
  plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
4607
4650
  return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
4608
4651
  }
4609
- function watchPlan(plan, deliver) {
4652
+ function watchPlan(plan, deliver, fail) {
4610
4653
  let seeded = false;
4611
4654
  let pending = true;
4612
4655
  const shown = () => {
@@ -4632,7 +4675,7 @@ export function composeRuntime(options) {
4632
4675
  }
4633
4676
  seeded = false;
4634
4677
  deliver(message);
4635
- });
4678
+ }, fail);
4636
4679
  return () => {
4637
4680
  watchers.delete(shown);
4638
4681
  if (watchers.size === 0)
@@ -4640,61 +4683,59 @@ export function composeRuntime(options) {
4640
4683
  stop();
4641
4684
  };
4642
4685
  }
4643
- async function searched(input, reach, locked) {
4686
+ async function searched(input, reach) {
4644
4687
  const asked = searchPlanOf(input);
4645
- const filter = {
4646
- ...filterOf(asked.documents ?? {}, roomFacts(reach), planReach(reach)),
4647
- locked,
4648
- };
4649
- const unlocking = (filter.rooms ?? [])
4650
- .filter((room) => locked.includes(room))
4651
- .map((room) => ({ room, awaits: 'unlock' }));
4652
- for (;;) {
4653
- const cover = covered(filter);
4654
- awaitPreparation(cover);
4655
- const pending = [...unlocking, ...('pending' in cover ? cover.pending : [])];
4656
- const complete = pending.length === 0 && !('pending' in cover);
4657
- const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
4658
- const answering = complete
4659
- ? filter
4660
- : { ...filter, rooms: reachedRooms(filter).filter((room) => !awaited.has(room)) };
4661
- const answered = covered(answering);
4662
- if ('pending' in answered)
4663
- continue;
4664
- const listed = conversations.searchable();
4665
- const named = reach !== null && !reach.conversations
4666
- ? []
4667
- : (asked.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
4668
- const hits = await (await openLocal()).search({
4669
- text: asked.text,
4670
- limit: capped(asked.limit),
4671
- after: asked.after,
4672
- total: asked.total,
4673
- documents: {
4674
- plan: answering,
4675
- rooms: answered.rooms,
4676
- beneath: asked.collection
4677
- ? answered.rooms.map(({ locator, key }) => ({
4678
- key,
4679
- roots: replicas.get(locator)?.record.typedSelection?.roots ?? [],
4680
- }))
4681
- : [],
4682
- },
4683
- conversations: named,
4684
- });
4685
- if (JSON.stringify(covered(answering)) !== JSON.stringify(answered))
4686
- continue;
4687
- const coverage = {
4688
- rooms: answered.coverage.rooms,
4689
- conversations: named,
4690
- overlay: answered.coverage.overlay,
4688
+ await ready;
4689
+ const search = options.adapter.search;
4690
+ if (search === undefined) {
4691
+ return {
4692
+ kind: 'undeclared',
4693
+ code: 'action-undeclared',
4694
+ fields: { name: 'search' },
4695
+ retryable: false,
4691
4696
  };
4692
- return complete
4693
- ? { ...hits, coverage }
4694
- : { pending, documents: hits.documents, messages: hits.messages, coverage };
4697
+ }
4698
+ if (closed || refusal !== null || connectionOf() !== 'connected')
4699
+ return OFFLINE;
4700
+ const reached = reach === null ? null : new Set(reach.rooms);
4701
+ const named = asked.rooms === null
4702
+ ? reached === null
4703
+ ? null
4704
+ : [...reached]
4705
+ : asked.rooms.filter((room) => reached === null || reached.has(room));
4706
+ const rooms = named === null ? null : named.filter((room) => replicas.has(room)).sort();
4707
+ if (rooms !== null && rooms.length === 0)
4708
+ return { kind: 'found', documents: [] };
4709
+ try {
4710
+ await ensureAuthorization();
4711
+ }
4712
+ catch {
4713
+ return UNCERTAIN;
4714
+ }
4715
+ if (closed || authorization === undefined || refusal !== null)
4716
+ return UNCERTAIN;
4717
+ journal.record('transfer search');
4718
+ try {
4719
+ const result = await search(authorization, { text: asked.text, rooms });
4720
+ if (result.kind === 'revoked-authorization')
4721
+ await refuse('revoked-authorization');
4722
+ journal.record(`outcome search ${result.kind}`);
4723
+ if (result.kind === 'rejected')
4724
+ return result;
4725
+ if (result.kind !== 'found')
4726
+ return failure(result);
4727
+ const kept = rooms === null ? null : new Set(rooms);
4728
+ return {
4729
+ kind: 'found',
4730
+ documents: result.documents.filter((hit) => kept === null || kept.has(hit.room)),
4731
+ };
4732
+ }
4733
+ catch {
4734
+ journal.record('outcome search transport-uncertainty');
4735
+ return UNCERTAIN;
4695
4736
  }
4696
4737
  }
4697
- const watch = (topic, input, deliver) => {
4738
+ const watch = (topic, input, deliver, fail) => {
4698
4739
  if (topic === 'room-drafts') {
4699
4740
  const listener = { notify: deliver, ready: false };
4700
4741
  roomDraftListeners.add(listener);
@@ -4743,7 +4784,7 @@ export function composeRuntime(options) {
4743
4784
  };
4744
4785
  }
4745
4786
  if (topic === 'readiness') {
4746
- return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
4787
+ return watchReadiness({ target: '', code: 'stored' }, null, deliver);
4747
4788
  }
4748
4789
  if (topic === 'rooms') {
4749
4790
  unlistedRooms.add(deliver);
@@ -4788,6 +4829,13 @@ export function composeRuntime(options) {
4788
4829
  memberListeners.delete(deliver);
4789
4830
  };
4790
4831
  }
4832
+ if (topic === 'authority-changes') {
4833
+ authorityChangeListeners.add(deliver);
4834
+ deliver(authorityChanges);
4835
+ return () => {
4836
+ authorityChangeListeners.delete(deliver);
4837
+ };
4838
+ }
4791
4839
  if (topic === 'queue') {
4792
4840
  const queue = { locator: input[0], notify: deliver };
4793
4841
  queueSubscriptions.add(queue);
@@ -4796,7 +4844,7 @@ export function composeRuntime(options) {
4796
4844
  queueSubscriptions.delete(queue);
4797
4845
  };
4798
4846
  }
4799
- return watchPlan(planned(input[0], null), deliver);
4847
+ return watchPlan(planned(input[0], null), deliver, fail);
4800
4848
  };
4801
4849
  const md = {
4802
4850
  loaded() {
@@ -4816,7 +4864,7 @@ export function composeRuntime(options) {
4816
4864
  });
4817
4865
  },
4818
4866
  query: ((input) => rowsQuery(planned(input, null))),
4819
- search: (input) => searched(input, null, []),
4867
+ search: (input) => searched(input, null),
4820
4868
  types(locator) {
4821
4869
  return {
4822
4870
  collection: modelTypes(modelOf(locator)),
@@ -4919,6 +4967,8 @@ export function composeRuntime(options) {
4919
4967
  return journal.export();
4920
4968
  },
4921
4969
  cleanUp,
4970
+ unsent: () => database.ask({ kind: 'authored.unsent' }),
4971
+ reset: () => options.shell.reset(),
4922
4972
  persistence() {
4923
4973
  return persistence === 'granted';
4924
4974
  },
@@ -4941,7 +4991,7 @@ export function composeRuntime(options) {
4941
4991
  authorization = undefined;
4942
4992
  discoveryPaused = false;
4943
4993
  if (options.adapter.roomStream === undefined)
4944
- await sync('unstreamed');
4994
+ await sync('unstreamed', null);
4945
4995
  else
4946
4996
  await task(OPENING_LANE, ensureAuthorization);
4947
4997
  },
@@ -4964,7 +5014,6 @@ export function composeRuntime(options) {
4964
5014
  md,
4965
5015
  seed,
4966
5016
  stop,
4967
- lose: loseRooms,
4968
5017
  watch,
4969
5018
  persisted(answer) {
4970
5019
  if (answer === persistence)