@agentshouse/mdruntime 0.1.0-alpha.7 → 0.1.0-alpha.8

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 (54) hide show
  1. package/dist/adapter.d.ts +40 -4
  2. package/dist/adapter.d.ts.map +1 -1
  3. package/dist/adapter.js.map +1 -1
  4. package/dist/bootstrap.d.ts +3 -1
  5. package/dist/bootstrap.d.ts.map +1 -1
  6. package/dist/bootstrap.js +12 -6
  7. package/dist/bootstrap.js.map +1 -1
  8. package/dist/change.d.ts +1 -0
  9. package/dist/change.d.ts.map +1 -1
  10. package/dist/change.js.map +1 -1
  11. package/dist/collection.d.ts +8 -3
  12. package/dist/collection.d.ts.map +1 -1
  13. package/dist/collection.js +66 -28
  14. package/dist/collection.js.map +1 -1
  15. package/dist/connection.d.ts +10 -3
  16. package/dist/connection.d.ts.map +1 -1
  17. package/dist/connection.js +330 -21
  18. package/dist/connection.js.map +1 -1
  19. package/dist/identity.d.ts +2 -0
  20. package/dist/identity.d.ts.map +1 -0
  21. package/dist/identity.js +16 -0
  22. package/dist/identity.js.map +1 -0
  23. package/dist/index.d.ts +4 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/memory.d.ts +2 -0
  27. package/dist/memory.d.ts.map +1 -1
  28. package/dist/memory.js +27 -6
  29. package/dist/memory.js.map +1 -1
  30. package/dist/model.d.ts +70 -0
  31. package/dist/model.d.ts.map +1 -0
  32. package/dist/model.js +28 -0
  33. package/dist/model.js.map +1 -0
  34. package/dist/query.d.ts +12 -0
  35. package/dist/query.d.ts.map +1 -0
  36. package/dist/query.js +18 -0
  37. package/dist/query.js.map +1 -0
  38. package/dist/refresh.d.ts +2 -1
  39. package/dist/refresh.d.ts.map +1 -1
  40. package/dist/refresh.js +6 -4
  41. package/dist/refresh.js.map +1 -1
  42. package/dist/replica.d.ts +3 -1
  43. package/dist/replica.d.ts.map +1 -1
  44. package/dist/replica.js +10 -6
  45. package/dist/replica.js.map +1 -1
  46. package/dist/save.d.ts +6 -1
  47. package/dist/save.d.ts.map +1 -1
  48. package/dist/save.js +31 -9
  49. package/dist/save.js.map +1 -1
  50. package/dist/store.d.ts +7 -2
  51. package/dist/store.d.ts.map +1 -1
  52. package/dist/store.js +1 -1
  53. package/dist/store.js.map +1 -1
  54. package/package.json +1 -1
@@ -1,7 +1,8 @@
1
- import { query as evaluate } from '@agentshouse/query';
1
+ import { rawQuery } from './query.js';
2
+ import { cached, modelTypes, NO_MODEL, sameModel } from './model.js';
2
3
  import { ROOM_CONTRACT_VERSION, VERSION_1_CAPABILITIES } from './adapter.js';
3
4
  import { bootstrapRoom } from './bootstrap.js';
4
- import { MD_MODEL, PRESENTED } from './collection.js';
5
+ import { MD_MODEL, PRESENTED, validRoot } from './collection.js';
5
6
  import { createLog } from './log.js';
6
7
  import { queuedOutcome, refused } from './outcome.js';
7
8
  import { refreshRoom } from './refresh.js';
@@ -25,6 +26,14 @@ export function createMdRuntime(options) {
25
26
  let capabilities = [];
26
27
  const rooms = new Map();
27
28
  const unreadable = new Set();
29
+ const models = new Map();
30
+ const liveModels = new Map();
31
+ const invalidModels = new Set();
32
+ const modelEpoch = new Map();
33
+ function modelOf(locator) {
34
+ return models.get(locator) ?? NO_MODEL;
35
+ }
36
+ const mismatchedRoots = new Set();
28
37
  let refusal = null;
29
38
  let storageLoss = null;
30
39
  let status = STARTING;
@@ -146,9 +155,9 @@ export function createMdRuntime(options) {
146
155
  announce();
147
156
  }
148
157
  function listed() {
149
- return [
158
+ return structuredClone([
150
159
  ...rooms.values()
151
- ];
160
+ ]);
152
161
  }
153
162
  function queryRooms() {
154
163
  return [
@@ -156,7 +165,8 @@ export function createMdRuntime(options) {
156
165
  ].map((replica)=>({
157
166
  room: replica.confirmed.record.locator,
158
167
  roomVersion: replica.confirmed.record.roomVersion,
159
- documents: replica.visible.collection.documents
168
+ documents: replica.visible.collection.documents,
169
+ model: modelTypes(modelOf(replica.confirmed.record.locator))
160
170
  }));
161
171
  }
162
172
  function emitRooms() {
@@ -193,7 +203,11 @@ export function createMdRuntime(options) {
193
203
  announce();
194
204
  }
195
205
  function visible(request) {
196
- return evaluate(queryRooms(), {
206
+ if (options.queryBinding === null) {
207
+ if (Object.keys(request).length > 0) throw new Error('query-binding-unavailable');
208
+ return rawQuery(queryRooms());
209
+ }
210
+ return options.queryBinding(queryRooms(), {
197
211
  ...request,
198
212
  selector: {
199
213
  $and: [
@@ -234,6 +248,38 @@ export function createMdRuntime(options) {
234
248
  }, period);
235
249
  }
236
250
  function refuse(reason) {
251
+ if (reason === 'revoked-authorization') {
252
+ const revoked = {
253
+ status: 'unavailable',
254
+ reason: 'revoked'
255
+ };
256
+ for (const locator of new Set([
257
+ ...models.keys(),
258
+ ...rooms.keys()
259
+ ])){
260
+ invalidModels.add(locator);
261
+ modelEpoch.set(locator, (modelEpoch.get(locator) ?? 0) + 1);
262
+ models.set(locator, revoked);
263
+ liveModels.delete(locator);
264
+ }
265
+ for (const [locator, room] of rooms)rooms.set(locator, {
266
+ ...room,
267
+ model: modelTypes(revoked)
268
+ });
269
+ emitRooms();
270
+ void exclusive(async ()=>{
271
+ const local = await openLocal();
272
+ await acrossTabs('mdruntime-replica', async ()=>{
273
+ for (const record of (await local.all()))await local.publish({
274
+ ...record,
275
+ model: revoked
276
+ });
277
+ });
278
+ announce();
279
+ }).catch(()=>{
280
+ journal.record('model invalidation durable-write failure');
281
+ });
282
+ }
237
283
  stopTimer();
238
284
  discoveryPaused = true;
239
285
  refusal = {
@@ -246,8 +292,12 @@ export function createMdRuntime(options) {
246
292
  function list(room, synchronizationAvailable) {
247
293
  rooms.set(room.locator, {
248
294
  name: room.name,
295
+ collectionRoot: room.collectionRoot,
249
296
  locator: room.locator,
250
- writeGrants: room.writeGrants,
297
+ operations: room.operations,
298
+ replica: room.replica,
299
+ attachedModel: room.attachedModel,
300
+ model: modelTypes(modelOf(room.locator)),
251
301
  providerOwnedPaths: room.providerOwnedPaths,
252
302
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
253
303
  synchronizationAvailable
@@ -256,7 +306,12 @@ export function createMdRuntime(options) {
256
306
  function applyDiscovery(discovered) {
257
307
  const listedLocators = new Set(discovered.map((room)=>room.locator));
258
308
  for (const room of discovered){
259
- list(room, !unreadable.has(room.locator));
309
+ const held = replicas.get(room.locator)?.confirmed.record;
310
+ if (held !== undefined && held.replica.selection !== room.replica.selection) {
311
+ list(held, false);
312
+ continue;
313
+ }
314
+ list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
260
315
  }
261
316
  for (const [locator, room] of rooms){
262
317
  if (listedLocators.has(locator)) continue;
@@ -291,7 +346,22 @@ export function createMdRuntime(options) {
291
346
  }
292
347
  replicas.clear();
293
348
  for (const record of records){
294
- const confirmed = readConfirmed(record);
349
+ const persisted = record.model;
350
+ const priorModel = models.get(record.locator);
351
+ if (priorModel !== undefined && priorModel.status !== 'unavailable' && (persisted.status === 'unavailable' || !sameModel(priorModel.snapshot, persisted.snapshot))) {
352
+ modelEpoch.set(record.locator, (modelEpoch.get(record.locator) ?? 0) + 1);
353
+ invalidModels.add(record.locator);
354
+ }
355
+ const model = persisted.status === 'unavailable' || liveModels.get(record.locator) === JSON.stringify(persisted.snapshot) ? persisted : cached(persisted);
356
+ models.set(record.locator, model);
357
+ if (model.status === 'unavailable') {
358
+ liveModels.delete(record.locator);
359
+ invalidModels.delete(record.locator);
360
+ }
361
+ const confirmed = readConfirmed({
362
+ ...record,
363
+ model
364
+ });
295
365
  if (confirmed === null) continue;
296
366
  replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? []));
297
367
  const existing = rooms.get(record.locator);
@@ -299,7 +369,11 @@ export function createMdRuntime(options) {
299
369
  else {
300
370
  rooms.set(record.locator, {
301
371
  ...existing,
302
- roomVersion: record.roomVersion
372
+ roomVersion: record.roomVersion,
373
+ operations: record.operations,
374
+ replica: record.replica,
375
+ attachedModel: record.attachedModel,
376
+ model: modelTypes(model)
303
377
  });
304
378
  }
305
379
  }
@@ -387,9 +461,14 @@ export function createMdRuntime(options) {
387
461
  return exclusive(async ()=>{
388
462
  await acrossTabs('mdruntime-replica', async ()=>{
389
463
  await hydrate(local);
390
- await local.publish(confirmed.record);
464
+ const retained = readConfirmed({
465
+ ...confirmed.record,
466
+ model: modelOf(room.locator)
467
+ });
468
+ if (retained === null) return;
469
+ await local.publish(retained.record);
391
470
  unreadable.delete(room.locator);
392
- replicas.set(room.locator, replicaOf(confirmed, replicas.get(room.locator)?.queue ?? []));
471
+ replicas.set(room.locator, replicaOf(retained, replicas.get(room.locator)?.queue ?? []));
393
472
  });
394
473
  list(room, true);
395
474
  emitRooms();
@@ -406,9 +485,19 @@ export function createMdRuntime(options) {
406
485
  if (replica === undefined || room === undefined) {
407
486
  throw new Error(`No Local Replica for ${locator}`);
408
487
  }
488
+ if (mismatchedRoots.has(locator)) return {
489
+ outcome: 'refused',
490
+ rule: 'collection-root-mismatch'
491
+ };
409
492
  const built = buildBatch({
410
493
  baseRoomVersion: replica.confirmed.record.roomVersion,
411
- writeGrants: room.writeGrants,
494
+ collectionRoot: replica.confirmed.record.collectionRoot,
495
+ operations: room.operations,
496
+ model: invalidModels.has(locator) ? {
497
+ status: 'unavailable',
498
+ reason: 'revoked'
499
+ } : modelOf(locator),
500
+ binding: options.modelBinding,
412
501
  providerOwnedPaths: room.providerOwnedPaths,
413
502
  providerLimits: limits,
414
503
  visible: replica.visible,
@@ -528,6 +617,10 @@ export function createMdRuntime(options) {
528
617
  }
529
618
  });
530
619
  journal.record(`outcome history ${result.kind}`);
620
+ if (result.kind === 'scoped-refusal') {
621
+ const room = replicas.get(result.locator)?.confirmed.record;
622
+ if (room !== undefined) await scoped(result, room, await openLocal());
623
+ }
531
624
  if (result.kind === 'revoked-authorization') refuse('revoked-authorization');
532
625
  return result;
533
626
  } catch {
@@ -571,6 +664,10 @@ export function createMdRuntime(options) {
571
664
  name,
572
665
  input
573
666
  });
667
+ if (result.kind === 'scoped-refusal') {
668
+ const room = replicas.get(result.locator)?.confirmed.record;
669
+ if (room !== undefined) await scoped(result, room, await openLocal());
670
+ }
574
671
  if (result.kind === 'revoked-authorization') refuse('revoked-authorization');
575
672
  journal.record(`outcome action ${result.kind}`);
576
673
  if (result.kind === 'rejected') journal.record(`refusal rejected ${result.text}`);
@@ -584,7 +681,7 @@ export function createMdRuntime(options) {
584
681
  }
585
682
  async function pullRefresh(authorized, confirmed) {
586
683
  try {
587
- const result = await refreshRoom(options.adapter, authorized, confirmed);
684
+ const result = await refreshRoom(options.adapter, authorized, confirmed, options.modelBinding);
588
685
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
589
686
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
590
687
  return result;
@@ -595,7 +692,7 @@ export function createMdRuntime(options) {
595
692
  }
596
693
  async function pullBootstrap(authorized, room) {
597
694
  try {
598
- const result = await bootstrapRoom(options.adapter, authorized, room);
695
+ const result = await bootstrapRoom(options.adapter, authorized, room, modelOf(room.locator), options.modelBinding);
599
696
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
600
697
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
601
698
  return result;
@@ -654,7 +751,12 @@ export function createMdRuntime(options) {
654
751
  return true;
655
752
  }
656
753
  const remaining = current.queue.filter((batch)=>batch.id !== submitted.id);
657
- const applied = overlay ? applyReturned(base, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion) : base;
754
+ const retainedBase = readConfirmed({
755
+ ...base.record,
756
+ model: modelOf(room.locator)
757
+ });
758
+ if (retainedBase === null) return false;
759
+ const applied = overlay ? applyReturned(retainedBase, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion, options.modelBinding) : retainedBase;
658
760
  if (applied === null) return false;
659
761
  await local.publish(applied.record);
660
762
  await persistQueue(local, room.locator, remaining);
@@ -710,6 +812,11 @@ export function createMdRuntime(options) {
710
812
  refuse('revoked-authorization');
711
813
  return 'stop';
712
814
  }
815
+ if (result.kind === 'scoped-refusal') {
816
+ await scoped(result, room, local);
817
+ progress.complete = false;
818
+ return null;
819
+ }
713
820
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
714
821
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}`);
715
822
  if (result.kind !== 'expired') journal.record(`refusal ${result.kind} ${result.text}`);
@@ -738,6 +845,11 @@ export function createMdRuntime(options) {
738
845
  refuse('revoked-authorization');
739
846
  return 'stop';
740
847
  }
848
+ if (pulled.kind === 'scoped-refusal') {
849
+ await scoped(pulled, room, local);
850
+ progress.complete = false;
851
+ return null;
852
+ }
741
853
  if (pulled.kind === 'still-current') {
742
854
  const committed = await exclusive(()=>commitAccept(room, local, head, result, from, true));
743
855
  if (!committed) {
@@ -761,6 +873,11 @@ export function createMdRuntime(options) {
761
873
  refuse('revoked-authorization');
762
874
  return 'stop';
763
875
  }
876
+ if (bootstrapped.kind === 'scoped-refusal') {
877
+ await scoped(bootstrapped, room, local);
878
+ progress.complete = false;
879
+ return null;
880
+ }
764
881
  if (bootstrapped.kind === 'staged') {
765
882
  const overlay = bootstrapped.confirmed.record.roomVersion === result.roomVersion;
766
883
  const committed = await exclusive(()=>commitAccept(room, local, head, result, bootstrapped.confirmed, overlay));
@@ -792,15 +909,172 @@ export function createMdRuntime(options) {
792
909
  }
793
910
  return null;
794
911
  }
912
+ async function retainModel(local, room, next) {
913
+ if (next.status === 'unavailable') modelEpoch.set(room.locator, (modelEpoch.get(room.locator) ?? 0) + 1);
914
+ const epoch = modelEpoch.get(room.locator) ?? 0;
915
+ const previous = modelOf(room.locator);
916
+ const heldBefore = replicas.get(room.locator)?.confirmed.record;
917
+ if (JSON.stringify(previous) === JSON.stringify(next) && (heldBefore === undefined || JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) && JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) && JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel))) return;
918
+ const queryChanged = previous.status === 'unavailable' || next.status === 'unavailable' ? previous.status !== next.status : JSON.stringify(previous.types) !== JSON.stringify(next.types);
919
+ if (next.status === 'unavailable') {
920
+ invalidModels.add(room.locator);
921
+ liveModels.delete(room.locator);
922
+ }
923
+ await exclusive(async ()=>acrossTabs('mdruntime-replica', async ()=>{
924
+ await hydrate(local);
925
+ if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(room.locator) ?? 0)) return;
926
+ const held = replicas.get(room.locator);
927
+ if (held !== undefined) {
928
+ const record = {
929
+ ...held.confirmed.record,
930
+ operations: room.operations,
931
+ replica: room.replica,
932
+ attachedModel: room.attachedModel,
933
+ model: next
934
+ };
935
+ await local.publish(record);
936
+ const confirmed = readConfirmed(record);
937
+ if (confirmed !== null) replicas.set(room.locator, replicaOf(confirmed, held.queue));
938
+ }
939
+ models.set(room.locator, next);
940
+ if (next.status === 'current') {
941
+ liveModels.set(room.locator, JSON.stringify(next.snapshot));
942
+ invalidModels.delete(room.locator);
943
+ }
944
+ list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
945
+ emitRooms();
946
+ if (queryChanged && held !== undefined) emitVisible();
947
+ announce();
948
+ }));
949
+ }
950
+ async function acquireModel(local, room, authorized) {
951
+ let previous = modelOf(room.locator);
952
+ if (previous.status !== 'unavailable' && (room.attachedModel === null || !sameModel(previous.snapshot, room.attachedModel))) {
953
+ previous = {
954
+ status: 'unavailable',
955
+ reason: 'version-changed'
956
+ };
957
+ await retainModel(local, room, previous);
958
+ }
959
+ const epoch = modelEpoch.get(room.locator) ?? 0;
960
+ let answer;
961
+ try {
962
+ answer = await options.adapter.model(authorized, {
963
+ locator: room.locator,
964
+ collectionRoot: room.collectionRoot,
965
+ model: room.attachedModel
966
+ });
967
+ } catch {
968
+ answer = {
969
+ kind: 'transport-uncertainty'
970
+ };
971
+ }
972
+ if (epoch !== (modelEpoch.get(room.locator) ?? 0)) return;
973
+ if (answer.kind === 'revoked-authorization') {
974
+ await retainModel(local, room, {
975
+ status: 'unavailable',
976
+ reason: 'revoked'
977
+ });
978
+ refuse('revoked-authorization');
979
+ return;
980
+ }
981
+ if (answer.kind === 'transport-uncertainty') {
982
+ await retainModel(local, room, cached(previous));
983
+ return;
984
+ }
985
+ if (answer.kind === 'model-unavailable') {
986
+ await retainModel(local, room, {
987
+ status: 'unavailable',
988
+ reason: answer.reason
989
+ });
990
+ return;
991
+ }
992
+ if (room.attachedModel === null || !sameModel(answer.snapshot, room.attachedModel)) {
993
+ await retainModel(local, room, {
994
+ status: 'unavailable',
995
+ reason: 'version-changed'
996
+ });
997
+ return;
998
+ }
999
+ if (previous.status !== 'unavailable' && sameModel(previous.snapshot, answer.snapshot) && JSON.stringify(Object.entries(previous.snapshot.definitions).sort()) !== JSON.stringify(Object.entries(answer.snapshot.definitions).sort())) {
1000
+ await retainModel(local, room, {
1001
+ status: 'unavailable',
1002
+ reason: 'invalid'
1003
+ });
1004
+ return;
1005
+ }
1006
+ const verified = options.modelBinding?.verify(answer.snapshot);
1007
+ await retainModel(local, room, verified?.kind === 'verified' ? {
1008
+ status: 'current',
1009
+ snapshot: structuredClone(answer.snapshot),
1010
+ verifiedWith: MD_MODEL,
1011
+ types: verified.types
1012
+ } : {
1013
+ status: 'unavailable',
1014
+ reason: verified === undefined ? 'binding-unavailable' : 'invalid'
1015
+ });
1016
+ }
1017
+ async function scoped(result, room, local) {
1018
+ if (result.scope === 'model') await retainModel(local, room, {
1019
+ status: 'unavailable',
1020
+ reason: 'not-readable'
1021
+ });
1022
+ else if (result.scope !== 'operation') {
1023
+ unreadable.add(room.locator);
1024
+ const unavailable = {
1025
+ ...room,
1026
+ replica: {
1027
+ ...room.replica,
1028
+ admission: 'unavailable'
1029
+ },
1030
+ operations: {
1031
+ ...room.operations,
1032
+ ...result.scope === 'content' ? {
1033
+ read: []
1034
+ } : {},
1035
+ ...result.scope === 'sync' ? {
1036
+ sync: []
1037
+ } : {}
1038
+ }
1039
+ };
1040
+ await retainModel(local, unavailable, modelOf(room.locator));
1041
+ list(unavailable, false);
1042
+ emitRooms();
1043
+ }
1044
+ journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.text}`);
1045
+ }
795
1046
  async function synchronize(discovered, local, progress) {
796
1047
  for (const room of discovered){
797
1048
  if (closed || authorization === undefined) {
798
1049
  progress.complete = false;
799
1050
  return;
800
1051
  }
1052
+ const prior = replicas.get(room.locator)?.confirmed.record;
1053
+ if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
1054
+ list(prior, false);
1055
+ emitRooms();
1056
+ journal.record(`selection changed ${room.locator}`);
1057
+ progress.complete = false;
1058
+ continue;
1059
+ }
1060
+ await acquireModel(local, room, authorization);
1061
+ if (discoveryPaused) return;
1062
+ if (room.replica.admission !== 'admitted') {
1063
+ progress.complete = false;
1064
+ continue;
1065
+ }
1066
+ const held = replicas.get(room.locator);
1067
+ if (held !== undefined && held.confirmed.record.replica.selection !== room.replica.selection) {
1068
+ progress.complete = false;
1069
+ continue;
1070
+ }
801
1071
  const withheld = await advance(room, local, authorization);
802
1072
  if (withheld !== null) {
803
1073
  progress.complete = false;
1074
+ if (withheld.kind === 'scoped-refusal') {
1075
+ await scoped(withheld, room, local);
1076
+ continue;
1077
+ }
804
1078
  if (withheld.kind === 'transport-uncertainty') {
805
1079
  progress.reachable = false;
806
1080
  continue;
@@ -829,10 +1103,10 @@ export function createMdRuntime(options) {
829
1103
  let result;
830
1104
  try {
831
1105
  result = await options.adapter.discover(authorization);
832
- } catch (error) {
833
- journal.record('connection discover transport-uncertainty');
834
- notifyReady();
835
- throw error;
1106
+ } catch {
1107
+ result = {
1108
+ kind: 'transport-uncertainty'
1109
+ };
836
1110
  }
837
1111
  if (closed) {
838
1112
  notifyReady();
@@ -840,10 +1114,17 @@ export function createMdRuntime(options) {
840
1114
  }
841
1115
  journal.record(`connection discover ${result.kind}`);
842
1116
  if (result.kind === 'transport-uncertainty') {
1117
+ for (const room of rooms.values())await retainModel(local, room, cached(modelOf(room.locator)));
843
1118
  progress.reachable = false;
844
1119
  notifyReady();
845
1120
  return;
846
1121
  }
1122
+ if (result.kind === 'contract-unavailable') {
1123
+ journal.record(`connection contract unavailable ${result.text}`);
1124
+ refuse('missing-required-guarantee');
1125
+ notifyReady();
1126
+ return;
1127
+ }
847
1128
  if (result.kind === 'revoked-authorization') {
848
1129
  refuse('revoked-authorization');
849
1130
  notifyReady();
@@ -859,6 +1140,34 @@ export function createMdRuntime(options) {
859
1140
  notifyReady();
860
1141
  return;
861
1142
  }
1143
+ mismatchedRoots.clear();
1144
+ for (const room of result.rooms){
1145
+ const held = replicas.get(room.locator)?.confirmed.record;
1146
+ const malformed = !validRoot(room.collectionRoot);
1147
+ if (room.operations === undefined || ![
1148
+ 'read',
1149
+ 'sync',
1150
+ 'write',
1151
+ 'delete'
1152
+ ].every((operation)=>Array.isArray(room.operations[operation]) && room.operations[operation].every((scope)=>typeof scope === 'string')) || room.replica === undefined || typeof room.replica.selection !== 'string' || room.replica.selection.length === 0 || ![
1153
+ 'admitted',
1154
+ 'unavailable'
1155
+ ].includes(room.replica.admission) || room.attachedModel === undefined || room.attachedModel !== null && (typeof room.attachedModel.identity !== 'string' || !room.attachedModel.identity || typeof room.attachedModel.version !== 'string' || !room.attachedModel.version)) {
1156
+ journal.record(`invalid model or replica declaration ${room.locator}`);
1157
+ refuse('missing-required-guarantee');
1158
+ notifyReady();
1159
+ return;
1160
+ }
1161
+ if (malformed || held !== undefined && held.collectionRoot !== room.collectionRoot) {
1162
+ mismatchedRoots.add(room.locator);
1163
+ journal.record(`collection root ${malformed ? 'invalid' : 'changed'} ${room.locator}`);
1164
+ }
1165
+ }
1166
+ if (mismatchedRoots.size > 0) {
1167
+ refuse('missing-required-guarantee');
1168
+ notifyReady();
1169
+ return;
1170
+ }
862
1171
  const declared = new Set(result.capabilities.map((capability)=>capability.name));
863
1172
  for (const required of options.requiredGuarantees){
864
1173
  if (!knownCapabilities.has(required) || !declared.has(required)) {
@@ -1005,7 +1314,7 @@ export function createMdRuntime(options) {
1005
1314
  return visible(request);
1006
1315
  },
1007
1316
  types (locator) {
1008
- return replicas.get(locator)?.visible.collection.types ?? [];
1317
+ return modelTypes(modelOf(locator));
1009
1318
  },
1010
1319
  syncStatus () {
1011
1320
  return status;