@agentshouse/mdruntime 0.1.0-cab6762661a85 → 0.1.0-cac707d698ca2

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 (112) hide show
  1. package/README.md +1 -1
  2. package/dist/adapter.d.ts +68 -52
  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 +3 -3
  8. package/dist/bootstrap.d.ts.map +1 -1
  9. package/dist/bootstrap.js +15 -11
  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 +613 -549
  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 +15 -14
  27. package/dist/conversation.d.ts.map +1 -1
  28. package/dist/conversation.js +136 -164
  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 +10 -4
  50. package/dist/memory.d.ts.map +1 -1
  51. package/dist/memory.js +163 -82
  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 +6 -4
  72. package/dist/refresh.d.ts.map +1 -1
  73. package/dist/refresh.js +28 -4
  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/search.d.ts +12 -63
  84. package/dist/search.d.ts.map +1 -1
  85. package/dist/search.js +9 -52
  86. package/dist/store.d.ts +6 -15
  87. package/dist/store.d.ts.map +1 -1
  88. package/dist/store.js +11 -34
  89. package/dist/stored.d.ts +3 -4
  90. package/dist/stored.d.ts.map +1 -1
  91. package/dist/stored.js +66 -122
  92. package/dist/tables.d.ts +8 -23
  93. package/dist/tables.d.ts.map +1 -1
  94. package/dist/tables.js +22 -50
  95. package/dist/validation.d.ts.map +1 -1
  96. package/dist/validation.js +4 -1
  97. package/dist/windows.d.ts +1 -12
  98. package/dist/windows.d.ts.map +1 -1
  99. package/dist/windows.js +2 -75
  100. package/dist/wire.d.ts +9 -4
  101. package/dist/wire.d.ts.map +1 -1
  102. package/dist/wire.js +3 -1
  103. package/package.json +2 -2
  104. package/dist/fulltext.d.ts +0 -27
  105. package/dist/fulltext.d.ts.map +0 -1
  106. package/dist/fulltext.js +0 -370
  107. package/dist/reads.d.ts +0 -11
  108. package/dist/reads.d.ts.map +0 -1
  109. package/dist/reads.js +0 -91
  110. package/dist/words.d.ts +0 -19
  111. package/dist/words.d.ts.map +0 -1
  112. package/dist/words.js +0 -156
@@ -1,8 +1,8 @@
1
- import { escapePointer, 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';
@@ -20,7 +20,6 @@ import { attachedFile } from './file.js';
20
20
  import { openedFiles } from './opened.js';
21
21
  import { ABSENT_TARGET, absentBinary, attachAnswer, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, OFFLINE, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
22
22
  import { createRecovery } from './recovery.js';
23
- import { doorReads } from './reads.js';
24
23
  import { advancement } from './refresh.js';
25
24
  import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
26
25
  import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
@@ -31,7 +30,7 @@ import { readinessOf, } from './readiness.js';
31
30
  import { FencedMap } from './fence.js';
32
31
  import { sameBinary } from './overlay.js';
33
32
  import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
34
- import { identityTime, roomDraftRevision } from './identity.js';
33
+ import { roomDraftRevision } from './identity.js';
35
34
  import { ancestors, beneath, normalize } from './paths.js';
36
35
  import { openRuntimeStore, } from './store.js';
37
36
  import { validatedNeighbourhood } from './validation.js';
@@ -77,17 +76,15 @@ function operationOf(input) {
77
76
  const operation = input?.operation_id;
78
77
  return typeof operation === 'string' ? operation : null;
79
78
  }
80
- function abandonedOn(signal) {
81
- return new Promise((resolve) => {
82
- if (signal.aborted)
83
- resolve(null);
84
- else
85
- signal.addEventListener('abort', () => resolve(null), { once: true });
79
+ function readUntil(read, signal) {
80
+ if (signal.aborted)
81
+ return Promise.resolve({ kind: 'transport-uncertainty' });
82
+ return new Promise((resolve, reject) => {
83
+ const stop = () => resolve({ kind: 'transport-uncertainty' });
84
+ signal.addEventListener('abort', stop, { once: true });
85
+ read.then(resolve, reject).finally(() => signal.removeEventListener('abort', stop));
86
86
  });
87
87
  }
88
- function roomLane(locator) {
89
- return `room ${locator}`;
90
- }
91
88
  function pendingKey(locator, id) {
92
89
  return `${locator}\u0000${id}`;
93
90
  }
@@ -107,6 +104,7 @@ export function composeRuntime(options) {
107
104
  const usableWhenHeld = options.usableWhenHeld ?? [];
108
105
  const queuesWithoutReplica = new FencedMap(isFenced);
109
106
  const pendings = new Map();
107
+ const landedAhead = new Map();
110
108
  const seeds = new Map();
111
109
  const seedWatchers = new Map();
112
110
  const roomDrafts = new FencedMap(isFenced);
@@ -118,11 +116,22 @@ export function composeRuntime(options) {
118
116
  const counted = new Map();
119
117
  const unreadable = new Set();
120
118
  const builtIn = builtInModel(roomTypes);
119
+ const typedPaths = options.adapter.typedPaths ?? [];
121
120
  const models = new FencedMap(isFenced);
122
121
  const liveModels = new FencedMap(isFenced);
123
122
  const invalidModels = new Set();
124
123
  const discovered = new Map();
125
- const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
124
+ let inFlight = new AbortController();
125
+ const refreshed = options.adapter.refresh?.bind(options.adapter);
126
+ const reads = {
127
+ bootstrap: (authorized, request) => readUntil(options.adapter.bootstrap(authorized, request), inFlight.signal),
128
+ ...(refreshed === undefined
129
+ ? {}
130
+ : {
131
+ refresh: (authorized, request) => readUntil(refreshed(authorized, request), inFlight.signal),
132
+ }),
133
+ reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
134
+ };
126
135
  let roomsListed = false;
127
136
  const scopedRefusals = new Map();
128
137
  function modelOf(locator) {
@@ -134,7 +143,7 @@ export function composeRuntime(options) {
134
143
  : modelOf(locator);
135
144
  }
136
145
  function composed(record) {
137
- return compositionOf(record, builtIn);
146
+ return compositionOf(record, builtIn, typedPaths);
138
147
  }
139
148
  function compositionFor(record) {
140
149
  return composed({ ...record, model: savedModel(record.locator) });
@@ -147,26 +156,26 @@ export function composeRuntime(options) {
147
156
  let closed = false;
148
157
  let stopping = false;
149
158
  let withdrawStream;
150
- const announced = new Map();
151
159
  let underway = null;
152
160
  let streamed = null;
153
161
  let handed = false;
162
+ let naming = 0;
154
163
  const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
155
164
  const repairing = new Set();
156
165
  const calling = new Set();
157
166
  const repairable = new Map();
158
- let outcomeRetention = null;
159
167
  const answering = new Map();
160
168
  let complete = false;
161
169
  let recorded = false;
162
170
  const stale = new Set();
163
171
  let generation = 0;
164
- let discovering = null;
165
- let opened = null;
166
- const owed = new Set();
167
- const reloadsDue = new Map();
172
+ const reloadsDue = new Set();
173
+ const highest = new Map();
174
+ const beyondHorizon = new Set();
168
175
  const lanes = new Map();
169
- let roomReads = { generation, free: ROOM_READS, waiting: [] };
176
+ const roomLanes = new Map();
177
+ let roomWork = 0;
178
+ const roomReads = { free: ROOM_READS, waiting: [] };
170
179
  const working = new Set();
171
180
  let mutations = Promise.resolve();
172
181
  let gathering = null;
@@ -182,6 +191,8 @@ export function composeRuntime(options) {
182
191
  let withdrawConversations;
183
192
  let currencyQueued = null;
184
193
  let socketOpen = false;
194
+ let announced = false;
195
+ let missedChange = false;
185
196
  const confirmed = new Set();
186
197
  let lockGrace = null;
187
198
  const lockListeners = new Set();
@@ -191,6 +202,8 @@ export function composeRuntime(options) {
191
202
  const serviceModeListeners = new Set();
192
203
  let member = null;
193
204
  const memberListeners = new Set();
205
+ let authorityChanges = 0;
206
+ const authorityChangeListeners = new Set();
194
207
  const unpreparedDemand = new Set();
195
208
  const unpreparable = new Set();
196
209
  let preparing = false;
@@ -222,7 +235,7 @@ export function composeRuntime(options) {
222
235
  const unindexed = new Map();
223
236
  const startedOver = new Map();
224
237
  let arrivalReported = 0;
225
- let streams = { generation, free: STREAMS_HELD };
238
+ let streamsFree = STREAMS_HELD;
226
239
  let queuedText = 0;
227
240
  const installWaiters = [];
228
241
  const stageBudget = {
@@ -239,30 +252,15 @@ export function composeRuntime(options) {
239
252
  };
240
253
  const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
241
254
  const conversations = createConversationStore({
242
- agents: declaredAgents,
243
- documents: (declaration, after) => rowsQuery(planOf({
244
- rooms: [declaration.locator],
245
- types: [declaration.type],
246
- select: {
247
- fields: [
248
- declaration.identity,
249
- declaration.name,
250
- ...(declaration.retired === undefined ? [] : [declaration.retired]),
251
- ].map((field) => `/${escapePointer(field)}`),
252
- },
253
- limit: MAXIMUM_WINDOW,
254
- ...(after === undefined ? {} : { after }),
255
- }, roomFacts())),
256
255
  call: (name, input) => invokeAction(name, input),
256
+ history: (request) => readConversationHistory(request),
257
257
  durably,
258
258
  stream: () => options.adapter.conversations,
259
259
  publish: publishChangeSet,
260
260
  online: () => connectionOf() !== 'unreachable' && refusal === null,
261
- listed: () => roomsListed,
262
261
  full: () => full,
263
262
  closed: () => closed,
264
263
  unanswered: resendLater,
265
- expired: (operation) => pastHorizon(operation),
266
264
  });
267
265
  const journal = createLog();
268
266
  const keptFiles = openedFiles(database, isFenced);
@@ -355,9 +353,9 @@ export function composeRuntime(options) {
355
353
  return value === undefined ? null : structuredClone(value);
356
354
  }
357
355
  async function writtenRoomDraft(room, path, revision, input, next) {
358
- const parts = readParts(input);
356
+ const parts = readParts(input, path);
359
357
  if (path === '' || normalize(path) !== path)
360
- 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`);
361
359
  const local = await openLocal();
362
360
  const current = roomDrafts.get(room)?.get(path);
363
361
  if ((current?.revision ?? null) !== revision) {
@@ -657,11 +655,7 @@ export function composeRuntime(options) {
657
655
  })();
658
656
  }
659
657
  function roomsOfReach(reach) {
660
- return new Set([
661
- ...reach.rooms,
662
- ...reach.documents.map(roomOfKey),
663
- ...reach.areas.flatMap((area) => (area.types.length === 0 ? [] : [area.room])),
664
- ]);
658
+ return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
665
659
  }
666
660
  function reachedRooms(plan) {
667
661
  const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
@@ -700,7 +694,7 @@ export function composeRuntime(options) {
700
694
  coverage.push({
701
695
  room: locator,
702
696
  generation: replica.record.transfer,
703
- current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
697
+ current: !(catching.has(locator) || transfers.has(locator)),
704
698
  });
705
699
  read.push({ locator, key: replica.record.key });
706
700
  for (const batch of replica.queue)
@@ -741,7 +735,7 @@ export function composeRuntime(options) {
741
735
  }
742
736
  function conversationCoverage(reach) {
743
737
  const declared = reach === null || reach.conversations;
744
- if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
738
+ if (!declared || options.adapter.conversations === undefined)
745
739
  return 'undeclared';
746
740
  if (!conversations.known() || !conversations.current())
747
741
  return 'acquiring';
@@ -800,7 +794,6 @@ export function composeRuntime(options) {
800
794
  }
801
795
  function readinessFor(port, reach) {
802
796
  const required = requiredRooms(reach);
803
- const search = port.search === 'required';
804
797
  const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
805
798
  const preparing = coverage
806
799
  .filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
@@ -813,12 +806,11 @@ export function composeRuntime(options) {
813
806
  listed: required.listed,
814
807
  rooms: coverage,
815
808
  conversations: conversationCoverage(reach),
816
- search: search ? 'required' : 'unused',
817
809
  },
818
810
  persistence,
819
811
  connection: connectionOf(),
820
812
  house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
821
- space: full ? 'full' : port.space,
813
+ space: full ? 'full' : null,
822
814
  installation: installationOf(coverage),
823
815
  lost: storageLoss !== null,
824
816
  established: targets[port.target]?.established === true ||
@@ -907,8 +899,12 @@ export function composeRuntime(options) {
907
899
  unpreparable.clear();
908
900
  journal.record('space freed');
909
901
  queueReadiness();
910
- if (!closed && authorization !== undefined)
911
- 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();
912
908
  }
913
909
  function seedRoomHeld(room, version) {
914
910
  return indexed(room) && reachedBy(room, version);
@@ -1056,6 +1052,8 @@ export function composeRuntime(options) {
1056
1052
  }
1057
1053
  async function refuse(reason, provider = null) {
1058
1054
  discoveryPaused = true;
1055
+ inFlight.abort();
1056
+ inFlight = new AbortController();
1059
1057
  try {
1060
1058
  if (reason === 'revoked-authorization') {
1061
1059
  await exclusive(async () => {
@@ -1143,7 +1141,7 @@ export function composeRuntime(options) {
1143
1141
  refusal: scopedRefusals.get(room.locator) ?? null,
1144
1142
  });
1145
1143
  }
1146
- async function applyDiscovery(found, started, covered) {
1144
+ async function applyDiscovery(found, started, left) {
1147
1145
  const lockedBefore = lockedRooms();
1148
1146
  for (const room of found) {
1149
1147
  const fence = fences.get(room.locator);
@@ -1157,18 +1155,11 @@ export function composeRuntime(options) {
1157
1155
  const listedLocators = new Set(listed.map((room) => room.locator));
1158
1156
  for (const locator of listedLocators)
1159
1157
  scopedRefusals.delete(locator);
1160
- if (covered === undefined)
1158
+ if (left === undefined)
1161
1159
  discovered.clear();
1162
- else
1163
- for (const locator of covered)
1164
- discovered.delete(locator);
1165
1160
  for (const room of listed)
1166
1161
  discovered.set(room.locator, room);
1167
- const unlisted = unlistedHeld(new Set(found.map((room) => room.locator))).filter((locator) => covered === undefined || covered.includes(locator));
1168
- if (options.adapter.exhaustiveDiscovery === true)
1169
- await loseRooms(unlisted);
1170
- else
1171
- unlistedKept(unlisted);
1162
+ await loseRooms(left ?? unlistedHeld(new Set(found.map((room) => room.locator))));
1172
1163
  for (const room of listed) {
1173
1164
  const held = replicas.get(room.locator)?.record;
1174
1165
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -1199,15 +1190,6 @@ export function composeRuntime(options) {
1199
1190
  ]),
1200
1191
  ].filter((locator) => !listed.has(locator));
1201
1192
  }
1202
- function unlistedKept(unlisted) {
1203
- for (const locator of unlisted) {
1204
- const held = replicas.get(locator)?.record;
1205
- if (held !== undefined)
1206
- list(held, false);
1207
- else
1208
- rooms.delete(locator);
1209
- }
1210
- }
1211
1193
  function fence(locators) {
1212
1194
  const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1213
1195
  const removed = fresh.map((locator) => ({
@@ -1218,9 +1200,13 @@ export function composeRuntime(options) {
1218
1200
  fences.set(locator, { at: discoveries, erased: false });
1219
1201
  lastFenced.set(locator, ++fencings);
1220
1202
  confirmed.delete(locator);
1221
- for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1203
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []) {
1222
1204
  pendings.delete(pendingKey(locator, batch.id));
1205
+ landedAhead.delete(pendingKey(locator, batch.id));
1206
+ }
1223
1207
  replicas.delete(locator);
1208
+ highest.delete(locator);
1209
+ beyondHorizon.delete(locator);
1224
1210
  queuesWithoutReplica.delete(locator);
1225
1211
  rooms.delete(locator);
1226
1212
  roomDrafts.delete(locator);
@@ -1229,7 +1215,6 @@ export function composeRuntime(options) {
1229
1215
  startedOver.delete(locator);
1230
1216
  catching.delete(locator);
1231
1217
  discovered.delete(locator);
1232
- announced.delete(locator);
1233
1218
  unreadable.delete(locator);
1234
1219
  stale.delete(locator);
1235
1220
  repairable.delete(locator);
@@ -1288,8 +1273,7 @@ export function composeRuntime(options) {
1288
1273
  function retyped(persisted) {
1289
1274
  if (persisted.status === 'unavailable')
1290
1275
  return persisted;
1291
- const { state } = attachedModel({ kind: 'model', snapshot: persisted.snapshot }, NO_MODEL, roomTypes);
1292
- return state.status === 'unavailable' ? state : { ...persisted, types: state.types };
1276
+ return { ...persisted, types: schemaTypes(persisted.snapshot.schema, roomTypes) };
1293
1277
  }
1294
1278
  function restored(key, held) {
1295
1279
  const persisted = retyped(held);
@@ -1335,8 +1319,9 @@ export function composeRuntime(options) {
1335
1319
  presentations.clear();
1336
1320
  for (const record of held.presentation)
1337
1321
  presentations.set(record.app, record.value);
1338
- conversations.hydrate(heldConversations, chatDrafts, held.conversationList);
1322
+ conversations.hydrate(heldConversations, chatDrafts, held.conversationList, held.conversationCursor);
1339
1323
  pendings.clear();
1324
+ landedAhead.clear();
1340
1325
  for (const queue of queues) {
1341
1326
  for (const { batch, state } of queue.pending) {
1342
1327
  pendings.set(pendingKey(queue.locator, batch.id), state);
@@ -1485,14 +1470,12 @@ export function composeRuntime(options) {
1485
1470
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
1486
1471
  };
1487
1472
  }
1488
- const REWRITE_LINKS = ['field', 'body'];
1489
- const CHECKED_LINKS = ['field', 'section'];
1490
1473
  async function sliceAt(record, view, queue, extra = [], moved = []) {
1491
1474
  const local = await openLocal();
1492
1475
  const needed = new Set([...touchedBy(queue), ...extra]);
1493
1476
  const movedPaths = [...new Set([...movedBy(queue, record.locator), ...moved])];
1494
1477
  if (movedPaths.length > 0) {
1495
- 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)) {
1496
1479
  needed.add(path);
1497
1480
  }
1498
1481
  }
@@ -1629,10 +1612,16 @@ export function composeRuntime(options) {
1629
1612
  ? read
1630
1613
  : await stepped(slicingWith(read, overrides.set, overrides.removed));
1631
1614
  const composition = composed(record);
1632
- const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
1633
- 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));
1634
1623
  const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
1635
- const unqueued = [];
1624
+ const unqueued = shown.map((batch) => ({ batch, landed: true }));
1636
1625
  for (const batch of withdrawn) {
1637
1626
  unqueued.push({ batch, landed: await landedWhole(locator, batch, confirmedLayer) });
1638
1627
  }
@@ -1697,8 +1686,10 @@ export function composeRuntime(options) {
1697
1686
  commit,
1698
1687
  apply: (answer) => {
1699
1688
  for (const batch of previousQueue) {
1700
- if (!staying.has(batch.id))
1701
- 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));
1702
1693
  }
1703
1694
  for (const draft of written.values())
1704
1695
  rememberRoomDraft(draft);
@@ -1747,26 +1738,33 @@ export function composeRuntime(options) {
1747
1738
  const answer = answers?.collection;
1748
1739
  if (answer === undefined)
1749
1740
  return held;
1750
- const { state, refused } = attachedModel(answer, held.collection, roomTypes);
1751
- if (refused !== null) {
1752
- const where = refused.location === undefined ? '' : ` ${refused.location}`;
1753
- journal.record(`model invalid ${locator} collection ${refused.identity} ${refused.rule}${where}`);
1754
- }
1741
+ const state = attachedModel(answer, roomTypes);
1755
1742
  models.set(locator, state);
1756
1743
  return { collection: state };
1757
1744
  }
1758
- function publish(local, room, record, landing, live) {
1745
+ function publish(local, room, reached, landing, live) {
1759
1746
  const landed = exclusive(async () => {
1760
1747
  if (!live()) {
1761
1748
  caughtUpTo(room.locator);
1762
1749
  return true;
1763
1750
  }
1764
1751
  const prior = replicas.get(room.locator);
1765
- if (landing.kind === 'refreshed' &&
1766
- (prior === undefined || prior.record.key !== record.key || prior.confirmed !== landing.base)) {
1767
- await local.unrefresh(record.key);
1752
+ if (!stands(room.locator, landing.from)) {
1753
+ if (landing.kind === 'refreshed') {
1754
+ await local.unrefresh(reached.key);
1755
+ return false;
1756
+ }
1757
+ caughtUpTo(room.locator);
1758
+ await abandoned(local, room.locator);
1768
1759
  return false;
1769
1760
  }
1761
+ const record = landing.kind === 'refreshed' && prior !== undefined
1762
+ ? {
1763
+ ...prior.record,
1764
+ roomVersion: reached.roomVersion,
1765
+ ...(landing.remodelled ? { model: reached.model } : {}),
1766
+ }
1767
+ : reached;
1770
1768
  adoptModels(room.locator, record);
1771
1769
  const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
1772
1770
  let set;
@@ -1793,11 +1791,14 @@ export function composeRuntime(options) {
1793
1791
  return true;
1794
1792
  }
1795
1793
  if (landing.kind === 'bootstrapped') {
1794
+ highest.delete(room.locator);
1795
+ beyondHorizon.delete(room.locator);
1796
1796
  untransferred(room.locator);
1797
1797
  startedOver.delete(room.locator);
1798
1798
  catching.delete(room.locator);
1799
1799
  if (prior !== undefined && prior.record.key !== record.key)
1800
1800
  sweep();
1801
+ name(room.locator);
1801
1802
  }
1802
1803
  stale.delete(room.locator);
1803
1804
  unpreparable.delete(room.locator);
@@ -1807,7 +1808,9 @@ export function composeRuntime(options) {
1807
1808
  publishChangeSet(set);
1808
1809
  await unpair(local);
1809
1810
  moveTo({ ...status, ...attention() });
1810
- options.adapter.roomStream?.published(room.locator, record.roomVersion);
1811
+ if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1812
+ submitLater(room.locator);
1813
+ }
1811
1814
  return true;
1812
1815
  });
1813
1816
  return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
@@ -1817,7 +1820,7 @@ export function composeRuntime(options) {
1817
1820
  const replica = replicas.get(locator);
1818
1821
  const room = rooms.get(locator);
1819
1822
  if (replica === undefined || room === undefined) {
1820
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1823
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
1821
1824
  }
1822
1825
  return { local, replica, room };
1823
1826
  }
@@ -1878,7 +1881,7 @@ export function composeRuntime(options) {
1878
1881
  const removed = changes.flatMap((change) => (change.kind === 'remove' ? [change.path] : []));
1879
1882
  const referring = removed.length === 0
1880
1883
  ? []
1881
- : (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
1884
+ : (await local.referrers(record.key, 'confirmed', removed)).map(({ path }) => path);
1882
1885
  const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
1883
1886
  const { visible } = await replayed(record, confirmedLayer, replica.queue);
1884
1887
  const composition = composed(record);
@@ -1971,7 +1974,7 @@ export function composeRuntime(options) {
1971
1974
  }
1972
1975
  function referrersOf(local, key, view) {
1973
1976
  return async (targets) => [
1974
- ...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)),
1975
1978
  ];
1976
1979
  }
1977
1980
  async function sideOf(locator, replica, room, saving) {
@@ -2043,7 +2046,7 @@ export function composeRuntime(options) {
2043
2046
  const target = replicas.get(destination);
2044
2047
  const listedRoom = rooms.get(destination);
2045
2048
  if (target === undefined || listedRoom === undefined) {
2046
- throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
2049
+ throw runtimeError('unknown-room', `Unknown Room ${destination}`);
2047
2050
  }
2048
2051
  const arriving = await savingIn(target, [], moves.map(({ to }) => to));
2049
2052
  const built = await buildMove({
@@ -2106,10 +2109,10 @@ export function composeRuntime(options) {
2106
2109
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2107
2110
  const { answer, touched } = committedBoth;
2108
2111
  if (answer.fenced !== null) {
2109
- throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2112
+ throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2110
2113
  }
2111
2114
  if (fences.has(locator) || fences.has(destination)) {
2112
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2115
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2113
2116
  }
2114
2117
  pendings.set(pendingKey(locator, id), {
2115
2118
  drafts: saved,
@@ -2280,10 +2283,10 @@ export function composeRuntime(options) {
2280
2283
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2281
2284
  const { answer, touched } = done;
2282
2285
  if (answer.fenced !== null) {
2283
- throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2286
+ throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2284
2287
  }
2285
2288
  if (fences.has(locator))
2286
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2289
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2287
2290
  for (const origin of withdrawn.keys())
2288
2291
  pendings.delete(pendingKey(locator, origin));
2289
2292
  for (const [id, drafts] of drafted) {
@@ -2332,7 +2335,7 @@ export function composeRuntime(options) {
2332
2335
  throw new Error('connection refused');
2333
2336
  await local.keepUpload(locator, identifier, upload);
2334
2337
  if (fences.has(locator))
2335
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2338
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2336
2339
  const key = pendingKey(locator, identifier);
2337
2340
  pendings.set(key, { ...pendings.get(key), upload });
2338
2341
  if (!continuing())
@@ -2606,13 +2609,6 @@ export function composeRuntime(options) {
2606
2609
  const freed = await database.ask({ kind: 'opened.clean' });
2607
2610
  return freed + ((await options.shell.cleanUp?.()) ?? 0);
2608
2611
  }
2609
- function declaredAgents() {
2610
- for (const capability of capabilities) {
2611
- if (capability.conversationAgents !== undefined)
2612
- return capability.conversationAgents;
2613
- }
2614
- return undefined;
2615
- }
2616
2612
  function declaredAction(name) {
2617
2613
  for (const capability of capabilities) {
2618
2614
  for (const action of capability.actions ?? []) {
@@ -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);
@@ -2845,18 +2893,12 @@ export function composeRuntime(options) {
2845
2893
  void yielded().then(nextInstallTurn);
2846
2894
  }
2847
2895
  async function heldStream() {
2848
- for (;;) {
2849
- if (streams.generation !== generation)
2850
- streams = { generation, free: STREAMS_HELD };
2851
- const held = streams;
2852
- if (held.free > 0) {
2853
- held.free -= 1;
2854
- return once(() => {
2855
- held.free += 1;
2856
- });
2857
- }
2896
+ while (streamsFree === 0)
2858
2897
  await new Promise((admit) => installWaiters.push(admit));
2859
- }
2898
+ streamsFree -= 1;
2899
+ return once(() => {
2900
+ streamsFree += 1;
2901
+ });
2860
2902
  }
2861
2903
  async function unqueued() {
2862
2904
  while (queuedText >= QUEUED_TEXT) {
@@ -2922,33 +2964,34 @@ export function composeRuntime(options) {
2922
2964
  if (installs.delete(locator))
2923
2965
  queueReadiness();
2924
2966
  }
2925
- async function pullRefresh(authorized, held, liveness, hold) {
2967
+ async function pullRefresh(authorized, held, liveness, hold, refresh) {
2926
2968
  if (!liveness())
2927
2969
  return unconfirmed;
2928
- const turn = generation;
2929
2970
  const { record } = held;
2930
- const base = held.confirmed;
2971
+ const from = standingOf(record.locator);
2931
2972
  const local = await openLocal();
2932
2973
  try {
2933
2974
  await exclusive(() => local.unrefresh(record.key));
2934
- const result = await advancement({
2935
- ...reads,
2936
- async refresh(authorization, request) {
2937
- hold.release();
2938
- try {
2939
- return await reads.refresh(authorization, request);
2940
- }
2941
- finally {
2942
- hold.take();
2943
- }
2944
- },
2945
- }, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, composed(record), liveness));
2975
+ const result = await advancement(async (authorization, request) => {
2976
+ hold.release();
2977
+ try {
2978
+ return await refresh(authorization, request);
2979
+ }
2980
+ finally {
2981
+ hold.take();
2982
+ }
2983
+ }, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.locator, record.key, composed(record), liveness));
2946
2984
  if (result.kind !== 'advanced') {
2947
2985
  journal.record(`transfer refresh ${record.locator} ${result.kind}`);
2948
2986
  return result;
2949
2987
  }
2950
2988
  journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
2951
- return { kind: 'advanced', record: result.record, base };
2989
+ return {
2990
+ kind: 'advanced',
2991
+ record: result.record,
2992
+ from,
2993
+ remodelled: result.record.model !== record.model,
2994
+ };
2952
2995
  }
2953
2996
  catch (error) {
2954
2997
  if (refusedForSpace(error))
@@ -2956,9 +2999,6 @@ export function composeRuntime(options) {
2956
2999
  journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
2957
3000
  throw error;
2958
3001
  }
2959
- finally {
2960
- reads.finished(record.locator, turn);
2961
- }
2962
3002
  }
2963
3003
  const heldContents = new Map();
2964
3004
  function heldContentOf(local, key) {
@@ -2976,6 +3016,13 @@ export function composeRuntime(options) {
2976
3016
  heldContents.set(key, made);
2977
3017
  return made;
2978
3018
  }
3019
+ async function abandoned(local, locator) {
3020
+ await local.abandon(locator);
3021
+ setBack(locator);
3022
+ untransferred(locator);
3023
+ liveQueries.recover();
3024
+ sweep();
3025
+ }
2979
3026
  function staging(local, liveness) {
2980
3027
  const derived = async (locator, key, composition) => {
2981
3028
  underived.delete(locator);
@@ -3028,10 +3075,7 @@ export function composeRuntime(options) {
3028
3075
  },
3029
3076
  };
3030
3077
  },
3031
- announced(locator) {
3032
- const later = announced.get(locator);
3033
- return later === undefined ? [] : [later];
3034
- },
3078
+ streamed: options.adapter.roomStream !== undefined,
3035
3079
  async stage(record, page, composition) {
3036
3080
  const { locator } = record;
3037
3081
  installing(locator);
@@ -3063,16 +3107,11 @@ export function composeRuntime(options) {
3063
3107
  derive: derived,
3064
3108
  async refreshing(locator, key, composition) {
3065
3109
  await exclusive(() => local.unrefresh(key));
3066
- return refreshingOf(local, key, composition, liveness, locator);
3110
+ return refreshingOf(local, locator, key, composition, liveness, true);
3067
3111
  },
3068
3112
  abandon: (locator) => exclusive(async () => {
3069
- if (!liveness())
3070
- return;
3071
- await local.abandon(locator);
3072
- setBack(locator);
3073
- untransferred(locator);
3074
- liveQueries.recover();
3075
- sweep();
3113
+ if (liveness())
3114
+ await abandoned(local, locator);
3076
3115
  }),
3077
3116
  setAside: async (locator) => {
3078
3117
  if (!liveness())
@@ -3097,15 +3136,21 @@ export function composeRuntime(options) {
3097
3136
  async function pullBootstrap(authorized, room, local, liveness) {
3098
3137
  if (!liveness())
3099
3138
  return unconfirmed;
3100
- const turn = generation;
3101
3139
  if (replicas.has(room.locator)) {
3102
3140
  catching.add(room.locator);
3103
3141
  liveQueries.recover();
3104
3142
  }
3105
3143
  try {
3106
- 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));
3107
3145
  const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
3108
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
+ }
3109
3154
  if (result.kind !== 'staged') {
3110
3155
  caughtUpTo(room.locator);
3111
3156
  installed(room.locator);
@@ -3120,9 +3165,6 @@ export function composeRuntime(options) {
3120
3165
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
3121
3166
  throw error;
3122
3167
  }
3123
- finally {
3124
- reads.finished(room.locator, turn);
3125
- }
3126
3168
  }
3127
3169
  function caughtUpTo(locator) {
3128
3170
  if (catching.delete(locator))
@@ -3130,45 +3172,51 @@ export function composeRuntime(options) {
3130
3172
  }
3131
3173
  function behind(room) {
3132
3174
  const held = replicas.get(room.locator)?.record.roomVersion;
3133
- return (held !== undefined &&
3134
- [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
3175
+ return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
3135
3176
  }
3136
- async function advance(room, local, authorized, live) {
3137
- const carried = reads.carried(room.locator);
3138
- const withheld = await advanceOnce(room, local, authorized, live);
3139
- if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
3140
- return withheld;
3141
- }
3142
- return advanceOnce(room, local, authorized, live);
3177
+ function standingOf(locator) {
3178
+ const held = replicas.get(locator);
3179
+ return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
3180
+ }
3181
+ function stands(locator, from) {
3182
+ const now = standingOf(locator);
3183
+ return now.key === from.key && now.confirmed === from.confirmed;
3143
3184
  }
3144
- async function advanceOnce(room, local, authorized, running) {
3185
+ async function advance(room, local, authorized, running) {
3145
3186
  const live = () => running() && !full;
3146
3187
  if (!live())
3147
3188
  return unconfirmed;
3148
3189
  const held = replicas.get(room.locator);
3149
- if (held !== undefined) {
3150
- const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold));
3190
+ if (held !== undefined && options.adapter.roomStream !== undefined) {
3191
+ stale.delete(room.locator);
3192
+ name(room.locator);
3193
+ return null;
3194
+ }
3195
+ const refresh = reads.refresh;
3196
+ if (held !== undefined && refresh !== undefined) {
3197
+ const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
3151
3198
  if (refreshed === 'behind')
3152
3199
  return advance(room, local, authorized, live);
3153
3200
  if (refreshed !== 'beyond-horizon')
3154
3201
  return refreshed;
3155
- if (opened !== null) {
3156
- owed.add(room.locator);
3157
- liveQueries.recover();
3158
- return unconfirmed;
3159
- }
3160
- if (reloadsDue.get(room.locator) === generation)
3202
+ if (reloadsDue.has(room.locator))
3161
3203
  return unconfirmed;
3204
+ replaced([room.locator]);
3162
3205
  }
3206
+ const from = standingOf(room.locator);
3163
3207
  const bootstrapped = await pullBootstrap(authorized, room, local, live);
3164
- if (bootstrapped.kind === 'staged') {
3165
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3208
+ if (bootstrapped.kind !== 'staged')
3209
+ return live() ? bootstrapped : unconfirmed;
3210
+ const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
3211
+ if (await publish(local, room, bootstrapped.record, landing, live)) {
3166
3212
  return live() ? null : unconfirmed;
3167
3213
  }
3168
- return live() ? bootstrapped : unconfirmed;
3214
+ return reachedBy(room.locator, bootstrapped.record.roomVersion)
3215
+ ? null
3216
+ : advance(room, local, authorized, live);
3169
3217
  }
3170
- async function refreshedRoom(room, local, authorized, held, live, hold) {
3171
- const refreshed = await pullRefresh(authorized, held, live, hold);
3218
+ async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
3219
+ const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
3172
3220
  if (!live())
3173
3221
  return unconfirmed;
3174
3222
  if (refreshed.kind === 'still-current') {
@@ -3181,7 +3229,11 @@ export function composeRuntime(options) {
3181
3229
  return null;
3182
3230
  }
3183
3231
  if (refreshed.kind === 'advanced') {
3184
- const landing = { kind: 'refreshed', base: refreshed.base };
3232
+ const landing = {
3233
+ kind: 'refreshed',
3234
+ from: refreshed.from,
3235
+ remodelled: refreshed.remodelled,
3236
+ };
3185
3237
  if (!(await publish(local, room, refreshed.record, landing, live))) {
3186
3238
  const now = replicas.get(room.locator)?.record.roomVersion;
3187
3239
  const reached = now !== undefined &&
@@ -3194,7 +3246,7 @@ export function composeRuntime(options) {
3194
3246
  return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
3195
3247
  }
3196
3248
  async function withhold(withheld, room, local, progress) {
3197
- if (withheld.kind === 'unconfirmed')
3249
+ if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
3198
3250
  return;
3199
3251
  stale.add(room.locator);
3200
3252
  if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
@@ -3223,8 +3275,6 @@ export function composeRuntime(options) {
3223
3275
  }
3224
3276
  }
3225
3277
  function leaving(batch, result) {
3226
- if (result.kind === 'expired')
3227
- return { batch, returned: returnedBy('expired') };
3228
3278
  return {
3229
3279
  batch,
3230
3280
  returned: {
@@ -3257,10 +3307,11 @@ export function composeRuntime(options) {
3257
3307
  logAccepted(room.locator, submitted, accepted);
3258
3308
  let record = current.record;
3259
3309
  let changes;
3260
- 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)) {
3261
3312
  const paths = [...returnedPaths(submitted, accepted)];
3262
3313
  const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
3263
- const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
3314
+ const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion, composed(record)));
3264
3315
  record = {
3265
3316
  ...record,
3266
3317
  roomVersion: options.adapter.follows(room.locator, held, accepted.roomVersion)
@@ -3274,6 +3325,7 @@ export function composeRuntime(options) {
3274
3325
  ...settled,
3275
3326
  apply(answer) {
3276
3327
  const set = settled.apply(answer);
3328
+ showing(room.locator, accepted.roomVersion);
3277
3329
  pendings.delete(pendingKey(room.locator, submitted.id));
3278
3330
  unreadable.delete(room.locator);
3279
3331
  list(room, true);
@@ -3423,15 +3475,10 @@ export function composeRuntime(options) {
3423
3475
  return;
3424
3476
  moveTo({ ...status, ...attention() });
3425
3477
  }
3426
- async function discardHeld(head, room, local, signal) {
3427
- const partner = partnerOf(head, room.locator);
3428
- const other = partner === null ? undefined : discovered.get(partner);
3429
- await discardPending(other === undefined ? [room] : [room, other], local, signal);
3430
- }
3431
3478
  async function submitRoom(room, local, authorized, progress, signal) {
3432
3479
  while (continuing() && authorization !== undefined && !signal.aborted) {
3433
3480
  const replica = replicas.get(room.locator);
3434
- const head = replica?.queue[0];
3481
+ const head = replica?.queue.find((batch) => !landedAhead.has(pendingKey(room.locator, batch.id)));
3435
3482
  if (replica === undefined || head === undefined)
3436
3483
  return 'done';
3437
3484
  const state = pendings.get(pendingKey(room.locator, head.id));
@@ -3458,10 +3505,6 @@ export function composeRuntime(options) {
3458
3505
  return 'done';
3459
3506
  }
3460
3507
  const lost = earlier || submission.state === 'submitted';
3461
- if (lost && pastHorizon(head.id)) {
3462
- await discardHeld(head, room, local, signal);
3463
- return 'done';
3464
- }
3465
3508
  let result;
3466
3509
  journal.record(`submission ${room.locator} ${head.id}`);
3467
3510
  await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
@@ -3539,32 +3582,12 @@ export function composeRuntime(options) {
3539
3582
  const other = partner === null ? undefined : discovered.get(partner);
3540
3583
  if (result.kind === 'conflicted' ||
3541
3584
  result.kind === 'rejected' ||
3542
- result.kind === 'expired' ||
3543
3585
  result.kind === 'retryable-refusal') {
3544
- if (result.kind === 'expired' && lost && result.restored !== true) {
3545
- journal.record(`outcome ${room.locator} ${head.id} expired`);
3546
- await discardHeld(head, room, local, signal);
3547
- return 'done';
3548
- }
3549
- if (result.kind === 'expired') {
3550
- await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
3551
- const there = other === undefined ? null : await advance(other, local, authorized, continuing);
3552
- if (other !== undefined && there !== null) {
3553
- await withhold(there, other, local, progress);
3554
- return 'done';
3555
- }
3556
- const withheld = await advance(room, local, authorized, continuing);
3557
- if (withheld !== null) {
3558
- await withhold(withheld, room, local, progress);
3559
- return 'done';
3560
- }
3561
- }
3562
3586
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
3563
3587
  (result.kind === 'conflicted' && result.roomVersion !== undefined
3564
3588
  ? ` ${result.roomVersion}`
3565
3589
  : ''));
3566
- if (result.kind !== 'expired')
3567
- journal.record(`refusal ${result.kind} ${result.code}`);
3590
+ journal.record(`refusal ${result.kind} ${result.code}`);
3568
3591
  }
3569
3592
  if (result.kind === 'retryable-refusal') {
3570
3593
  const reset = result.reset;
@@ -3572,11 +3595,11 @@ export function composeRuntime(options) {
3572
3595
  await exclusive(() => mark(local, room.locator, head, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
3573
3596
  return reset === undefined ? 'again' : { at: Date.parse(reset) };
3574
3597
  }
3575
- if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
3598
+ if (result.kind === 'conflicted' || result.kind === 'rejected') {
3576
3599
  const left = leaving(head, result);
3577
3600
  const refreshed = async () => ({
3578
- there: other === undefined ? null : await advance(other, local, authorized, continuing),
3579
- here: await advance(room, local, authorized, continuing),
3601
+ there: other === undefined ? null : await advanced(other, local, authorized),
3602
+ here: await advanced(room, local, authorized),
3580
3603
  });
3581
3604
  const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
3582
3605
  if (judged !== null && (judged.there !== null || judged.here !== null)) {
@@ -3596,16 +3619,19 @@ export function composeRuntime(options) {
3596
3619
  return;
3597
3620
  }
3598
3621
  const confirmedLayer = await sliceAt(current.record, 'confirmed', [head]);
3599
- const held = (result.kind === 'expired' &&
3600
- (await landedWhole(room.locator, head, confirmedLayer))) ||
3601
- (result.kind === 'conflicted' &&
3602
- (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
+ }
3603
3631
  publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
3604
3632
  await unpair(local);
3605
3633
  moveTo({ ...status, ...attention() });
3606
3634
  });
3607
- if (result.kind === 'expired')
3608
- continue;
3609
3635
  const { there, here } = judged ?? (await refreshed());
3610
3636
  if (other !== undefined && there !== null)
3611
3637
  await withhold(there, other, local, progress);
@@ -3625,80 +3651,16 @@ export function composeRuntime(options) {
3625
3651
  }
3626
3652
  return 'done';
3627
3653
  }
3628
- function pastHorizon(identity) {
3629
- return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
3630
- }
3631
- async function discardPending(rooms, local, signal) {
3632
- const discarded = await exclusive(async () => {
3633
- const held = rooms
3634
- .filter(({ locator }) => !fences.has(locator))
3635
- .map((room) => ({
3636
- room,
3637
- before: replicas.get(room.locator),
3638
- queue: queueIn(room.locator),
3639
- }));
3640
- if (held.length === 0)
3641
- return [];
3642
- const answer = await local.commit({
3643
- rooms: held.flatMap(({ room, before }) => before === undefined
3644
- ? []
3645
- : [
3646
- {
3647
- locator: room.locator,
3648
- key: before.record.key,
3649
- roots: before.record.typedSelection?.roots ?? [],
3650
- composed: composedOf(composed(before.record)),
3651
- overlay: { set: [], cleared: [...before.overlaid] },
3652
- },
3653
- ]),
3654
- discards: held.map(({ room }) => room.locator),
3655
- });
3656
- for (const { room, before, queue } of held) {
3657
- const { locator } = room;
3658
- lastFenced.set(locator, ++fencings);
3659
- for (const batch of queue)
3660
- pendings.delete(pendingKey(locator, batch.id));
3661
- queuesWithoutReplica.delete(locator);
3662
- if (before !== undefined) {
3663
- replicas.set(locator, {
3664
- ...before,
3665
- queue: [],
3666
- overlaid: new Set(),
3667
- count: answer.counts[locator] ?? before.count,
3668
- });
3669
- }
3670
- journal.record(`room discarded ${locator}`);
3671
- }
3672
- publishChangeSet(mergeChangeSets(held.map(({ room }) => changeSet(room.locator, {
3673
- documents: answer.changed[room.locator] ?? [],
3674
- queue: true,
3675
- }))));
3676
- moveTo({ ...status, ...attention() });
3677
- return held.map(({ room }) => room);
3678
- });
3679
- for (const room of discarded) {
3680
- if (signal.aborted)
3681
- return;
3682
- const progress = { complete: true, reachable: true };
3683
- await roomTask(room.locator, async (live) => {
3684
- await Promise.race([
3685
- reload(room, local, progress, () => live() && !signal.aborted),
3686
- abandonedOn(signal),
3687
- ]);
3688
- });
3689
- if (signal.aborted)
3690
- return;
3691
- await rest(progress);
3692
- }
3693
- }
3694
3654
  function awaitingAnswer(locator) {
3655
+ answering.get(locator)?.release();
3695
3656
  let release = () => { };
3696
- const awaited = new Promise((resolve) => {
3657
+ const answered = new Promise((resolve) => {
3697
3658
  release = resolve;
3698
3659
  });
3699
- answering.set(locator, { generation, answered: awaited });
3660
+ const awaited = { answered, release };
3661
+ answering.set(locator, awaited);
3700
3662
  return () => {
3701
- if (answering.get(locator)?.answered === awaited)
3663
+ if (answering.get(locator) === awaited)
3702
3664
  answering.delete(locator);
3703
3665
  release();
3704
3666
  };
@@ -3710,6 +3672,30 @@ export function composeRuntime(options) {
3710
3672
  JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
3711
3673
  JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
3712
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
+ }
3713
3699
  async function retainModel(local, room, next) {
3714
3700
  const key = room.locator;
3715
3701
  const previous = modelOf(room.locator);
@@ -3727,6 +3713,8 @@ export function composeRuntime(options) {
3727
3713
  if (held !== undefined) {
3728
3714
  const record = {
3729
3715
  ...held.record,
3716
+ name: room.name,
3717
+ handle: room.handle,
3730
3718
  operations: room.operations,
3731
3719
  replica: room.replica,
3732
3720
  typedSelection: room.typedSelection ?? null,
@@ -3797,6 +3785,17 @@ export function composeRuntime(options) {
3797
3785
  emitRooms();
3798
3786
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
3799
3787
  }
3788
+ function name(locator) {
3789
+ const stream = options.adapter.roomStream;
3790
+ const record = replicas.get(locator)?.record;
3791
+ const room = discovered.get(locator);
3792
+ if (stream === undefined || record === undefined || room === undefined)
3793
+ return;
3794
+ if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
3795
+ return;
3796
+ naming += 1;
3797
+ stream.hold([{ locator, roomVersion: record.roomVersion }]);
3798
+ }
3800
3799
  function heldRooms(listed) {
3801
3800
  return listed.flatMap((room) => {
3802
3801
  const record = replicas.get(room.locator)?.record;
@@ -3814,14 +3813,19 @@ export function composeRuntime(options) {
3814
3813
  return outcome?.status === 'fulfilled' && outcome.value;
3815
3814
  });
3816
3815
  if (attempt === 'opened') {
3816
+ naming += 1;
3817
3817
  options.adapter.roomStream?.hold(heldRooms(ready));
3818
3818
  handed = true;
3819
3819
  }
3820
- discovering?.release();
3821
- const loaded = await Promise.allSettled(ready
3822
- .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
3823
- .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
3824
- const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
3820
+ for (const room of ready) {
3821
+ if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
3822
+ if (attempt === 'authorized')
3823
+ name(room.locator);
3824
+ continue;
3825
+ }
3826
+ void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
3827
+ }
3828
+ const failed = prepared.find((room) => room.status === 'rejected');
3825
3829
  if (failed !== undefined)
3826
3830
  throw failed.reason;
3827
3831
  }
@@ -3840,12 +3844,16 @@ export function composeRuntime(options) {
3840
3844
  }
3841
3845
  if (prior !== undefined)
3842
3846
  await retainModel(local, room, modelOf(room.locator));
3847
+ if (prior !== undefined)
3848
+ await keepNaming(local, room);
3843
3849
  if (discoveryPaused || authorization === undefined)
3844
3850
  return false;
3845
3851
  return room.replica.admission === 'admitted';
3846
3852
  }
3847
3853
  async function caughtUp(room, local, progress, live) {
3848
- if (authorization === undefined || !live() || !behind(room))
3854
+ if (options.adapter.roomStream !== undefined || authorization === undefined)
3855
+ return;
3856
+ if (!live() || !behind(room))
3849
3857
  return;
3850
3858
  const withheld = await advance(room, local, authorization, live);
3851
3859
  if (withheld !== null)
@@ -3858,6 +3866,8 @@ export function composeRuntime(options) {
3858
3866
  async function loadRoom(room, local, progress, live) {
3859
3867
  if (!live() || authorization === undefined)
3860
3868
  return;
3869
+ if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
3870
+ return;
3861
3871
  if (replicas.has(room.locator) && revoked(room.locator)) {
3862
3872
  await reload(room, local, progress, live);
3863
3873
  return;
@@ -3875,20 +3885,53 @@ export function composeRuntime(options) {
3875
3885
  reloadsDue.delete(room.locator);
3876
3886
  if (!live() || authorization === undefined)
3877
3887
  return;
3888
+ const from = standingOf(room.locator);
3878
3889
  const bootstrapped = await pullBootstrap(authorization, room, local, live);
3879
3890
  if (bootstrapped.kind !== 'staged') {
3880
3891
  if (live())
3881
3892
  await withhold(bootstrapped, room, local, progress);
3882
3893
  return;
3883
3894
  }
3884
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3895
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
3885
3896
  await caughtUp(room, local, progress, live);
3886
3897
  }
3887
3898
  finally {
3888
3899
  caughtUpTo(room.locator);
3889
3900
  }
3890
3901
  }
3891
- 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) {
3892
3935
  if (closed || authorization === undefined) {
3893
3936
  notifyReady();
3894
3937
  return;
@@ -3896,13 +3939,7 @@ export function composeRuntime(options) {
3896
3939
  moveTo(transferring(status));
3897
3940
  discoveries += 1;
3898
3941
  const started = discoveries;
3899
- let result;
3900
- try {
3901
- result = await options.adapter.discover(authorization, scope);
3902
- }
3903
- catch {
3904
- result = { kind: 'transport-uncertainty' };
3905
- }
3942
+ const result = found ?? (await asked(authorization));
3906
3943
  if (result.kind === 'discovery')
3907
3944
  roomsNamed = true;
3908
3945
  if (!live()) {
@@ -3912,7 +3949,7 @@ export function composeRuntime(options) {
3912
3949
  }
3913
3950
  journal.record(`connection discover ${result.kind}`);
3914
3951
  if (result.kind === 'transport-uncertainty') {
3915
- await settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
3952
+ await cachedModels(local);
3916
3953
  progress.reachable = false;
3917
3954
  progress.complete = false;
3918
3955
  notifyReady();
@@ -3933,26 +3970,12 @@ export function composeRuntime(options) {
3933
3970
  notifyReady();
3934
3971
  return;
3935
3972
  }
3936
- for (const room of result.rooms) {
3937
- if (room.operations === undefined ||
3938
- !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
3939
- room.operations[operation].every((scope) => typeof scope === 'string')) ||
3940
- room.replica === undefined ||
3941
- typeof room.replica.selection !== 'string' ||
3942
- room.replica.selection.length === 0 ||
3943
- !['admitted', 'unavailable'].includes(room.replica.admission) ||
3944
- !['locked', 'open'].includes(room.replica.offline)) {
3945
- journal.record(`invalid model or replica declaration ${room.locator}`);
3946
- await refuse('missing-required-guarantee');
3947
- notifyReady();
3948
- return;
3949
- }
3950
- if (!declaredSelection(room)) {
3951
- journal.record(`invalid typed selection declaration ${room.locator}`);
3952
- await refuse('missing-required-guarantee');
3953
- notifyReady();
3954
- return;
3955
- }
3973
+ const invalid = malformed(result.rooms);
3974
+ if (invalid !== null) {
3975
+ journal.record(invalid);
3976
+ await refuse('missing-required-guarantee');
3977
+ notifyReady();
3978
+ return;
3956
3979
  }
3957
3980
  if (refusal !== null) {
3958
3981
  refusal = null;
@@ -3960,22 +3983,15 @@ export function composeRuntime(options) {
3960
3983
  }
3961
3984
  const previousCapabilities = capabilities;
3962
3985
  capabilities = result.capabilities;
3963
- outcomeRetention = result.retention.outcome;
3964
3986
  notifyReady();
3965
3987
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
3966
3988
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
3967
3989
  limits = result.providerLimits;
3968
3990
  await local.fact('providerLimits', limits);
3969
3991
  await local.fact('capabilities', capabilities);
3970
- conversations.declared();
3971
3992
  }
3972
- const signedIn = result.member ?? null;
3973
- if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
3974
- await local.fact('member', signedIn);
3975
- member = signedIn;
3976
- emitMember();
3977
- }
3978
- 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);
3979
3995
  }
3980
3996
  async function ensureAuthorization() {
3981
3997
  if (authorization !== undefined)
@@ -3990,23 +4006,45 @@ export function composeRuntime(options) {
3990
4006
  }
3991
4007
  }
3992
4008
  async function admittedRead(work) {
3993
- if (roomReads.generation !== generation) {
3994
- roomReads = { generation, free: ROOM_READS, waiting: [] };
4009
+ while (roomReads.free === 0) {
4010
+ await new Promise((admit) => roomReads.waiting.push(admit));
3995
4011
  }
3996
- const reads = roomReads;
3997
- while (reads.free === 0)
3998
- await new Promise((admit) => reads.waiting.push(admit));
3999
- reads.free -= 1;
4012
+ roomReads.free -= 1;
4000
4013
  try {
4001
4014
  await work();
4002
4015
  }
4003
4016
  finally {
4004
- reads.free += 1;
4005
- reads.waiting.shift()?.();
4017
+ roomReads.free += 1;
4018
+ roomReads.waiting.shift()?.();
4006
4019
  }
4007
4020
  }
4021
+ function inRoom(locator, work) {
4022
+ roomWork += 1;
4023
+ const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
4024
+ try {
4025
+ return await work(continuing);
4026
+ }
4027
+ finally {
4028
+ roomWork -= 1;
4029
+ }
4030
+ });
4031
+ const chain = ran.then(() => undefined, () => undefined);
4032
+ roomLanes.set(locator, chain);
4033
+ working.add(chain);
4034
+ void chain.finally(() => {
4035
+ working.delete(chain);
4036
+ if (roomLanes.get(locator) === chain)
4037
+ roomLanes.delete(locator);
4038
+ });
4039
+ return ran;
4040
+ }
4008
4041
  function roomTask(locator, work) {
4009
- return task(roomLane(locator), (live) => admittedRead(() => work(live)));
4042
+ if (stopping)
4043
+ return Promise.resolve();
4044
+ return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
4045
+ }
4046
+ function advanced(room, local, authorized) {
4047
+ return inRoom(room.locator, (live) => advance(room, local, authorized, live));
4010
4048
  }
4011
4049
  function task(lane, work) {
4012
4050
  if (stopping)
@@ -4022,23 +4060,19 @@ export function composeRuntime(options) {
4022
4060
  void run.finally(() => working.delete(run));
4023
4061
  return run;
4024
4062
  }
4025
- function sync(attempt) {
4063
+ function sync(attempt, found) {
4026
4064
  return task(OPENING_LANE, async (live) => {
4027
4065
  if (!live()) {
4028
4066
  readyFromStore();
4029
4067
  return;
4030
4068
  }
4031
4069
  const progress = { complete: true, reachable: true };
4032
- const gate = discover();
4033
- if (attempt === 'opened')
4034
- opened = generation;
4035
- underway = attempt === 'opened' ? { generation, progress } : null;
4070
+ underway = attempt === 'opened' ? progress : null;
4036
4071
  if (attempt === 'opened')
4037
4072
  streamed = progress;
4038
4073
  complete = false;
4039
4074
  recorded = false;
4040
4075
  moveTo(transferring(status));
4041
- handed = false;
4042
4076
  try {
4043
4077
  const local = await openLocal();
4044
4078
  if (!live())
@@ -4046,7 +4080,7 @@ export function composeRuntime(options) {
4046
4080
  await ensureAuthorization();
4047
4081
  if (!live())
4048
4082
  return;
4049
- await discoverRooms(local, progress, attempt, live);
4083
+ await discoverRooms(local, progress, attempt, live, found);
4050
4084
  }
4051
4085
  catch {
4052
4086
  progress.complete = false;
@@ -4054,7 +4088,6 @@ export function composeRuntime(options) {
4054
4088
  progress.reachable = false;
4055
4089
  }
4056
4090
  finally {
4057
- gate.release();
4058
4091
  notifyReady();
4059
4092
  }
4060
4093
  if (!live())
@@ -4066,35 +4099,59 @@ export function composeRuntime(options) {
4066
4099
  if (handed || closed || progress.reachable)
4067
4100
  return;
4068
4101
  underway = null;
4069
- opened = null;
4070
- journal.record('connection reopen');
4071
4102
  await rest(progress, live);
4072
- options.adapter.roomStream?.reopen();
4073
4103
  });
4074
4104
  }
4075
- function rediscover(scope) {
4105
+ function resynchronize() {
4076
4106
  return task(OPENING_LANE, async (live) => {
4077
- if (!live() || !socketOpen)
4107
+ if (!live() || !announced)
4078
4108
  return;
4079
4109
  const progress = { complete: true, reachable: true };
4080
- const gate = discover();
4081
4110
  moveTo(transferring(status));
4082
4111
  try {
4083
4112
  const local = await openLocal();
4084
4113
  if (!live())
4085
4114
  return;
4086
- 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();
4087
4138
  if (!live())
4088
4139
  return;
4089
- 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);
4090
4151
  }
4091
4152
  catch {
4092
4153
  progress.complete = false;
4093
4154
  }
4094
- finally {
4095
- gate.release();
4096
- notifyReady();
4097
- }
4098
4155
  if (!live())
4099
4156
  return;
4100
4157
  if (!progress.complete)
@@ -4102,15 +4159,17 @@ export function composeRuntime(options) {
4102
4159
  await rest(progress, live);
4103
4160
  });
4104
4161
  }
4105
- function closedSocket(revoked, lost) {
4106
- notifyDiscovered();
4107
- const opened = socketOpen;
4162
+ function closedSocket(revoked, lost, authorityChanged) {
4163
+ notifyReady();
4164
+ const opened = announced;
4108
4165
  socketOpen = false;
4166
+ announced = false;
4167
+ missedChange ||= authorityChanged;
4168
+ handed = false;
4169
+ naming = 0;
4109
4170
  connectionLost();
4110
- if (lost) {
4111
- reads.lost();
4171
+ if (lost)
4112
4172
  conversations.lost();
4113
- }
4114
4173
  if (revoked) {
4115
4174
  void refuse('revoked-authorization');
4116
4175
  return;
@@ -4121,18 +4180,32 @@ export function composeRuntime(options) {
4121
4180
  moveTo({
4122
4181
  ...status,
4123
4182
  state: status.state === 'synced' ? 'syncing' : status.state,
4124
- providerUnreachable: status.providerUnreachable || !opened,
4183
+ providerUnreachable: status.providerUnreachable || (!opened && lost),
4125
4184
  });
4185
+ void closing(opened);
4126
4186
  }
4127
- function applyOpening(expired) {
4128
- const progress = underway?.generation === generation ? underway.progress : null;
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) {
4201
+ const progress = underway;
4202
+ underway = null;
4203
+ naming = Math.max(0, naming - 1);
4129
4204
  const cycle = progress ?? { complete: true, reachable: true };
4130
- if (opened === generation)
4131
- opened = null;
4205
+ replaced(expired);
4132
4206
  const reloading = progress === null ? [] : owedReloads(expired);
4133
4207
  const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
4134
4208
  return task(OPENING_LANE, async (live) => {
4135
- underway = null;
4136
4209
  if (!live())
4137
4210
  return;
4138
4211
  if (progress === null) {
@@ -4140,20 +4213,14 @@ export function composeRuntime(options) {
4140
4213
  await rest(cycle, live);
4141
4214
  return;
4142
4215
  }
4143
- await roomsCarried([...expired, ...reloading]);
4144
- if (!live())
4145
- return;
4146
4216
  try {
4147
4217
  const local = await openLocal();
4148
- const reloaded = await replacing;
4218
+ await replacing;
4149
4219
  if (progress.reachable) {
4150
4220
  await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
4151
4221
  !expired.includes(room.locator) &&
4152
4222
  !reloading.includes(room.locator)), 'current', local);
4153
4223
  }
4154
- const failed = reloaded.find((outcome) => outcome.status === 'rejected');
4155
- if (failed !== undefined)
4156
- throw failed.reason;
4157
4224
  }
4158
4225
  catch {
4159
4226
  progress.complete = false;
@@ -4165,11 +4232,8 @@ export function composeRuntime(options) {
4165
4232
  }
4166
4233
  function owedReloads(expired) {
4167
4234
  const reloading = [...discovered.values()]
4168
- .filter((room) => replicas.has(room.locator) &&
4169
- !expired.includes(room.locator) &&
4170
- (owed.has(room.locator) || revoked(room.locator)))
4235
+ .filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
4171
4236
  .map((room) => room.locator);
4172
- owed.clear();
4173
4237
  for (const locator of reloading)
4174
4238
  catching.add(locator);
4175
4239
  liveQueries.recover();
@@ -4178,16 +4242,17 @@ export function composeRuntime(options) {
4178
4242
  async function reloadNow(replaced, progress) {
4179
4243
  const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
4180
4244
  for (const room of reloaded)
4181
- reloadsDue.set(room.locator, generation);
4245
+ reloadsDue.add(room.locator);
4182
4246
  try {
4183
4247
  const local = await openLocal();
4184
- return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
4248
+ for (const room of reloaded) {
4249
+ void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
4250
+ }
4185
4251
  }
4186
4252
  catch {
4187
4253
  for (const room of reloaded)
4188
4254
  reloadsDue.delete(room.locator);
4189
4255
  progress.reachable = false;
4190
- return [];
4191
4256
  }
4192
4257
  }
4193
4258
  async function settleModels(held, status, local) {
@@ -4258,14 +4323,10 @@ export function composeRuntime(options) {
4258
4323
  try {
4259
4324
  const local = await openLocal();
4260
4325
  await roomTask(locator, async (live) => {
4261
- const current = () => live() && !signal.aborted;
4262
- if (!current() || authorization === undefined)
4326
+ if (!live() || authorization === undefined)
4263
4327
  return;
4264
- const withheld = await Promise.race([
4265
- advance(room, local, authorization, current),
4266
- abandonedOn(signal),
4267
- ]);
4268
- if (withheld !== null && current())
4328
+ const withheld = await advance(room, local, authorization, live);
4329
+ if (withheld !== null && live())
4269
4330
  await withhold(withheld, room, local, progress);
4270
4331
  });
4271
4332
  }
@@ -4320,7 +4381,6 @@ export function composeRuntime(options) {
4320
4381
  progress.complete = false;
4321
4382
  return;
4322
4383
  }
4323
- reads.applied();
4324
4384
  submitAll();
4325
4385
  for (const locator of stale)
4326
4386
  if (repairable.has(locator))
@@ -4351,7 +4411,7 @@ export function composeRuntime(options) {
4351
4411
  if (!live())
4352
4412
  return;
4353
4413
  let lastCompleted = status.lastCompleted;
4354
- let synced = complete && stale.size === 0 && !pending();
4414
+ let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
4355
4415
  if (synced && !recorded) {
4356
4416
  const completedAt = new Date().toISOString();
4357
4417
  try {
@@ -4366,75 +4426,69 @@ export function composeRuntime(options) {
4366
4426
  }
4367
4427
  }
4368
4428
  moveTo({
4369
- state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
4429
+ state: pending()
4430
+ ? 'pending'
4431
+ : synced && complete && roomWork === 0 && naming === 0
4432
+ ? 'synced'
4433
+ : 'syncing',
4370
4434
  lastCompleted,
4371
- providerUnreachable: !progress.reachable,
4435
+ providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
4372
4436
  ...attention(),
4373
4437
  });
4374
4438
  }
4375
- function discover() {
4376
- let release = () => { };
4377
- const done = new Promise((resolve) => {
4378
- release = resolve;
4379
- });
4380
- discovering = { generation, done, release };
4381
- return discovering;
4382
- }
4383
- function applyRoom(locator, roomVersion) {
4384
- announced.set(locator, roomVersion);
4385
- return task(roomLane(locator), async (live) => {
4386
- if (discovering?.generation === generation)
4387
- await discovering.done;
4388
- const saving = answering.get(locator);
4389
- if (saving?.generation === generation)
4390
- await saving.answered;
4391
- if (!live() || authorization === undefined)
4392
- return;
4393
- const room = discovered.get(locator);
4394
- const held = replicas.get(locator);
4395
- if (room === undefined || room.replica.admission !== 'admitted')
4396
- return;
4397
- if (held !== undefined &&
4398
- (held.record.replica.selection !== room.replica.selection ||
4399
- options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
4400
- return;
4401
- }
4402
- const during = underway?.generation === generation ? underway.progress : null;
4403
- const progress = during ?? { complete: true, reachable: true };
4404
- if (during === null)
4405
- moveTo(transferring(status));
4439
+ function applyRoom(change) {
4440
+ const { locator } = change;
4441
+ if (stopping)
4442
+ return Promise.resolve();
4443
+ return inRoom(locator, async (live) => {
4444
+ await answering.get(locator)?.answered;
4445
+ const progress = underway ?? { complete: true, reachable: true };
4406
4446
  try {
4407
4447
  const local = await openLocal();
4408
- await admittedRead(async () => {
4409
- if (!live() || authorization === undefined)
4410
- return;
4411
- const withheld = await advance(room, local, authorization, live);
4412
- if (withheld !== null)
4413
- await withhold(withheld, room, local, progress);
4414
- });
4448
+ for (;;) {
4449
+ if (!live())
4450
+ return null;
4451
+ const room = discovered.get(locator);
4452
+ const held = replicas.get(locator);
4453
+ if (room === undefined ||
4454
+ held === undefined ||
4455
+ room.replica.admission !== 'admitted' ||
4456
+ held.record.replica.selection !== room.replica.selection) {
4457
+ return null;
4458
+ }
4459
+ const at = held.record.roomVersion;
4460
+ if (options.adapter.reached(locator, at, change.roomVersion))
4461
+ return progress;
4462
+ if (!options.adapter.reached(locator, at, change.from)) {
4463
+ name(locator);
4464
+ return progress;
4465
+ }
4466
+ if (underway === null)
4467
+ moveTo(transferring(status));
4468
+ const writing = () => live() && !full;
4469
+ const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, () => change.read()));
4470
+ if (refreshed.kind !== 'advanced')
4471
+ return progress;
4472
+ const landing = {
4473
+ kind: 'refreshed',
4474
+ from: refreshed.from,
4475
+ remodelled: refreshed.remodelled,
4476
+ };
4477
+ if (await publish(local, room, refreshed.record, landing, writing)) {
4478
+ stale.delete(locator);
4479
+ await retainModel(local, room, modelOf(locator));
4480
+ return progress;
4481
+ }
4482
+ }
4415
4483
  }
4416
4484
  catch {
4485
+ if (!stale.has(locator))
4486
+ name(locator);
4417
4487
  stale.add(locator);
4488
+ return progress;
4418
4489
  }
4419
- finally {
4420
- if (during === null)
4421
- await rest(progress, live);
4422
- }
4423
- });
4490
+ }).then((progress) => rest(progress), () => undefined);
4424
4491
  }
4425
- function roomsCarried(replacing) {
4426
- const replaced = new Set(replacing.map(roomLane));
4427
- return Promise.all([...lanes]
4428
- .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
4429
- .map(([, held]) => held.chain));
4430
- }
4431
- const receipts = {
4432
- heads: async () => (await openLocal()).receivedHeads(),
4433
- parts: async (key, count) => (await openLocal()).receivedParts(key, count),
4434
- keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
4435
- drop: async (keys) => (await openLocal()).dropReceived(keys),
4436
- ahead,
4437
- };
4438
4492
  function observeStream() {
4439
4493
  const stream = options.adapter.roomStream;
4440
4494
  if (stream === undefined || withdrawStream !== undefined)
@@ -4444,31 +4498,33 @@ export function composeRuntime(options) {
4444
4498
  return;
4445
4499
  if (event.kind === 'opened') {
4446
4500
  socketOpen = true;
4501
+ announced = false;
4502
+ handed = false;
4503
+ naming = 0;
4447
4504
  connectionLive();
4448
4505
  generation += 1;
4449
- for (const admit of installWaiters.splice(0))
4450
- admit();
4451
- reads.opened();
4452
4506
  conversations.reopened();
4453
4507
  recovery.resume();
4454
- 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);
4455
4518
  }
4456
4519
  else if (event.kind === 'closed')
4457
- closedSocket(event.revoked, event.lost);
4458
- else if (event.kind === 'opening')
4459
- void applyOpening(event.expired);
4520
+ closedSocket(event.revoked, event.lost, event.authorityChanged);
4521
+ else if (event.kind === 'caught-up')
4522
+ void caughtUpRooms(event.expired);
4460
4523
  else if (event.kind === 'authority-changed')
4461
- void rediscover(event.scope);
4524
+ void changeAuthority(event);
4462
4525
  else
4463
- void applyRoom(event.locator, event.roomVersion);
4464
- }, receipts, heldRevisions);
4465
- }
4466
- async function heldRevisions(locator, paths) {
4467
- const held = replicas.get(locator);
4468
- if (held === undefined || paths.length === 0)
4469
- return new Map();
4470
- const entries = await ahead(async () => (await openLocal()).entries(held.record.key, 'confirmed', paths));
4471
- return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
4526
+ void applyRoom(event);
4527
+ });
4472
4528
  }
4473
4529
  function writesHeld() {
4474
4530
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -4558,6 +4614,7 @@ export function composeRuntime(options) {
4558
4614
  statusListeners.clear();
4559
4615
  serviceModeListeners.clear();
4560
4616
  memberListeners.clear();
4617
+ authorityChangeListeners.clear();
4561
4618
  liveQueries.close();
4562
4619
  queueSubscriptions.clear();
4563
4620
  roomDraftListeners.clear();
@@ -4565,6 +4622,7 @@ export function composeRuntime(options) {
4565
4622
  }
4566
4623
  async function stop() {
4567
4624
  stopping = true;
4625
+ inFlight.abort();
4568
4626
  await recovery.retire();
4569
4627
  await Promise.all(working);
4570
4628
  await mutations;
@@ -4583,15 +4641,15 @@ export function composeRuntime(options) {
4583
4641
  readyFromStore();
4584
4642
  });
4585
4643
  if (options.adapter.roomStream === undefined)
4586
- void sync('unstreamed');
4644
+ void sync('unstreamed', null);
4587
4645
  function planned(input, reach) {
4588
- const plan = planOf(input, roomFacts(reach), planReach(reach));
4646
+ const plan = planWithin(input, roomFacts(reach), planReach(reach));
4589
4647
  const named = plan.paths !== null &&
4590
4648
  plan.rooms !== null &&
4591
4649
  plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
4592
4650
  return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
4593
4651
  }
4594
- function watchPlan(plan, deliver) {
4652
+ function watchPlan(plan, deliver, fail) {
4595
4653
  let seeded = false;
4596
4654
  let pending = true;
4597
4655
  const shown = () => {
@@ -4617,7 +4675,7 @@ export function composeRuntime(options) {
4617
4675
  }
4618
4676
  seeded = false;
4619
4677
  deliver(message);
4620
- });
4678
+ }, fail);
4621
4679
  return () => {
4622
4680
  watchers.delete(shown);
4623
4681
  if (watchers.size === 0)
@@ -4625,61 +4683,59 @@ export function composeRuntime(options) {
4625
4683
  stop();
4626
4684
  };
4627
4685
  }
4628
- async function searched(input, reach, locked) {
4686
+ async function searched(input, reach) {
4629
4687
  const asked = searchPlanOf(input);
4630
- const filter = {
4631
- ...filterOf(asked.documents ?? {}, roomFacts(reach), planReach(reach)),
4632
- locked,
4633
- };
4634
- const unlocking = (filter.rooms ?? [])
4635
- .filter((room) => locked.includes(room))
4636
- .map((room) => ({ room, awaits: 'unlock' }));
4637
- for (;;) {
4638
- const cover = covered(filter);
4639
- awaitPreparation(cover);
4640
- const pending = [...unlocking, ...('pending' in cover ? cover.pending : [])];
4641
- const complete = pending.length === 0 && !('pending' in cover);
4642
- const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
4643
- const answering = complete
4644
- ? filter
4645
- : { ...filter, rooms: reachedRooms(filter).filter((room) => !awaited.has(room)) };
4646
- const answered = covered(answering);
4647
- if ('pending' in answered)
4648
- continue;
4649
- const listed = conversations.searchable();
4650
- const named = reach !== null && !reach.conversations
4651
- ? []
4652
- : (asked.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
4653
- const hits = await (await openLocal()).search({
4654
- text: asked.text,
4655
- limit: capped(asked.limit),
4656
- after: asked.after,
4657
- total: asked.total,
4658
- documents: {
4659
- plan: answering,
4660
- rooms: answered.rooms,
4661
- beneath: asked.collection
4662
- ? answered.rooms.map(({ locator, key }) => ({
4663
- key,
4664
- roots: replicas.get(locator)?.record.typedSelection?.roots ?? [],
4665
- }))
4666
- : [],
4667
- },
4668
- conversations: named,
4669
- });
4670
- if (JSON.stringify(covered(answering)) !== JSON.stringify(answered))
4671
- continue;
4672
- const coverage = {
4673
- rooms: answered.coverage.rooms,
4674
- conversations: named,
4675
- 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,
4676
4696
  };
4677
- return complete
4678
- ? { ...hits, coverage }
4679
- : { 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;
4680
4736
  }
4681
4737
  }
4682
- const watch = (topic, input, deliver) => {
4738
+ const watch = (topic, input, deliver, fail) => {
4683
4739
  if (topic === 'room-drafts') {
4684
4740
  const listener = { notify: deliver, ready: false };
4685
4741
  roomDraftListeners.add(listener);
@@ -4728,7 +4784,7 @@ export function composeRuntime(options) {
4728
4784
  };
4729
4785
  }
4730
4786
  if (topic === 'readiness') {
4731
- return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
4787
+ return watchReadiness({ target: '', code: 'stored' }, null, deliver);
4732
4788
  }
4733
4789
  if (topic === 'rooms') {
4734
4790
  unlistedRooms.add(deliver);
@@ -4773,6 +4829,13 @@ export function composeRuntime(options) {
4773
4829
  memberListeners.delete(deliver);
4774
4830
  };
4775
4831
  }
4832
+ if (topic === 'authority-changes') {
4833
+ authorityChangeListeners.add(deliver);
4834
+ deliver(authorityChanges);
4835
+ return () => {
4836
+ authorityChangeListeners.delete(deliver);
4837
+ };
4838
+ }
4776
4839
  if (topic === 'queue') {
4777
4840
  const queue = { locator: input[0], notify: deliver };
4778
4841
  queueSubscriptions.add(queue);
@@ -4781,7 +4844,7 @@ export function composeRuntime(options) {
4781
4844
  queueSubscriptions.delete(queue);
4782
4845
  };
4783
4846
  }
4784
- return watchPlan(planned(input[0], null), deliver);
4847
+ return watchPlan(planned(input[0], null), deliver, fail);
4785
4848
  };
4786
4849
  const md = {
4787
4850
  loaded() {
@@ -4801,7 +4864,7 @@ export function composeRuntime(options) {
4801
4864
  });
4802
4865
  },
4803
4866
  query: ((input) => rowsQuery(planned(input, null))),
4804
- search: (input) => searched(input, null, []),
4867
+ search: (input) => searched(input, null),
4805
4868
  types(locator) {
4806
4869
  return {
4807
4870
  collection: modelTypes(modelOf(locator)),
@@ -4904,6 +4967,8 @@ export function composeRuntime(options) {
4904
4967
  return journal.export();
4905
4968
  },
4906
4969
  cleanUp,
4970
+ unsent: () => database.ask({ kind: 'authored.unsent' }),
4971
+ reset: () => options.shell.reset(),
4907
4972
  persistence() {
4908
4973
  return persistence === 'granted';
4909
4974
  },
@@ -4926,7 +4991,7 @@ export function composeRuntime(options) {
4926
4991
  authorization = undefined;
4927
4992
  discoveryPaused = false;
4928
4993
  if (options.adapter.roomStream === undefined)
4929
- await sync('unstreamed');
4994
+ await sync('unstreamed', null);
4930
4995
  else
4931
4996
  await task(OPENING_LANE, ensureAuthorization);
4932
4997
  },
@@ -4949,7 +5014,6 @@ export function composeRuntime(options) {
4949
5014
  md,
4950
5015
  seed,
4951
5016
  stop,
4952
- lose: loseRooms,
4953
5017
  watch,
4954
5018
  persisted(answer) {
4955
5019
  if (answer === persistence)