@agentshouse/mdruntime 0.1.0-cc7bd7a53214e → 0.1.0-cd3486bd400ed

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 (127) hide show
  1. package/AGENTS.md +106 -31
  2. package/README.md +11 -10
  3. package/dist/adapter.d.ts +65 -28
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/adapter.js +1 -9
  6. package/dist/attached-model.d.ts +11 -0
  7. package/dist/attached-model.d.ts.map +1 -0
  8. package/dist/attached-model.js +47 -0
  9. package/dist/attempt.d.ts +16 -0
  10. package/dist/attempt.d.ts.map +1 -0
  11. package/dist/attempt.js +57 -0
  12. package/dist/bootstrap.d.ts +20 -6
  13. package/dist/bootstrap.d.ts.map +1 -1
  14. package/dist/bootstrap.js +89 -35
  15. package/dist/change-set.d.ts.map +1 -1
  16. package/dist/change-set.js +13 -7
  17. package/dist/change.d.ts +4 -0
  18. package/dist/change.d.ts.map +1 -1
  19. package/dist/chat/acts.d.ts +34 -0
  20. package/dist/chat/acts.d.ts.map +1 -0
  21. package/dist/chat/acts.js +36 -0
  22. package/dist/chat/messages.d.ts +21 -0
  23. package/dist/chat/messages.d.ts.map +1 -0
  24. package/dist/chat/messages.js +151 -0
  25. package/dist/chat/overlay.d.ts +31 -0
  26. package/dist/chat/overlay.d.ts.map +1 -0
  27. package/dist/chat/overlay.js +97 -0
  28. package/dist/chat/parts.d.ts +57 -0
  29. package/dist/chat/parts.d.ts.map +1 -0
  30. package/dist/chat/parts.js +119 -0
  31. package/dist/chat/process.d.ts +20 -0
  32. package/dist/chat/process.d.ts.map +1 -0
  33. package/dist/chat/process.js +32 -0
  34. package/dist/chat/provider.d.ts +57 -0
  35. package/dist/chat/provider.d.ts.map +1 -0
  36. package/dist/chat/provider.js +637 -0
  37. package/dist/chat/questions.d.ts +31 -0
  38. package/dist/chat/questions.d.ts.map +1 -0
  39. package/dist/chat/questions.js +111 -0
  40. package/dist/chat/read.d.ts +5 -0
  41. package/dist/chat/read.d.ts.map +1 -0
  42. package/dist/chat/read.js +13 -0
  43. package/dist/chat.d.ts +7 -0
  44. package/dist/chat.d.ts.map +1 -0
  45. package/dist/chat.js +2 -0
  46. package/dist/client.d.ts +11 -7
  47. package/dist/client.d.ts.map +1 -1
  48. package/dist/client.js +58 -25
  49. package/dist/collection.d.ts +6 -11
  50. package/dist/collection.d.ts.map +1 -1
  51. package/dist/collection.js +56 -19
  52. package/dist/connection.d.ts +18 -13
  53. package/dist/connection.d.ts.map +1 -1
  54. package/dist/connection.js +679 -568
  55. package/dist/conversation.d.ts +27 -8
  56. package/dist/conversation.d.ts.map +1 -1
  57. package/dist/conversation.js +210 -81
  58. package/dist/expectation.d.ts +21 -0
  59. package/dist/expectation.d.ts.map +1 -0
  60. package/dist/expectation.js +70 -0
  61. package/dist/failure.d.ts +13 -0
  62. package/dist/failure.d.ts.map +1 -0
  63. package/dist/failure.js +19 -0
  64. package/dist/host.d.ts +1 -0
  65. package/dist/host.d.ts.map +1 -1
  66. package/dist/host.js +8 -19
  67. package/dist/identity.d.ts +2 -0
  68. package/dist/identity.d.ts.map +1 -1
  69. package/dist/identity.js +10 -1
  70. package/dist/index.d.ts +7 -4
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +2 -0
  73. package/dist/memory.d.ts +12 -5
  74. package/dist/memory.d.ts.map +1 -1
  75. package/dist/memory.js +108 -36
  76. package/dist/model.d.ts +13 -12
  77. package/dist/model.d.ts.map +1 -1
  78. package/dist/model.js +6 -3
  79. package/dist/open.d.ts +2 -0
  80. package/dist/open.d.ts.map +1 -0
  81. package/dist/open.js +1 -0
  82. package/dist/outcome.d.ts +22 -3
  83. package/dist/outcome.d.ts.map +1 -1
  84. package/dist/outcome.js +35 -1
  85. package/dist/overlay.d.ts +1 -1
  86. package/dist/overlay.d.ts.map +1 -1
  87. package/dist/overlay.js +19 -15
  88. package/dist/paths.d.ts +0 -1
  89. package/dist/paths.d.ts.map +1 -1
  90. package/dist/paths.js +0 -16
  91. package/dist/query.d.ts +3 -1
  92. package/dist/query.d.ts.map +1 -1
  93. package/dist/query.js +74 -33
  94. package/dist/reach.d.ts.map +1 -1
  95. package/dist/reach.js +29 -35
  96. package/dist/react.d.ts +41 -0
  97. package/dist/react.d.ts.map +1 -0
  98. package/dist/react.js +64 -0
  99. package/dist/read-side.d.ts.map +1 -1
  100. package/dist/read-side.js +102 -46
  101. package/dist/refresh.d.ts +12 -1
  102. package/dist/refresh.d.ts.map +1 -1
  103. package/dist/refresh.js +35 -22
  104. package/dist/replica.d.ts +6 -6
  105. package/dist/replica.d.ts.map +1 -1
  106. package/dist/replica.js +53 -39
  107. package/dist/room-draft.d.ts +8 -8
  108. package/dist/room-draft.d.ts.map +1 -1
  109. package/dist/room-draft.js +13 -22
  110. package/dist/rows.d.ts +4 -1
  111. package/dist/rows.d.ts.map +1 -1
  112. package/dist/rows.js +82 -39
  113. package/dist/save.d.ts +4 -3
  114. package/dist/save.d.ts.map +1 -1
  115. package/dist/save.js +127 -68
  116. package/dist/search.d.ts +3 -2
  117. package/dist/search.d.ts.map +1 -1
  118. package/dist/status.d.ts +2 -2
  119. package/dist/status.d.ts.map +1 -1
  120. package/dist/status.js +0 -8
  121. package/dist/store.d.ts +26 -2
  122. package/dist/store.d.ts.map +1 -1
  123. package/dist/store.js +206 -90
  124. package/dist/wire.d.ts +6 -15
  125. package/dist/wire.d.ts.map +1 -1
  126. package/dist/wire.js +14 -17
  127. package/package.json +14 -2
@@ -1,57 +1,43 @@
1
- import { verify } from '@agentshouse/mdmodel';
1
+ import { attachedModel } from './attached-model.js';
2
2
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
3
3
  import { observing } from './observation.js';
4
4
  import { filterOf, identityOf, planOf, } from './query.js';
5
5
  import { createReadSide } from './read-side.js';
6
+ import { markdownPath, roomOfKey } from './rows.js';
6
7
  import { createSearchIndex, searchPlanOf, } from './search.js';
7
- import { ROOM_CONTRACT_VERSION, VERSION_1_CAPABILITIES, } from './adapter.js';
8
8
  import { bootstrapRoom } from './bootstrap.js';
9
9
  import { changeSet, mergeChangeSets } from './change-set.js';
10
10
  import { createConversationStore, } from './conversation.js';
11
- import { MD_MODEL, parseCollection, validRoot } from './collection.js';
11
+ import { validRoot } from './collection.js';
12
+ import { runtimeError } from './failure.js';
12
13
  import { createLog } from './log.js';
14
+ import { calledResult, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
13
15
  import { refreshRoom } from './refresh.js';
14
16
  import { buildBatch } from './save.js';
15
- import { applyReturned, compositionOf, landed, readConfirmed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
16
- import { changedParts, changesOf, held, readParts, returnedInto, underneath, } from './room-draft.js';
17
- import { settled, same, saving, STARTING, transferring, } from './status.js';
17
+ import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
18
+ import { changedParts, changesOf, held, readParts, returnedBy, returnedInto, staleWrite, underneath, } from './room-draft.js';
19
+ import { same, saving, STARTING, transferring, } from './status.js';
18
20
  import { detectStorageLoss, nextGeneration } from './durability.js';
19
21
  import { roomDraftRevision } from './identity.js';
20
22
  import { beneath, normalize } from './paths.js';
21
23
  import { openRuntimeStore, } from './store.js';
22
24
  import { unconfirmed } from './transfer.js';
23
25
  import { governs, holds, narrowed, planReach, } from './reach.js';
24
- // Discovery repeats each minute so a Room the stream stopped serving stays unrefreshed no longer.
25
- const DEFAULT_DISCOVERY_INTERVAL_MS = 60_000;
26
- const EMPTY_SCHEMA = { verdict: 'accept', model: { types: {} } };
27
- const knownCapabilities = new Set(VERSION_1_CAPABILITIES);
28
26
  function within(root, path) {
29
27
  return root === '' || beneath(root, path);
30
28
  }
31
- function declaredIdentity(model) {
32
- if (model === undefined)
33
- return false;
34
- if (model === null)
35
- return true;
36
- return (typeof model.identity === 'string' &&
37
- model.identity.length > 0 &&
38
- typeof model.version === 'string' &&
39
- model.version.length > 0);
40
- }
41
29
  function declaredSelection(room) {
42
30
  const selection = room.typedSelection;
43
31
  if (selection === undefined)
44
32
  return true;
45
33
  if (!Array.isArray(selection.roots) || selection.roots.length === 0)
46
34
  return false;
47
- if (!declaredIdentity(selection.attachedModel))
48
- return false;
49
35
  return selection.roots.every((root, index) => validRoot(root) &&
50
36
  root !== '' &&
51
37
  selection.roots.every((other, at) => at === index || !within(other, root)));
52
38
  }
53
39
  export function composeRuntime(options) {
54
- const period = options.discoveryIntervalMs ?? DEFAULT_DISCOVERY_INTERVAL_MS;
40
+ const localSaves = options.adapter.localSaves === true;
55
41
  let authorization = options.authorization;
56
42
  let opening;
57
43
  let store;
@@ -59,20 +45,14 @@ export function composeRuntime(options) {
59
45
  const pendings = new Map();
60
46
  const roomDrafts = new Map();
61
47
  const presentations = new Map();
62
- const wanted = new Map();
63
48
  let limits = [];
64
49
  let capabilities = [];
65
- function rewriting() {
66
- return capabilities.some((capability) => capability.name === 'move-reference-rewrites')
67
- ? 'provider'
68
- : 'runtime';
69
- }
70
50
  const rooms = new Map();
51
+ const counted = new Map();
71
52
  const unreadable = new Set();
72
53
  const models = new Map();
73
54
  const liveModels = new Map();
74
55
  const invalidModels = new Set();
75
- const modelEpoch = new Map();
76
56
  const discovered = new Map();
77
57
  let roomsListed = false;
78
58
  const scopedRefusals = new Map();
@@ -101,16 +81,21 @@ export function composeRuntime(options) {
101
81
  let status = STARTING;
102
82
  let shown = STARTING;
103
83
  let discoveryPaused = false;
104
- let timer;
84
+ let resetTimer;
105
85
  let closed = false;
106
86
  let stopping = false;
107
- let withdrawNotifications;
108
- let handed;
109
- const notices = new Map();
87
+ let withdrawStream;
88
+ const announced = new Map();
89
+ let underway = null;
90
+ let streamed = null;
91
+ let handed = false;
92
+ const submitting = new Map();
93
+ const answering = new Map();
94
+ let complete = false;
95
+ let recorded = false;
96
+ const stale = new Set();
110
97
  let chain = Promise.resolve();
111
98
  let mutations = Promise.resolve();
112
- let inflight = false;
113
- let queued = false;
114
99
  const roomListeners = new Set();
115
100
  const refusalListeners = new Set();
116
101
  const storageLossListeners = new Set();
@@ -121,13 +106,19 @@ export function composeRuntime(options) {
121
106
  const readSide = createReadSide();
122
107
  let withdrawConversations;
123
108
  let currencyQueued = false;
109
+ let socketOpen = false;
124
110
  let serviceMode = 'open';
125
111
  let haltedRefusal = false;
126
112
  let openSignals = 0;
127
113
  const serviceModeListeners = new Set();
114
+ let member = null;
115
+ const memberListeners = new Set();
116
+ const materialized = new Set();
117
+ let everyRoom = false;
118
+ const transfers = new Map();
128
119
  const conversations = createConversationStore({
129
- cards: declaredCards,
130
- documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
120
+ agents: declaredAgents,
121
+ documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
131
122
  call: (name, input) => invokeAction(name, input),
132
123
  durably,
133
124
  stream: () => options.adapter.conversations,
@@ -203,7 +194,7 @@ export function composeRuntime(options) {
203
194
  }
204
195
  }
205
196
  function pending() {
206
- return [...replicas.values()].some((replica) => replica.queue.length > 0);
197
+ return !localSaves && [...replicas.values()].some((replica) => replica.queue.length > 0);
207
198
  }
208
199
  function attention() {
209
200
  const attentionRequired = [];
@@ -232,8 +223,24 @@ export function composeRuntime(options) {
232
223
  }
233
224
  return { attentionRequired, reset };
234
225
  }
226
+ function documentsIn(locator) {
227
+ const replica = replicas.get(locator);
228
+ if (replica === undefined)
229
+ return 0;
230
+ const roots = replica.confirmed.record.typedSelection?.roots ?? [];
231
+ let count = 0;
232
+ for (const path of Object.keys(replica.visible.documents)) {
233
+ if (markdownPath(path) && !roots.some((root) => beneath(root, path)))
234
+ count += 1;
235
+ }
236
+ return count;
237
+ }
235
238
  function listed() {
236
- return structuredClone([...rooms.values()]);
239
+ return structuredClone([...rooms.values()].map((room) => {
240
+ const documents = documentsIn(room.locator);
241
+ counted.set(room.locator, documents);
242
+ return { ...room, documents };
243
+ }));
237
244
  }
238
245
  function roomFacts(reach = null) {
239
246
  const held = [...rooms.values()]
@@ -265,6 +272,10 @@ export function composeRuntime(options) {
265
272
  for (const listener of storageLossListeners)
266
273
  listener(storageLoss);
267
274
  }
275
+ function emitMember() {
276
+ for (const listener of memberListeners)
277
+ listener(member);
278
+ }
268
279
  function moveTo(next) {
269
280
  if (closed)
270
281
  return;
@@ -284,6 +295,8 @@ export function composeRuntime(options) {
284
295
  listener(shown);
285
296
  }
286
297
  function entryOf(batch) {
298
+ if (localSaves)
299
+ return Object.freeze({ batch, state: 'local' });
287
300
  const state = pendings.get(batch.id);
288
301
  if (state?.accepted !== undefined) {
289
302
  return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
@@ -304,12 +317,47 @@ export function composeRuntime(options) {
304
317
  }
305
318
  }
306
319
  function applyChangeSet(set) {
320
+ const held = everyRoom
321
+ ? set
322
+ : { ...set, documents: set.documents.filter(({ locator }) => materialized.has(locator)) };
323
+ readSide.apply(held);
324
+ searchIndex.changed(held);
325
+ }
326
+ function materialize(locators) {
327
+ if (everyRoom)
328
+ return;
329
+ if (locators === null)
330
+ everyRoom = true;
331
+ const sets = [];
332
+ for (const locator of locators ?? [...replicas.keys()]) {
333
+ if (materialized.has(locator))
334
+ continue;
335
+ materialized.add(locator);
336
+ const replica = replicas.get(locator);
337
+ if (replica !== undefined)
338
+ sets.push(changeSet(locator, null, replica));
339
+ }
340
+ if (sets.length === 0)
341
+ return;
342
+ const set = mergeChangeSets(sets);
307
343
  readSide.apply(set);
308
344
  searchIndex.changed(set);
309
345
  }
346
+ function reachedRooms(plan) {
347
+ if (plan.rooms !== null)
348
+ return plan.rooms;
349
+ if (plan.reach === null)
350
+ return null;
351
+ return [...plan.reach.rooms, ...plan.reach.documents.map(roomOfKey)];
352
+ }
353
+ function rowsQuery(plan) {
354
+ materialize(reachedRooms(plan));
355
+ return readSide.query(plan);
356
+ }
310
357
  function publishChangeSet(set) {
311
358
  applyChangeSet(set);
312
- if (set.rooms.length > 0)
359
+ if (set.rooms.length > 0 ||
360
+ [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
313
361
  emitRooms();
314
362
  for (const locator of set.queues)
315
363
  emitQueue(locator);
@@ -320,23 +368,32 @@ export function composeRuntime(options) {
320
368
  for (const app of set.presentations)
321
369
  emitPresentation(app);
322
370
  conversations.changed(set);
323
- const cards = declaredCards();
324
- if (cards !== undefined && set.documents.some((change) => change.locator === cards.locator)) {
325
- scheduleCurrency();
326
- }
327
371
  }
328
372
  function stopTimer() {
329
- if (timer === undefined)
373
+ if (resetTimer === undefined)
330
374
  return;
331
- clearInterval(timer);
332
- timer = undefined;
375
+ clearTimeout(resetTimer);
376
+ resetTimer = undefined;
333
377
  }
334
- function startTimer() {
335
- if (timer !== undefined)
378
+ function armTimer() {
379
+ stopTimer();
380
+ if (closed || stopping)
336
381
  return;
337
- timer = setInterval(() => {
338
- void enqueue();
339
- }, period);
382
+ let earliest = null;
383
+ for (const { submission } of pendings.values()) {
384
+ if (submission.state !== 'reset')
385
+ continue;
386
+ const at = Date.parse(submission.reset);
387
+ if (earliest === null || at < earliest)
388
+ earliest = at;
389
+ }
390
+ if (earliest === null)
391
+ return;
392
+ // A Save House held back goes again only once the reset its answer named has passed.
393
+ resetTimer = setTimeout(() => {
394
+ resetTimer = undefined;
395
+ submitAll();
396
+ }, Math.max(0, earliest - Date.now()));
340
397
  }
341
398
  async function refuse(reason, provider = null) {
342
399
  stopTimer();
@@ -362,7 +419,6 @@ export function composeRuntime(options) {
362
419
  }
363
420
  for (const key of keys) {
364
421
  invalidModels.add(key);
365
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
366
422
  models.set(key, revoked);
367
423
  liveModels.delete(key);
368
424
  }
@@ -375,10 +431,7 @@ export function composeRuntime(options) {
375
431
  model: revoked,
376
432
  selectionModel: revoked,
377
433
  };
378
- const read = readConfirmed(record, before.confirmed);
379
- replicas.set(locator, read.kind === 'staged'
380
- ? replicaOf(read.confirmed, before.queue, rewriting(), before)
381
- : { ...before, confirmed: { ...before.confirmed, record } });
434
+ replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
382
435
  }
383
436
  rooms.set(locator, {
384
437
  ...room,
@@ -395,7 +448,8 @@ export function composeRuntime(options) {
395
448
  }
396
449
  finally {
397
450
  refusal = {
398
- reason,
451
+ code: reason,
452
+ retryable: false,
399
453
  reauthorizationOffered: reason === 'revoked-authorization',
400
454
  refusal: provider,
401
455
  };
@@ -408,15 +462,14 @@ export function composeRuntime(options) {
408
462
  rooms.set(room.locator, {
409
463
  name: room.name,
410
464
  locator: room.locator,
465
+ handle: room.handle,
411
466
  operations: room.operations,
412
467
  replica: room.replica,
413
- attachedModel: room.attachedModel,
414
468
  model: modelTypes(modelOf(room.locator)),
415
469
  typedSelection: selection === null
416
470
  ? null
417
471
  : {
418
472
  roots: selection.roots,
419
- attachedModel: selection.attachedModel,
420
473
  model: modelTypes(modelOf(room.locator, 'typed-selection')),
421
474
  },
422
475
  providerOwnedPaths: room.providerOwnedPaths,
@@ -433,9 +486,6 @@ export function composeRuntime(options) {
433
486
  discovered.clear();
434
487
  for (const room of listed)
435
488
  discovered.set(room.locator, room);
436
- for (const locator of wanted.keys())
437
- if (!listedLocators.has(locator))
438
- wanted.delete(locator);
439
489
  await dropUnlisted(listedLocators, local);
440
490
  for (const room of listed) {
441
491
  const held = replicas.get(room.locator)?.confirmed.record;
@@ -449,7 +499,7 @@ export function composeRuntime(options) {
449
499
  emitRooms();
450
500
  }
451
501
  function unlistedHeld(listed) {
452
- return [...new Set([...rooms.keys(), ...roomDrafts.keys()])].filter((locator) => !listed.has(locator));
502
+ return [...new Set([...rooms.keys(), ...roomDrafts.keys(), ...transfers.keys()])].filter((locator) => !listed.has(locator));
453
503
  }
454
504
  async function dropUnlisted(listed, local) {
455
505
  if (unlistedHeld(listed).length === 0)
@@ -468,7 +518,9 @@ export function composeRuntime(options) {
468
518
  replicas.delete(locator);
469
519
  rooms.delete(locator);
470
520
  roomDrafts.delete(locator);
521
+ transfers.delete(locator);
471
522
  unreadable.delete(locator);
523
+ stale.delete(locator);
472
524
  scopedRefusals.delete(locator);
473
525
  sourceRefusals.delete(locator);
474
526
  declarationRefusals.delete(locator);
@@ -477,7 +529,6 @@ export function composeRuntime(options) {
477
529
  models.delete(key);
478
530
  liveModels.delete(key);
479
531
  invalidModels.delete(key);
480
- modelEpoch.delete(key);
481
532
  }
482
533
  journal.record(`room unlisted ${locator}`);
483
534
  }
@@ -490,7 +541,6 @@ export function composeRuntime(options) {
490
541
  if (prior !== undefined &&
491
542
  prior.status !== 'unavailable' &&
492
543
  (persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
493
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
494
544
  invalidModels.add(key);
495
545
  }
496
546
  const model = persisted.status === 'unavailable' ||
@@ -505,7 +555,7 @@ export function composeRuntime(options) {
505
555
  return model;
506
556
  }
507
557
  async function hydrate(opened) {
508
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
558
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
509
559
  opened.all(),
510
560
  opened.queues(),
511
561
  opened.roomDrafts(),
@@ -515,9 +565,15 @@ export function composeRuntime(options) {
515
565
  opened.lastCompleted(),
516
566
  opened.providerLimits(),
517
567
  opened.capabilities(),
568
+ opened.member(),
569
+ opened.transfers(),
518
570
  ]);
571
+ transfers.clear();
572
+ for (const record of staged)
573
+ transfers.set(record.locator, record);
519
574
  limits = declared;
520
575
  capabilities = named;
576
+ member = known;
521
577
  roomDrafts.clear();
522
578
  for (const record of kept)
523
579
  rememberRoomDraft(record);
@@ -537,15 +593,8 @@ export function composeRuntime(options) {
537
593
  const model = restored(modelKey(record.locator, 'collection'), record.model);
538
594
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
539
595
  const prior = previousReplicas.get(record.locator);
540
- const retyped = { ...record, model, selectionModel };
541
- const read = readConfirmed(retyped, prior?.confirmed);
542
- const confirmed = read.kind === 'staged'
543
- ? read.confirmed
544
- : {
545
- record: retyped,
546
- collection: parseCollection(retyped.documents, compositionOf(retyped)),
547
- };
548
- replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], rewriting(), prior));
596
+ const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
597
+ replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
549
598
  const existing = rooms.get(record.locator);
550
599
  if (existing === undefined)
551
600
  list(record, false);
@@ -556,21 +605,15 @@ export function composeRuntime(options) {
556
605
  attachments: record.attachments,
557
606
  operations: record.operations,
558
607
  replica: record.replica,
559
- attachedModel: record.attachedModel,
560
608
  model: modelTypes(model),
561
609
  typedSelection: record.typedSelection === null
562
610
  ? null
563
611
  : {
564
612
  roots: record.typedSelection.roots,
565
- attachedModel: record.typedSelection.attachedModel,
566
613
  model: modelTypes(selectionModel),
567
614
  },
568
615
  });
569
616
  }
570
- if (read.kind === 'refused') {
571
- refusedSource(record, read);
572
- declarationRefusals.add(record.locator);
573
- }
574
617
  }
575
618
  return {
576
619
  lastCompleted,
@@ -583,6 +626,8 @@ export function composeRuntime(options) {
583
626
  applyChangeSet(read);
584
627
  if (replicas.size > 0)
585
628
  emitRooms();
629
+ if (member !== null)
630
+ emitMember();
586
631
  for (const locator of locators)
587
632
  emitQueue(locator);
588
633
  publishRoomDrafts(false);
@@ -630,10 +675,14 @@ export function composeRuntime(options) {
630
675
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
631
676
  }
632
677
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
633
- const withdrawn = withdrawals(confirmed, queue, withdrawable, rewriting());
678
+ const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
634
679
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
680
+ const unqueued = withdrawn.map((batch) => ({
681
+ batch,
682
+ returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
683
+ }));
635
684
  const returning = [
636
- ...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
685
+ ...[...unqueued].reverse(),
637
686
  ...(settling.leaving === undefined ? [] : [settling.leaving]),
638
687
  ];
639
688
  const written = new Map();
@@ -654,6 +703,7 @@ export function composeRuntime(options) {
654
703
  await local.write({
655
704
  next: confirmed.record,
656
705
  previous: before?.confirmed.record ?? null,
706
+ ...(settling.stored === undefined ? {} : { stored: settling.stored }),
657
707
  queue: kept,
658
708
  previousQueue: before?.queue ?? [],
659
709
  roomDrafts: [...written.values()],
@@ -664,44 +714,80 @@ export function composeRuntime(options) {
664
714
  }
665
715
  for (const record of written.values())
666
716
  rememberRoomDraft(record);
667
- for (const batch of withdrawn)
668
- journal.record(`outcome ${locator} ${batch.id} withdrawn`);
669
- replicas.set(locator, replicaOf(confirmed, kept, rewriting(), before));
717
+ for (const { batch, returned } of unqueued) {
718
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
719
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
720
+ }
721
+ replicas.set(locator, replicaOf(confirmed, kept, before));
670
722
  return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
671
723
  room: settling.room,
672
724
  queue: kept.length !== (before?.queue.length ?? 0),
673
725
  drafts: [...written.keys()],
674
726
  });
675
727
  }
676
- function publish(local, room, confirmed) {
728
+ function adoptModels(locator, record) {
729
+ for (const [attached, model] of [
730
+ ['collection', record.model],
731
+ ['typed-selection', record.selectionModel],
732
+ ]) {
733
+ const key = modelKey(locator, attached);
734
+ models.set(key, model);
735
+ invalidModels.delete(key);
736
+ if (model.status === 'current')
737
+ liveModels.set(key, JSON.stringify(model.snapshot));
738
+ else if (model.status === 'unavailable')
739
+ liveModels.delete(key);
740
+ }
741
+ }
742
+ function remodel(locator, answers, held) {
743
+ const taken = (answer, previous, attached) => {
744
+ if (answer === undefined)
745
+ return previous;
746
+ const { state, refused } = attachedModel(answer, previous);
747
+ if (refused !== null) {
748
+ const where = refused.location === undefined ? '' : ` ${refused.location}`;
749
+ journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
750
+ }
751
+ models.set(modelKey(locator, attached), state);
752
+ return state;
753
+ };
754
+ return {
755
+ collection: taken(answers?.collection, held.collection, 'collection'),
756
+ selection: taken(answers?.selection, held.selection, 'typed-selection'),
757
+ };
758
+ }
759
+ function publish(local, room, confirmed, landing = {}) {
760
+ const { base, stored } = landing;
677
761
  return exclusive(async () => {
678
762
  if (!continuing())
679
- return;
763
+ return true;
680
764
  const prior = replicas.get(room.locator);
681
- const retained = readConfirmed({
682
- ...confirmed.record,
683
- model: modelOf(room.locator),
684
- selectionModel: modelOf(room.locator, 'typed-selection'),
685
- }, prior?.confirmed);
686
- if (retained.kind !== 'staged')
687
- throw new Error(`Cannot retype staged Room ${room.locator}`);
765
+ if (base !== undefined &&
766
+ (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
767
+ prior.confirmed.record.revisions !== base.record.revisions)) {
768
+ return false;
769
+ }
770
+ adoptModels(room.locator, confirmed.record);
688
771
  const queue = prior?.queue ?? [];
689
- const roomVersion = retained.confirmed.record.roomVersion;
772
+ const roomVersion = confirmed.record.roomVersion;
690
773
  const accepted = queue.filter((batch) => {
691
774
  const accepted = pendings.get(batch.id)?.accepted;
692
775
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
693
776
  });
694
- const set = await settle(local, room.locator, prior, retained.confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
695
- const target = wanted.get(room.locator);
696
- if (target !== undefined && options.adapter.reached(room.locator, roomVersion, target)) {
697
- wanted.delete(room.locator);
698
- }
699
- unreadable.delete(room.locator);
777
+ const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
778
+ if (stored !== undefined)
779
+ transfers.delete(room.locator);
780
+ for (const batch of accepted)
781
+ pendings.delete(batch.id);
782
+ stale.delete(room.locator);
783
+ if (unreadable.delete(room.locator))
784
+ scopedRefusals.delete(room.locator);
700
785
  sourceRefusals.delete(room.locator);
701
786
  declarationRefusals.delete(room.locator);
702
787
  list(room, true);
703
788
  publishChangeSet(set);
704
789
  moveTo({ ...status, ...attention() });
790
+ return true;
705
791
  });
706
792
  }
707
793
  async function saveRoomDrafts(locator, named, writtenBy) {
@@ -709,28 +795,29 @@ export function composeRuntime(options) {
709
795
  const replica = replicas.get(locator);
710
796
  const room = rooms.get(locator);
711
797
  if (replica === undefined || room === undefined) {
712
- throw new Error(`No Local Replica for ${locator}`);
798
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
713
799
  }
714
800
  const saved = [];
715
801
  for (const { path, revision } of named) {
716
802
  const record = roomDrafts.get(locator)?.get(path);
717
803
  if (record === undefined || record.revision !== revision) {
718
- return { outcome: 'refused', rule: 'stale', file: path };
804
+ return { outcome: 'refused', code: 'stale', retryable: false, file: path };
719
805
  }
720
806
  if (!saved.some((draft) => draft.path === path))
721
807
  saved.push({ path, parts: record.parts });
722
808
  }
723
809
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
724
810
  if (changed.length > 0)
725
- return { outcome: 'refused', rule: 'changed-underneath', parts: changed };
811
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
726
812
  const parts = saved.map(({ path, parts }) => changesOf(path, parts));
727
813
  const built = buildBatch({
814
+ local: localSaves,
728
815
  baseRoomVersion: replica.confirmed.record.roomVersion,
729
816
  composition: compositionFor(replica.confirmed.record),
730
817
  operations: room.operations,
731
818
  providerOwnedPaths: room.providerOwnedPaths,
732
819
  providerLimits: limits,
733
- rewriting: rewriting(),
820
+ handle: room.handle,
734
821
  visible: replica.visible,
735
822
  changes: parts.flatMap(({ changes }) => changes),
736
823
  bases: parts.flatMap(({ bases }) => bases),
@@ -751,18 +838,20 @@ export function composeRuntime(options) {
751
838
  });
752
839
  }
753
840
  catch {
754
- return { outcome: 'refused', rule: 'durable-write' };
841
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
755
842
  }
756
843
  pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
844
+ if (!localSaves)
845
+ recorded = false;
757
846
  for (const path of consumed)
758
847
  forgetRoomDraft(locator, path);
759
- replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting(), replica));
848
+ replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
760
849
  publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
761
850
  queue: true,
762
851
  drafts: consumed,
763
852
  }));
764
- moveTo({ ...saving(status), ...attention() });
765
- void enqueue();
853
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
854
+ submitLater(locator);
766
855
  return { outcome: 'saved', identifier: built.batch.id };
767
856
  }
768
857
  async function mark(local, locator, identifier, submission, lost) {
@@ -789,16 +878,15 @@ export function composeRuntime(options) {
789
878
  }
790
879
  async function readHistory(locator, path, query = {}) {
791
880
  if (closed || refusal !== null)
792
- return { kind: 'transport-uncertainty' };
881
+ return UNCERTAIN;
793
882
  try {
794
883
  await ensureAuthorization();
795
884
  }
796
885
  catch {
797
- return { kind: 'transport-uncertainty' };
798
- }
799
- if (closed || authorization === undefined || refusal !== null) {
800
- return { kind: 'transport-uncertainty' };
886
+ return UNCERTAIN;
801
887
  }
888
+ if (closed || authorization === undefined || refusal !== null)
889
+ return UNCERTAIN;
802
890
  const transfer = ['transfer', 'history', locator, path];
803
891
  if (query.revision !== undefined)
804
892
  transfer.push(query.revision);
@@ -820,17 +908,17 @@ export function composeRuntime(options) {
820
908
  }
821
909
  if (result.kind === 'revoked-authorization')
822
910
  await refuse('revoked-authorization');
823
- return result;
911
+ return historyAnswer(result);
824
912
  }
825
913
  catch {
826
914
  journal.record('outcome history transport-uncertainty');
827
- return { kind: 'transport-uncertainty' };
915
+ return UNCERTAIN;
828
916
  }
829
917
  }
830
- function declaredCards() {
918
+ function declaredAgents() {
831
919
  for (const capability of capabilities) {
832
- if (capability.conversationCards !== undefined)
833
- return capability.conversationCards;
920
+ if (capability.conversationAgents !== undefined)
921
+ return capability.conversationAgents;
834
922
  }
835
923
  return undefined;
836
924
  }
@@ -846,23 +934,20 @@ export function composeRuntime(options) {
846
934
  async function invokeAction(name, input) {
847
935
  await ready;
848
936
  if (closed || refusal !== null)
849
- return { kind: 'transport-uncertainty' };
937
+ return UNCERTAIN;
850
938
  if (declaredAction(name) === undefined)
851
- return { kind: 'undeclared' };
939
+ return undeclared(name);
852
940
  try {
853
941
  await ensureAuthorization();
854
942
  }
855
943
  catch {
856
- return { kind: 'transport-uncertainty' };
857
- }
858
- if (closed || authorization === undefined || refusal !== null) {
859
- return { kind: 'transport-uncertainty' };
944
+ return UNCERTAIN;
860
945
  }
946
+ if (closed || authorization === undefined || refusal !== null)
947
+ return UNCERTAIN;
861
948
  journal.record(`transfer action ${name}`);
862
949
  try {
863
950
  const result = await options.adapter.action(authorization, { name, input });
864
- if (result.kind === 'action' && result.at !== undefined)
865
- want(result.at);
866
951
  if (result.kind === 'scoped-refusal') {
867
952
  const room = replicas.get(result.locator)?.confirmed.record;
868
953
  if (room !== undefined)
@@ -873,11 +958,11 @@ export function composeRuntime(options) {
873
958
  journal.record(`outcome action ${result.kind}`);
874
959
  if (result.kind === 'rejected')
875
960
  journal.record(`refusal rejected ${result.code}`);
876
- return result;
961
+ return calledResult(result);
877
962
  }
878
963
  catch {
879
964
  journal.record('outcome action transport-uncertainty');
880
- return { kind: 'transport-uncertainty' };
965
+ return UNCERTAIN;
881
966
  }
882
967
  }
883
968
  function continuing() {
@@ -894,32 +979,11 @@ export function composeRuntime(options) {
894
979
  options.adapter.reached(locator, written, roomVersion) &&
895
980
  !options.adapter.reached(locator, roomVersion, written));
896
981
  }
897
- function want({ locator, roomVersion }) {
898
- if (reachedBy(locator, roomVersion))
899
- return;
900
- const held = wanted.get(locator);
901
- if (held === undefined || options.adapter.reached(locator, roomVersion, held)) {
902
- wanted.set(locator, roomVersion);
903
- }
904
- }
905
- function reachOf(locator) {
906
- let target = wanted.get(locator);
907
- const versions = (replicas.get(locator)?.queue ?? []).flatMap((batch) => {
908
- const accepted = pendings.get(batch.id)?.accepted;
909
- return accepted === undefined ? [] : [accepted];
910
- });
911
- for (const version of versions) {
912
- if (target === undefined || options.adapter.reached(locator, version, target)) {
913
- target = version;
914
- }
915
- }
916
- return target === undefined || reachedBy(locator, target) ? undefined : target;
917
- }
918
- async function pullRefresh(authorized, confirmed, minPosition) {
982
+ async function pullRefresh(authorized, confirmed) {
919
983
  if (!continuing())
920
984
  return unconfirmed;
921
985
  try {
922
- const result = await refreshRoom(options.adapter, authorized, confirmed, continuing, minPosition);
986
+ const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
923
987
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
924
988
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
925
989
  return result;
@@ -929,14 +993,33 @@ export function composeRuntime(options) {
929
993
  throw error;
930
994
  }
931
995
  }
932
- async function pullBootstrap(authorized, room) {
996
+ function staging(local) {
997
+ return {
998
+ held: (locator) => transfers.get(locator) ?? null,
999
+ staged: (record) => local.staged(record),
1000
+ stage: (record, page) => exclusive(async () => {
1001
+ if (!continuing())
1002
+ return false;
1003
+ await local.stage(record, page);
1004
+ transfers.set(record.locator, record);
1005
+ return true;
1006
+ }),
1007
+ abandon: (locator) => exclusive(async () => {
1008
+ if (!continuing())
1009
+ return;
1010
+ await local.abandon(locator);
1011
+ transfers.delete(locator);
1012
+ }),
1013
+ };
1014
+ }
1015
+ async function pullBootstrap(authorized, room, local) {
933
1016
  if (!continuing())
934
1017
  return unconfirmed;
935
1018
  try {
936
1019
  const result = await bootstrapRoom(options.adapter, authorized, room, {
937
1020
  collection: modelOf(room.locator),
938
1021
  selection: modelOf(room.locator, 'typed-selection'),
939
- }, continuing);
1022
+ }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
940
1023
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
941
1024
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
942
1025
  return result;
@@ -949,35 +1032,46 @@ export function composeRuntime(options) {
949
1032
  async function advance(room, local, authorized) {
950
1033
  const held = replicas.get(room.locator);
951
1034
  if (held !== undefined) {
952
- const refreshed = await pullRefresh(authorized, held.confirmed, reachOf(room.locator));
1035
+ const refreshed = await pullRefresh(authorized, held.confirmed);
953
1036
  if (!continuing())
954
1037
  return unconfirmed;
955
1038
  if (refreshed.kind === 'still-current') {
1039
+ stale.delete(room.locator);
956
1040
  if (sourceRefusals.has(room.locator))
957
- return unconfirmed;
1041
+ return null;
958
1042
  if (unreadable.delete(room.locator)) {
1043
+ scopedRefusals.delete(room.locator);
959
1044
  list(room, true);
960
1045
  emitRooms();
961
1046
  }
962
1047
  return null;
963
1048
  }
964
1049
  if (refreshed.kind === 'advanced') {
965
- await publish(local, room, refreshed.confirmed);
1050
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
1051
+ const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
1052
+ const reached = now !== undefined &&
1053
+ options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
1054
+ return reached ? null : advance(room, local, authorized);
1055
+ }
1056
+ await retainModel(local, room, 'collection', modelOf(room.locator));
966
1057
  return null;
967
1058
  }
968
1059
  if (refreshed.kind !== 'beyond-horizon')
969
1060
  return refreshed;
970
1061
  }
971
- const bootstrapped = await pullBootstrap(authorized, room);
1062
+ const bootstrapped = await pullBootstrap(authorized, room, local);
972
1063
  if (!continuing())
973
1064
  return unconfirmed;
974
1065
  if (bootstrapped.kind !== 'staged')
975
1066
  return bootstrapped;
976
- await publish(local, room, bootstrapped.confirmed);
1067
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
977
1068
  return null;
978
1069
  }
979
1070
  async function withhold(withheld, room, local, progress) {
980
- progress.complete = false;
1071
+ if (withheld.kind === 'refused')
1072
+ stale.delete(room.locator);
1073
+ else
1074
+ stale.add(room.locator);
981
1075
  if (withheld.kind === 'scoped-refusal') {
982
1076
  await scoped(withheld, room, local);
983
1077
  }
@@ -1002,6 +1096,7 @@ export function composeRuntime(options) {
1002
1096
  function refusedSource(room, { refusal }) {
1003
1097
  sourceRefusals.set(room.locator, {
1004
1098
  code: refusal.rule,
1099
+ retryable: false,
1005
1100
  fields: {
1006
1101
  file: refusal.file,
1007
1102
  ...(refusal.location === undefined ? {} : { location: refusal.location }),
@@ -1016,52 +1111,46 @@ export function composeRuntime(options) {
1016
1111
  }
1017
1112
  function leaving(batch, result) {
1018
1113
  if (result.kind === 'expired')
1019
- return { batch, returned: { kind: 'expired' } };
1020
- return { batch, returned: { kind: result.kind, code: result.code, fields: result.fields } };
1114
+ return { batch, returned: returnedBy('expired') };
1115
+ return {
1116
+ batch,
1117
+ returned: {
1118
+ kind: result.kind,
1119
+ code: result.code,
1120
+ fields: result.fields,
1121
+ retryable: result.retryable,
1122
+ },
1123
+ };
1021
1124
  }
1022
1125
  function logAccepted(locator, submitted, accepted) {
1023
1126
  journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
1024
1127
  (accepted.author === undefined ? '' : ` author ${accepted.author}`) +
1025
1128
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1026
1129
  }
1027
- async function recordAccepted(room, local, submitted, accepted) {
1028
- await exclusive(async () => {
1029
- if (!continuing())
1030
- return;
1031
- const current = replicas.get(room.locator);
1032
- if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1033
- return;
1034
- }
1035
- const state = pendings.get(submitted.id);
1036
- if (state === undefined)
1037
- return;
1038
- await local.accept(room.locator, submitted.id, accepted.roomVersion);
1039
- pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1040
- logAccepted(room.locator, submitted, accepted);
1041
- emitQueue(room.locator);
1042
- });
1043
- }
1044
- async function commitAccept(room, local, submitted, accepted, base, overlay) {
1130
+ async function adoptAccepted(room, local, submitted, accepted) {
1045
1131
  if (!continuing())
1046
1132
  return unconfirmed;
1047
1133
  const current = replicas.get(room.locator);
1048
1134
  if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1049
1135
  return null;
1050
1136
  }
1051
- const remaining = current.queue.filter((batch) => batch.id !== submitted.id);
1052
- const retainedBase = readConfirmed({
1053
- ...base.record,
1054
- model: modelOf(room.locator),
1055
- selectionModel: modelOf(room.locator, 'typed-selection'),
1056
- }, base);
1057
- const applied = overlay && retainedBase.kind === 'staged'
1058
- ? applyReturned(retainedBase.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion)
1059
- : retainedBase;
1060
- if (applied.kind !== 'staged')
1137
+ const held = current.confirmed.record.roomVersion;
1138
+ logAccepted(room.locator, submitted, accepted);
1139
+ const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1140
+ ? { kind: 'staged', confirmed: current.confirmed }
1141
+ : applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.changes, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
1142
+ ? accepted.roomVersion
1143
+ : held, accepted.roomVersion);
1144
+ if (applied.kind !== 'staged') {
1145
+ const state = pendings.get(submitted.id);
1146
+ await local.accept(room.locator, submitted.id, accepted.roomVersion);
1147
+ if (state !== undefined)
1148
+ pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1149
+ emitQueue(room.locator);
1061
1150
  return applied;
1062
- const set = await settle(local, room.locator, current, applied.confirmed, remaining, {
1063
- room: true,
1064
- });
1151
+ }
1152
+ const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1153
+ pendings.delete(submitted.id);
1065
1154
  unreadable.delete(room.locator);
1066
1155
  sourceRefusals.delete(room.locator);
1067
1156
  declarationRefusals.delete(room.locator);
@@ -1070,17 +1159,8 @@ export function composeRuntime(options) {
1070
1159
  moveTo({ ...status, ...attention() });
1071
1160
  return null;
1072
1161
  }
1073
- async function accepting(room, local, submitted, accepted, pulled, fresh) {
1074
- const roomVersion = pulled.record.roomVersion;
1075
- if (options.adapter.reached(room.locator, roomVersion, accepted.roomVersion)) {
1076
- return committed(room, local, submitted, accepted, pulled, roomVersion === accepted.roomVersion);
1077
- }
1078
- if (fresh && continuing())
1079
- await publish(local, room, pulled);
1080
- return false;
1081
- }
1082
- async function committed(room, local, submitted, accepted, base, overlay) {
1083
- const failed = await exclusive(() => commitAccept(room, local, submitted, accepted, base, overlay));
1162
+ async function adopted(room, local, submitted, accepted) {
1163
+ const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
1084
1164
  if (failed === null)
1085
1165
  return true;
1086
1166
  if (failed.kind === 'refused' && continuing())
@@ -1093,10 +1173,8 @@ export function composeRuntime(options) {
1093
1173
  const head = replica?.queue[0];
1094
1174
  if (replica === undefined || head === undefined)
1095
1175
  return;
1096
- if (pendings.get(head.id)?.accepted !== undefined) {
1097
- progress.complete = false;
1176
+ if (pendings.get(head.id)?.accepted !== undefined)
1098
1177
  return;
1099
- }
1100
1178
  const state = pendings.get(head.id);
1101
1179
  const { submission, lost: earlier } = state ?? {
1102
1180
  submission: { state: 'waiting' },
@@ -1124,6 +1202,7 @@ export function composeRuntime(options) {
1124
1202
  return;
1125
1203
  }
1126
1204
  const opensBefore = openSignals;
1205
+ const answered = awaitingAnswer(room.locator);
1127
1206
  try {
1128
1207
  result = await options.adapter.mutate(authorized, {
1129
1208
  locator: room.locator,
@@ -1132,11 +1211,14 @@ export function composeRuntime(options) {
1132
1211
  }, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
1133
1212
  }
1134
1213
  catch {
1214
+ answered();
1135
1215
  progress.complete = false;
1136
1216
  if (continuing())
1137
1217
  progress.reachable = false;
1138
1218
  return;
1139
1219
  }
1220
+ if (result.kind !== 'accepted' || !continuing())
1221
+ answered();
1140
1222
  if (!continuing())
1141
1223
  return;
1142
1224
  if (result.kind === 'transport-uncertainty') {
@@ -1189,6 +1271,7 @@ export function composeRuntime(options) {
1189
1271
  const reset = result.reset;
1190
1272
  progress.complete = false;
1191
1273
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1274
+ armTimer();
1192
1275
  return;
1193
1276
  }
1194
1277
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
@@ -1201,7 +1284,8 @@ export function composeRuntime(options) {
1201
1284
  return;
1202
1285
  }
1203
1286
  const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1204
- leaving: result.kind === 'expired' && landed(head, current.confirmed)
1287
+ leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1288
+ (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1205
1289
  ? { ...left, returned: null }
1206
1290
  : left,
1207
1291
  });
@@ -1215,95 +1299,33 @@ export function composeRuntime(options) {
1215
1299
  return withhold(withheld, room, local, progress);
1216
1300
  continue;
1217
1301
  }
1218
- await recordAccepted(room, local, head, result);
1219
- const from = replicas.get(room.locator)?.confirmed;
1220
- if (from === undefined)
1221
- return;
1222
- const pulled = await pullRefresh(authorized, from, reachOf(room.locator));
1223
- if (!continuing())
1224
- return;
1225
- if (pulled.kind === 'revoked-authorization') {
1226
- await refuse('revoked-authorization');
1227
- return;
1228
- }
1229
- if (pulled.kind === 'scoped-refusal') {
1230
- await scoped(pulled, room, local);
1302
+ const kept = await adopted(room, local, head, result).finally(answered);
1303
+ if (!kept) {
1231
1304
  progress.complete = false;
1232
1305
  return;
1233
1306
  }
1234
- if (pulled.kind === 'still-current' || pulled.kind === 'advanced') {
1235
- const fresh = pulled.kind === 'advanced';
1236
- if (!(await accepting(room, local, head, result, fresh ? pulled.confirmed : from, fresh))) {
1237
- progress.complete = false;
1238
- return;
1239
- }
1240
- continue;
1241
- }
1242
- if (pulled.kind === 'beyond-horizon') {
1243
- const bootstrapped = await pullBootstrap(authorized, room);
1244
- if (!continuing())
1245
- return;
1246
- if (bootstrapped.kind === 'revoked-authorization') {
1247
- await refuse('revoked-authorization');
1248
- return;
1249
- }
1250
- if (bootstrapped.kind === 'scoped-refusal') {
1251
- await scoped(bootstrapped, room, local);
1252
- progress.complete = false;
1253
- return;
1254
- }
1255
- if (bootstrapped.kind === 'staged') {
1256
- if (!(await accepting(room, local, head, result, bootstrapped.confirmed, true))) {
1257
- progress.complete = false;
1258
- return;
1259
- }
1260
- continue;
1261
- }
1262
- progress.complete = false;
1263
- if (bootstrapped.kind === 'transport-uncertainty') {
1264
- progress.reachable = false;
1265
- }
1266
- else if (bootstrapped.kind === 'room-not-readable') {
1267
- unreadable.add(room.locator);
1268
- list(room, false);
1269
- emitRooms();
1270
- }
1271
- else if (bootstrapped.kind === 'refused') {
1272
- refusedSource(room, bootstrapped);
1273
- }
1274
- return;
1275
- }
1276
- progress.complete = false;
1277
- if (pulled.kind === 'transport-uncertainty') {
1278
- progress.reachable = false;
1279
- }
1280
- else if (pulled.kind === 'room-not-readable') {
1281
- unreadable.add(room.locator);
1282
- list(room, false);
1283
- emitRooms();
1284
- }
1285
- else if (pulled.kind === 'retryable-refusal') {
1286
- journal.record(`refusal ${pulled.kind} ${pulled.code}`);
1287
- }
1288
- else if (pulled.kind === 'refused') {
1289
- refusedSource(room, pulled);
1290
- }
1291
- return;
1292
1307
  }
1293
- return;
1308
+ }
1309
+ function awaitingAnswer(locator) {
1310
+ let release = () => { };
1311
+ const awaited = new Promise((resolve) => {
1312
+ release = resolve;
1313
+ });
1314
+ answering.set(locator, awaited);
1315
+ return () => {
1316
+ if (answering.get(locator) === awaited)
1317
+ answering.delete(locator);
1318
+ release();
1319
+ };
1294
1320
  }
1295
1321
  async function retainModel(local, room, attached, next) {
1296
1322
  const key = modelKey(room.locator, attached);
1297
- if (next.status === 'unavailable')
1298
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
1299
- const epoch = modelEpoch.get(key) ?? 0;
1300
1323
  const previous = modelOf(room.locator, attached);
1301
1324
  const heldBefore = replicas.get(room.locator)?.confirmed.record;
1302
1325
  if (JSON.stringify(previous) === JSON.stringify(next) &&
1303
1326
  (heldBefore === undefined ||
1304
1327
  (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
1305
1328
  JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
1306
- JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
1307
1329
  JSON.stringify(heldBefore.typedSelection) ===
1308
1330
  JSON.stringify(room.typedSelection ?? null))))
1309
1331
  return;
@@ -1314,20 +1336,17 @@ export function composeRuntime(options) {
1314
1336
  await exclusive(async () => {
1315
1337
  if (!continuing())
1316
1338
  return;
1317
- if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
1318
- return;
1319
1339
  const held = replicas.get(room.locator);
1320
1340
  if (held !== undefined) {
1321
1341
  const declared = {
1322
1342
  ...held.confirmed.record,
1323
1343
  operations: room.operations,
1324
1344
  replica: room.replica,
1325
- attachedModel: room.attachedModel,
1326
1345
  typedSelection: room.typedSelection ?? null,
1327
1346
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1328
1347
  };
1329
1348
  const reclassified = sameMembership(declared, held.confirmed.record)
1330
- ? readConfirmed(declared, held.confirmed)
1349
+ ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
1331
1350
  : readReclassified(declared, held.confirmed);
1332
1351
  const record = reclassified.kind === 'staged'
1333
1352
  ? declared
@@ -1335,14 +1354,16 @@ export function composeRuntime(options) {
1335
1354
  ...declared,
1336
1355
  typedSelection: held.confirmed.record.typedSelection,
1337
1356
  };
1338
- const read = reclassified.kind === 'deferred' ? readConfirmed(record, held.confirmed) : reclassified;
1357
+ const read = reclassified.kind === 'deferred'
1358
+ ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
1359
+ : reclassified;
1339
1360
  await local.write({
1340
1361
  next: record,
1341
1362
  previous: held.confirmed.record,
1342
1363
  queue: held.queue,
1343
1364
  });
1344
1365
  if (read.kind === 'staged') {
1345
- replicas.set(room.locator, replicaOf(read.confirmed, held.queue, rewriting(), held));
1366
+ replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1346
1367
  if (declarationRefusals.delete(room.locator)) {
1347
1368
  sourceRefusals.delete(room.locator);
1348
1369
  unreadable.delete(room.locator);
@@ -1366,79 +1387,12 @@ export function composeRuntime(options) {
1366
1387
  publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
1367
1388
  });
1368
1389
  }
1369
- async function acquireModel(local, room, authorized, selection) {
1370
- const attached = selection === null ? 'collection' : 'typed-selection';
1371
- const key = modelKey(room.locator, attached);
1372
- const declared = selection === null ? room.attachedModel : selection.attachedModel;
1373
- let previous = modelOf(room.locator, attached);
1374
- if (previous.status !== 'unavailable' &&
1375
- (declared === null || !sameModel(previous.snapshot, declared))) {
1376
- previous = { status: 'unavailable', reason: 'version-changed' };
1377
- await retainModel(local, room, attached, previous);
1378
- if (discoveryPaused)
1379
- return;
1380
- }
1381
- const epoch = modelEpoch.get(key) ?? 0;
1382
- let answer;
1383
- try {
1384
- answer = await options.adapter.model(authorized, selection === null
1385
- ? { locator: room.locator, attached: 'collection', model: declared }
1386
- : {
1387
- locator: room.locator,
1388
- attached: 'typed-selection',
1389
- roots: selection.roots,
1390
- model: declared,
1391
- });
1392
- }
1393
- catch {
1394
- answer = { kind: 'transport-uncertainty' };
1395
- }
1396
- if (discoveryPaused || epoch !== (modelEpoch.get(key) ?? 0))
1397
- return;
1398
- if (answer.kind === 'revoked-authorization') {
1399
- await refuse('revoked-authorization');
1400
- return;
1401
- }
1402
- if (answer.kind === 'transport-uncertainty') {
1403
- await retainModel(local, room, attached, cached(previous));
1404
- return;
1405
- }
1406
- if (answer.kind === 'model-unavailable') {
1407
- await retainModel(local, room, attached, {
1408
- status: 'unavailable',
1409
- reason: answer.reason,
1410
- });
1411
- return;
1412
- }
1413
- if (declared === null || !sameModel(answer.snapshot, declared)) {
1414
- await retainModel(local, room, attached, {
1415
- status: 'unavailable',
1416
- reason: 'version-changed',
1417
- });
1418
- return;
1419
- }
1420
- if (previous.status !== 'unavailable' &&
1421
- sameModel(previous.snapshot, answer.snapshot) &&
1422
- previous.snapshot.schema !== answer.snapshot.schema) {
1423
- await retainModel(local, room, attached, { status: 'unavailable', reason: 'invalid' });
1424
- return;
1425
- }
1426
- const verified = answer.snapshot.schema === null ? EMPTY_SCHEMA : verify(answer.snapshot.schema);
1427
- if (verified.verdict === 'refuse') {
1428
- const where = verified.location === undefined ? '' : ` ${verified.location}`;
1429
- journal.record(`model invalid ${room.locator} ${attached} ${answer.snapshot.identity} ${verified.rule}${where}`);
1430
- }
1431
- await retainModel(local, room, attached, verified.verdict === 'accept'
1432
- ? {
1433
- status: 'current',
1434
- snapshot: structuredClone(answer.snapshot),
1435
- verifiedWith: MD_MODEL,
1436
- types: verified.model.types,
1437
- }
1438
- : { status: 'unavailable', reason: 'invalid' });
1439
- }
1440
1390
  async function scoped(result, room, local) {
1441
- scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
1391
+ scopedRefusals.set(room.locator, {
1392
+ code: result.code,
1393
+ fields: result.fields,
1394
+ retryable: result.retryable,
1395
+ });
1442
1396
  let scopedAway = room;
1443
1397
  if (result.scope === 'model')
1444
1398
  await retainModel(local, room, 'collection', {
@@ -1464,14 +1418,36 @@ export function composeRuntime(options) {
1464
1418
  emitRooms();
1465
1419
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
1466
1420
  }
1467
- async function synchronize(discovered, local, progress) {
1468
- const prepared = await Promise.allSettled(discovered.map((room) => prepareRoom(room, local, progress)));
1469
- const ready = discovered.filter((_, index) => {
1421
+ function heldRooms(listed) {
1422
+ return listed.flatMap((room) => {
1423
+ const record = replicas.get(room.locator)?.confirmed.record;
1424
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1425
+ return [];
1426
+ return [
1427
+ {
1428
+ locator: room.locator,
1429
+ roomVersion: record.roomVersion,
1430
+ documents: record.documents,
1431
+ revisions: record.revisions,
1432
+ attachments: record.attachments,
1433
+ },
1434
+ ];
1435
+ });
1436
+ }
1437
+ async function synchronize(listed, local, progress, attempt) {
1438
+ const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1439
+ const ready = listed.filter((_, index) => {
1470
1440
  const outcome = prepared[index];
1471
1441
  return outcome?.status === 'fulfilled' && outcome.value;
1472
1442
  });
1473
- const rooms = await Promise.allSettled(ready.map((room) => synchronizeRoom(room, local, progress)));
1474
- const failed = [...prepared, ...rooms].find((room) => room.status === 'rejected');
1443
+ if (attempt === 'opened') {
1444
+ options.adapter.roomStream?.hold(heldRooms(ready));
1445
+ handed = true;
1446
+ }
1447
+ const loaded = await Promise.allSettled(ready
1448
+ .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1449
+ .map((room) => loadRoom(room, local, progress)));
1450
+ const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1475
1451
  if (failed !== undefined)
1476
1452
  throw failed.reason;
1477
1453
  }
@@ -1488,11 +1464,8 @@ export function composeRuntime(options) {
1488
1464
  progress.complete = false;
1489
1465
  return false;
1490
1466
  }
1491
- const selection = room.typedSelection;
1492
- await Promise.all([
1493
- acquireModel(local, room, authorization, null),
1494
- ...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
1495
- ]);
1467
+ if (prior !== undefined)
1468
+ await retainModel(local, room, 'collection', modelOf(room.locator));
1496
1469
  if (discoveryPaused || authorization === undefined)
1497
1470
  return false;
1498
1471
  if (room.replica.admission !== 'admitted') {
@@ -1500,24 +1473,53 @@ export function composeRuntime(options) {
1500
1473
  progress.complete = false;
1501
1474
  return false;
1502
1475
  }
1503
- const held = replicas.get(room.locator);
1504
- if (held !== undefined && held.confirmed.record.replica.selection !== room.replica.selection) {
1505
- progress.complete = false;
1506
- return false;
1507
- }
1508
1476
  return true;
1509
1477
  }
1510
- async function synchronizeRoom(room, local, progress) {
1478
+ async function caughtUp(room, local, progress) {
1479
+ const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1480
+ if (held === undefined || authorization === undefined)
1481
+ return;
1482
+ const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1483
+ if (!behind)
1484
+ return;
1485
+ const withheld = await advance(room, local, authorization);
1486
+ if (withheld !== null)
1487
+ await withhold(withheld, room, local, progress);
1488
+ }
1489
+ function revoked(locator) {
1490
+ return ['collection', 'typed-selection'].some((attached) => {
1491
+ const model = modelOf(locator, attached);
1492
+ return model.status === 'unavailable' && model.reason === 'revoked';
1493
+ });
1494
+ }
1495
+ async function loadRoom(room, local, progress) {
1511
1496
  if (discoveryPaused || authorization === undefined)
1512
1497
  return;
1498
+ if (replicas.has(room.locator) && revoked(room.locator)) {
1499
+ await reload(room, local, progress);
1500
+ return;
1501
+ }
1513
1502
  const withheld = await advance(room, local, authorization);
1514
1503
  if (withheld !== null) {
1515
1504
  await withhold(withheld, room, local, progress);
1516
1505
  return;
1517
1506
  }
1518
- await submitRoom(room, local, authorization, progress);
1507
+ await caughtUp(room, local, progress);
1508
+ }
1509
+ async function reload(room, local, progress) {
1510
+ if (discoveryPaused || authorization === undefined)
1511
+ return;
1512
+ const bootstrapped = await pullBootstrap(authorization, room, local);
1513
+ if (!continuing())
1514
+ return;
1515
+ if (bootstrapped.kind !== 'staged') {
1516
+ await withhold(bootstrapped, room, local, progress);
1517
+ return;
1518
+ }
1519
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1520
+ await caughtUp(room, local, progress);
1519
1521
  }
1520
- async function cycle(local, progress) {
1522
+ async function discoverRooms(local, progress, attempt) {
1521
1523
  if (closed || authorization === undefined) {
1522
1524
  notifyReady();
1523
1525
  return;
@@ -1543,12 +1545,17 @@ export function composeRuntime(options) {
1543
1545
  }
1544
1546
  }
1545
1547
  progress.reachable = false;
1548
+ progress.complete = false;
1546
1549
  notifyReady();
1547
1550
  return;
1548
1551
  }
1549
1552
  if (result.kind === 'contract-unavailable') {
1550
1553
  journal.record(`connection contract unavailable ${result.code}`);
1551
- await refuse('missing-required-guarantee', { code: result.code, fields: result.fields });
1554
+ await refuse('missing-required-guarantee', {
1555
+ code: result.code,
1556
+ fields: result.fields,
1557
+ retryable: result.retryable,
1558
+ });
1552
1559
  notifyReady();
1553
1560
  return;
1554
1561
  }
@@ -1557,17 +1564,6 @@ export function composeRuntime(options) {
1557
1564
  notifyReady();
1558
1565
  return;
1559
1566
  }
1560
- if (!result.supportedVersions.includes(ROOM_CONTRACT_VERSION)) {
1561
- await refuse('no-matching-version');
1562
- notifyReady();
1563
- return;
1564
- }
1565
- if (result.mdModel.identity !== MD_MODEL.identity ||
1566
- result.mdModel.version !== MD_MODEL.version) {
1567
- await refuse('md-model-version-mismatch');
1568
- notifyReady();
1569
- return;
1570
- }
1571
1567
  for (const room of result.rooms) {
1572
1568
  if (room.operations === undefined ||
1573
1569
  !['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
@@ -1575,8 +1571,7 @@ export function composeRuntime(options) {
1575
1571
  room.replica === undefined ||
1576
1572
  typeof room.replica.selection !== 'string' ||
1577
1573
  room.replica.selection.length === 0 ||
1578
- !['admitted', 'unavailable'].includes(room.replica.admission) ||
1579
- !declaredIdentity(room.attachedModel)) {
1574
+ !['admitted', 'unavailable'].includes(room.replica.admission)) {
1580
1575
  journal.record(`invalid model or replica declaration ${room.locator}`);
1581
1576
  await refuse('missing-required-guarantee');
1582
1577
  notifyReady();
@@ -1589,14 +1584,6 @@ export function composeRuntime(options) {
1589
1584
  return;
1590
1585
  }
1591
1586
  }
1592
- const declared = new Set(result.capabilities.map((capability) => capability.name));
1593
- for (const required of options.requiredGuarantees) {
1594
- if (!knownCapabilities.has(required) || !declared.has(required)) {
1595
- await refuse('missing-required-guarantee');
1596
- notifyReady();
1597
- return;
1598
- }
1599
- }
1600
1587
  if (refusal !== null) {
1601
1588
  refusal = null;
1602
1589
  emitRefusal();
@@ -1604,26 +1591,19 @@ export function composeRuntime(options) {
1604
1591
  const previousCapabilities = capabilities;
1605
1592
  capabilities = result.capabilities;
1606
1593
  notifyReady();
1607
- if (declared.has('room-version-notifications'))
1608
- observeRoomVersions();
1609
1594
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1610
1595
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
1611
1596
  limits = result.providerLimits;
1612
1597
  await local.declare(limits, capabilities);
1613
1598
  }
1599
+ const signedIn = result.member ?? null;
1600
+ if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
1601
+ await local.keepMember(signedIn);
1602
+ member = signedIn;
1603
+ emitMember();
1604
+ }
1614
1605
  await applyDiscovery(result.rooms, local);
1615
- handSelected(result.rooms
1616
- .filter((room) => room.replica.admission === 'admitted')
1617
- .map((room) => room.locator)
1618
- .sort());
1619
- progress.complete = true;
1620
- await synchronize(result.rooms, local, progress);
1621
- if (closed || discoveryPaused)
1622
- return;
1623
- await conversations.reconcile();
1624
- if (status.providerUnreachable)
1625
- conversations.reconnected();
1626
- await conversations.catchUp(progress);
1606
+ await synchronize(result.rooms, local, progress, attempt);
1627
1607
  }
1628
1608
  async function ensureAuthorization() {
1629
1609
  if (authorization !== undefined)
@@ -1637,64 +1617,262 @@ export function composeRuntime(options) {
1637
1617
  throw error;
1638
1618
  }
1639
1619
  }
1640
- async function connect() {
1641
- if (closed || stopping || discoveryPaused) {
1642
- notifyReady();
1620
+ function task(work) {
1621
+ if (stopping)
1622
+ return chain;
1623
+ chain = chain.then(work, work).catch(() => undefined);
1624
+ return chain;
1625
+ }
1626
+ function sync(attempt) {
1627
+ return task(async () => {
1628
+ if (closed || stopping || discoveryPaused) {
1629
+ notifyReady();
1630
+ return;
1631
+ }
1632
+ const progress = { complete: true, reachable: true };
1633
+ underway = attempt === 'opened' ? progress : null;
1634
+ if (attempt === 'opened')
1635
+ streamed = progress;
1636
+ complete = false;
1637
+ recorded = false;
1638
+ moveTo(transferring(status));
1639
+ handed = false;
1640
+ try {
1641
+ const local = await openLocal();
1642
+ if (closed)
1643
+ return;
1644
+ await ensureAuthorization();
1645
+ if (closed)
1646
+ return;
1647
+ await discoverRooms(local, progress, attempt);
1648
+ }
1649
+ catch {
1650
+ progress.complete = false;
1651
+ if (attempt === 'opened' && !handed)
1652
+ progress.reachable = false;
1653
+ }
1654
+ finally {
1655
+ notifyReady();
1656
+ }
1657
+ if (attempt !== 'opened') {
1658
+ await finishOpening(progress);
1659
+ return;
1660
+ }
1661
+ if (handed || closed || progress.reachable)
1662
+ return;
1663
+ underway = null;
1664
+ journal.record('connection reopen');
1665
+ await rest(progress);
1666
+ options.adapter.roomStream?.reopen();
1667
+ });
1668
+ }
1669
+ function closedSocket(revoked) {
1670
+ const opened = socketOpen;
1671
+ socketOpen = false;
1672
+ if (revoked) {
1673
+ void refuse('revoked-authorization');
1643
1674
  return;
1644
1675
  }
1645
- startTimer();
1646
- const progress = { complete: false, reachable: true };
1647
- let completedAt = new Date().toISOString();
1648
- try {
1649
- const local = await openLocal();
1650
- if (closed)
1651
- return;
1652
- await ensureAuthorization();
1653
- if (closed)
1676
+ complete = false;
1677
+ if (streamed !== null)
1678
+ streamed.complete = false;
1679
+ moveTo({
1680
+ ...status,
1681
+ state: status.state === 'synced' ? 'syncing' : status.state,
1682
+ providerUnreachable: status.providerUnreachable || !opened,
1683
+ });
1684
+ }
1685
+ function applyOpening(expired) {
1686
+ return task(async () => {
1687
+ const progress = underway;
1688
+ underway = null;
1689
+ if (closed || stopping)
1654
1690
  return;
1655
- await cycle(local, progress);
1656
- if (closed)
1691
+ if (progress === null) {
1692
+ await reloadExpired(expired);
1657
1693
  return;
1658
- if (progress.complete) {
1659
- completedAt = new Date().toISOString();
1660
- await local.complete(completedAt);
1661
1694
  }
1695
+ try {
1696
+ const local = await openLocal();
1697
+ const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1698
+ const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1699
+ const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
1700
+ if (progress.reachable) {
1701
+ for (const room of held)
1702
+ if (!reloading.includes(room))
1703
+ await confirmModels(room, local);
1704
+ }
1705
+ const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1706
+ if (failed !== undefined)
1707
+ throw failed.reason;
1708
+ }
1709
+ catch {
1710
+ progress.complete = false;
1711
+ progress.reachable = false;
1712
+ }
1713
+ await finishOpening(progress);
1714
+ });
1715
+ }
1716
+ async function reloadExpired(expired) {
1717
+ const progress = { complete: true, reachable: true };
1718
+ try {
1719
+ const local = await openLocal();
1720
+ await Promise.allSettled([...discovered.values()]
1721
+ .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1722
+ .map((room) => reload(room, local, progress)));
1662
1723
  }
1663
1724
  catch {
1664
- progress.complete = false;
1665
- notifyReady();
1725
+ progress.reachable = false;
1726
+ }
1727
+ await rest(progress);
1728
+ }
1729
+ async function confirmModels(room, local) {
1730
+ for (const attached of ['collection', 'typed-selection']) {
1731
+ const model = modelOf(room.locator, attached);
1732
+ if (model.status === 'cached') {
1733
+ await retainModel(local, room, attached, { ...model, status: 'current' });
1734
+ }
1735
+ }
1736
+ }
1737
+ function submitLater(locator) {
1738
+ if (localSaves)
1739
+ return;
1740
+ const running = submitting.get(locator);
1741
+ if (running !== undefined) {
1742
+ running.again = true;
1666
1743
  return;
1667
1744
  }
1668
- finally {
1669
- notifyReady();
1670
- moveTo(settled(status, progress, completedAt, pending(), attention()));
1745
+ const run = { again: false, done: Promise.resolve() };
1746
+ submitting.set(locator, run);
1747
+ run.done = (async () => {
1748
+ try {
1749
+ do {
1750
+ run.again = false;
1751
+ const room = discovered.get(locator);
1752
+ if (closed || stopping || discoveryPaused || room === undefined)
1753
+ return;
1754
+ const progress = { complete: true, reachable: true };
1755
+ try {
1756
+ await ensureAuthorization();
1757
+ if (authorization === undefined)
1758
+ return;
1759
+ await submitRoom(room, await openLocal(), authorization, progress);
1760
+ }
1761
+ catch {
1762
+ progress.reachable = false;
1763
+ }
1764
+ armTimer();
1765
+ await rest(progress);
1766
+ } while (run.again);
1767
+ }
1768
+ finally {
1769
+ submitting.delete(locator);
1770
+ }
1771
+ })();
1772
+ }
1773
+ function submitAll() {
1774
+ for (const [locator, replica] of replicas) {
1775
+ if (replica.queue.length > 0)
1776
+ submitLater(locator);
1671
1777
  }
1672
1778
  }
1673
- async function run() {
1779
+ async function finishOpening(progress) {
1674
1780
  try {
1675
- await connect();
1781
+ if (closed || discoveryPaused) {
1782
+ progress.complete = false;
1783
+ return;
1784
+ }
1785
+ submitAll();
1786
+ await conversations.reconcile();
1787
+ if (status.providerUnreachable)
1788
+ conversations.reconnected();
1789
+ const reading = { complete: true, reachable: true };
1790
+ await conversations.catchUp(reading);
1791
+ if (!reading.reachable)
1792
+ progress.reachable = false;
1793
+ }
1794
+ catch {
1795
+ progress.complete = false;
1676
1796
  }
1677
1797
  finally {
1678
- inflight = false;
1679
- if (queued && !closed) {
1680
- queued = false;
1681
- void enqueue();
1682
- }
1798
+ complete = progress.complete;
1799
+ await rest(progress);
1683
1800
  }
1684
1801
  }
1685
- function observeRoomVersions() {
1686
- const path = options.adapter.roomVersions;
1687
- if (path === undefined || withdrawNotifications !== undefined)
1802
+ async function rest(progress) {
1803
+ if (closed)
1688
1804
  return;
1689
- withdrawNotifications = path.observe((notification) => {
1690
- if (closed || stopping || discoveryPaused)
1805
+ let lastCompleted = status.lastCompleted;
1806
+ let synced = complete && stale.size === 0 && !pending();
1807
+ if (synced && !recorded) {
1808
+ const completedAt = new Date().toISOString();
1809
+ try {
1810
+ await (await openLocal()).complete(completedAt);
1811
+ recorded = true;
1812
+ lastCompleted = completedAt;
1813
+ }
1814
+ catch {
1815
+ synced = false;
1816
+ }
1817
+ }
1818
+ moveTo({
1819
+ state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
1820
+ lastCompleted,
1821
+ providerUnreachable: !progress.reachable,
1822
+ ...attention(),
1823
+ });
1824
+ }
1825
+ function applyRoom(locator, roomVersion) {
1826
+ announced.set(locator, roomVersion);
1827
+ return task(async () => {
1828
+ await answering.get(locator);
1829
+ if (closed || stopping || discoveryPaused || authorization === undefined)
1830
+ return;
1831
+ const room = discovered.get(locator);
1832
+ const held = replicas.get(locator);
1833
+ if (room === undefined || room.replica.admission !== 'admitted')
1691
1834
  return;
1692
- if (notification.modelChanged === true && handed?.includes(notification.locator) === true) {
1693
- void enqueue();
1835
+ if (held !== undefined &&
1836
+ (held.confirmed.record.replica.selection !== room.replica.selection ||
1837
+ options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
1694
1838
  return;
1695
1839
  }
1696
- notices.set(notification.locator, notification.roomVersion);
1697
- chain = chain.then(runNotified, runNotified);
1840
+ const during = underway;
1841
+ const progress = during ?? { complete: true, reachable: true };
1842
+ if (during === null)
1843
+ moveTo(transferring(status));
1844
+ try {
1845
+ const local = await openLocal();
1846
+ const withheld = await advance(room, local, authorization);
1847
+ if (withheld !== null)
1848
+ await withhold(withheld, room, local, progress);
1849
+ }
1850
+ catch {
1851
+ stale.add(locator);
1852
+ }
1853
+ finally {
1854
+ if (during === null)
1855
+ await rest(progress);
1856
+ }
1857
+ });
1858
+ }
1859
+ function observeStream() {
1860
+ const stream = options.adapter.roomStream;
1861
+ if (stream === undefined || withdrawStream !== undefined)
1862
+ return;
1863
+ withdrawStream = stream.observe((event) => {
1864
+ if (closed || stopping)
1865
+ return;
1866
+ if (event.kind === 'opened') {
1867
+ socketOpen = true;
1868
+ void sync('opened');
1869
+ }
1870
+ else if (event.kind === 'closed')
1871
+ closedSocket(event.revoked);
1872
+ else if (event.kind === 'opening')
1873
+ void applyOpening(event.expired);
1874
+ else
1875
+ void applyRoom(event.locator, event.roomVersion);
1698
1876
  });
1699
1877
  }
1700
1878
  function writesHeld() {
@@ -1715,14 +1893,17 @@ export function composeRuntime(options) {
1715
1893
  listener(mode);
1716
1894
  }
1717
1895
  if (releasing)
1718
- void enqueue();
1896
+ submitAll();
1719
1897
  }
1720
1898
  function observeConversations() {
1721
1899
  const stream = options.adapter.conversations;
1722
1900
  if (stream === undefined || withdrawConversations !== undefined)
1723
1901
  return;
1724
1902
  withdrawConversations = stream.observe((event) => {
1725
- conversations.receive(event);
1903
+ if (conversations.receive(event)) {
1904
+ void ready.then(scheduleCurrency);
1905
+ return;
1906
+ }
1726
1907
  if (event.kind !== 'gap' || conversations.current())
1727
1908
  return;
1728
1909
  show();
@@ -1733,7 +1914,7 @@ export function composeRuntime(options) {
1733
1914
  if (currencyQueued || closed || stopping)
1734
1915
  return;
1735
1916
  currencyQueued = true;
1736
- chain = chain.then(runCurrency, runCurrency);
1917
+ void task(runCurrency);
1737
1918
  }
1738
1919
  async function runCurrency() {
1739
1920
  currencyQueued = false;
@@ -1744,110 +1925,22 @@ export function composeRuntime(options) {
1744
1925
  show();
1745
1926
  return;
1746
1927
  }
1747
- const resting = status;
1748
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1749
- moveTo(transferring(resting));
1928
+ const progress = { complete: false, reachable: !status.providerUnreachable };
1929
+ moveTo(transferring(status));
1750
1930
  try {
1751
1931
  await conversations.catchUp(progress);
1752
1932
  }
1753
1933
  catch {
1754
1934
  progress.reachable = false;
1755
1935
  }
1756
- finally {
1757
- if (!closed) {
1758
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1759
- }
1760
- }
1761
- }
1762
- function handSelected(locators) {
1763
- const path = options.adapter.roomVersions;
1764
- if (path === undefined || withdrawNotifications === undefined)
1765
- return;
1766
- if (handed !== undefined && JSON.stringify(locators) === JSON.stringify(handed))
1767
- return;
1768
- handed = locators;
1769
- path.select(locators);
1770
- }
1771
- async function runNotified() {
1772
- const announced = [...notices];
1773
- notices.clear();
1774
- if (announced.length === 0 || closed || stopping || discoveryPaused)
1775
- return;
1776
- const resting = status;
1777
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1778
- moveTo(transferring(resting));
1779
- try {
1780
- const local = await openLocal();
1781
- for (const [locator, roomVersion] of announced) {
1782
- if (closed || discoveryPaused || authorization === undefined)
1783
- return;
1784
- if (handed?.includes(locator) !== true)
1785
- continue;
1786
- const room = discovered.get(locator);
1787
- const held = replicas.get(locator);
1788
- if (room === undefined)
1789
- continue;
1790
- if (sourceRefusals.has(locator)) {
1791
- void enqueue();
1792
- continue;
1793
- }
1794
- if (held === undefined)
1795
- continue;
1796
- if (held.confirmed.record.roomVersion === roomVersion)
1797
- continue;
1798
- const withheld = await advance(room, local, authorization);
1799
- if (withheld === null)
1800
- continue;
1801
- if (withheld.kind === 'refused') {
1802
- void enqueue();
1803
- continue;
1804
- }
1805
- if (withheld.kind === 'scoped-refusal') {
1806
- await scoped(withheld, room, local);
1807
- continue;
1808
- }
1809
- if (withheld.kind === 'transport-uncertainty') {
1810
- progress.reachable = false;
1811
- continue;
1812
- }
1813
- if (withheld.kind === 'room-not-readable') {
1814
- unreadable.add(locator);
1815
- list(room, false);
1816
- emitRooms();
1817
- continue;
1818
- }
1819
- if (withheld.kind === 'revoked-authorization') {
1820
- await refuse('revoked-authorization');
1821
- return;
1822
- }
1823
- }
1824
- }
1825
- catch {
1826
- progress.reachable = false;
1827
- }
1828
- finally {
1829
- if (!closed) {
1830
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1831
- }
1832
- }
1833
- }
1834
- function enqueue() {
1835
- if (stopping)
1836
- return chain;
1837
- if (inflight) {
1838
- queued = true;
1839
- return chain;
1840
- }
1841
- inflight = true;
1842
- chain = chain.then(run, run);
1843
- return chain;
1936
+ await rest(progress);
1844
1937
  }
1845
1938
  function close() {
1846
1939
  if (closed)
1847
1940
  return;
1848
1941
  closed = true;
1849
- withdrawNotifications?.();
1850
- withdrawNotifications = undefined;
1942
+ withdrawStream?.();
1943
+ withdrawStream = undefined;
1851
1944
  withdrawConversations?.();
1852
1945
  withdrawConversations = undefined;
1853
1946
  withdrawServiceMode?.();
@@ -1862,6 +1955,7 @@ export function composeRuntime(options) {
1862
1955
  storageLossListeners.clear();
1863
1956
  statusListeners.clear();
1864
1957
  serviceModeListeners.clear();
1958
+ memberListeners.clear();
1865
1959
  readSide.close();
1866
1960
  queueSubscriptions.clear();
1867
1961
  roomDraftListeners.clear();
@@ -1871,6 +1965,7 @@ export function composeRuntime(options) {
1871
1965
  stopping = true;
1872
1966
  stopTimer();
1873
1967
  await chain.catch(() => undefined);
1968
+ await Promise.all([...submitting.values()].map((run) => run.done));
1874
1969
  await mutations;
1875
1970
  close();
1876
1971
  }
@@ -1879,21 +1974,29 @@ export function composeRuntime(options) {
1879
1974
  options.signal?.addEventListener('abort', close, { once: true });
1880
1975
  if (options.signal?.aborted)
1881
1976
  close();
1882
- void openLocal().catch(() => {
1977
+ observeStream();
1978
+ void openLocal().then(() => {
1979
+ if (capabilities.length > 0)
1980
+ notifyReady();
1981
+ }, () => {
1883
1982
  notifyReady();
1884
1983
  });
1885
- void enqueue();
1984
+ if (options.adapter.roomStream === undefined)
1985
+ void sync('unstreamed');
1886
1986
  function planned(input, reach) {
1887
1987
  return planOf(input, roomFacts(reach), planReach(reach));
1888
1988
  }
1889
1989
  function watchPlan(plan, deliver) {
1990
+ materialize(reachedRooms(plan));
1890
1991
  return readSide.watch(plan, identityOf(plan), deliver);
1891
1992
  }
1892
1993
  async function searched(input, reach) {
1893
1994
  const plan = searchPlanOf(input);
1894
1995
  const selected = plan.documents === undefined && reach === null
1895
1996
  ? null
1896
- : (await readSide.query(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
1997
+ : (await rowsQuery(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
1998
+ if (selected === null)
1999
+ materialize(null);
1897
2000
  return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
1898
2001
  }
1899
2002
  const watch = (topic, input, deliver) => {
@@ -1917,6 +2020,10 @@ export function composeRuntime(options) {
1917
2020
  if (topic === 'conversation') {
1918
2021
  return conversations.watch(input[0], input[1], deliver);
1919
2022
  }
2023
+ if (topic === 'conversations')
2024
+ return conversations.watchList(deliver);
2025
+ if (topic === 'agents')
2026
+ return conversations.watchAgents(deliver);
1920
2027
  if (topic === 'presentation') {
1921
2028
  const listener = {
1922
2029
  app: input[0],
@@ -1971,6 +2078,13 @@ export function composeRuntime(options) {
1971
2078
  serviceModeListeners.delete(deliver);
1972
2079
  };
1973
2080
  }
2081
+ if (topic === 'member') {
2082
+ memberListeners.add(deliver);
2083
+ deliver(member);
2084
+ return () => {
2085
+ memberListeners.delete(deliver);
2086
+ };
2087
+ }
1974
2088
  if (topic === 'queue') {
1975
2089
  const queue = { locator: input[0], notify: deliver };
1976
2090
  queueSubscriptions.add(queue);
@@ -1988,7 +2102,7 @@ export function composeRuntime(options) {
1988
2102
  rooms() {
1989
2103
  return listed();
1990
2104
  },
1991
- query: ((input) => readSide.query(planned(input, null))),
2105
+ query: ((input) => rowsQuery(planned(input, null))),
1992
2106
  search: (input) => searched(input, null),
1993
2107
  types(locator) {
1994
2108
  return {
@@ -2017,14 +2131,11 @@ export function composeRuntime(options) {
2017
2131
  return exclusive(async () => {
2018
2132
  const parts = readParts(input);
2019
2133
  if (path === '' || normalize(path) !== path)
2020
- throw new Error('room-draft-malformed');
2134
+ throw runtimeError('room-draft-malformed');
2021
2135
  const local = await openLocal();
2022
2136
  const current = roomDrafts.get(room)?.get(path);
2023
2137
  if ((current?.revision ?? null) !== revision) {
2024
- return {
2025
- outcome: 'stale',
2026
- current: current === undefined ? null : roomDraftOf(current),
2027
- };
2138
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2028
2139
  }
2029
2140
  const record = {
2030
2141
  locator: room,
@@ -2044,10 +2155,7 @@ export function composeRuntime(options) {
2044
2155
  const local = await openLocal();
2045
2156
  const current = roomDrafts.get(room)?.get(path);
2046
2157
  if (current === undefined || current.revision !== revision) {
2047
- return {
2048
- outcome: 'stale',
2049
- current: current === undefined ? null : roomDraftOf(current),
2050
- };
2158
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2051
2159
  }
2052
2160
  await local.discardRoomDraft(room, path);
2053
2161
  forgetRoomDraft(room, path);
@@ -2105,14 +2213,17 @@ export function composeRuntime(options) {
2105
2213
  await mutations;
2106
2214
  authorization = undefined;
2107
2215
  discoveryPaused = false;
2108
- await enqueue();
2216
+ if (options.adapter.roomStream === undefined)
2217
+ await sync('unstreamed');
2218
+ else
2219
+ await task(ensureAuthorization);
2109
2220
  },
2110
2221
  };
2111
2222
  const reachable = {
2112
2223
  md,
2113
2224
  watch,
2114
2225
  plan: planned,
2115
- query: (plan) => readSide.query(plan),
2226
+ query: (plan) => rowsQuery(plan),
2116
2227
  watchPlan,
2117
2228
  search: searched,
2118
2229
  };