@agentshouse/mdruntime 0.1.0-c5b50992a4193 → 0.1.0-c698c0ebd65bd

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 (169) hide show
  1. package/AGENTS.md +151 -96
  2. package/README.md +13 -3
  3. package/dist/adapter.d.ts +47 -14
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/attempt.d.ts +16 -0
  6. package/dist/attempt.d.ts.map +1 -0
  7. package/dist/attempt.js +70 -0
  8. package/dist/binary.d.ts +23 -0
  9. package/dist/binary.d.ts.map +1 -0
  10. package/dist/binary.js +8 -0
  11. package/dist/bootstrap.d.ts +17 -5
  12. package/dist/bootstrap.d.ts.map +1 -1
  13. package/dist/bootstrap.js +92 -38
  14. package/dist/change-set.d.ts.map +1 -1
  15. package/dist/change-set.js +19 -12
  16. package/dist/change.d.ts +15 -0
  17. package/dist/change.d.ts.map +1 -1
  18. package/dist/change.js +26 -1
  19. package/dist/chat/acts.d.ts +33 -0
  20. package/dist/chat/acts.d.ts.map +1 -0
  21. package/dist/chat/acts.js +36 -0
  22. package/dist/chat/context.d.ts +3 -0
  23. package/dist/chat/context.d.ts.map +1 -0
  24. package/dist/chat/context.js +10 -0
  25. package/dist/chat/input.d.ts +9 -0
  26. package/dist/chat/input.d.ts.map +1 -0
  27. package/dist/chat/input.js +99 -0
  28. package/dist/chat/kit.d.ts +60 -0
  29. package/dist/chat/kit.d.ts.map +1 -0
  30. package/dist/chat/kit.js +123 -0
  31. package/dist/chat/lasting.d.ts +6 -0
  32. package/dist/chat/lasting.d.ts.map +1 -0
  33. package/dist/chat/lasting.js +19 -0
  34. package/dist/chat/list.d.ts +26 -0
  35. package/dist/chat/list.d.ts.map +1 -0
  36. package/dist/chat/list.js +62 -0
  37. package/dist/chat/markdown.d.ts +20 -0
  38. package/dist/chat/markdown.d.ts.map +1 -0
  39. package/dist/chat/markdown.js +188 -0
  40. package/dist/chat/message.d.ts +6 -0
  41. package/dist/chat/message.d.ts.map +1 -0
  42. package/dist/chat/message.js +192 -0
  43. package/dist/chat/messages.d.ts +21 -0
  44. package/dist/chat/messages.d.ts.map +1 -0
  45. package/dist/chat/messages.js +151 -0
  46. package/dist/chat/name.d.ts +6 -0
  47. package/dist/chat/name.d.ts.map +1 -0
  48. package/dist/chat/name.js +37 -0
  49. package/dist/chat/opened.d.ts +11 -0
  50. package/dist/chat/opened.d.ts.map +1 -0
  51. package/dist/chat/opened.js +17 -0
  52. package/dist/chat/overlay.d.ts +31 -0
  53. package/dist/chat/overlay.d.ts.map +1 -0
  54. package/dist/chat/overlay.js +97 -0
  55. package/dist/chat/parts.d.ts +57 -0
  56. package/dist/chat/parts.d.ts.map +1 -0
  57. package/dist/chat/parts.js +119 -0
  58. package/dist/chat/process.d.ts +20 -0
  59. package/dist/chat/process.d.ts.map +1 -0
  60. package/dist/chat/process.js +32 -0
  61. package/dist/chat/provider.d.ts +63 -0
  62. package/dist/chat/provider.d.ts.map +1 -0
  63. package/dist/chat/provider.js +620 -0
  64. package/dist/chat/question.d.ts +9 -0
  65. package/dist/chat/question.d.ts.map +1 -0
  66. package/dist/chat/question.js +80 -0
  67. package/dist/chat/questions.d.ts +31 -0
  68. package/dist/chat/questions.d.ts.map +1 -0
  69. package/dist/chat/questions.js +111 -0
  70. package/dist/chat/read.d.ts +5 -0
  71. package/dist/chat/read.d.ts.map +1 -0
  72. package/dist/chat/read.js +13 -0
  73. package/dist/chat/ready.d.ts +33 -0
  74. package/dist/chat/ready.d.ts.map +1 -0
  75. package/dist/chat/ready.js +91 -0
  76. package/dist/chat/style.d.ts +4 -0
  77. package/dist/chat/style.d.ts.map +1 -0
  78. package/dist/chat/style.js +302 -0
  79. package/dist/chat.d.ts +14 -0
  80. package/dist/chat.d.ts.map +1 -0
  81. package/dist/chat.js +8 -0
  82. package/dist/client.d.ts +15 -6
  83. package/dist/client.d.ts.map +1 -1
  84. package/dist/client.js +66 -16
  85. package/dist/collection.d.ts +6 -11
  86. package/dist/collection.d.ts.map +1 -1
  87. package/dist/collection.js +56 -19
  88. package/dist/connection.d.ts +22 -9
  89. package/dist/connection.d.ts.map +1 -1
  90. package/dist/connection.js +566 -175
  91. package/dist/conversation.d.ts +15 -5
  92. package/dist/conversation.d.ts.map +1 -1
  93. package/dist/conversation.js +189 -64
  94. package/dist/entry.d.ts +17 -0
  95. package/dist/entry.d.ts.map +1 -0
  96. package/dist/entry.js +156 -0
  97. package/dist/expectation.d.ts +21 -0
  98. package/dist/expectation.d.ts.map +1 -0
  99. package/dist/expectation.js +70 -0
  100. package/dist/failure.d.ts +13 -0
  101. package/dist/failure.d.ts.map +1 -0
  102. package/dist/failure.js +19 -0
  103. package/dist/fence.d.ts +6 -0
  104. package/dist/fence.d.ts.map +1 -0
  105. package/dist/fence.js +10 -0
  106. package/dist/host.d.ts +3 -1
  107. package/dist/host.d.ts.map +1 -1
  108. package/dist/host.js +16 -12
  109. package/dist/identity.d.ts +3 -0
  110. package/dist/identity.d.ts.map +1 -1
  111. package/dist/identity.js +13 -1
  112. package/dist/index.d.ts +9 -4
  113. package/dist/index.d.ts.map +1 -1
  114. package/dist/index.js +3 -0
  115. package/dist/memory.d.ts +5 -3
  116. package/dist/memory.d.ts.map +1 -1
  117. package/dist/memory.js +62 -20
  118. package/dist/model.d.ts +3 -2
  119. package/dist/model.d.ts.map +1 -1
  120. package/dist/open.d.ts +2 -0
  121. package/dist/open.d.ts.map +1 -0
  122. package/dist/open.js +1 -0
  123. package/dist/outcome.d.ts +32 -3
  124. package/dist/outcome.d.ts.map +1 -1
  125. package/dist/outcome.js +42 -1
  126. package/dist/overlay.d.ts +14 -3
  127. package/dist/overlay.d.ts.map +1 -1
  128. package/dist/overlay.js +75 -37
  129. package/dist/query.d.ts +4 -0
  130. package/dist/query.d.ts.map +1 -1
  131. package/dist/query.js +3 -0
  132. package/dist/reach.d.ts +2 -0
  133. package/dist/reach.d.ts.map +1 -1
  134. package/dist/reach.js +44 -20
  135. package/dist/react.d.ts +25 -16
  136. package/dist/react.d.ts.map +1 -1
  137. package/dist/react.js +8 -6
  138. package/dist/read-side.d.ts.map +1 -1
  139. package/dist/read-side.js +9 -2
  140. package/dist/refresh.d.ts +10 -0
  141. package/dist/refresh.d.ts.map +1 -1
  142. package/dist/refresh.js +42 -38
  143. package/dist/replica.d.ts +7 -4
  144. package/dist/replica.d.ts.map +1 -1
  145. package/dist/replica.js +74 -54
  146. package/dist/room-draft.d.ts +26 -9
  147. package/dist/room-draft.d.ts.map +1 -1
  148. package/dist/room-draft.js +132 -23
  149. package/dist/rows.d.ts +5 -1
  150. package/dist/rows.d.ts.map +1 -1
  151. package/dist/rows.js +10 -2
  152. package/dist/save.d.ts +26 -1
  153. package/dist/save.d.ts.map +1 -1
  154. package/dist/save.js +345 -37
  155. package/dist/search.d.ts +3 -2
  156. package/dist/search.d.ts.map +1 -1
  157. package/dist/status.d.ts +2 -2
  158. package/dist/status.d.ts.map +1 -1
  159. package/dist/status.js +0 -8
  160. package/dist/store.d.ts +48 -14
  161. package/dist/store.d.ts.map +1 -1
  162. package/dist/store.js +366 -170
  163. package/dist/wire.d.ts +7 -3
  164. package/dist/wire.d.ts.map +1 -1
  165. package/dist/wire.js +15 -2
  166. package/package.json +26 -14
  167. package/dist/attachment.d.ts +0 -3
  168. package/dist/attachment.d.ts.map +0 -1
  169. package/dist/attachment.js +0 -8
@@ -3,18 +3,24 @@ 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
8
  import { bootstrapRoom } from './bootstrap.js';
8
9
  import { changeSet, mergeChangeSets } from './change-set.js';
9
10
  import { createConversationStore, } from './conversation.js';
10
- import { parseCollection, validRoot } from './collection.js';
11
+ import { queuedFile } from './change.js';
12
+ import { validRoot } from './collection.js';
13
+ import { runtimeError } from './failure.js';
11
14
  import { createLog } from './log.js';
15
+ import { binaryRecord } from './binary.js';
16
+ import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
12
17
  import { refreshRoom } from './refresh.js';
13
- import { buildBatch } from './save.js';
14
- import { applyReturned, compositionOf, landed, readConfirmed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
15
- import { changedParts, changesOf, held, readParts, returnedInto, underneath, } from './room-draft.js';
16
- import { settled, same, saving, STARTING, transferring, } from './status.js';
18
+ import { buildBatch, joined, limitedIn, requeued } from './save.js';
19
+ import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
20
+ import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
21
+ import { same, saving, STARTING, transferring, } from './status.js';
17
22
  import { detectStorageLoss, nextGeneration } from './durability.js';
23
+ import { FencedMap } from './fence.js';
18
24
  import { roomDraftRevision } from './identity.js';
19
25
  import { beneath, normalize } from './paths.js';
20
26
  import { openRuntimeStore, } from './store.js';
@@ -34,19 +40,29 @@ function declaredSelection(room) {
34
40
  selection.roots.every((other, at) => at === index || !within(other, root)));
35
41
  }
36
42
  export function composeRuntime(options) {
43
+ const localSaves = options.adapter.localSaves === true;
37
44
  let authorization = options.authorization;
38
45
  let opening;
39
46
  let store;
40
- const replicas = new Map();
47
+ const fences = new Map();
48
+ const isFenced = (locator) => fences.has(locator);
49
+ let fencings = 0;
50
+ const lastFenced = new Map();
51
+ const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
52
+ let discoveries = 0;
53
+ const replicas = new FencedMap(isFenced);
54
+ const queuesWithoutReplica = new FencedMap(isFenced);
41
55
  const pendings = new Map();
42
- const roomDrafts = new Map();
56
+ const roomDrafts = new FencedMap(isFenced);
43
57
  const presentations = new Map();
44
58
  let limits = [];
45
59
  let capabilities = [];
46
- const rooms = new Map();
60
+ const rooms = new FencedMap(isFenced);
61
+ const counted = new Map();
47
62
  const unreadable = new Set();
48
- const models = new Map();
49
- const liveModels = new Map();
63
+ const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
64
+ const models = new FencedMap(isModelFenced);
65
+ const liveModels = new FencedMap(isModelFenced);
50
66
  const invalidModels = new Set();
51
67
  const discovered = new Map();
52
68
  let roomsListed = false;
@@ -82,6 +98,7 @@ export function composeRuntime(options) {
82
98
  let withdrawStream;
83
99
  const announced = new Map();
84
100
  let underway = null;
101
+ let streamed = null;
85
102
  let handed = false;
86
103
  const submitting = new Map();
87
104
  const answering = new Map();
@@ -105,13 +122,19 @@ export function composeRuntime(options) {
105
122
  let haltedRefusal = false;
106
123
  let openSignals = 0;
107
124
  const serviceModeListeners = new Set();
125
+ let member = null;
126
+ const memberListeners = new Set();
127
+ const materialized = new Set();
128
+ let everyRoom = false;
129
+ const transfers = new FencedMap(isFenced);
108
130
  const conversations = createConversationStore({
109
131
  agents: declaredAgents,
110
- documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
132
+ documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
111
133
  call: (name, input) => invokeAction(name, input),
112
134
  durably,
113
135
  stream: () => options.adapter.conversations,
114
136
  publish: publishChangeSet,
137
+ online: () => !status.providerUnreachable && refusal === null,
115
138
  closed: () => closed,
116
139
  });
117
140
  const searchIndex = createSearchIndex({
@@ -183,7 +206,9 @@ export function composeRuntime(options) {
183
206
  }
184
207
  }
185
208
  function pending() {
186
- return [...replicas.values()].some((replica) => replica.queue.length > 0);
209
+ return (!localSaves &&
210
+ (queuesWithoutReplica.size > 0 ||
211
+ [...replicas.values()].some((replica) => replica.queue.length > 0)));
187
212
  }
188
213
  function attention() {
189
214
  const attentionRequired = [];
@@ -212,8 +237,23 @@ export function composeRuntime(options) {
212
237
  }
213
238
  return { attentionRequired, reset };
214
239
  }
240
+ function documentsIn(locator) {
241
+ const replica = replicas.get(locator);
242
+ if (replica === undefined)
243
+ return 0;
244
+ const roots = replica.confirmed.record.typedSelection?.roots ?? [];
245
+ const paths = [
246
+ ...Object.keys(replica.visible.documents).filter(markdownPath),
247
+ ...Object.keys(replica.visible.binaries),
248
+ ];
249
+ return paths.filter((path) => !roots.some((root) => beneath(root, path))).length;
250
+ }
215
251
  function listed() {
216
- return structuredClone([...rooms.values()]);
252
+ return structuredClone([...rooms.values()].map((room) => {
253
+ const documents = documentsIn(room.locator);
254
+ counted.set(room.locator, documents);
255
+ return { ...room, documents };
256
+ }));
217
257
  }
218
258
  function roomFacts(reach = null) {
219
259
  const held = [...rooms.values()]
@@ -245,6 +285,10 @@ export function composeRuntime(options) {
245
285
  for (const listener of storageLossListeners)
246
286
  listener(storageLoss);
247
287
  }
288
+ function emitMember() {
289
+ for (const listener of memberListeners)
290
+ listener(member);
291
+ }
248
292
  function moveTo(next) {
249
293
  if (closed)
250
294
  return;
@@ -264,6 +308,8 @@ export function composeRuntime(options) {
264
308
  listener(shown);
265
309
  }
266
310
  function entryOf(batch) {
311
+ if (localSaves)
312
+ return Object.freeze({ batch, state: 'local' });
267
313
  const state = pendings.get(batch.id);
268
314
  if (state?.accepted !== undefined) {
269
315
  return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
@@ -271,10 +317,7 @@ export function composeRuntime(options) {
271
317
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
272
318
  }
273
319
  function entriesOf(locator) {
274
- const replica = replicas.get(locator);
275
- if (replica === undefined)
276
- return [];
277
- return replica.queue.map(entryOf);
320
+ return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
278
321
  }
279
322
  function emitQueue(locator) {
280
323
  const entries = entriesOf(locator);
@@ -284,12 +327,47 @@ export function composeRuntime(options) {
284
327
  }
285
328
  }
286
329
  function applyChangeSet(set) {
330
+ const held = everyRoom
331
+ ? set
332
+ : { ...set, documents: set.documents.filter(({ locator }) => materialized.has(locator)) };
333
+ readSide.apply(held);
334
+ searchIndex.changed(held);
335
+ }
336
+ function materialize(locators) {
337
+ if (everyRoom)
338
+ return;
339
+ if (locators === null)
340
+ everyRoom = true;
341
+ const sets = [];
342
+ for (const locator of locators ?? [...replicas.keys()]) {
343
+ if (materialized.has(locator))
344
+ continue;
345
+ materialized.add(locator);
346
+ const replica = replicas.get(locator);
347
+ if (replica !== undefined)
348
+ sets.push(changeSet(locator, null, replica));
349
+ }
350
+ if (sets.length === 0)
351
+ return;
352
+ const set = mergeChangeSets(sets);
287
353
  readSide.apply(set);
288
354
  searchIndex.changed(set);
289
355
  }
356
+ function reachedRooms(plan) {
357
+ if (plan.rooms !== null)
358
+ return plan.rooms;
359
+ if (plan.reach === null)
360
+ return null;
361
+ return [...plan.reach.rooms, ...plan.reach.documents.map(roomOfKey)];
362
+ }
363
+ function rowsQuery(plan) {
364
+ materialize(reachedRooms(plan));
365
+ return readSide.query(plan);
366
+ }
290
367
  function publishChangeSet(set) {
291
368
  applyChangeSet(set);
292
- if (set.rooms.length > 0)
369
+ if (set.rooms.length > 0 ||
370
+ [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
293
371
  emitRooms();
294
372
  for (const locator of set.queues)
295
373
  emitQueue(locator);
@@ -336,10 +414,7 @@ export function composeRuntime(options) {
336
414
  const revoked = { status: 'unavailable', reason: 'revoked' };
337
415
  try {
338
416
  const local = await openLocal();
339
- await local.revokeModels([...replicas.values()].map((replica) => ({
340
- record: replica.confirmed.record,
341
- queue: replica.queue,
342
- })));
417
+ await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
343
418
  }
344
419
  catch {
345
420
  journal.record('model revocation persistence failed');
@@ -363,10 +438,7 @@ export function composeRuntime(options) {
363
438
  model: revoked,
364
439
  selectionModel: revoked,
365
440
  };
366
- const read = readConfirmed(record, before.confirmed);
367
- replicas.set(locator, read.kind === 'staged'
368
- ? replicaOf(read.confirmed, before.queue, before)
369
- : { ...before, confirmed: { ...before.confirmed, record } });
441
+ replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
370
442
  }
371
443
  rooms.set(locator, {
372
444
  ...room,
@@ -383,7 +455,8 @@ export function composeRuntime(options) {
383
455
  }
384
456
  finally {
385
457
  refusal = {
386
- reason,
458
+ code: reason,
459
+ retryable: false,
387
460
  reauthorizationOffered: reason === 'revoked-authorization',
388
461
  refusal: provider,
389
462
  };
@@ -408,19 +481,28 @@ export function composeRuntime(options) {
408
481
  },
409
482
  providerOwnedPaths: room.providerOwnedPaths,
410
483
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
411
- attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
412
484
  synchronizationAvailable,
413
485
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
414
486
  });
415
487
  }
416
- async function applyDiscovery(listed, local) {
488
+ async function applyDiscovery(found, started) {
489
+ for (const room of found) {
490
+ const fence = fences.get(room.locator);
491
+ if (fence !== undefined && fence.erased && fence.at < started)
492
+ fences.delete(room.locator);
493
+ }
494
+ const listed = found.filter((room) => !fences.has(room.locator));
417
495
  const listedLocators = new Set(listed.map((room) => room.locator));
418
496
  for (const locator of listedLocators)
419
497
  scopedRefusals.delete(locator);
420
498
  discovered.clear();
421
499
  for (const room of listed)
422
500
  discovered.set(room.locator, room);
423
- await dropUnlisted(listedLocators, local);
501
+ const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
502
+ if (options.adapter.exhaustiveDiscovery === true)
503
+ await loseRooms(unlisted);
504
+ else
505
+ unlistedKept(unlisted);
424
506
  for (const room of listed) {
425
507
  const held = replicas.get(room.locator)?.confirmed.record;
426
508
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -431,43 +513,79 @@ export function composeRuntime(options) {
431
513
  }
432
514
  roomsListed = true;
433
515
  emitRooms();
516
+ return listed;
434
517
  }
435
518
  function unlistedHeld(listed) {
436
- return [...new Set([...rooms.keys(), ...roomDrafts.keys()])].filter((locator) => !listed.has(locator));
437
- }
438
- async function dropUnlisted(listed, local) {
439
- if (unlistedHeld(listed).length === 0)
440
- return;
441
- await exclusive(async () => {
442
- const unlisted = unlistedHeld(listed);
443
- await local.dropRooms(unlisted);
444
- const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
445
- room: true,
446
- queue: true,
447
- drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
448
- }));
449
- for (const locator of unlisted) {
450
- for (const batch of replicas.get(locator)?.queue ?? [])
451
- pendings.delete(batch.id);
452
- replicas.delete(locator);
519
+ return [
520
+ ...new Set([
521
+ ...rooms.keys(),
522
+ ...roomDrafts.keys(),
523
+ ...transfers.keys(),
524
+ ...queuesWithoutReplica.keys(),
525
+ ]),
526
+ ].filter((locator) => !listed.has(locator));
527
+ }
528
+ function unlistedKept(unlisted) {
529
+ for (const locator of unlisted) {
530
+ const held = replicas.get(locator)?.confirmed.record;
531
+ if (held !== undefined)
532
+ list(held, false);
533
+ else
453
534
  rooms.delete(locator);
454
- roomDrafts.delete(locator);
455
- unreadable.delete(locator);
456
- stale.delete(locator);
457
- scopedRefusals.delete(locator);
458
- sourceRefusals.delete(locator);
459
- declarationRefusals.delete(locator);
460
- for (const attached of ['collection', 'typed-selection']) {
461
- const key = modelKey(locator, attached);
462
- models.delete(key);
463
- liveModels.delete(key);
464
- invalidModels.delete(key);
465
- }
466
- journal.record(`room unlisted ${locator}`);
535
+ }
536
+ }
537
+ function fence(locators) {
538
+ const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
539
+ const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
540
+ room: true,
541
+ queue: true,
542
+ drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
543
+ }));
544
+ for (const locator of fresh) {
545
+ fences.set(locator, { at: discoveries, erased: false });
546
+ lastFenced.set(locator, ++fencings);
547
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
548
+ pendings.delete(batch.id);
549
+ replicas.delete(locator);
550
+ queuesWithoutReplica.delete(locator);
551
+ rooms.delete(locator);
552
+ roomDrafts.delete(locator);
553
+ transfers.delete(locator);
554
+ discovered.delete(locator);
555
+ announced.delete(locator);
556
+ unreadable.delete(locator);
557
+ stale.delete(locator);
558
+ scopedRefusals.delete(locator);
559
+ sourceRefusals.delete(locator);
560
+ declarationRefusals.delete(locator);
561
+ for (const attached of ['collection', 'typed-selection']) {
562
+ const key = modelKey(locator, attached);
563
+ models.delete(key);
564
+ liveModels.delete(key);
565
+ invalidModels.delete(key);
467
566
  }
468
- publishChangeSet(mergeChangeSets(removed));
469
- moveTo({ ...status, ...attention() });
470
- });
567
+ journal.record(`room lost ${locator}`);
568
+ }
569
+ if (fresh.length === 0)
570
+ return fresh;
571
+ publishChangeSet(mergeChangeSets(removed));
572
+ for (const locator of fresh)
573
+ materialized.delete(locator);
574
+ moveTo({ ...status, ...attention() });
575
+ return fresh;
576
+ }
577
+ async function erased(local, locators) {
578
+ await local.erase(locators);
579
+ for (const locator of locators) {
580
+ const held = fences.get(locator);
581
+ if (held !== undefined)
582
+ held.erased = true;
583
+ }
584
+ }
585
+ async function loseRooms(locators) {
586
+ const fresh = fence(locators);
587
+ if (fresh.length > 0)
588
+ await durably((local) => erased(local, fresh));
471
589
  }
472
590
  function restored(key, persisted) {
473
591
  const prior = models.get(key);
@@ -488,7 +606,7 @@ export function composeRuntime(options) {
488
606
  return model;
489
607
  }
490
608
  async function hydrate(opened) {
491
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
609
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
492
610
  opened.all(),
493
611
  opened.queues(),
494
612
  opened.roomDrafts(),
@@ -498,9 +616,15 @@ export function composeRuntime(options) {
498
616
  opened.lastCompleted(),
499
617
  opened.providerLimits(),
500
618
  opened.capabilities(),
619
+ opened.member(),
620
+ opened.transfers(),
501
621
  ]);
622
+ transfers.clear();
623
+ for (const record of staged)
624
+ transfers.set(record.locator, record);
502
625
  limits = declared;
503
626
  capabilities = named;
627
+ member = known;
504
628
  roomDrafts.clear();
505
629
  for (const record of kept)
506
630
  rememberRoomDraft(record);
@@ -516,18 +640,17 @@ export function composeRuntime(options) {
516
640
  }
517
641
  const previousReplicas = new Map(replicas);
518
642
  replicas.clear();
643
+ queuesWithoutReplica.clear();
644
+ const replicated = new Set(records.map((record) => record.locator));
645
+ for (const [locator, queue] of queuedBatches) {
646
+ if (!replicated.has(locator))
647
+ queuesWithoutReplica.set(locator, queue);
648
+ }
519
649
  for (const record of records) {
520
650
  const model = restored(modelKey(record.locator, 'collection'), record.model);
521
651
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
522
652
  const prior = previousReplicas.get(record.locator);
523
- const retyped = { ...record, model, selectionModel };
524
- const read = readConfirmed(retyped, prior?.confirmed);
525
- const confirmed = read.kind === 'staged'
526
- ? read.confirmed
527
- : {
528
- record: retyped,
529
- collection: parseCollection(retyped.documents, compositionOf(retyped)),
530
- };
653
+ const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
531
654
  replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
532
655
  const existing = rooms.get(record.locator);
533
656
  if (existing === undefined)
@@ -536,7 +659,6 @@ export function composeRuntime(options) {
536
659
  rooms.set(record.locator, {
537
660
  ...existing,
538
661
  roomVersion: record.roomVersion,
539
- attachments: record.attachments,
540
662
  operations: record.operations,
541
663
  replica: record.replica,
542
664
  model: modelTypes(model),
@@ -548,10 +670,6 @@ export function composeRuntime(options) {
548
670
  },
549
671
  });
550
672
  }
551
- if (read.kind === 'refused') {
552
- refusedSource(record, read);
553
- declarationRefusals.add(record.locator);
554
- }
555
673
  }
556
674
  return {
557
675
  lastCompleted,
@@ -564,14 +682,23 @@ export function composeRuntime(options) {
564
682
  applyChangeSet(read);
565
683
  if (replicas.size > 0)
566
684
  emitRooms();
685
+ if (member !== null)
686
+ emitMember();
567
687
  for (const locator of locators)
568
688
  emitQueue(locator);
569
689
  publishRoomDrafts(false);
570
690
  return lastCompleted;
571
691
  }
572
692
  async function open() {
573
- const opened = await openRuntimeStore();
693
+ if (closed)
694
+ throw new Error('runtime closed');
695
+ const opened = await openRuntimeStore(isFenced);
574
696
  store = opened;
697
+ const interrupted = await opened.fences();
698
+ for (const locator of interrupted) {
699
+ fences.set(locator, { at: discoveries, erased: false });
700
+ lastFenced.set(locator, ++fencings);
701
+ }
575
702
  const kept = options.shell.generation;
576
703
  const storedGeneration = await opened.generation();
577
704
  const loss = detectStorageLoss(storedGeneration, kept);
@@ -585,6 +712,8 @@ export function composeRuntime(options) {
585
712
  emitStorageLoss();
586
713
  }
587
714
  const lastCompleted = await readState(opened);
715
+ if (interrupted.length > 0)
716
+ await erased(opened, interrupted);
588
717
  moveTo({ ...status, lastCompleted, ...attention() });
589
718
  return opened;
590
719
  }
@@ -611,11 +740,12 @@ export function composeRuntime(options) {
611
740
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
612
741
  }
613
742
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
614
- const withdrawn = withdrawals(confirmed, queue, withdrawable);
743
+ const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
744
+ const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
615
745
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
616
746
  const unqueued = withdrawn.map((batch) => ({
617
747
  batch,
618
- returned: landed(batch, confirmed) ? null : { kind: 'withdrawn' },
748
+ returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
619
749
  }));
620
750
  const returning = [
621
751
  ...[...unqueued].reverse(),
@@ -636,26 +766,44 @@ export function composeRuntime(options) {
636
766
  });
637
767
  }
638
768
  }
639
- await local.write({
640
- next: confirmed.record,
641
- previous: before?.confirmed.record ?? null,
642
- queue: kept,
643
- previousQueue: before?.queue ?? [],
644
- roomDrafts: [...written.values()],
645
- });
646
- for (const batch of before?.queue ?? []) {
769
+ await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
770
+ for (const batch of previousQueue) {
647
771
  if (!kept.includes(batch))
648
772
  pendings.delete(batch.id);
649
773
  }
650
774
  for (const record of written.values())
651
775
  rememberRoomDraft(record);
776
+ try {
777
+ await local.write({
778
+ next: confirmed.record,
779
+ previous: before?.confirmed.record ?? null,
780
+ ...(settling.stored === undefined ? {} : { stored: settling.stored }),
781
+ });
782
+ }
783
+ catch (error) {
784
+ const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
785
+ if (held !== null)
786
+ replicas.set(locator, held);
787
+ else if (kept.length > 0)
788
+ queuesWithoutReplica.set(locator, kept);
789
+ else
790
+ queuesWithoutReplica.delete(locator);
791
+ publishChangeSet(changeSet(locator, before ?? null, held, {
792
+ queue: kept.length !== previousQueue.length,
793
+ drafts: [...written.keys()],
794
+ }));
795
+ moveTo({ ...status, ...attention() });
796
+ throw error;
797
+ }
798
+ queuesWithoutReplica.delete(locator);
652
799
  for (const { batch, returned } of unqueued) {
653
- journal.record(`outcome ${locator} ${batch.id} ${returned === null ? 'landed' : 'withdrawn'}`);
800
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
801
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
654
802
  }
655
803
  replicas.set(locator, replicaOf(confirmed, kept, before));
656
804
  return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
657
805
  room: settling.room,
658
- queue: kept.length !== (before?.queue.length ?? 0),
806
+ queue: kept.length !== previousQueue.length,
659
807
  drafts: [...written.keys()],
660
808
  });
661
809
  }
@@ -690,7 +838,8 @@ export function composeRuntime(options) {
690
838
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
691
839
  };
692
840
  }
693
- function publish(local, room, confirmed, base) {
841
+ function publish(local, room, confirmed, landing = {}) {
842
+ const { base, stored } = landing;
694
843
  return exclusive(async () => {
695
844
  if (!continuing())
696
845
  return true;
@@ -701,13 +850,15 @@ export function composeRuntime(options) {
701
850
  return false;
702
851
  }
703
852
  adoptModels(room.locator, confirmed.record);
704
- const queue = prior?.queue ?? [];
853
+ const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
705
854
  const roomVersion = confirmed.record.roomVersion;
706
855
  const accepted = queue.filter((batch) => {
707
856
  const accepted = pendings.get(batch.id)?.accepted;
708
857
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
709
858
  });
710
- const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
859
+ const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
860
+ if (stored !== undefined)
861
+ transfers.delete(room.locator);
711
862
  for (const batch of accepted)
712
863
  pendings.delete(batch.id);
713
864
  stale.delete(room.locator);
@@ -726,22 +877,40 @@ export function composeRuntime(options) {
726
877
  const replica = replicas.get(locator);
727
878
  const room = rooms.get(locator);
728
879
  if (replica === undefined || room === undefined) {
729
- throw new Error(`No Local Replica for ${locator}`);
880
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
730
881
  }
731
882
  const saved = [];
732
883
  for (const { path, revision } of named) {
733
884
  const record = roomDrafts.get(locator)?.get(path);
734
885
  if (record === undefined || record.revision !== revision) {
735
- return { outcome: 'refused', rule: 'stale', file: path };
886
+ return { outcome: 'refused', code: 'stale', retryable: false, file: path };
736
887
  }
737
888
  if (!saved.some((draft) => draft.path === path))
738
889
  saved.push({ path, parts: record.parts });
739
890
  }
740
891
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
741
892
  if (changed.length > 0)
742
- return { outcome: 'refused', rule: 'changed-underneath', parts: changed };
893
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
743
894
  const parts = saved.map(({ path, parts }) => changesOf(path, parts));
744
- const built = buildBatch({
895
+ const changes = parts.flatMap(({ changes }) => changes);
896
+ const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
897
+ if (requeue.kind === 'refused')
898
+ return { outcome: 'refused', ...requeue.refusal };
899
+ const foreign = [...requeue.withdrawn.keys()].some((origin) => {
900
+ const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
901
+ return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
902
+ });
903
+ if (foreign) {
904
+ return {
905
+ outcome: 'refused',
906
+ code: 'unknown-revision',
907
+ retryable: false,
908
+ file: [...requeue.queued].at(-1),
909
+ };
910
+ }
911
+ const withdrawing = requeue.withdrawn.size > 0;
912
+ const input = {
913
+ local: localSaves,
745
914
  baseRoomVersion: replica.confirmed.record.roomVersion,
746
915
  composition: compositionFor(replica.confirmed.record),
747
916
  operations: room.operations,
@@ -749,29 +918,88 @@ export function composeRuntime(options) {
749
918
  providerLimits: limits,
750
919
  handle: room.handle,
751
920
  visible: replica.visible,
752
- changes: parts.flatMap(({ changes }) => changes),
921
+ changes,
753
922
  bases: parts.flatMap(({ bases }) => bases),
754
923
  writtenBy,
924
+ };
925
+ const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
926
+ if (checked?.kind === 'refused')
927
+ return { outcome: 'refused', ...checked.refusal };
928
+ const owners = [...requeue.owners].flatMap((origin) => {
929
+ const batch = requeue.withdrawn.get(origin);
930
+ return batch === undefined || batch === null ? [] : [{ origin, batch }];
755
931
  });
756
- if (built.kind === 'refused')
932
+ if (owners.length > 1) {
933
+ return {
934
+ outcome: 'refused',
935
+ code: 'unknown-revision',
936
+ retryable: false,
937
+ file: [...requeue.queued].at(-1),
938
+ };
939
+ }
940
+ const built = !withdrawing
941
+ ? buildBatch(input)
942
+ : requeue.changes.length === 0
943
+ ? null
944
+ : buildBatch({
945
+ ...input,
946
+ visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
947
+ changes: requeue.changes,
948
+ });
949
+ if (built?.kind === 'refused')
757
950
  return { outcome: 'refused', ...built.refusal };
758
- const batches = [...replica.queue, built.batch];
951
+ const [owner] = owners;
952
+ const join = built === null || owner === undefined
953
+ ? null
954
+ : joined(requeue.queue, owner.batch, built.batch);
955
+ if (join?.kind === 'refused')
956
+ return { outcome: 'refused', ...join.refusal };
957
+ const withdrawn = new Map(requeue.withdrawn);
958
+ if (join !== null && owner !== undefined)
959
+ withdrawn.set(owner.origin, join.batch);
960
+ const drafted = new Map();
961
+ for (const [origin, replaced] of withdrawn) {
962
+ if (replaced === null)
963
+ continue;
964
+ const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
965
+ drafted.set(replaced.id, replaced === join?.batch
966
+ ? [
967
+ ...carried.map((held) => joinedDraft(held, saved)),
968
+ ...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
969
+ ]
970
+ : carried);
971
+ }
972
+ if (built !== null && join === null)
973
+ drafted.set(built.batch.id, saved);
974
+ const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
975
+ for (const replaced of withdrawn.values()) {
976
+ if (replaced === null || !batches.includes(replaced))
977
+ continue;
978
+ const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
979
+ if (limited !== null)
980
+ return { outcome: 'refused', ...limited };
981
+ }
759
982
  const consumed = saved.map(({ path }) => path);
760
983
  try {
761
- await local.write({
762
- next: replica.confirmed.record,
763
- previous: replica.confirmed.record,
984
+ await local.keepAuthored({
985
+ locator,
764
986
  queue: batches,
765
- previousQueue: replica.queue,
766
- saved,
987
+ saved: drafted,
767
988
  consumedRoomDrafts: consumed,
768
989
  });
769
990
  }
770
991
  catch {
771
- return { outcome: 'refused', rule: 'durable-write' };
992
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
772
993
  }
773
- pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
774
- recorded = false;
994
+ if (fences.has(locator))
995
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
996
+ for (const origin of withdrawn.keys())
997
+ pendings.delete(origin);
998
+ for (const [id, drafts] of drafted) {
999
+ pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
1000
+ }
1001
+ if (!localSaves)
1002
+ recorded = false;
775
1003
  for (const path of consumed)
776
1004
  forgetRoomDraft(locator, path);
777
1005
  replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
@@ -779,9 +1007,12 @@ export function composeRuntime(options) {
779
1007
  queue: true,
780
1008
  drafts: consumed,
781
1009
  }));
782
- moveTo({ ...saving(status), ...attention() });
1010
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
783
1011
  submitLater(locator);
784
- return { outcome: 'saved', identifier: built.batch.id };
1012
+ const identifier = join?.batch.id ??
1013
+ built?.batch.id ??
1014
+ [...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
1015
+ return { outcome: 'saved', identifier };
785
1016
  }
786
1017
  async function mark(local, locator, identifier, submission, lost) {
787
1018
  const state = pendings.get(identifier);
@@ -791,6 +1022,8 @@ export function composeRuntime(options) {
791
1022
  return;
792
1023
  }
793
1024
  await local.submit(locator, identifier, submission, lost);
1025
+ if (fences.has(locator))
1026
+ return;
794
1027
  pendings.set(identifier, { ...state, submission, lost });
795
1028
  const replica = replicas.get(locator);
796
1029
  if (replica !== undefined)
@@ -801,22 +1034,24 @@ export function composeRuntime(options) {
801
1034
  if (!continuing())
802
1035
  throw new Error('connection refused');
803
1036
  await local.keepUpload(locator, identifier, upload);
1037
+ if (fences.has(locator))
1038
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
804
1039
  pendings.set(identifier, { ...pendings.get(identifier), upload });
805
1040
  if (!continuing())
806
1041
  throw new Error('connection refused');
807
1042
  }
808
1043
  async function readHistory(locator, path, query = {}) {
809
1044
  if (closed || refusal !== null)
810
- return { kind: 'transport-uncertainty' };
1045
+ return UNCERTAIN;
811
1046
  try {
812
1047
  await ensureAuthorization();
813
1048
  }
814
1049
  catch {
815
- return { kind: 'transport-uncertainty' };
816
- }
817
- if (closed || authorization === undefined || refusal !== null) {
818
- return { kind: 'transport-uncertainty' };
1050
+ return UNCERTAIN;
819
1051
  }
1052
+ if (closed || authorization === undefined || refusal !== null)
1053
+ return UNCERTAIN;
1054
+ const since = fencings;
820
1055
  const transfer = ['transfer', 'history', locator, path];
821
1056
  if (query.revision !== undefined)
822
1057
  transfer.push(query.revision);
@@ -831,6 +1066,8 @@ export function composeRuntime(options) {
831
1066
  ...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
832
1067
  });
833
1068
  journal.record(`outcome history ${result.kind}`);
1069
+ if (fencedSince(locator, since))
1070
+ return failure({ kind: 'room-not-readable', locator });
834
1071
  if (result.kind === 'scoped-refusal') {
835
1072
  const room = replicas.get(result.locator)?.confirmed.record;
836
1073
  if (room !== undefined)
@@ -838,12 +1075,106 @@ export function composeRuntime(options) {
838
1075
  }
839
1076
  if (result.kind === 'revoked-authorization')
840
1077
  await refuse('revoked-authorization');
841
- return result;
1078
+ return historyAnswer(result);
842
1079
  }
843
1080
  catch {
844
1081
  journal.record('outcome history transport-uncertainty');
845
- return { kind: 'transport-uncertainty' };
1082
+ return UNCERTAIN;
1083
+ }
1084
+ }
1085
+ function queuedBlob(locator, path, localId) {
1086
+ for (const batch of replicas.get(locator)?.queue ?? []) {
1087
+ const change = batch.changes[queuedFile(batch, path, localId)];
1088
+ if (change?.kind === 'binary')
1089
+ return change.blob;
1090
+ }
1091
+ return null;
1092
+ }
1093
+ function binaryRoom(request) {
1094
+ if (request.kind === 'path')
1095
+ return request.locator;
1096
+ const scope = options.adapter.referrerScope(request.referrer);
1097
+ return scope?.kind === 'room' ? scope.locator : null;
1098
+ }
1099
+ async function readBinary(request) {
1100
+ if (closed || refusal !== null)
1101
+ return UNCERTAIN;
1102
+ try {
1103
+ await ensureAuthorization();
846
1104
  }
1105
+ catch {
1106
+ return UNCERTAIN;
1107
+ }
1108
+ if (closed || authorization === undefined || refusal !== null)
1109
+ return UNCERTAIN;
1110
+ const since = fencings;
1111
+ journal.record(request.kind === 'path'
1112
+ ? `transfer binary ${request.locator} ${request.path}`
1113
+ : `transfer binary ${request.fileRef}`);
1114
+ try {
1115
+ const result = await options.adapter.binary(authorization, request);
1116
+ journal.record(`outcome binary ${result.kind}`);
1117
+ const locator = binaryRoom(request);
1118
+ if (locator !== null && fencedSince(locator, since))
1119
+ return failure({ kind: 'room-not-readable', locator });
1120
+ if (result.kind === 'scoped-refusal') {
1121
+ const room = replicas.get(result.locator)?.confirmed.record;
1122
+ if (room !== undefined)
1123
+ await scoped(result, room, await openLocal());
1124
+ }
1125
+ if (result.kind === 'revoked-authorization')
1126
+ await refuse('revoked-authorization');
1127
+ return binaryAnswer(result);
1128
+ }
1129
+ catch {
1130
+ journal.record('outcome binary transport-uncertainty');
1131
+ return UNCERTAIN;
1132
+ }
1133
+ }
1134
+ async function readBinaryPath(locator, path, revision) {
1135
+ await ready;
1136
+ if (revision !== undefined)
1137
+ return readBinary({ kind: 'path', locator, path, revision });
1138
+ const visible = replicas.get(locator)?.visible;
1139
+ const held = visible?.binaries[path];
1140
+ if (visible === undefined || held === undefined)
1141
+ return absentBinary(path);
1142
+ if (held.state === 'pending') {
1143
+ const blob = queuedBlob(locator, path, held.localId);
1144
+ return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1145
+ }
1146
+ const record = binaryRecord(path, held, visible.revisions[path]);
1147
+ if (record.state !== 'accepted')
1148
+ return absentBinary(path);
1149
+ const replica = replicas.get(locator);
1150
+ const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
1151
+ const holds = (at) => {
1152
+ const kept = replica?.confirmed.record.binaries[at];
1153
+ return (kept?.state === 'accepted' &&
1154
+ kept.fileRef === record.fileRef &&
1155
+ replica?.confirmed.record.revisions[at] === record.revision);
1156
+ };
1157
+ let confirmed = path;
1158
+ for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
1159
+ const change = queued[index];
1160
+ if (change.kind === 'move' && change.to === confirmed)
1161
+ confirmed = change.path;
1162
+ }
1163
+ return readBinary({
1164
+ kind: 'path',
1165
+ locator,
1166
+ path: confirmed,
1167
+ revision: record.revision,
1168
+ fileRef: record.fileRef,
1169
+ });
1170
+ }
1171
+ async function readBinaryReference(reference) {
1172
+ await ready;
1173
+ return readBinary({
1174
+ kind: 'reference',
1175
+ fileRef: reference.fileRef,
1176
+ referrer: reference.referrer,
1177
+ });
847
1178
  }
848
1179
  function declaredAgents() {
849
1180
  for (const capability of capabilities) {
@@ -864,18 +1195,17 @@ export function composeRuntime(options) {
864
1195
  async function invokeAction(name, input) {
865
1196
  await ready;
866
1197
  if (closed || refusal !== null)
867
- return { kind: 'transport-uncertainty' };
1198
+ return UNCERTAIN;
868
1199
  if (declaredAction(name) === undefined)
869
- return { kind: 'undeclared' };
1200
+ return undeclared(name);
870
1201
  try {
871
1202
  await ensureAuthorization();
872
1203
  }
873
1204
  catch {
874
- return { kind: 'transport-uncertainty' };
875
- }
876
- if (closed || authorization === undefined || refusal !== null) {
877
- return { kind: 'transport-uncertainty' };
1205
+ return UNCERTAIN;
878
1206
  }
1207
+ if (closed || authorization === undefined || refusal !== null)
1208
+ return UNCERTAIN;
879
1209
  journal.record(`transfer action ${name}`);
880
1210
  try {
881
1211
  const result = await options.adapter.action(authorization, { name, input });
@@ -889,11 +1219,11 @@ export function composeRuntime(options) {
889
1219
  journal.record(`outcome action ${result.kind}`);
890
1220
  if (result.kind === 'rejected')
891
1221
  journal.record(`refusal rejected ${result.code}`);
892
- return result;
1222
+ return calledResult(result);
893
1223
  }
894
1224
  catch {
895
1225
  journal.record('outcome action transport-uncertainty');
896
- return { kind: 'transport-uncertainty' };
1226
+ return UNCERTAIN;
897
1227
  }
898
1228
  }
899
1229
  function continuing() {
@@ -924,14 +1254,33 @@ export function composeRuntime(options) {
924
1254
  throw error;
925
1255
  }
926
1256
  }
927
- async function pullBootstrap(authorized, room) {
1257
+ function staging(local) {
1258
+ return {
1259
+ held: (locator) => transfers.get(locator) ?? null,
1260
+ staged: (record) => local.staged(record),
1261
+ stage: (record, page) => exclusive(async () => {
1262
+ if (!continuing())
1263
+ return false;
1264
+ await local.stage(record, page);
1265
+ transfers.set(record.locator, record);
1266
+ return true;
1267
+ }),
1268
+ abandon: (locator) => exclusive(async () => {
1269
+ if (!continuing())
1270
+ return;
1271
+ await local.abandon(locator);
1272
+ transfers.delete(locator);
1273
+ }),
1274
+ };
1275
+ }
1276
+ async function pullBootstrap(authorized, room, local) {
928
1277
  if (!continuing())
929
1278
  return unconfirmed;
930
1279
  try {
931
1280
  const result = await bootstrapRoom(options.adapter, authorized, room, {
932
1281
  collection: modelOf(room.locator),
933
1282
  selection: modelOf(room.locator, 'typed-selection'),
934
- }, (models, held) => remodel(room.locator, models, held), continuing);
1283
+ }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
935
1284
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
936
1285
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
937
1286
  return result;
@@ -959,7 +1308,7 @@ export function composeRuntime(options) {
959
1308
  return null;
960
1309
  }
961
1310
  if (refreshed.kind === 'advanced') {
962
- if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
1311
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
963
1312
  const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
964
1313
  const reached = now !== undefined &&
965
1314
  options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
@@ -971,12 +1320,12 @@ export function composeRuntime(options) {
971
1320
  if (refreshed.kind !== 'beyond-horizon')
972
1321
  return refreshed;
973
1322
  }
974
- const bootstrapped = await pullBootstrap(authorized, room);
1323
+ const bootstrapped = await pullBootstrap(authorized, room, local);
975
1324
  if (!continuing())
976
1325
  return unconfirmed;
977
1326
  if (bootstrapped.kind !== 'staged')
978
1327
  return bootstrapped;
979
- await publish(local, room, bootstrapped.confirmed);
1328
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
980
1329
  return null;
981
1330
  }
982
1331
  async function withhold(withheld, room, local, progress) {
@@ -1008,6 +1357,7 @@ export function composeRuntime(options) {
1008
1357
  function refusedSource(room, { refusal }) {
1009
1358
  sourceRefusals.set(room.locator, {
1010
1359
  code: refusal.rule,
1360
+ retryable: false,
1011
1361
  fields: {
1012
1362
  file: refusal.file,
1013
1363
  ...(refusal.location === undefined ? {} : { location: refusal.location }),
@@ -1022,8 +1372,16 @@ export function composeRuntime(options) {
1022
1372
  }
1023
1373
  function leaving(batch, result) {
1024
1374
  if (result.kind === 'expired')
1025
- return { batch, returned: { kind: 'expired' } };
1026
- return { batch, returned: { kind: result.kind, code: result.code, fields: result.fields } };
1375
+ return { batch, returned: returnedBy('expired') };
1376
+ return {
1377
+ batch,
1378
+ returned: {
1379
+ kind: result.kind,
1380
+ code: result.code,
1381
+ fields: result.fields,
1382
+ retryable: result.retryable,
1383
+ },
1384
+ };
1027
1385
  }
1028
1386
  function logAccepted(locator, submitted, accepted) {
1029
1387
  journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
@@ -1041,13 +1399,13 @@ export function composeRuntime(options) {
1041
1399
  logAccepted(room.locator, submitted, accepted);
1042
1400
  const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1043
1401
  ? { kind: 'staged', confirmed: current.confirmed }
1044
- : applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
1402
+ : applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
1045
1403
  ? accepted.roomVersion
1046
1404
  : held, accepted.roomVersion);
1047
1405
  if (applied.kind !== 'staged') {
1048
1406
  const state = pendings.get(submitted.id);
1049
1407
  await local.accept(room.locator, submitted.id, accepted.roomVersion);
1050
- if (state !== undefined)
1408
+ if (state !== undefined && !fences.has(room.locator))
1051
1409
  pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1052
1410
  emitQueue(room.locator);
1053
1411
  return applied;
@@ -1096,7 +1454,7 @@ export function composeRuntime(options) {
1096
1454
  let result;
1097
1455
  journal.record(`submission ${room.locator} ${head.id}`);
1098
1456
  await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1099
- if (discoveryPaused)
1457
+ if (discoveryPaused || fences.has(room.locator))
1100
1458
  return;
1101
1459
  if (writesHeld()) {
1102
1460
  await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
@@ -1249,7 +1607,7 @@ export function composeRuntime(options) {
1249
1607
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1250
1608
  };
1251
1609
  const reclassified = sameMembership(declared, held.confirmed.record)
1252
- ? readConfirmed(declared, held.confirmed)
1610
+ ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
1253
1611
  : readReclassified(declared, held.confirmed);
1254
1612
  const record = reclassified.kind === 'staged'
1255
1613
  ? declared
@@ -1257,12 +1615,10 @@ export function composeRuntime(options) {
1257
1615
  ...declared,
1258
1616
  typedSelection: held.confirmed.record.typedSelection,
1259
1617
  };
1260
- const read = reclassified.kind === 'deferred' ? readConfirmed(record, held.confirmed) : reclassified;
1261
- await local.write({
1262
- next: record,
1263
- previous: held.confirmed.record,
1264
- queue: held.queue,
1265
- });
1618
+ const read = reclassified.kind === 'deferred'
1619
+ ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
1620
+ : reclassified;
1621
+ await local.write({ next: record, previous: held.confirmed.record });
1266
1622
  if (read.kind === 'staged') {
1267
1623
  replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1268
1624
  if (declarationRefusals.delete(room.locator)) {
@@ -1289,7 +1645,11 @@ export function composeRuntime(options) {
1289
1645
  });
1290
1646
  }
1291
1647
  async function scoped(result, room, local) {
1292
- scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
1648
+ scopedRefusals.set(room.locator, {
1649
+ code: result.code,
1650
+ fields: result.fields,
1651
+ retryable: result.retryable,
1652
+ });
1293
1653
  let scopedAway = room;
1294
1654
  if (result.scope === 'model')
1295
1655
  await retainModel(local, room, 'collection', {
@@ -1326,7 +1686,6 @@ export function composeRuntime(options) {
1326
1686
  roomVersion: record.roomVersion,
1327
1687
  documents: record.documents,
1328
1688
  revisions: record.revisions,
1329
- attachments: record.attachments,
1330
1689
  },
1331
1690
  ];
1332
1691
  });
@@ -1406,14 +1765,14 @@ export function composeRuntime(options) {
1406
1765
  async function reload(room, local, progress) {
1407
1766
  if (discoveryPaused || authorization === undefined)
1408
1767
  return;
1409
- const bootstrapped = await pullBootstrap(authorization, room);
1768
+ const bootstrapped = await pullBootstrap(authorization, room, local);
1410
1769
  if (!continuing())
1411
1770
  return;
1412
1771
  if (bootstrapped.kind !== 'staged') {
1413
1772
  await withhold(bootstrapped, room, local, progress);
1414
1773
  return;
1415
1774
  }
1416
- await publish(local, room, bootstrapped.confirmed);
1775
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1417
1776
  await caughtUp(room, local, progress);
1418
1777
  }
1419
1778
  async function discoverRooms(local, progress, attempt) {
@@ -1422,6 +1781,8 @@ export function composeRuntime(options) {
1422
1781
  return;
1423
1782
  }
1424
1783
  moveTo(transferring(status));
1784
+ discoveries += 1;
1785
+ const started = discoveries;
1425
1786
  let result;
1426
1787
  try {
1427
1788
  result = await options.adapter.discover(authorization);
@@ -1448,7 +1809,11 @@ export function composeRuntime(options) {
1448
1809
  }
1449
1810
  if (result.kind === 'contract-unavailable') {
1450
1811
  journal.record(`connection contract unavailable ${result.code}`);
1451
- await refuse('missing-required-guarantee', { code: result.code, fields: result.fields });
1812
+ await refuse('missing-required-guarantee', {
1813
+ code: result.code,
1814
+ fields: result.fields,
1815
+ retryable: result.retryable,
1816
+ });
1452
1817
  notifyReady();
1453
1818
  return;
1454
1819
  }
@@ -1489,8 +1854,13 @@ export function composeRuntime(options) {
1489
1854
  limits = result.providerLimits;
1490
1855
  await local.declare(limits, capabilities);
1491
1856
  }
1492
- await applyDiscovery(result.rooms, local);
1493
- await synchronize(result.rooms, local, progress, attempt);
1857
+ const signedIn = result.member ?? null;
1858
+ if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
1859
+ await local.keepMember(signedIn);
1860
+ member = signedIn;
1861
+ emitMember();
1862
+ }
1863
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt);
1494
1864
  }
1495
1865
  async function ensureAuthorization() {
1496
1866
  if (authorization !== undefined)
@@ -1518,6 +1888,8 @@ export function composeRuntime(options) {
1518
1888
  }
1519
1889
  const progress = { complete: true, reachable: true };
1520
1890
  underway = attempt === 'opened' ? progress : null;
1891
+ if (attempt === 'opened')
1892
+ streamed = progress;
1521
1893
  complete = false;
1522
1894
  recorded = false;
1523
1895
  moveTo(transferring(status));
@@ -1558,9 +1930,14 @@ export function composeRuntime(options) {
1558
1930
  void refuse('revoked-authorization');
1559
1931
  return;
1560
1932
  }
1561
- if (opened || status.providerUnreachable)
1562
- return;
1563
- moveTo({ ...status, providerUnreachable: true });
1933
+ complete = false;
1934
+ if (streamed !== null)
1935
+ streamed.complete = false;
1936
+ moveTo({
1937
+ ...status,
1938
+ state: status.state === 'synced' ? 'syncing' : status.state,
1939
+ providerUnreachable: status.providerUnreachable || !opened,
1940
+ });
1564
1941
  }
1565
1942
  function applyOpening(expired) {
1566
1943
  return task(async () => {
@@ -1615,6 +1992,8 @@ export function composeRuntime(options) {
1615
1992
  }
1616
1993
  }
1617
1994
  function submitLater(locator) {
1995
+ if (localSaves)
1996
+ return;
1618
1997
  const running = submitting.get(locator);
1619
1998
  if (running !== undefined) {
1620
1999
  running.again = true;
@@ -1666,6 +2045,8 @@ export function composeRuntime(options) {
1666
2045
  conversations.reconnected();
1667
2046
  const reading = { complete: true, reachable: true };
1668
2047
  await conversations.catchUp(reading);
2048
+ if (progress.reachable && reading.reachable)
2049
+ await conversations.resend(reading);
1669
2050
  if (!reading.reachable)
1670
2051
  progress.reachable = false;
1671
2052
  }
@@ -1694,7 +2075,7 @@ export function composeRuntime(options) {
1694
2075
  }
1695
2076
  }
1696
2077
  moveTo({
1697
- state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
2078
+ state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
1698
2079
  lastCompleted,
1699
2080
  providerUnreachable: !progress.reachable,
1700
2081
  ...attention(),
@@ -1803,20 +2184,15 @@ export function composeRuntime(options) {
1803
2184
  show();
1804
2185
  return;
1805
2186
  }
1806
- const resting = status;
1807
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1808
- moveTo(transferring(resting));
2187
+ const progress = { complete: false, reachable: !status.providerUnreachable };
2188
+ moveTo(transferring(status));
1809
2189
  try {
1810
2190
  await conversations.catchUp(progress);
1811
2191
  }
1812
2192
  catch {
1813
2193
  progress.reachable = false;
1814
2194
  }
1815
- finally {
1816
- if (!closed) {
1817
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1818
- }
1819
- }
2195
+ await rest(progress);
1820
2196
  }
1821
2197
  function close() {
1822
2198
  if (closed)
@@ -1838,6 +2214,7 @@ export function composeRuntime(options) {
1838
2214
  storageLossListeners.clear();
1839
2215
  statusListeners.clear();
1840
2216
  serviceModeListeners.clear();
2217
+ memberListeners.clear();
1841
2218
  readSide.close();
1842
2219
  queueSubscriptions.clear();
1843
2220
  roomDraftListeners.clear();
@@ -1869,13 +2246,16 @@ export function composeRuntime(options) {
1869
2246
  return planOf(input, roomFacts(reach), planReach(reach));
1870
2247
  }
1871
2248
  function watchPlan(plan, deliver) {
2249
+ materialize(reachedRooms(plan));
1872
2250
  return readSide.watch(plan, identityOf(plan), deliver);
1873
2251
  }
1874
2252
  async function searched(input, reach) {
1875
2253
  const plan = searchPlanOf(input);
1876
2254
  const selected = plan.documents === undefined && reach === null
1877
2255
  ? null
1878
- : (await readSide.query(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
2256
+ : (await rowsQuery(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
2257
+ if (selected === null)
2258
+ materialize(null);
1879
2259
  return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
1880
2260
  }
1881
2261
  const watch = (topic, input, deliver) => {
@@ -1901,6 +2281,8 @@ export function composeRuntime(options) {
1901
2281
  }
1902
2282
  if (topic === 'conversations')
1903
2283
  return conversations.watchList(deliver);
2284
+ if (topic === 'agents')
2285
+ return conversations.watchAgents(deliver);
1904
2286
  if (topic === 'presentation') {
1905
2287
  const listener = {
1906
2288
  app: input[0],
@@ -1955,6 +2337,13 @@ export function composeRuntime(options) {
1955
2337
  serviceModeListeners.delete(deliver);
1956
2338
  };
1957
2339
  }
2340
+ if (topic === 'member') {
2341
+ memberListeners.add(deliver);
2342
+ deliver(member);
2343
+ return () => {
2344
+ memberListeners.delete(deliver);
2345
+ };
2346
+ }
1958
2347
  if (topic === 'queue') {
1959
2348
  const queue = { locator: input[0], notify: deliver };
1960
2349
  queueSubscriptions.add(queue);
@@ -1972,7 +2361,7 @@ export function composeRuntime(options) {
1972
2361
  rooms() {
1973
2362
  return listed();
1974
2363
  },
1975
- query: ((input) => readSide.query(planned(input, null))),
2364
+ query: ((input) => rowsQuery(planned(input, null))),
1976
2365
  search: (input) => searched(input, null),
1977
2366
  types(locator) {
1978
2367
  return {
@@ -2001,20 +2390,20 @@ export function composeRuntime(options) {
2001
2390
  return exclusive(async () => {
2002
2391
  const parts = readParts(input);
2003
2392
  if (path === '' || normalize(path) !== path)
2004
- throw new Error('room-draft-malformed');
2393
+ throw runtimeError('room-draft-malformed');
2005
2394
  const local = await openLocal();
2006
2395
  const current = roomDrafts.get(room)?.get(path);
2007
2396
  if ((current?.revision ?? null) !== revision) {
2008
- return {
2009
- outcome: 'stale',
2010
- current: current === undefined ? null : roomDraftOf(current),
2011
- };
2397
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2012
2398
  }
2399
+ const file = parts.file === undefined
2400
+ ? undefined
2401
+ : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
2013
2402
  const record = {
2014
2403
  locator: room,
2015
2404
  path,
2016
2405
  revision: roomDraftRevision(),
2017
- parts: held(current?.parts, parts),
2406
+ parts: held(current?.parts, parts, file),
2018
2407
  returned: current?.returned ?? null,
2019
2408
  };
2020
2409
  await local.keepRoomDraft(record);
@@ -2028,10 +2417,7 @@ export function composeRuntime(options) {
2028
2417
  const local = await openLocal();
2029
2418
  const current = roomDrafts.get(room)?.get(path);
2030
2419
  if (current === undefined || current.revision !== revision) {
2031
- return {
2032
- outcome: 'stale',
2033
- current: current === undefined ? null : roomDraftOf(current),
2034
- };
2420
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2035
2421
  }
2036
2422
  await local.discardRoomDraft(room, path);
2037
2423
  forgetRoomDraft(room, path);
@@ -2066,6 +2452,10 @@ export function composeRuntime(options) {
2066
2452
  history(locator, path, query) {
2067
2453
  return readHistory(locator, path, query);
2068
2454
  },
2455
+ binaryFile: {
2456
+ read: readBinaryPath,
2457
+ readReference: readBinaryReference,
2458
+ },
2069
2459
  call(name, input) {
2070
2460
  return invokeAction(name, input);
2071
2461
  },
@@ -2098,10 +2488,11 @@ export function composeRuntime(options) {
2098
2488
  const reachable = {
2099
2489
  md,
2100
2490
  watch,
2491
+ referrerScope: (referrer) => options.adapter.referrerScope(referrer),
2101
2492
  plan: planned,
2102
- query: (plan) => readSide.query(plan),
2493
+ query: (plan) => rowsQuery(plan),
2103
2494
  watchPlan,
2104
2495
  search: searched,
2105
2496
  };
2106
- return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2497
+ return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
2107
2498
  }