@agentshouse/mdruntime 0.1.0-cfb0d45b48da0 → 0.1.0-cfb2ae0107054

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 (56) hide show
  1. package/AGENTS.md +5 -5
  2. package/dist/adapter.d.ts +24 -2
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/attempt.d.ts.map +1 -1
  5. package/dist/attempt.js +14 -1
  6. package/dist/bootstrap.d.ts +2 -2
  7. package/dist/bootstrap.d.ts.map +1 -1
  8. package/dist/bootstrap.js +28 -6
  9. package/dist/chat/acts.d.ts +3 -4
  10. package/dist/chat/acts.d.ts.map +1 -1
  11. package/dist/chat/acts.js +10 -10
  12. package/dist/chat/list.d.ts +6 -0
  13. package/dist/chat/list.d.ts.map +1 -1
  14. package/dist/chat/list.js +6 -3
  15. package/dist/chat/markdown.d.ts +2 -1
  16. package/dist/chat/markdown.d.ts.map +1 -1
  17. package/dist/chat/markdown.js +7 -6
  18. package/dist/chat/provider.d.ts.map +1 -1
  19. package/dist/chat/provider.js +25 -50
  20. package/dist/chat/ready.d.ts.map +1 -1
  21. package/dist/chat/ready.js +2 -2
  22. package/dist/chat/style.js +3 -3
  23. package/dist/client.d.ts +2 -2
  24. package/dist/client.d.ts.map +1 -1
  25. package/dist/client.js +1 -1
  26. package/dist/connection.d.ts +1 -0
  27. package/dist/connection.d.ts.map +1 -1
  28. package/dist/connection.js +337 -150
  29. package/dist/conversation.d.ts +4 -2
  30. package/dist/conversation.d.ts.map +1 -1
  31. package/dist/conversation.js +143 -58
  32. package/dist/entry.d.ts +17 -0
  33. package/dist/entry.d.ts.map +1 -0
  34. package/dist/entry.js +156 -0
  35. package/dist/fence.d.ts +6 -0
  36. package/dist/fence.d.ts.map +1 -0
  37. package/dist/fence.js +10 -0
  38. package/dist/host.d.ts +2 -1
  39. package/dist/host.d.ts.map +1 -1
  40. package/dist/host.js +5 -2
  41. package/dist/index.d.ts +2 -2
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/memory.d.ts +1 -0
  44. package/dist/memory.d.ts.map +1 -1
  45. package/dist/memory.js +9 -0
  46. package/dist/outcome.d.ts +4 -0
  47. package/dist/outcome.d.ts.map +1 -1
  48. package/dist/outcome.js +1 -0
  49. package/dist/reach.js +1 -1
  50. package/dist/save.d.ts +1 -2
  51. package/dist/save.d.ts.map +1 -1
  52. package/dist/save.js +6 -5
  53. package/dist/store.d.ts +22 -7
  54. package/dist/store.d.ts.map +1 -1
  55. package/dist/store.js +125 -36
  56. package/package.json +5 -1
@@ -13,13 +13,14 @@ import { validRoot } from './collection.js';
13
13
  import { runtimeError } from './failure.js';
14
14
  import { createLog } from './log.js';
15
15
  import { binaryRecord } from './binary.js';
16
- import { absentBinary, binaryAnswer, calledResult, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
16
+ import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
17
17
  import { refreshRoom } from './refresh.js';
18
18
  import { buildBatch, joined, limitedIn, requeued } from './save.js';
19
19
  import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
20
20
  import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
21
21
  import { same, saving, STARTING, transferring, } from './status.js';
22
22
  import { detectStorageLoss, nextGeneration } from './durability.js';
23
+ import { FencedMap } from './fence.js';
23
24
  import { roomDraftRevision } from './identity.js';
24
25
  import { beneath, normalize } from './paths.js';
25
26
  import { openRuntimeStore, } from './store.js';
@@ -38,23 +39,36 @@ function declaredSelection(room) {
38
39
  root !== '' &&
39
40
  selection.roots.every((other, at) => at === index || !within(other, root)));
40
41
  }
42
+ const OPENING_LANE = 'opening';
43
+ const CONVERSATION_LANE = 'conversations';
44
+ const ROOM_READS = 256;
45
+ function roomLane(locator) {
46
+ return `room ${locator}`;
47
+ }
41
48
  export function composeRuntime(options) {
42
49
  const localSaves = options.adapter.localSaves === true;
43
50
  let authorization = options.authorization;
44
51
  let opening;
45
52
  let store;
46
- const replicas = new Map();
47
- const queuesWithoutReplica = new Map();
53
+ const fences = new Map();
54
+ const isFenced = (locator) => fences.has(locator);
55
+ let fencings = 0;
56
+ const lastFenced = new Map();
57
+ const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
58
+ let discoveries = 0;
59
+ const replicas = new FencedMap(isFenced);
60
+ const queuesWithoutReplica = new FencedMap(isFenced);
48
61
  const pendings = new Map();
49
- const roomDrafts = new Map();
62
+ const roomDrafts = new FencedMap(isFenced);
50
63
  const presentations = new Map();
51
64
  let limits = [];
52
65
  let capabilities = [];
53
- const rooms = new Map();
66
+ const rooms = new FencedMap(isFenced);
54
67
  const counted = new Map();
55
68
  const unreadable = new Set();
56
- const models = new Map();
57
- const liveModels = new Map();
69
+ const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
70
+ const models = new FencedMap(isModelFenced);
71
+ const liveModels = new FencedMap(isModelFenced);
58
72
  const invalidModels = new Set();
59
73
  const discovered = new Map();
60
74
  let roomsListed = false;
@@ -97,7 +111,13 @@ export function composeRuntime(options) {
97
111
  let complete = false;
98
112
  let recorded = false;
99
113
  const stale = new Set();
100
- let chain = Promise.resolve();
114
+ let generation = 0;
115
+ let discovering = null;
116
+ let opened = null;
117
+ const owed = new Set();
118
+ const lanes = new Map();
119
+ let roomReads = { generation, free: ROOM_READS, waiting: [] };
120
+ const working = new Set();
101
121
  let mutations = Promise.resolve();
102
122
  const roomListeners = new Set();
103
123
  const refusalListeners = new Set();
@@ -108,7 +128,7 @@ export function composeRuntime(options) {
108
128
  const presentationListeners = new Set();
109
129
  const readSide = createReadSide();
110
130
  let withdrawConversations;
111
- let currencyQueued = false;
131
+ let currencyQueued = null;
112
132
  let socketOpen = false;
113
133
  let serviceMode = 'open';
114
134
  let haltedRefusal = false;
@@ -118,7 +138,7 @@ export function composeRuntime(options) {
118
138
  const memberListeners = new Set();
119
139
  const materialized = new Set();
120
140
  let everyRoom = false;
121
- const transfers = new Map();
141
+ const transfers = new FencedMap(isFenced);
122
142
  const conversations = createConversationStore({
123
143
  agents: declaredAgents,
124
144
  documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
@@ -126,6 +146,7 @@ export function composeRuntime(options) {
126
146
  durably,
127
147
  stream: () => options.adapter.conversations,
128
148
  publish: publishChangeSet,
149
+ online: () => !status.providerUnreachable && refusal === null,
129
150
  closed: () => closed,
130
151
  });
131
152
  const searchIndex = createSearchIndex({
@@ -233,12 +254,11 @@ export function composeRuntime(options) {
233
254
  if (replica === undefined)
234
255
  return 0;
235
256
  const roots = replica.confirmed.record.typedSelection?.roots ?? [];
236
- let count = 0;
237
- for (const path of Object.keys(replica.visible.documents)) {
238
- if (markdownPath(path) && !roots.some((root) => beneath(root, path)))
239
- count += 1;
240
- }
241
- return count;
257
+ const paths = [
258
+ ...Object.keys(replica.visible.documents).filter(markdownPath),
259
+ ...Object.keys(replica.visible.binaries),
260
+ ];
261
+ return paths.filter((path) => !roots.some((root) => beneath(root, path))).length;
242
262
  }
243
263
  function listed() {
244
264
  return structuredClone([...rooms.values()].map((room) => {
@@ -477,14 +497,24 @@ export function composeRuntime(options) {
477
497
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
478
498
  });
479
499
  }
480
- async function applyDiscovery(listed, local) {
500
+ async function applyDiscovery(found, started) {
501
+ for (const room of found) {
502
+ const fence = fences.get(room.locator);
503
+ if (fence !== undefined && fence.erased && fence.at < started)
504
+ fences.delete(room.locator);
505
+ }
506
+ const listed = found.filter((room) => !fences.has(room.locator));
481
507
  const listedLocators = new Set(listed.map((room) => room.locator));
482
508
  for (const locator of listedLocators)
483
509
  scopedRefusals.delete(locator);
484
510
  discovered.clear();
485
511
  for (const room of listed)
486
512
  discovered.set(room.locator, room);
487
- await dropUnlisted(listedLocators, local);
513
+ const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
514
+ if (options.adapter.exhaustiveDiscovery === true)
515
+ await loseRooms(unlisted);
516
+ else
517
+ unlistedKept(unlisted);
488
518
  for (const room of listed) {
489
519
  const held = replicas.get(room.locator)?.confirmed.record;
490
520
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -495,6 +525,7 @@ export function composeRuntime(options) {
495
525
  }
496
526
  roomsListed = true;
497
527
  emitRooms();
528
+ return listed;
498
529
  }
499
530
  function unlistedHeld(listed) {
500
531
  return [
@@ -506,41 +537,67 @@ export function composeRuntime(options) {
506
537
  ]),
507
538
  ].filter((locator) => !listed.has(locator));
508
539
  }
509
- async function dropUnlisted(listed, local) {
510
- if (unlistedHeld(listed).length === 0)
511
- return;
512
- await exclusive(async () => {
513
- const unlisted = unlistedHeld(listed);
514
- await local.dropRooms(unlisted);
515
- const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
516
- room: true,
517
- queue: true,
518
- drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
519
- }));
520
- for (const locator of unlisted) {
521
- for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
522
- pendings.delete(batch.id);
523
- replicas.delete(locator);
524
- queuesWithoutReplica.delete(locator);
540
+ function unlistedKept(unlisted) {
541
+ for (const locator of unlisted) {
542
+ const held = replicas.get(locator)?.confirmed.record;
543
+ if (held !== undefined)
544
+ list(held, false);
545
+ else
525
546
  rooms.delete(locator);
526
- roomDrafts.delete(locator);
527
- transfers.delete(locator);
528
- unreadable.delete(locator);
529
- stale.delete(locator);
530
- scopedRefusals.delete(locator);
531
- sourceRefusals.delete(locator);
532
- declarationRefusals.delete(locator);
533
- for (const attached of ['collection', 'typed-selection']) {
534
- const key = modelKey(locator, attached);
535
- models.delete(key);
536
- liveModels.delete(key);
537
- invalidModels.delete(key);
538
- }
539
- journal.record(`room unlisted ${locator}`);
547
+ }
548
+ }
549
+ function fence(locators) {
550
+ const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
551
+ const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
552
+ room: true,
553
+ queue: true,
554
+ drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
555
+ }));
556
+ for (const locator of fresh) {
557
+ fences.set(locator, { at: discoveries, erased: false });
558
+ lastFenced.set(locator, ++fencings);
559
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
560
+ pendings.delete(batch.id);
561
+ replicas.delete(locator);
562
+ queuesWithoutReplica.delete(locator);
563
+ rooms.delete(locator);
564
+ roomDrafts.delete(locator);
565
+ transfers.delete(locator);
566
+ discovered.delete(locator);
567
+ announced.delete(locator);
568
+ unreadable.delete(locator);
569
+ stale.delete(locator);
570
+ scopedRefusals.delete(locator);
571
+ sourceRefusals.delete(locator);
572
+ declarationRefusals.delete(locator);
573
+ for (const attached of ['collection', 'typed-selection']) {
574
+ const key = modelKey(locator, attached);
575
+ models.delete(key);
576
+ liveModels.delete(key);
577
+ invalidModels.delete(key);
540
578
  }
541
- publishChangeSet(mergeChangeSets(removed));
542
- moveTo({ ...status, ...attention() });
543
- });
579
+ journal.record(`room lost ${locator}`);
580
+ }
581
+ if (fresh.length === 0)
582
+ return fresh;
583
+ publishChangeSet(mergeChangeSets(removed));
584
+ for (const locator of fresh)
585
+ materialized.delete(locator);
586
+ moveTo({ ...status, ...attention() });
587
+ return fresh;
588
+ }
589
+ async function erased(local, locators) {
590
+ await local.erase(locators);
591
+ for (const locator of locators) {
592
+ const held = fences.get(locator);
593
+ if (held !== undefined)
594
+ held.erased = true;
595
+ }
596
+ }
597
+ async function loseRooms(locators) {
598
+ const fresh = fence(locators);
599
+ if (fresh.length > 0)
600
+ await durably((local) => erased(local, fresh));
544
601
  }
545
602
  function restored(key, persisted) {
546
603
  const prior = models.get(key);
@@ -645,8 +702,15 @@ export function composeRuntime(options) {
645
702
  return lastCompleted;
646
703
  }
647
704
  async function open() {
648
- const opened = await openRuntimeStore();
705
+ if (closed)
706
+ throw new Error('runtime closed');
707
+ const opened = await openRuntimeStore(isFenced);
649
708
  store = opened;
709
+ const interrupted = await opened.fences();
710
+ for (const locator of interrupted) {
711
+ fences.set(locator, { at: discoveries, erased: false });
712
+ lastFenced.set(locator, ++fencings);
713
+ }
650
714
  const kept = options.shell.generation;
651
715
  const storedGeneration = await opened.generation();
652
716
  const loss = detectStorageLoss(storedGeneration, kept);
@@ -660,6 +724,8 @@ export function composeRuntime(options) {
660
724
  emitStorageLoss();
661
725
  }
662
726
  const lastCompleted = await readState(opened);
727
+ if (interrupted.length > 0)
728
+ await erased(opened, interrupted);
663
729
  moveTo({ ...status, lastCompleted, ...attention() });
664
730
  return opened;
665
731
  }
@@ -784,10 +850,10 @@ export function composeRuntime(options) {
784
850
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
785
851
  };
786
852
  }
787
- function publish(local, room, confirmed, landing = {}) {
853
+ function publish(local, room, confirmed, landing, live) {
788
854
  const { base, stored } = landing;
789
855
  return exclusive(async () => {
790
- if (!continuing())
856
+ if (!live())
791
857
  return true;
792
858
  const prior = replicas.get(room.locator);
793
859
  if (base !== undefined &&
@@ -815,6 +881,7 @@ export function composeRuntime(options) {
815
881
  list(room, true);
816
882
  publishChangeSet(set);
817
883
  moveTo({ ...status, ...attention() });
884
+ options.adapter.roomStream?.published(room.locator, roomVersion);
818
885
  return true;
819
886
  });
820
887
  }
@@ -937,6 +1004,8 @@ export function composeRuntime(options) {
937
1004
  catch {
938
1005
  return { outcome: 'refused', code: 'durable-write', retryable: true };
939
1006
  }
1007
+ if (fences.has(locator))
1008
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
940
1009
  for (const origin of withdrawn.keys())
941
1010
  pendings.delete(origin);
942
1011
  for (const [id, drafts] of drafted) {
@@ -966,6 +1035,8 @@ export function composeRuntime(options) {
966
1035
  return;
967
1036
  }
968
1037
  await local.submit(locator, identifier, submission, lost);
1038
+ if (fences.has(locator))
1039
+ return;
969
1040
  pendings.set(identifier, { ...state, submission, lost });
970
1041
  const replica = replicas.get(locator);
971
1042
  if (replica !== undefined)
@@ -976,6 +1047,8 @@ export function composeRuntime(options) {
976
1047
  if (!continuing())
977
1048
  throw new Error('connection refused');
978
1049
  await local.keepUpload(locator, identifier, upload);
1050
+ if (fences.has(locator))
1051
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
979
1052
  pendings.set(identifier, { ...pendings.get(identifier), upload });
980
1053
  if (!continuing())
981
1054
  throw new Error('connection refused');
@@ -991,6 +1064,7 @@ export function composeRuntime(options) {
991
1064
  }
992
1065
  if (closed || authorization === undefined || refusal !== null)
993
1066
  return UNCERTAIN;
1067
+ const since = fencings;
994
1068
  const transfer = ['transfer', 'history', locator, path];
995
1069
  if (query.revision !== undefined)
996
1070
  transfer.push(query.revision);
@@ -1005,6 +1079,8 @@ export function composeRuntime(options) {
1005
1079
  ...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
1006
1080
  });
1007
1081
  journal.record(`outcome history ${result.kind}`);
1082
+ if (fencedSince(locator, since))
1083
+ return failure({ kind: 'room-not-readable', locator });
1008
1084
  if (result.kind === 'scoped-refusal') {
1009
1085
  const room = replicas.get(result.locator)?.confirmed.record;
1010
1086
  if (room !== undefined)
@@ -1027,6 +1103,12 @@ export function composeRuntime(options) {
1027
1103
  }
1028
1104
  return null;
1029
1105
  }
1106
+ function binaryRoom(request) {
1107
+ if (request.kind === 'path')
1108
+ return request.locator;
1109
+ const scope = options.adapter.referrerScope(request.referrer);
1110
+ return scope?.kind === 'room' ? scope.locator : null;
1111
+ }
1030
1112
  async function readBinary(request) {
1031
1113
  if (closed || refusal !== null)
1032
1114
  return UNCERTAIN;
@@ -1038,12 +1120,16 @@ export function composeRuntime(options) {
1038
1120
  }
1039
1121
  if (closed || authorization === undefined || refusal !== null)
1040
1122
  return UNCERTAIN;
1123
+ const since = fencings;
1041
1124
  journal.record(request.kind === 'path'
1042
1125
  ? `transfer binary ${request.locator} ${request.path}`
1043
1126
  : `transfer binary ${request.fileRef}`);
1044
1127
  try {
1045
1128
  const result = await options.adapter.binary(authorization, request);
1046
1129
  journal.record(`outcome binary ${result.kind}`);
1130
+ const locator = binaryRoom(request);
1131
+ if (locator !== null && fencedSince(locator, since))
1132
+ return failure({ kind: 'room-not-readable', locator });
1047
1133
  if (result.kind === 'scoped-refusal') {
1048
1134
  const room = replicas.get(result.locator)?.confirmed.record;
1049
1135
  if (room !== undefined)
@@ -1167,11 +1253,11 @@ export function composeRuntime(options) {
1167
1253
  options.adapter.reached(locator, written, roomVersion) &&
1168
1254
  !options.adapter.reached(locator, roomVersion, written));
1169
1255
  }
1170
- async function pullRefresh(authorized, confirmed) {
1171
- if (!continuing())
1256
+ async function pullRefresh(authorized, confirmed, live) {
1257
+ if (!live())
1172
1258
  return unconfirmed;
1173
1259
  try {
1174
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
1260
+ const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1175
1261
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
1176
1262
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
1177
1263
  return result;
@@ -1181,33 +1267,39 @@ export function composeRuntime(options) {
1181
1267
  throw error;
1182
1268
  }
1183
1269
  }
1184
- function staging(local) {
1270
+ function staging(local, live) {
1185
1271
  return {
1186
1272
  held: (locator) => transfers.get(locator) ?? null,
1187
1273
  staged: (record) => local.staged(record),
1188
1274
  stage: (record, page) => exclusive(async () => {
1189
- if (!continuing())
1275
+ if (!live())
1190
1276
  return false;
1191
1277
  await local.stage(record, page);
1192
1278
  transfers.set(record.locator, record);
1193
1279
  return true;
1194
1280
  }),
1195
1281
  abandon: (locator) => exclusive(async () => {
1196
- if (!continuing())
1282
+ if (!live())
1197
1283
  return;
1198
1284
  await local.abandon(locator);
1199
1285
  transfers.delete(locator);
1200
1286
  }),
1201
1287
  };
1202
1288
  }
1203
- async function pullBootstrap(authorized, room, local) {
1204
- if (!continuing())
1289
+ function reusable(room) {
1290
+ const record = replicas.get(room.locator)?.confirmed.record;
1291
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1292
+ return null;
1293
+ return { documents: record.documents, revisions: record.revisions };
1294
+ }
1295
+ async function pullBootstrap(authorized, room, local, live) {
1296
+ if (!live())
1205
1297
  return unconfirmed;
1206
1298
  try {
1207
1299
  const result = await bootstrapRoom(options.adapter, authorized, room, {
1208
1300
  collection: modelOf(room.locator),
1209
1301
  selection: modelOf(room.locator, 'typed-selection'),
1210
- }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
1302
+ }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
1211
1303
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
1212
1304
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1213
1305
  return result;
@@ -1217,11 +1309,11 @@ export function composeRuntime(options) {
1217
1309
  throw error;
1218
1310
  }
1219
1311
  }
1220
- async function advance(room, local, authorized) {
1312
+ async function advance(room, local, authorized, live) {
1221
1313
  const held = replicas.get(room.locator);
1222
1314
  if (held !== undefined) {
1223
- const refreshed = await pullRefresh(authorized, held.confirmed);
1224
- if (!continuing())
1315
+ const refreshed = await pullRefresh(authorized, held.confirmed, live);
1316
+ if (!live())
1225
1317
  return unconfirmed;
1226
1318
  if (refreshed.kind === 'still-current') {
1227
1319
  stale.delete(room.locator);
@@ -1235,24 +1327,28 @@ export function composeRuntime(options) {
1235
1327
  return null;
1236
1328
  }
1237
1329
  if (refreshed.kind === 'advanced') {
1238
- if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
1330
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
1239
1331
  const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
1240
1332
  const reached = now !== undefined &&
1241
1333
  options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
1242
- return reached ? null : advance(room, local, authorized);
1334
+ return reached ? null : advance(room, local, authorized, live);
1243
1335
  }
1244
1336
  await retainModel(local, room, 'collection', modelOf(room.locator));
1245
1337
  return null;
1246
1338
  }
1247
1339
  if (refreshed.kind !== 'beyond-horizon')
1248
1340
  return refreshed;
1341
+ if (opened !== null) {
1342
+ owed.add(room.locator);
1343
+ return unconfirmed;
1344
+ }
1249
1345
  }
1250
- const bootstrapped = await pullBootstrap(authorized, room, local);
1251
- if (!continuing())
1346
+ const bootstrapped = await pullBootstrap(authorized, room, local, live);
1347
+ if (!live())
1252
1348
  return unconfirmed;
1253
1349
  if (bootstrapped.kind !== 'staged')
1254
1350
  return bootstrapped;
1255
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1351
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
1256
1352
  return null;
1257
1353
  }
1258
1354
  async function withhold(withheld, room, local, progress) {
@@ -1332,7 +1428,7 @@ export function composeRuntime(options) {
1332
1428
  if (applied.kind !== 'staged') {
1333
1429
  const state = pendings.get(submitted.id);
1334
1430
  await local.accept(room.locator, submitted.id, accepted.roomVersion);
1335
- if (state !== undefined)
1431
+ if (state !== undefined && !fences.has(room.locator))
1336
1432
  pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1337
1433
  emitQueue(room.locator);
1338
1434
  return applied;
@@ -1381,7 +1477,7 @@ export function composeRuntime(options) {
1381
1477
  let result;
1382
1478
  journal.record(`submission ${room.locator} ${head.id}`);
1383
1479
  await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1384
- if (discoveryPaused)
1480
+ if (discoveryPaused || fences.has(room.locator))
1385
1481
  return;
1386
1482
  if (writesHeld()) {
1387
1483
  await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
@@ -1444,7 +1540,7 @@ export function composeRuntime(options) {
1444
1540
  result.kind === 'expired' ||
1445
1541
  result.kind === 'retryable-refusal') {
1446
1542
  if (result.kind === 'expired') {
1447
- const withheld = await advance(room, local, authorized);
1543
+ const withheld = await advance(room, local, authorized, continuing);
1448
1544
  if (withheld !== null)
1449
1545
  return withhold(withheld, room, local, progress);
1450
1546
  }
@@ -1482,7 +1578,7 @@ export function composeRuntime(options) {
1482
1578
  });
1483
1579
  if (result.kind === 'expired')
1484
1580
  continue;
1485
- const withheld = await advance(room, local, authorized);
1581
+ const withheld = await advance(room, local, authorized, continuing);
1486
1582
  if (withheld !== null)
1487
1583
  return withhold(withheld, room, local, progress);
1488
1584
  continue;
@@ -1499,9 +1595,9 @@ export function composeRuntime(options) {
1499
1595
  const awaited = new Promise((resolve) => {
1500
1596
  release = resolve;
1501
1597
  });
1502
- answering.set(locator, awaited);
1598
+ answering.set(locator, { generation, answered: awaited });
1503
1599
  return () => {
1504
- if (answering.get(locator) === awaited)
1600
+ if (answering.get(locator)?.answered === awaited)
1505
1601
  answering.delete(locator);
1506
1602
  release();
1507
1603
  };
@@ -1617,8 +1713,10 @@ export function composeRuntime(options) {
1617
1713
  ];
1618
1714
  });
1619
1715
  }
1620
- async function synchronize(listed, local, progress, attempt) {
1716
+ async function synchronize(listed, local, progress, attempt, live) {
1621
1717
  const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1718
+ if (!live())
1719
+ return;
1622
1720
  const ready = listed.filter((_, index) => {
1623
1721
  const outcome = prepared[index];
1624
1722
  return outcome?.status === 'fulfilled' && outcome.value;
@@ -1627,9 +1725,10 @@ export function composeRuntime(options) {
1627
1725
  options.adapter.roomStream?.hold(heldRooms(ready));
1628
1726
  handed = true;
1629
1727
  }
1728
+ discovering?.release();
1630
1729
  const loaded = await Promise.allSettled(ready
1631
1730
  .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1632
- .map((room) => loadRoom(room, local, progress)));
1731
+ .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
1633
1732
  const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1634
1733
  if (failed !== undefined)
1635
1734
  throw failed.reason;
@@ -1658,14 +1757,14 @@ export function composeRuntime(options) {
1658
1757
  }
1659
1758
  return true;
1660
1759
  }
1661
- async function caughtUp(room, local, progress) {
1760
+ async function caughtUp(room, local, progress, live) {
1662
1761
  const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1663
- if (held === undefined || authorization === undefined)
1762
+ if (held === undefined || authorization === undefined || !live())
1664
1763
  return;
1665
1764
  const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1666
1765
  if (!behind)
1667
1766
  return;
1668
- const withheld = await advance(room, local, authorization);
1767
+ const withheld = await advance(room, local, authorization, live);
1669
1768
  if (withheld !== null)
1670
1769
  await withhold(withheld, room, local, progress);
1671
1770
  }
@@ -1675,39 +1774,41 @@ export function composeRuntime(options) {
1675
1774
  return model.status === 'unavailable' && model.reason === 'revoked';
1676
1775
  });
1677
1776
  }
1678
- async function loadRoom(room, local, progress) {
1679
- if (discoveryPaused || authorization === undefined)
1777
+ async function loadRoom(room, local, progress, live) {
1778
+ if (!live() || authorization === undefined)
1680
1779
  return;
1681
1780
  if (replicas.has(room.locator) && revoked(room.locator)) {
1682
- await reload(room, local, progress);
1781
+ await reload(room, local, progress, live);
1683
1782
  return;
1684
1783
  }
1685
- const withheld = await advance(room, local, authorization);
1784
+ const withheld = await advance(room, local, authorization, live);
1686
1785
  if (withheld !== null) {
1687
1786
  await withhold(withheld, room, local, progress);
1688
1787
  return;
1689
1788
  }
1690
- await caughtUp(room, local, progress);
1789
+ await caughtUp(room, local, progress, live);
1691
1790
  }
1692
- async function reload(room, local, progress) {
1693
- if (discoveryPaused || authorization === undefined)
1791
+ async function reload(room, local, progress, live) {
1792
+ if (!live() || authorization === undefined)
1694
1793
  return;
1695
- const bootstrapped = await pullBootstrap(authorization, room, local);
1696
- if (!continuing())
1794
+ const bootstrapped = await pullBootstrap(authorization, room, local, live);
1795
+ if (!live())
1697
1796
  return;
1698
1797
  if (bootstrapped.kind !== 'staged') {
1699
1798
  await withhold(bootstrapped, room, local, progress);
1700
1799
  return;
1701
1800
  }
1702
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1703
- await caughtUp(room, local, progress);
1801
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
1802
+ await caughtUp(room, local, progress, live);
1704
1803
  }
1705
- async function discoverRooms(local, progress, attempt) {
1804
+ async function discoverRooms(local, progress, attempt, live) {
1706
1805
  if (closed || authorization === undefined) {
1707
1806
  notifyReady();
1708
1807
  return;
1709
1808
  }
1710
1809
  moveTo(transferring(status));
1810
+ discoveries += 1;
1811
+ const started = discoveries;
1711
1812
  let result;
1712
1813
  try {
1713
1814
  result = await options.adapter.discover(authorization);
@@ -1715,7 +1816,8 @@ export function composeRuntime(options) {
1715
1816
  catch {
1716
1817
  result = { kind: 'transport-uncertainty' };
1717
1818
  }
1718
- if (closed) {
1819
+ if (!live()) {
1820
+ progress.complete = false;
1719
1821
  notifyReady();
1720
1822
  return;
1721
1823
  }
@@ -1785,8 +1887,7 @@ export function composeRuntime(options) {
1785
1887
  member = signedIn;
1786
1888
  emitMember();
1787
1889
  }
1788
- await applyDiscovery(result.rooms, local);
1789
- await synchronize(result.rooms, local, progress, attempt);
1890
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
1790
1891
  }
1791
1892
  async function ensureAuthorization() {
1792
1893
  if (authorization !== undefined)
@@ -1800,20 +1901,50 @@ export function composeRuntime(options) {
1800
1901
  throw error;
1801
1902
  }
1802
1903
  }
1803
- function task(work) {
1904
+ async function admittedRead(work) {
1905
+ if (roomReads.generation !== generation) {
1906
+ roomReads = { generation, free: ROOM_READS, waiting: [] };
1907
+ }
1908
+ const reads = roomReads;
1909
+ while (reads.free === 0)
1910
+ await new Promise((admit) => reads.waiting.push(admit));
1911
+ reads.free -= 1;
1912
+ try {
1913
+ await work();
1914
+ }
1915
+ finally {
1916
+ reads.free += 1;
1917
+ reads.waiting.shift()?.();
1918
+ }
1919
+ }
1920
+ function roomTask(locator, work) {
1921
+ return task(roomLane(locator), (live) => admittedRead(() => work(live)));
1922
+ }
1923
+ function task(lane, work) {
1804
1924
  if (stopping)
1805
- return chain;
1806
- chain = chain.then(work, work).catch(() => undefined);
1807
- return chain;
1925
+ return Promise.resolve();
1926
+ const held = lanes.get(lane);
1927
+ const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
1928
+ const turn = current.generation;
1929
+ const live = () => turn === generation && continuing();
1930
+ const run = current.chain.then(() => work(live)).catch(() => undefined);
1931
+ current.chain = run;
1932
+ lanes.set(lane, current);
1933
+ working.add(run);
1934
+ void run.finally(() => working.delete(run));
1935
+ return run;
1808
1936
  }
1809
1937
  function sync(attempt) {
1810
- return task(async () => {
1811
- if (closed || stopping || discoveryPaused) {
1938
+ return task(OPENING_LANE, async (live) => {
1939
+ if (!live()) {
1812
1940
  notifyReady();
1813
1941
  return;
1814
1942
  }
1815
1943
  const progress = { complete: true, reachable: true };
1816
- underway = attempt === 'opened' ? progress : null;
1944
+ const gate = discover();
1945
+ if (attempt === 'opened')
1946
+ opened = generation;
1947
+ underway = attempt === 'opened' ? { generation, progress } : null;
1817
1948
  if (attempt === 'opened')
1818
1949
  streamed = progress;
1819
1950
  complete = false;
@@ -1822,12 +1953,12 @@ export function composeRuntime(options) {
1822
1953
  handed = false;
1823
1954
  try {
1824
1955
  const local = await openLocal();
1825
- if (closed)
1956
+ if (!live())
1826
1957
  return;
1827
1958
  await ensureAuthorization();
1828
- if (closed)
1959
+ if (!live())
1829
1960
  return;
1830
- await discoverRooms(local, progress, attempt);
1961
+ await discoverRooms(local, progress, attempt, live);
1831
1962
  }
1832
1963
  catch {
1833
1964
  progress.complete = false;
@@ -1835,17 +1966,21 @@ export function composeRuntime(options) {
1835
1966
  progress.reachable = false;
1836
1967
  }
1837
1968
  finally {
1969
+ gate.release();
1838
1970
  notifyReady();
1839
1971
  }
1972
+ if (!live())
1973
+ return;
1840
1974
  if (attempt !== 'opened') {
1841
- await finishOpening(progress);
1975
+ await finishOpening(progress, live);
1842
1976
  return;
1843
1977
  }
1844
1978
  if (handed || closed || progress.reachable)
1845
1979
  return;
1846
1980
  underway = null;
1981
+ opened = null;
1847
1982
  journal.record('connection reopen');
1848
- await rest(progress);
1983
+ await rest(progress, live);
1849
1984
  options.adapter.roomStream?.reopen();
1850
1985
  });
1851
1986
  }
@@ -1866,24 +2001,38 @@ export function composeRuntime(options) {
1866
2001
  });
1867
2002
  }
1868
2003
  function applyOpening(expired) {
1869
- return task(async () => {
1870
- const progress = underway;
2004
+ const progress = underway?.generation === generation ? underway.progress : null;
2005
+ const cycle = progress ?? { complete: true, reachable: true };
2006
+ const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
2007
+ return task(OPENING_LANE, async (live) => {
1871
2008
  underway = null;
1872
- if (closed || stopping)
2009
+ if (!live())
1873
2010
  return;
1874
2011
  if (progress === null) {
1875
- await reloadExpired(expired);
2012
+ await replacing;
2013
+ await rest(cycle, live);
1876
2014
  return;
1877
2015
  }
2016
+ await roomsCarried(expired);
2017
+ if (!live())
2018
+ return;
2019
+ if (opened === generation)
2020
+ opened = null;
2021
+ const replaced = new Set(owed);
2022
+ owed.clear();
1878
2023
  try {
1879
2024
  const local = await openLocal();
1880
2025
  const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1881
- const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1882
- const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
2026
+ const reloading = held.filter((room) => !expired.includes(room.locator) &&
2027
+ (replaced.has(room.locator) || revoked(room.locator)));
2028
+ const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
2029
+ const reloaded = [...(await reloads), ...(await replacing)];
1883
2030
  if (progress.reachable) {
1884
- for (const room of held)
1885
- if (!reloading.includes(room))
2031
+ for (const room of held) {
2032
+ if (!expired.includes(room.locator) && !reloading.includes(room)) {
1886
2033
  await confirmModels(room, local);
2034
+ }
2035
+ }
1887
2036
  }
1888
2037
  const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1889
2038
  if (failed !== undefined)
@@ -1893,21 +2042,21 @@ export function composeRuntime(options) {
1893
2042
  progress.complete = false;
1894
2043
  progress.reachable = false;
1895
2044
  }
1896
- await finishOpening(progress);
2045
+ if (live())
2046
+ await finishOpening(progress, live);
1897
2047
  });
1898
2048
  }
1899
- async function reloadExpired(expired) {
1900
- const progress = { complete: true, reachable: true };
2049
+ async function reloadNow(replaced, progress) {
1901
2050
  try {
1902
2051
  const local = await openLocal();
1903
- await Promise.allSettled([...discovered.values()]
1904
- .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1905
- .map((room) => reload(room, local, progress)));
2052
+ return await Promise.allSettled([...discovered.values()]
2053
+ .filter((room) => replicas.has(room.locator) && replaced(room))
2054
+ .map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
1906
2055
  }
1907
2056
  catch {
1908
2057
  progress.reachable = false;
2058
+ return [];
1909
2059
  }
1910
- await rest(progress);
1911
2060
  }
1912
2061
  async function confirmModels(room, local) {
1913
2062
  for (const attached of ['collection', 'typed-selection']) {
@@ -1959,9 +2108,9 @@ export function composeRuntime(options) {
1959
2108
  submitLater(locator);
1960
2109
  }
1961
2110
  }
1962
- async function finishOpening(progress) {
2111
+ async function finishOpening(progress, live) {
1963
2112
  try {
1964
- if (closed || discoveryPaused) {
2113
+ if (!live()) {
1965
2114
  progress.complete = false;
1966
2115
  return;
1967
2116
  }
@@ -1971,6 +2120,8 @@ export function composeRuntime(options) {
1971
2120
  conversations.reconnected();
1972
2121
  const reading = { complete: true, reachable: true };
1973
2122
  await conversations.catchUp(reading);
2123
+ if (progress.reachable && reading.reachable)
2124
+ await conversations.resend(reading);
1974
2125
  if (!reading.reachable)
1975
2126
  progress.reachable = false;
1976
2127
  }
@@ -1978,12 +2129,14 @@ export function composeRuntime(options) {
1978
2129
  progress.complete = false;
1979
2130
  }
1980
2131
  finally {
1981
- complete = progress.complete;
1982
- await rest(progress);
2132
+ if (live()) {
2133
+ complete = progress.complete;
2134
+ await rest(progress, live);
2135
+ }
1983
2136
  }
1984
2137
  }
1985
- async function rest(progress) {
1986
- if (closed)
2138
+ async function rest(progress, live = () => !closed) {
2139
+ if (!live())
1987
2140
  return;
1988
2141
  let lastCompleted = status.lastCompleted;
1989
2142
  let synced = complete && stale.size === 0 && !pending();
@@ -1991,6 +2144,8 @@ export function composeRuntime(options) {
1991
2144
  const completedAt = new Date().toISOString();
1992
2145
  try {
1993
2146
  await (await openLocal()).complete(completedAt);
2147
+ if (!live())
2148
+ return;
1994
2149
  recorded = true;
1995
2150
  lastCompleted = completedAt;
1996
2151
  }
@@ -2005,11 +2160,23 @@ export function composeRuntime(options) {
2005
2160
  ...attention(),
2006
2161
  });
2007
2162
  }
2163
+ function discover() {
2164
+ let release = () => { };
2165
+ const done = new Promise((resolve) => {
2166
+ release = resolve;
2167
+ });
2168
+ discovering = { generation, done, release };
2169
+ return discovering;
2170
+ }
2008
2171
  function applyRoom(locator, roomVersion) {
2009
2172
  announced.set(locator, roomVersion);
2010
- return task(async () => {
2011
- await answering.get(locator);
2012
- if (closed || stopping || discoveryPaused || authorization === undefined)
2173
+ return task(roomLane(locator), async (live) => {
2174
+ if (discovering?.generation === generation)
2175
+ await discovering.done;
2176
+ const saving = answering.get(locator);
2177
+ if (saving?.generation === generation)
2178
+ await saving.answered;
2179
+ if (!live() || authorization === undefined)
2013
2180
  return;
2014
2181
  const room = discovered.get(locator);
2015
2182
  const held = replicas.get(locator);
@@ -2020,25 +2187,41 @@ export function composeRuntime(options) {
2020
2187
  options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
2021
2188
  return;
2022
2189
  }
2023
- const during = underway;
2190
+ const during = underway?.generation === generation ? underway.progress : null;
2024
2191
  const progress = during ?? { complete: true, reachable: true };
2025
2192
  if (during === null)
2026
2193
  moveTo(transferring(status));
2027
2194
  try {
2028
2195
  const local = await openLocal();
2029
- const withheld = await advance(room, local, authorization);
2030
- if (withheld !== null)
2031
- await withhold(withheld, room, local, progress);
2196
+ await admittedRead(async () => {
2197
+ if (!live() || authorization === undefined)
2198
+ return;
2199
+ const withheld = await advance(room, local, authorization, live);
2200
+ if (withheld !== null)
2201
+ await withhold(withheld, room, local, progress);
2202
+ });
2032
2203
  }
2033
2204
  catch {
2034
2205
  stale.add(locator);
2035
2206
  }
2036
2207
  finally {
2037
2208
  if (during === null)
2038
- await rest(progress);
2209
+ await rest(progress, live);
2039
2210
  }
2040
2211
  });
2041
2212
  }
2213
+ function roomsCarried(replacing) {
2214
+ const replaced = new Set(replacing.map(roomLane));
2215
+ return Promise.all([...lanes]
2216
+ .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
2217
+ .map(([, held]) => held.chain));
2218
+ }
2219
+ const receipts = {
2220
+ heads: async () => (await openLocal()).receivedHeads(),
2221
+ parts: async (key, count) => (await openLocal()).receivedParts(key, count),
2222
+ keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
2223
+ drop: async (keys) => (await openLocal()).dropReceived(keys),
2224
+ };
2042
2225
  function observeStream() {
2043
2226
  const stream = options.adapter.roomStream;
2044
2227
  if (stream === undefined || withdrawStream !== undefined)
@@ -2048,6 +2231,7 @@ export function composeRuntime(options) {
2048
2231
  return;
2049
2232
  if (event.kind === 'opened') {
2050
2233
  socketOpen = true;
2234
+ generation += 1;
2051
2235
  void sync('opened');
2052
2236
  }
2053
2237
  else if (event.kind === 'closed')
@@ -2056,7 +2240,7 @@ export function composeRuntime(options) {
2056
2240
  void applyOpening(event.expired);
2057
2241
  else
2058
2242
  void applyRoom(event.locator, event.roomVersion);
2059
- });
2243
+ }, receipts);
2060
2244
  }
2061
2245
  function writesHeld() {
2062
2246
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -2094,14 +2278,15 @@ export function composeRuntime(options) {
2094
2278
  });
2095
2279
  }
2096
2280
  function scheduleCurrency() {
2097
- if (currencyQueued || closed || stopping)
2281
+ if (currencyQueued === generation || closed || stopping)
2098
2282
  return;
2099
- currencyQueued = true;
2100
- void task(runCurrency);
2283
+ currencyQueued = generation;
2284
+ void task(CONVERSATION_LANE, runCurrency);
2101
2285
  }
2102
- async function runCurrency() {
2103
- currencyQueued = false;
2104
- if (closed || stopping || discoveryPaused)
2286
+ async function runCurrency(live) {
2287
+ if (currencyQueued === generation)
2288
+ currencyQueued = null;
2289
+ if (!live())
2105
2290
  return;
2106
2291
  await conversations.reconcile();
2107
2292
  if (conversations.current()) {
@@ -2116,7 +2301,7 @@ export function composeRuntime(options) {
2116
2301
  catch {
2117
2302
  progress.reachable = false;
2118
2303
  }
2119
- await rest(progress);
2304
+ await rest(progress, live);
2120
2305
  }
2121
2306
  function close() {
2122
2307
  if (closed)
@@ -2147,7 +2332,7 @@ export function composeRuntime(options) {
2147
2332
  async function stop() {
2148
2333
  stopping = true;
2149
2334
  stopTimer();
2150
- await chain.catch(() => undefined);
2335
+ await Promise.all(working);
2151
2336
  await Promise.all([...submitting.values()].map((run) => run.done));
2152
2337
  await mutations;
2153
2338
  close();
@@ -2399,14 +2584,16 @@ export function composeRuntime(options) {
2399
2584
  if (refusal === null || !refusal.reauthorizationOffered)
2400
2585
  return;
2401
2586
  journal.record('connection reauthorize');
2402
- await chain.catch(() => undefined);
2587
+ await Promise.all([...lanes.values()]
2588
+ .filter((held) => held.generation === generation)
2589
+ .map((held) => held.chain));
2403
2590
  await mutations;
2404
2591
  authorization = undefined;
2405
2592
  discoveryPaused = false;
2406
2593
  if (options.adapter.roomStream === undefined)
2407
2594
  await sync('unstreamed');
2408
2595
  else
2409
- await task(ensureAuthorization);
2596
+ await task(OPENING_LANE, ensureAuthorization);
2410
2597
  },
2411
2598
  };
2412
2599
  const reachable = {
@@ -2418,5 +2605,5 @@ export function composeRuntime(options) {
2418
2605
  watchPlan,
2419
2606
  search: searched,
2420
2607
  };
2421
- return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2608
+ return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
2422
2609
  }