@agentshouse/mdruntime 0.1.0-c5b50992a4193 → 0.1.0-c5d3fa6fb67bc

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 (193) hide show
  1. package/AGENTS.md +265 -210
  2. package/README.md +13 -3
  3. package/dist/acquisition.d.ts +14 -0
  4. package/dist/acquisition.d.ts.map +1 -0
  5. package/dist/acquisition.js +24 -0
  6. package/dist/adapter.d.ts +80 -20
  7. package/dist/adapter.d.ts.map +1 -1
  8. package/dist/attempt.d.ts +16 -0
  9. package/dist/attempt.d.ts.map +1 -0
  10. package/dist/attempt.js +70 -0
  11. package/dist/binary.d.ts +23 -0
  12. package/dist/binary.d.ts.map +1 -0
  13. package/dist/binary.js +8 -0
  14. package/dist/bootstrap.d.ts +17 -5
  15. package/dist/bootstrap.d.ts.map +1 -1
  16. package/dist/bootstrap.js +113 -37
  17. package/dist/change-set.d.ts +2 -0
  18. package/dist/change-set.d.ts.map +1 -1
  19. package/dist/change-set.js +49 -16
  20. package/dist/change.d.ts +15 -0
  21. package/dist/change.d.ts.map +1 -1
  22. package/dist/change.js +26 -1
  23. package/dist/chat/acts.d.ts +33 -0
  24. package/dist/chat/acts.d.ts.map +1 -0
  25. package/dist/chat/acts.js +36 -0
  26. package/dist/chat/context.d.ts +3 -0
  27. package/dist/chat/context.d.ts.map +1 -0
  28. package/dist/chat/context.js +10 -0
  29. package/dist/chat/input.d.ts +9 -0
  30. package/dist/chat/input.d.ts.map +1 -0
  31. package/dist/chat/input.js +99 -0
  32. package/dist/chat/kit.d.ts +61 -0
  33. package/dist/chat/kit.d.ts.map +1 -0
  34. package/dist/chat/kit.js +123 -0
  35. package/dist/chat/lasting.d.ts +6 -0
  36. package/dist/chat/lasting.d.ts.map +1 -0
  37. package/dist/chat/lasting.js +19 -0
  38. package/dist/chat/list.d.ts +27 -0
  39. package/dist/chat/list.d.ts.map +1 -0
  40. package/dist/chat/list.js +63 -0
  41. package/dist/chat/markdown.d.ts +20 -0
  42. package/dist/chat/markdown.d.ts.map +1 -0
  43. package/dist/chat/markdown.js +188 -0
  44. package/dist/chat/message.d.ts +6 -0
  45. package/dist/chat/message.d.ts.map +1 -0
  46. package/dist/chat/message.js +192 -0
  47. package/dist/chat/messages.d.ts +22 -0
  48. package/dist/chat/messages.d.ts.map +1 -0
  49. package/dist/chat/messages.js +155 -0
  50. package/dist/chat/name.d.ts +6 -0
  51. package/dist/chat/name.d.ts.map +1 -0
  52. package/dist/chat/name.js +37 -0
  53. package/dist/chat/opened.d.ts +11 -0
  54. package/dist/chat/opened.d.ts.map +1 -0
  55. package/dist/chat/opened.js +17 -0
  56. package/dist/chat/overlay.d.ts +31 -0
  57. package/dist/chat/overlay.d.ts.map +1 -0
  58. package/dist/chat/overlay.js +97 -0
  59. package/dist/chat/parts.d.ts +57 -0
  60. package/dist/chat/parts.d.ts.map +1 -0
  61. package/dist/chat/parts.js +119 -0
  62. package/dist/chat/process.d.ts +20 -0
  63. package/dist/chat/process.d.ts.map +1 -0
  64. package/dist/chat/process.js +32 -0
  65. package/dist/chat/provider.d.ts +63 -0
  66. package/dist/chat/provider.d.ts.map +1 -0
  67. package/dist/chat/provider.js +629 -0
  68. package/dist/chat/question.d.ts +9 -0
  69. package/dist/chat/question.d.ts.map +1 -0
  70. package/dist/chat/question.js +80 -0
  71. package/dist/chat/questions.d.ts +31 -0
  72. package/dist/chat/questions.d.ts.map +1 -0
  73. package/dist/chat/questions.js +111 -0
  74. package/dist/chat/read.d.ts +5 -0
  75. package/dist/chat/read.d.ts.map +1 -0
  76. package/dist/chat/read.js +13 -0
  77. package/dist/chat/ready.d.ts +34 -0
  78. package/dist/chat/ready.d.ts.map +1 -0
  79. package/dist/chat/ready.js +91 -0
  80. package/dist/chat/style.d.ts +4 -0
  81. package/dist/chat/style.d.ts.map +1 -0
  82. package/dist/chat/style.js +302 -0
  83. package/dist/chat.d.ts +14 -0
  84. package/dist/chat.d.ts.map +1 -0
  85. package/dist/chat.js +8 -0
  86. package/dist/client.d.ts +25 -12
  87. package/dist/client.d.ts.map +1 -1
  88. package/dist/client.js +68 -16
  89. package/dist/collection.d.ts +21 -12
  90. package/dist/collection.d.ts.map +1 -1
  91. package/dist/collection.js +271 -23
  92. package/dist/connection.d.ts +42 -15
  93. package/dist/connection.d.ts.map +1 -1
  94. package/dist/connection.js +1636 -378
  95. package/dist/conversation.d.ts +25 -10
  96. package/dist/conversation.d.ts.map +1 -1
  97. package/dist/conversation.js +374 -143
  98. package/dist/durability.d.ts +7 -1
  99. package/dist/durability.d.ts.map +1 -1
  100. package/dist/entry.d.ts +17 -0
  101. package/dist/entry.d.ts.map +1 -0
  102. package/dist/entry.js +156 -0
  103. package/dist/expectation.d.ts +21 -0
  104. package/dist/expectation.d.ts.map +1 -0
  105. package/dist/expectation.js +70 -0
  106. package/dist/failure.d.ts +13 -0
  107. package/dist/failure.d.ts.map +1 -0
  108. package/dist/failure.js +19 -0
  109. package/dist/fence.d.ts +6 -0
  110. package/dist/fence.d.ts.map +1 -0
  111. package/dist/fence.js +10 -0
  112. package/dist/host.d.ts +14 -2
  113. package/dist/host.d.ts.map +1 -1
  114. package/dist/host.js +60 -17
  115. package/dist/identity.d.ts +5 -0
  116. package/dist/identity.d.ts.map +1 -1
  117. package/dist/identity.js +19 -1
  118. package/dist/index.d.ts +14 -7
  119. package/dist/index.d.ts.map +1 -1
  120. package/dist/index.js +4 -0
  121. package/dist/memory.d.ts +7 -3
  122. package/dist/memory.d.ts.map +1 -1
  123. package/dist/memory.js +77 -22
  124. package/dist/model.d.ts +7 -4
  125. package/dist/model.d.ts.map +1 -1
  126. package/dist/model.js +10 -1
  127. package/dist/observation.d.ts +5 -2
  128. package/dist/observation.d.ts.map +1 -1
  129. package/dist/observation.js +22 -11
  130. package/dist/open.d.ts +2 -0
  131. package/dist/open.d.ts.map +1 -0
  132. package/dist/open.js +1 -0
  133. package/dist/outcome.d.ts +32 -3
  134. package/dist/outcome.d.ts.map +1 -1
  135. package/dist/outcome.js +42 -1
  136. package/dist/overlay.d.ts +16 -3
  137. package/dist/overlay.d.ts.map +1 -1
  138. package/dist/overlay.js +118 -77
  139. package/dist/query.d.ts +36 -3
  140. package/dist/query.d.ts.map +1 -1
  141. package/dist/query.js +6 -1
  142. package/dist/reach.d.ts +8 -3
  143. package/dist/reach.d.ts.map +1 -1
  144. package/dist/reach.js +60 -24
  145. package/dist/react.d.ts +28 -17
  146. package/dist/react.d.ts.map +1 -1
  147. package/dist/react.js +8 -6
  148. package/dist/read-side.d.ts +13 -6
  149. package/dist/read-side.d.ts.map +1 -1
  150. package/dist/read-side.js +294 -85
  151. package/dist/readiness.d.ts +53 -0
  152. package/dist/readiness.d.ts.map +1 -0
  153. package/dist/readiness.js +33 -0
  154. package/dist/recovery.d.ts +25 -0
  155. package/dist/recovery.d.ts.map +1 -0
  156. package/dist/recovery.js +197 -0
  157. package/dist/refresh.d.ts +10 -0
  158. package/dist/refresh.d.ts.map +1 -1
  159. package/dist/refresh.js +49 -38
  160. package/dist/replica.d.ts +8 -5
  161. package/dist/replica.d.ts.map +1 -1
  162. package/dist/replica.js +124 -67
  163. package/dist/room-draft.d.ts +26 -9
  164. package/dist/room-draft.d.ts.map +1 -1
  165. package/dist/room-draft.js +132 -23
  166. package/dist/rows.d.ts +9 -13
  167. package/dist/rows.d.ts.map +1 -1
  168. package/dist/rows.js +14 -14
  169. package/dist/save.d.ts +31 -1
  170. package/dist/save.d.ts.map +1 -1
  171. package/dist/save.js +376 -45
  172. package/dist/search.d.ts +37 -8
  173. package/dist/search.d.ts.map +1 -1
  174. package/dist/search.js +116 -45
  175. package/dist/shared.d.ts +11 -0
  176. package/dist/shared.d.ts.map +1 -0
  177. package/dist/shared.js +368 -0
  178. package/dist/status.d.ts +6 -2
  179. package/dist/status.d.ts.map +1 -1
  180. package/dist/status.js +0 -8
  181. package/dist/store.d.ts +75 -18
  182. package/dist/store.d.ts.map +1 -1
  183. package/dist/store.js +539 -200
  184. package/dist/turn.d.ts +7 -0
  185. package/dist/turn.d.ts.map +1 -0
  186. package/dist/turn.js +45 -0
  187. package/dist/wire.d.ts +141 -33
  188. package/dist/wire.d.ts.map +1 -1
  189. package/dist/wire.js +61 -31
  190. package/package.json +26 -14
  191. package/dist/attachment.d.ts +0 -3
  192. package/dist/attachment.d.ts.map +0 -1
  193. package/dist/attachment.js +0 -8
@@ -3,19 +3,30 @@ 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
- import { changeSet, mergeChangeSets } from './change-set.js';
9
+ import { changeSet, entering, 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 { indexReferrers, referencesIndexed, 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';
17
+ import { createRecovery } from './recovery.js';
12
18
  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';
19
+ import { buildBatch, joined, limitedIn, requeued } from './save.js';
20
+ import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
21
+ import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
22
+ import { same, saving, STARTING, transferring, } from './status.js';
17
23
  import { detectStorageLoss, nextGeneration } from './durability.js';
18
- import { roomDraftRevision } from './identity.js';
24
+ import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
25
+ import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
26
+ import { FencedMap } from './fence.js';
27
+ import { tally } from './shared.js';
28
+ import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
29
+ import { identityTime, roomDraftRevision } from './identity.js';
19
30
  import { beneath, normalize } from './paths.js';
20
31
  import { openRuntimeStore, } from './store.js';
21
32
  import { unconfirmed } from './transfer.js';
@@ -33,20 +44,59 @@ function declaredSelection(room) {
33
44
  root !== '' &&
34
45
  selection.roots.every((other, at) => at === index || !within(other, root)));
35
46
  }
47
+ const OPENING_LANE = 'opening';
48
+ const CONVERSATION_LANE = 'conversations';
49
+ const ROOM_READS = 256;
50
+ const ROOM_ATTEMPTS = 16;
51
+ const outside = new Map();
52
+ function outsideOf(roots) {
53
+ const key = JSON.stringify(roots);
54
+ const held = outside.get(key);
55
+ if (held !== undefined)
56
+ return held;
57
+ const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
58
+ const made = {
59
+ documents: (path) => (markdownPath(path) ? counted(path) : 0),
60
+ binaries: counted,
61
+ };
62
+ outside.set(key, made);
63
+ return made;
64
+ }
65
+ function abandonedOn(signal) {
66
+ return new Promise((resolve) => {
67
+ if (signal.aborted)
68
+ resolve(null);
69
+ else
70
+ signal.addEventListener('abort', () => resolve(null), { once: true });
71
+ });
72
+ }
73
+ function roomLane(locator) {
74
+ return `room ${locator}`;
75
+ }
36
76
  export function composeRuntime(options) {
77
+ const localSaves = options.adapter.localSaves === true;
37
78
  let authorization = options.authorization;
38
79
  let opening;
39
80
  let store;
40
- const replicas = new Map();
81
+ const fences = new Map();
82
+ const isFenced = (locator) => fences.has(locator);
83
+ let fencings = 0;
84
+ const lastFenced = new Map();
85
+ const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
86
+ let discoveries = 0;
87
+ const replicas = new FencedMap(isFenced);
88
+ const queuesWithoutReplica = new FencedMap(isFenced);
41
89
  const pendings = new Map();
42
- const roomDrafts = new Map();
90
+ const roomDrafts = new FencedMap(isFenced);
43
91
  const presentations = new Map();
44
92
  let limits = [];
45
93
  let capabilities = [];
46
- const rooms = new Map();
94
+ const rooms = new FencedMap(isFenced);
95
+ const counted = new Map();
47
96
  const unreadable = new Set();
48
- const models = new Map();
49
- const liveModels = new Map();
97
+ const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
98
+ const models = new FencedMap(isModelFenced);
99
+ const liveModels = new FencedMap(isModelFenced);
50
100
  const invalidModels = new Set();
51
101
  const discovered = new Map();
52
102
  let roomsListed = false;
@@ -76,19 +126,28 @@ export function composeRuntime(options) {
76
126
  let status = STARTING;
77
127
  let shown = STARTING;
78
128
  let discoveryPaused = false;
79
- let resetTimer;
80
129
  let closed = false;
81
130
  let stopping = false;
82
131
  let withdrawStream;
83
132
  const announced = new Map();
84
133
  let underway = null;
134
+ let streamed = null;
85
135
  let handed = false;
86
- const submitting = new Map();
136
+ const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
137
+ const repairing = new Set();
138
+ const repairable = new Map();
139
+ let outcomeRetention = null;
87
140
  const answering = new Map();
88
141
  let complete = false;
89
142
  let recorded = false;
90
143
  const stale = new Set();
91
- let chain = Promise.resolve();
144
+ let generation = 0;
145
+ let discovering = null;
146
+ let opened = null;
147
+ const owed = new Set();
148
+ const lanes = new Map();
149
+ let roomReads = { generation, free: ROOM_READS, waiting: [] };
150
+ const working = new Set();
92
151
  let mutations = Promise.resolve();
93
152
  const roomListeners = new Set();
94
153
  const refusalListeners = new Set();
@@ -99,27 +158,49 @@ export function composeRuntime(options) {
99
158
  const presentationListeners = new Set();
100
159
  const readSide = createReadSide();
101
160
  let withdrawConversations;
102
- let currencyQueued = false;
161
+ let currencyQueued = null;
103
162
  let socketOpen = false;
104
163
  let serviceMode = 'open';
105
164
  let haltedRefusal = false;
106
165
  let openSignals = 0;
107
166
  const serviceModeListeners = new Set();
167
+ let member = null;
168
+ const memberListeners = new Set();
169
+ const prepared = new Set();
170
+ const projecting = new Set();
171
+ const unpreparedDemand = new Set();
172
+ const unindexable = new Set();
173
+ let preparing = false;
174
+ let demand = WHOLE_DEMAND;
175
+ let narrowing = null;
176
+ let targets = {};
177
+ let persistence = options.shell.persistence;
178
+ let capacity = null;
179
+ const paused = new Set();
180
+ let rechecking = null;
181
+ const readinessWatches = new Set();
182
+ let readinessQueued = false;
183
+ const catching = new Set();
184
+ const transfers = new FencedMap(isFenced);
108
185
  const conversations = createConversationStore({
109
186
  agents: declaredAgents,
110
- documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
187
+ documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
111
188
  call: (name, input) => invokeAction(name, input),
112
189
  durably,
113
190
  stream: () => options.adapter.conversations,
114
191
  publish: publishChangeSet,
192
+ online: () => !status.providerUnreachable && refusal === null,
115
193
  closed: () => closed,
194
+ unanswered: resendLater,
195
+ expired: (operation) => pastHorizon(operation),
116
196
  });
117
197
  const searchIndex = createSearchIndex({
118
- documents: () => readSide.documents(),
198
+ room: (room) => readSide.room(room),
119
199
  document: (room, path) => readSide.document(room, path),
120
- messages: () => conversations.messages(),
121
- message: (conversation, identity) => conversations.message(conversation, identity),
122
200
  });
201
+ const roomsToIndex = new Set();
202
+ const conversationsToIndex = new Set();
203
+ let indexing = false;
123
204
  const journal = createLog();
124
205
  let notifyReady = () => { };
125
206
  const ready = new Promise((resolve) => {
@@ -183,7 +264,9 @@ export function composeRuntime(options) {
183
264
  }
184
265
  }
185
266
  function pending() {
186
- return [...replicas.values()].some((replica) => replica.queue.length > 0);
267
+ return (!localSaves &&
268
+ (queuesWithoutReplica.size > 0 ||
269
+ [...replicas.values()].some((replica) => replica.queue.length > 0)));
187
270
  }
188
271
  function attention() {
189
272
  const attentionRequired = [];
@@ -196,9 +279,16 @@ export function composeRuntime(options) {
196
279
  returned: structuredClone(record.returned),
197
280
  });
198
281
  }
282
+ for (const [room, refusal] of sourceRefusals)
283
+ attentionRequired.push({ room, refusal: structuredClone(refusal) });
199
284
  attentionRequired.sort((left, right) => {
200
285
  if (left.room !== right.room)
201
286
  return left.room < right.room ? -1 : 1;
287
+ if ('refusal' in left || 'refusal' in right) {
288
+ if ('refusal' in left && 'refusal' in right)
289
+ return 0;
290
+ return 'refusal' in left ? -1 : 1;
291
+ }
202
292
  if (left.path !== right.path)
203
293
  return left.path < right.path ? -1 : 1;
204
294
  return 0;
@@ -212,8 +302,21 @@ export function composeRuntime(options) {
212
302
  }
213
303
  return { attentionRequired, reset };
214
304
  }
305
+ function documentsIn(locator) {
306
+ const replica = replicas.get(locator);
307
+ if (replica === undefined)
308
+ return 0;
309
+ const roots = replica.confirmed.record.typedSelection?.roots ?? [];
310
+ const weighed = outsideOf(roots);
311
+ return (tally(replica.visible.documents, weighed.documents) +
312
+ tally(replica.visible.binaries, weighed.binaries));
313
+ }
215
314
  function listed() {
216
- return structuredClone([...rooms.values()]);
315
+ return structuredClone([...rooms.values()].map((room) => {
316
+ const documents = documentsIn(room.locator);
317
+ counted.set(room.locator, documents);
318
+ return { ...room, documents };
319
+ }));
217
320
  }
218
321
  function roomFacts(reach = null) {
219
322
  const held = [...rooms.values()]
@@ -233,18 +336,26 @@ export function composeRuntime(options) {
233
336
  return { listed: roomsListed, rooms: held };
234
337
  }
235
338
  function emitRooms() {
339
+ readSide.recover();
340
+ queueReadiness();
236
341
  const value = listed();
237
342
  for (const listener of roomListeners)
238
343
  listener(value);
239
344
  }
240
345
  function emitRefusal() {
346
+ queueReadiness();
241
347
  for (const listener of refusalListeners)
242
348
  listener(refusal);
243
349
  }
244
350
  function emitStorageLoss() {
351
+ queueReadiness();
245
352
  for (const listener of storageLossListeners)
246
353
  listener(storageLoss);
247
354
  }
355
+ function emitMember() {
356
+ for (const listener of memberListeners)
357
+ listener(member);
358
+ }
248
359
  function moveTo(next) {
249
360
  if (closed)
250
361
  return;
@@ -254,7 +365,8 @@ export function composeRuntime(options) {
254
365
  function show() {
255
366
  if (closed)
256
367
  return;
257
- const next = status.state === 'synced' && !conversations.current()
368
+ queueReadiness();
369
+ const next = status.state === 'synced' && demand.conversations && !conversations.current()
258
370
  ? { ...status, state: 'syncing' }
259
371
  : status;
260
372
  if (same(shown, next))
@@ -264,6 +376,8 @@ export function composeRuntime(options) {
264
376
  listener(shown);
265
377
  }
266
378
  function entryOf(batch) {
379
+ if (localSaves)
380
+ return Object.freeze({ batch, state: 'local' });
267
381
  const state = pendings.get(batch.id);
268
382
  if (state?.accepted !== undefined) {
269
383
  return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
@@ -271,10 +385,7 @@ export function composeRuntime(options) {
271
385
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
272
386
  }
273
387
  function entriesOf(locator) {
274
- const replica = replicas.get(locator);
275
- if (replica === undefined)
276
- return [];
277
- return replica.queue.map(entryOf);
388
+ return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
278
389
  }
279
390
  function emitQueue(locator) {
280
391
  const entries = entriesOf(locator);
@@ -284,12 +395,477 @@ export function composeRuntime(options) {
284
395
  }
285
396
  }
286
397
  function applyChangeSet(set) {
287
- readSide.apply(set);
288
- searchIndex.changed(set);
398
+ const held = {
399
+ ...set,
400
+ documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
401
+ };
402
+ readSide.apply(held);
403
+ searchIndex.changed(held);
404
+ }
405
+ async function referenced(locator) {
406
+ for (;;) {
407
+ const record = replicas.get(locator)?.confirmed.record;
408
+ if (record === undefined || closed)
409
+ return;
410
+ const held = { text: record.documents, binaries: record.binaries };
411
+ const composition = compositionOf(record);
412
+ if (referencesIndexed(held, composition))
413
+ return;
414
+ await indexReferrers(held, composition);
415
+ }
416
+ }
417
+ async function prepareReplica(locator) {
418
+ await referenced(locator);
419
+ const replica = replicas.get(locator);
420
+ if (replica === undefined || closed || prepared.has(locator))
421
+ return;
422
+ const visible = replica.visible;
423
+ const paths = [
424
+ ...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
425
+ ];
426
+ const entered = [];
427
+ projecting.add(locator);
428
+ try {
429
+ await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
430
+ const current = replicas.get(locator);
431
+ if (current === undefined || closed)
432
+ return;
433
+ const set = entering(locator, current, chunk);
434
+ readSide.apply(set);
435
+ entered.push(set);
436
+ });
437
+ if (!replicas.has(locator) || closed)
438
+ return;
439
+ prepared.add(locator);
440
+ }
441
+ finally {
442
+ projecting.delete(locator);
443
+ }
444
+ conversations.changed(mergeChangeSets(entered));
445
+ readSide.recover();
446
+ queueReadiness();
447
+ }
448
+ function unprepared() {
449
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
450
+ }
451
+ function unbuilt(locator, error) {
452
+ unindexable.add(locator);
453
+ journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
454
+ queueReadiness();
455
+ }
456
+ function prepare(locators = []) {
457
+ for (const locator of locators)
458
+ unpreparedDemand.add(locator);
459
+ if (preparing)
460
+ return;
461
+ preparing = true;
462
+ void (async () => {
463
+ try {
464
+ for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
465
+ await yielded();
466
+ if (closed)
467
+ return;
468
+ unpreparedDemand.delete(waiting);
469
+ await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
470
+ }
471
+ }
472
+ finally {
473
+ preparing = false;
474
+ }
475
+ })();
476
+ }
477
+ function reachedRooms(plan) {
478
+ const reach = plan.reach;
479
+ const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
480
+ const named = plan.rooms ??
481
+ (reached === null
482
+ ? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
483
+ : [...reached]);
484
+ return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
485
+ }
486
+ function awaited(locator) {
487
+ return rooms.has(locator) || !roomsListed;
488
+ }
489
+ function covered(plan) {
490
+ const locators = reachedRooms(plan);
491
+ if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
492
+ return { pending: [] };
493
+ }
494
+ const pending = [];
495
+ const coverage = [];
496
+ const overlay = [];
497
+ for (const locator of locators) {
498
+ const replica = replicas.get(locator);
499
+ if (replica === undefined) {
500
+ if (awaited(locator))
501
+ pending.push({ room: locator, awaits: 'acquisition' });
502
+ continue;
503
+ }
504
+ if (!prepared.has(locator)) {
505
+ pending.push({ room: locator, awaits: 'preparation' });
506
+ continue;
507
+ }
508
+ coverage.push({
509
+ room: locator,
510
+ generation: replica.confirmed.record.transfer,
511
+ current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
512
+ });
513
+ for (const batch of replica.queue)
514
+ overlay.push(batch.id);
515
+ }
516
+ if (pending.length > 0)
517
+ return { pending };
518
+ return { coverage: { rooms: coverage, indexes: [], overlay } };
519
+ }
520
+ function awaitPreparation(cover) {
521
+ if (!('pending' in cover))
522
+ return;
523
+ const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
524
+ if (waiting.length > 0)
525
+ prepare(waiting.map(({ room }) => room));
526
+ }
527
+ function acquires(room) {
528
+ return (demanded(demand, room) ||
529
+ (replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
530
+ queuesWithoutReplica.has(room.locator));
531
+ }
532
+ function acquiredLocators() {
533
+ return [...discovered.values()]
534
+ .filter(acquires)
535
+ .map((room) => room.locator)
536
+ .sort();
537
+ }
538
+ function heldDemand(acquired) {
539
+ if (demand.whole)
540
+ return null;
541
+ return { rooms: acquired.map((room) => room.locator), conversations: demand.conversations };
542
+ }
543
+ function applyDemand(next) {
544
+ const before = acquiredLocators();
545
+ const conversed = demand.conversations;
546
+ demand = next;
547
+ const after = acquiredLocators();
548
+ queueReadiness();
549
+ if (!conversed && next.conversations)
550
+ scheduleCurrency();
551
+ if (JSON.stringify(before) === JSON.stringify(after) && conversed === next.conversations)
552
+ return;
553
+ journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
554
+ if (options.adapter.roomStream === undefined)
555
+ void sync('unstreamed');
556
+ else if (socketOpen)
557
+ options.adapter.roomStream.reopen();
558
+ }
559
+ function acquire(next) {
560
+ if (closed)
561
+ return;
562
+ if (narrowing !== null)
563
+ clearTimeout(narrowing);
564
+ narrowing = null;
565
+ const widening = wider(next, demand);
566
+ if (!widening && !narrower(next, demand))
567
+ return;
568
+ if (widening || !roomsListed) {
569
+ applyDemand(next);
570
+ return;
571
+ }
572
+ // A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
573
+ narrowing = setTimeout(() => {
574
+ narrowing = null;
575
+ applyDemand(next);
576
+ }, 5_000);
577
+ }
578
+ function requiredRooms(reach) {
579
+ if (reach === null)
580
+ return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
581
+ const named = [...new Set(reach.rooms)].sort();
582
+ if (roomsListed)
583
+ return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
584
+ return { listed: named.every((room) => replicas.has(room)), rooms: named };
585
+ }
586
+ function roomState(locator, search) {
587
+ if (sourceRefusals.has(locator))
588
+ return 'refused';
589
+ const listedRoom = discovered.get(locator);
590
+ if (roomsListed &&
591
+ (listedRoom === undefined ||
592
+ listedRoom.replica.admission !== 'admitted' ||
593
+ unreadable.has(locator)))
594
+ return 'unadmitted';
595
+ if (!replicas.has(locator))
596
+ return 'acquiring';
597
+ if (!prepared.has(locator))
598
+ return 'preparing';
599
+ if (search && !searchIndex.rooms().has(locator))
600
+ return 'indexing';
601
+ return 'complete';
602
+ }
603
+ function conversationCoverage(reach, search) {
604
+ const declared = reach === null || reach.conversations;
605
+ if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
606
+ return 'undeclared';
607
+ if (!conversations.known() || !conversations.current())
608
+ return 'acquiring';
609
+ if (!search)
610
+ return 'complete';
611
+ const unindexed = conversations
612
+ .searchable()
613
+ .filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
614
+ if (unindexed.length === 0)
615
+ return 'complete';
616
+ index([], unindexed);
617
+ return 'indexing';
618
+ }
619
+ function connectionOf() {
620
+ if (socketOpen)
621
+ return 'connected';
622
+ if (status.providerUnreachable)
623
+ return 'unreachable';
624
+ return options.adapter.roomStream === undefined && status.lastCompleted !== null
625
+ ? 'connected'
626
+ : 'connecting';
627
+ }
628
+ function refusedIn(locators) {
629
+ return locators.flatMap((room) => {
630
+ const held = sourceRefusals.get(room);
631
+ if (held === undefined)
632
+ return [];
633
+ const { file, location } = held.fields;
634
+ return [
635
+ {
636
+ room,
637
+ file: typeof file === 'string' ? file : '',
638
+ rule: held.code,
639
+ location: typeof location === 'string' ? location : null,
640
+ },
641
+ ];
642
+ });
643
+ }
644
+ function readinessFor(port, reach) {
645
+ const required = requiredRooms(reach);
646
+ const search = port.search === 'required';
647
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
648
+ const building = (state) => coverage
649
+ .filter((room) => room.state === state && !unindexable.has(room.room))
650
+ .map(({ room }) => room);
651
+ const preparing = building('preparing');
652
+ if (preparing.length > 0)
653
+ prepare(preparing);
654
+ const indexing = building('indexing');
655
+ if (indexing.length > 0)
656
+ index(indexing, []);
657
+ return readinessOf({
658
+ code: port.code,
659
+ coverage: {
660
+ listed: required.listed,
661
+ rooms: coverage,
662
+ conversations: conversationCoverage(reach, search),
663
+ search: search ? 'required' : 'unused',
664
+ },
665
+ persistence,
666
+ connection: connectionOf(),
667
+ house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
668
+ capacity: capacity !== null &&
669
+ coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
670
+ ? capacity
671
+ : port.capacity,
672
+ refused: refusedIn(required.rooms),
673
+ lost: storageLoss !== null,
674
+ established: targets[port.target]?.established === true,
675
+ });
676
+ }
677
+ function establish(name) {
678
+ targets = { ...targets, [name]: { established: true } };
679
+ queueReadiness();
680
+ void durably((local) => local.keepTargets(targets)).catch(() => {
681
+ journal.record(`target record failed ${name}`);
682
+ });
683
+ }
684
+ function deliverReadiness(watch) {
685
+ if (!readinessWatches.has(watch))
686
+ return;
687
+ const readiness = readinessFor(watch.port, watch.reach);
688
+ if (readiness.ready && targets[watch.port.target]?.established !== true) {
689
+ establish(watch.port.target);
690
+ }
691
+ const shown = JSON.stringify(readiness);
692
+ if (shown === watch.last)
693
+ return;
694
+ watch.last = shown;
695
+ watch.deliver(structuredClone(readiness));
696
+ }
697
+ function queueReadiness() {
698
+ if (readinessQueued || closed || readinessWatches.size === 0)
699
+ return;
700
+ readinessQueued = true;
701
+ queueMicrotask(() => {
702
+ readinessQueued = false;
703
+ if (closed)
704
+ return;
705
+ for (const watch of [...readinessWatches])
706
+ deliverReadiness(watch);
707
+ });
708
+ }
709
+ function watchReadiness(port, reach, deliver) {
710
+ const watch = { port, reach, deliver, last: null };
711
+ readinessWatches.add(watch);
712
+ void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
713
+ return () => {
714
+ readinessWatches.delete(watch);
715
+ };
716
+ }
717
+ function layerBytes(layer) {
718
+ let bytes = 0;
719
+ for (const [path, text] of Object.entries(layer.documents))
720
+ bytes += (path.length + text.length) * 2;
721
+ return bytes;
722
+ }
723
+ function grownBy(next, previous) {
724
+ if (previous === undefined)
725
+ return layerBytes(next);
726
+ const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
727
+ return layerBytes({ ...next, documents });
728
+ }
729
+ function exceeded(error) {
730
+ return (typeof error === 'object' &&
731
+ error !== null &&
732
+ error.name === 'QuotaExceededError');
733
+ }
734
+ async function estimated() {
735
+ const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
736
+ return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
737
+ }
738
+ function pause(locator, cause) {
739
+ paused.add(locator);
740
+ capacity = cause;
741
+ journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
742
+ queueReadiness();
743
+ if (rechecking !== null)
744
+ return;
745
+ // A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
746
+ rechecking = setInterval(() => {
747
+ void recheck();
748
+ }, 60_000);
749
+ }
750
+ async function recheck() {
751
+ const held = capacity;
752
+ if (held === null || closed)
753
+ return;
754
+ const available = await estimated().catch(() => null);
755
+ if (available === null || available < (held.needed ?? 0))
756
+ return;
757
+ if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
758
+ return;
759
+ resume();
760
+ }
761
+ function resume() {
762
+ if (rechecking !== null)
763
+ clearInterval(rechecking);
764
+ rechecking = null;
765
+ if (paused.size === 0 && capacity === null)
766
+ return;
767
+ paused.clear();
768
+ capacity = null;
769
+ journal.record('capacity resumed');
770
+ queueReadiness();
771
+ if (!closed && authorization !== undefined)
772
+ void sync('unstreamed');
773
+ }
774
+ async function reclaimed(local) {
775
+ const unreferenced = [...transfers.keys()].filter((locator) => {
776
+ const room = discovered.get(locator);
777
+ return room === undefined || !acquires(room);
778
+ });
779
+ for (const locator of unreferenced) {
780
+ await local.abandon(locator);
781
+ transfers.delete(locator);
782
+ }
783
+ if (unreferenced.length > 0)
784
+ journal.record(`capacity reclaimed ${unreferenced.length}`);
785
+ return unreferenced.length > 0;
786
+ }
787
+ async function admitted(local, locator, needed) {
788
+ if (paused.has(locator))
789
+ return false;
790
+ let available = await estimated().catch(() => null);
791
+ if (available === null || available >= needed)
792
+ return true;
793
+ if (await reclaimed(local))
794
+ available = await estimated().catch(() => null);
795
+ if (available === null || available >= needed)
796
+ return true;
797
+ pause(locator, { cause: 'insufficient-space', needed, available });
798
+ return false;
799
+ }
800
+ async function exceededOn(locator, bytes) {
801
+ const available = await estimated().catch(() => null);
802
+ pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
803
+ }
804
+ async function rowsQuery(plan) {
805
+ const before = covered(plan);
806
+ awaitPreparation(before);
807
+ if ('pending' in before)
808
+ return before;
809
+ const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
810
+ awaitPreparation(cover);
811
+ if ('pending' in cover)
812
+ return cover;
813
+ return 'groups' in rows
814
+ ? { groups: rows.groups, coverage: cover.coverage }
815
+ : { documents: rows.documents, coverage: cover.coverage };
816
+ }
817
+ async function indexConversation(conversation) {
818
+ searchIndex.indexMessages(conversation, [], false);
819
+ let after = null;
820
+ for (;;) {
821
+ const batch = await durably((local) => local.messagesAfter(conversation, after));
822
+ if (closed)
823
+ return;
824
+ searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
825
+ if (batch.length === 0)
826
+ return;
827
+ after = batch[batch.length - 1].identity;
828
+ }
829
+ }
830
+ function indexNext() {
831
+ const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
832
+ if (room !== undefined) {
833
+ roomsToIndex.delete(room);
834
+ return () => prepared.has(room)
835
+ ? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
836
+ : undefined;
837
+ }
838
+ const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
839
+ if (conversation === undefined)
840
+ return null;
841
+ conversationsToIndex.delete(conversation);
842
+ return () => indexConversation(conversation);
843
+ }
844
+ function index(rooms, named) {
845
+ for (const room of rooms)
846
+ roomsToIndex.add(room);
847
+ for (const conversation of named)
848
+ conversationsToIndex.add(conversation);
849
+ if (indexing)
850
+ return;
851
+ indexing = true;
852
+ void (async () => {
853
+ try {
854
+ for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
855
+ await yielded();
856
+ await unit();
857
+ queueReadiness();
858
+ }
859
+ }
860
+ finally {
861
+ indexing = false;
862
+ }
863
+ })().catch(() => { });
289
864
  }
290
865
  function publishChangeSet(set) {
291
866
  applyChangeSet(set);
292
- if (set.rooms.length > 0)
867
+ if (set.rooms.length > 0 ||
868
+ [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
293
869
  emitRooms();
294
870
  for (const locator of set.queues)
295
871
  emitQueue(locator);
@@ -301,34 +877,7 @@ export function composeRuntime(options) {
301
877
  emitPresentation(app);
302
878
  conversations.changed(set);
303
879
  }
304
- function stopTimer() {
305
- if (resetTimer === undefined)
306
- return;
307
- clearTimeout(resetTimer);
308
- resetTimer = undefined;
309
- }
310
- function armTimer() {
311
- stopTimer();
312
- if (closed || stopping)
313
- return;
314
- let earliest = null;
315
- for (const { submission } of pendings.values()) {
316
- if (submission.state !== 'reset')
317
- continue;
318
- const at = Date.parse(submission.reset);
319
- if (earliest === null || at < earliest)
320
- earliest = at;
321
- }
322
- if (earliest === null)
323
- return;
324
- // A Save House held back goes again only once the reset its answer named has passed.
325
- resetTimer = setTimeout(() => {
326
- resetTimer = undefined;
327
- submitAll();
328
- }, Math.max(0, earliest - Date.now()));
329
- }
330
880
  async function refuse(reason, provider = null) {
331
- stopTimer();
332
881
  discoveryPaused = true;
333
882
  try {
334
883
  if (reason === 'revoked-authorization') {
@@ -336,10 +885,7 @@ export function composeRuntime(options) {
336
885
  const revoked = { status: 'unavailable', reason: 'revoked' };
337
886
  try {
338
887
  const local = await openLocal();
339
- await local.revokeModels([...replicas.values()].map((replica) => ({
340
- record: replica.confirmed.record,
341
- queue: replica.queue,
342
- })));
888
+ await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
343
889
  }
344
890
  catch {
345
891
  journal.record('model revocation persistence failed');
@@ -363,10 +909,7 @@ export function composeRuntime(options) {
363
909
  model: revoked,
364
910
  selectionModel: revoked,
365
911
  };
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 } });
912
+ replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
370
913
  }
371
914
  rooms.set(locator, {
372
915
  ...room,
@@ -383,7 +926,8 @@ export function composeRuntime(options) {
383
926
  }
384
927
  finally {
385
928
  refusal = {
386
- reason,
929
+ code: reason,
930
+ retryable: false,
387
931
  reauthorizationOffered: reason === 'revoked-authorization',
388
932
  refusal: provider,
389
933
  };
@@ -408,19 +952,28 @@ export function composeRuntime(options) {
408
952
  },
409
953
  providerOwnedPaths: room.providerOwnedPaths,
410
954
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
411
- attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
412
955
  synchronizationAvailable,
413
956
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
414
957
  });
415
958
  }
416
- async function applyDiscovery(listed, local) {
959
+ async function applyDiscovery(found, started) {
960
+ for (const room of found) {
961
+ const fence = fences.get(room.locator);
962
+ if (fence !== undefined && fence.erased && fence.at < started)
963
+ fences.delete(room.locator);
964
+ }
965
+ const listed = found.filter((room) => !fences.has(room.locator));
417
966
  const listedLocators = new Set(listed.map((room) => room.locator));
418
967
  for (const locator of listedLocators)
419
968
  scopedRefusals.delete(locator);
420
969
  discovered.clear();
421
970
  for (const room of listed)
422
971
  discovered.set(room.locator, room);
423
- await dropUnlisted(listedLocators, local);
972
+ const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
973
+ if (options.adapter.exhaustiveDiscovery === true)
974
+ await loseRooms(unlisted);
975
+ else
976
+ unlistedKept(unlisted);
424
977
  for (const room of listed) {
425
978
  const held = replicas.get(room.locator)?.confirmed.record;
426
979
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -431,43 +984,85 @@ export function composeRuntime(options) {
431
984
  }
432
985
  roomsListed = true;
433
986
  emitRooms();
987
+ return listed;
434
988
  }
435
989
  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);
990
+ return [
991
+ ...new Set([
992
+ ...rooms.keys(),
993
+ ...roomDrafts.keys(),
994
+ ...transfers.keys(),
995
+ ...queuesWithoutReplica.keys(),
996
+ ]),
997
+ ].filter((locator) => !listed.has(locator));
998
+ }
999
+ function unlistedKept(unlisted) {
1000
+ for (const locator of unlisted) {
1001
+ const held = replicas.get(locator)?.confirmed.record;
1002
+ if (held !== undefined)
1003
+ list(held, false);
1004
+ else
453
1005
  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}`);
1006
+ }
1007
+ }
1008
+ function fence(locators) {
1009
+ const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1010
+ const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
1011
+ room: true,
1012
+ queue: true,
1013
+ drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
1014
+ }));
1015
+ for (const locator of fresh) {
1016
+ fences.set(locator, { at: discoveries, erased: false });
1017
+ lastFenced.set(locator, ++fencings);
1018
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1019
+ pendings.delete(batch.id);
1020
+ replicas.delete(locator);
1021
+ queuesWithoutReplica.delete(locator);
1022
+ rooms.delete(locator);
1023
+ roomDrafts.delete(locator);
1024
+ transfers.delete(locator);
1025
+ catching.delete(locator);
1026
+ discovered.delete(locator);
1027
+ announced.delete(locator);
1028
+ unreadable.delete(locator);
1029
+ stale.delete(locator);
1030
+ repairable.delete(locator);
1031
+ unindexable.delete(locator);
1032
+ paused.delete(locator);
1033
+ scopedRefusals.delete(locator);
1034
+ sourceRefusals.delete(locator);
1035
+ declarationRefusals.delete(locator);
1036
+ for (const attached of ['collection', 'typed-selection']) {
1037
+ const key = modelKey(locator, attached);
1038
+ models.delete(key);
1039
+ liveModels.delete(key);
1040
+ invalidModels.delete(key);
467
1041
  }
468
- publishChangeSet(mergeChangeSets(removed));
469
- moveTo({ ...status, ...attention() });
470
- });
1042
+ journal.record(`room lost ${locator}`);
1043
+ }
1044
+ if (fresh.length === 0)
1045
+ return fresh;
1046
+ publishChangeSet(mergeChangeSets(removed));
1047
+ for (const locator of fresh)
1048
+ prepared.delete(locator);
1049
+ searchIndex.forget(fresh);
1050
+ readSide.recover();
1051
+ moveTo({ ...status, ...attention() });
1052
+ return fresh;
1053
+ }
1054
+ async function erased(local, locators) {
1055
+ await local.erase(locators);
1056
+ for (const locator of locators) {
1057
+ const held = fences.get(locator);
1058
+ if (held !== undefined)
1059
+ held.erased = true;
1060
+ }
1061
+ }
1062
+ async function loseRooms(locators) {
1063
+ const fresh = fence(locators);
1064
+ if (fresh.length > 0)
1065
+ await durably((local) => erased(local, fresh));
471
1066
  }
472
1067
  function restored(key, persisted) {
473
1068
  const prior = models.get(key);
@@ -488,7 +1083,7 @@ export function composeRuntime(options) {
488
1083
  return model;
489
1084
  }
490
1085
  async function hydrate(opened) {
491
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
1086
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
492
1087
  opened.all(),
493
1088
  opened.queues(),
494
1089
  opened.roomDrafts(),
@@ -498,9 +1093,17 @@ export function composeRuntime(options) {
498
1093
  opened.lastCompleted(),
499
1094
  opened.providerLimits(),
500
1095
  opened.capabilities(),
1096
+ opened.member(),
1097
+ opened.transfers(),
1098
+ opened.targets(),
501
1099
  ]);
1100
+ targets = heldTargets;
1101
+ transfers.clear();
1102
+ for (const record of staged)
1103
+ transfers.set(record.locator, record);
502
1104
  limits = declared;
503
1105
  capabilities = named;
1106
+ member = known;
504
1107
  roomDrafts.clear();
505
1108
  for (const record of kept)
506
1109
  rememberRoomDraft(record);
@@ -516,18 +1119,17 @@ export function composeRuntime(options) {
516
1119
  }
517
1120
  const previousReplicas = new Map(replicas);
518
1121
  replicas.clear();
1122
+ queuesWithoutReplica.clear();
1123
+ const replicated = new Set(records.map((record) => record.locator));
1124
+ for (const [locator, queue] of queuedBatches) {
1125
+ if (!replicated.has(locator))
1126
+ queuesWithoutReplica.set(locator, queue);
1127
+ }
519
1128
  for (const record of records) {
520
1129
  const model = restored(modelKey(record.locator, 'collection'), record.model);
521
1130
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
522
1131
  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
- };
1132
+ const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
531
1133
  replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
532
1134
  const existing = rooms.get(record.locator);
533
1135
  if (existing === undefined)
@@ -536,7 +1138,6 @@ export function composeRuntime(options) {
536
1138
  rooms.set(record.locator, {
537
1139
  ...existing,
538
1140
  roomVersion: record.roomVersion,
539
- attachments: record.attachments,
540
1141
  operations: record.operations,
541
1142
  replica: record.replica,
542
1143
  model: modelTypes(model),
@@ -548,10 +1149,6 @@ export function composeRuntime(options) {
548
1149
  },
549
1150
  });
550
1151
  }
551
- if (read.kind === 'refused') {
552
- refusedSource(record, read);
553
- declarationRefusals.add(record.locator);
554
- }
555
1152
  }
556
1153
  return {
557
1154
  lastCompleted,
@@ -562,16 +1159,26 @@ export function composeRuntime(options) {
562
1159
  async function readState(opened) {
563
1160
  const { lastCompleted, locators, read } = await hydrate(opened);
564
1161
  applyChangeSet(read);
1162
+ prepare();
565
1163
  if (replicas.size > 0)
566
1164
  emitRooms();
1165
+ if (member !== null)
1166
+ emitMember();
567
1167
  for (const locator of locators)
568
1168
  emitQueue(locator);
569
1169
  publishRoomDrafts(false);
570
1170
  return lastCompleted;
571
1171
  }
572
1172
  async function open() {
573
- const opened = await openRuntimeStore();
1173
+ if (closed)
1174
+ throw new Error('runtime closed');
1175
+ const opened = await openRuntimeStore(isFenced);
574
1176
  store = opened;
1177
+ const interrupted = await opened.fences();
1178
+ for (const locator of interrupted) {
1179
+ fences.set(locator, { at: discoveries, erased: false });
1180
+ lastFenced.set(locator, ++fencings);
1181
+ }
575
1182
  const kept = options.shell.generation;
576
1183
  const storedGeneration = await opened.generation();
577
1184
  const loss = detectStorageLoss(storedGeneration, kept);
@@ -585,6 +1192,8 @@ export function composeRuntime(options) {
585
1192
  emitStorageLoss();
586
1193
  }
587
1194
  const lastCompleted = await readState(opened);
1195
+ if (interrupted.length > 0)
1196
+ await erased(opened, interrupted);
588
1197
  moveTo({ ...status, lastCompleted, ...attention() });
589
1198
  return opened;
590
1199
  }
@@ -611,11 +1220,12 @@ export function composeRuntime(options) {
611
1220
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
612
1221
  }
613
1222
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
614
- const withdrawn = withdrawals(confirmed, queue, withdrawable);
1223
+ const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
1224
+ const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
615
1225
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
616
1226
  const unqueued = withdrawn.map((batch) => ({
617
1227
  batch,
618
- returned: landed(batch, confirmed) ? null : { kind: 'withdrawn' },
1228
+ returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
619
1229
  }));
620
1230
  const returning = [
621
1231
  ...[...unqueued].reverse(),
@@ -636,28 +1246,48 @@ export function composeRuntime(options) {
636
1246
  });
637
1247
  }
638
1248
  }
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 ?? []) {
1249
+ await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
1250
+ for (const batch of previousQueue) {
647
1251
  if (!kept.includes(batch))
648
1252
  pendings.delete(batch.id);
649
1253
  }
650
1254
  for (const record of written.values())
651
1255
  rememberRoomDraft(record);
652
- for (const { batch, returned } of unqueued) {
653
- journal.record(`outcome ${locator} ${batch.id} ${returned === null ? 'landed' : 'withdrawn'}`);
654
- }
655
- replicas.set(locator, replicaOf(confirmed, kept, before));
656
- return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
657
- room: settling.room,
658
- queue: kept.length !== (before?.queue.length ?? 0),
659
- drafts: [...written.keys()],
660
- });
1256
+ try {
1257
+ await local.write({
1258
+ next: confirmed.record,
1259
+ previous: before?.confirmed.record ?? null,
1260
+ ...(settling.stored === undefined ? {} : { stored: settling.stored }),
1261
+ });
1262
+ }
1263
+ catch (error) {
1264
+ const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
1265
+ if (held !== null)
1266
+ replicas.set(locator, held);
1267
+ else if (kept.length > 0)
1268
+ queuesWithoutReplica.set(locator, kept);
1269
+ else
1270
+ queuesWithoutReplica.delete(locator);
1271
+ publishChangeSet(changeSet(locator, before ?? null, held, {
1272
+ queue: kept.length !== previousQueue.length,
1273
+ drafts: [...written.keys()],
1274
+ }));
1275
+ moveTo({ ...status, ...attention() });
1276
+ throw error;
1277
+ }
1278
+ return () => {
1279
+ queuesWithoutReplica.delete(locator);
1280
+ for (const { batch, returned } of unqueued) {
1281
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
1282
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1283
+ }
1284
+ replicas.set(locator, replicaOf(confirmed, kept, before));
1285
+ return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
1286
+ room: settling.room,
1287
+ queue: kept.length !== previousQueue.length,
1288
+ drafts: [...written.keys()],
1289
+ });
1290
+ };
661
1291
  }
662
1292
  function adoptModels(locator, record) {
663
1293
  for (const [attached, model] of [
@@ -690,10 +1320,16 @@ export function composeRuntime(options) {
690
1320
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
691
1321
  };
692
1322
  }
693
- function publish(local, room, confirmed, base) {
1323
+ function publish(local, room, confirmed, landing, live) {
1324
+ const { base, stored } = landing;
694
1325
  return exclusive(async () => {
695
- if (!continuing())
1326
+ if (stored !== undefined) {
1327
+ await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
1328
+ }
1329
+ if (!live()) {
1330
+ caughtUpTo(room.locator);
696
1331
  return true;
1332
+ }
697
1333
  const prior = replicas.get(room.locator);
698
1334
  if (base !== undefined &&
699
1335
  (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
@@ -701,47 +1337,85 @@ export function composeRuntime(options) {
701
1337
  return false;
702
1338
  }
703
1339
  adoptModels(room.locator, confirmed.record);
704
- const queue = prior?.queue ?? [];
1340
+ const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
705
1341
  const roomVersion = confirmed.record.roomVersion;
706
1342
  const accepted = queue.filter((batch) => {
707
1343
  const accepted = pendings.get(batch.id)?.accepted;
708
1344
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
709
1345
  });
710
- const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
1346
+ let settled;
1347
+ try {
1348
+ settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
1349
+ }
1350
+ catch (error) {
1351
+ if (!exceeded(error))
1352
+ throw error;
1353
+ await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
1354
+ caughtUpTo(room.locator);
1355
+ return true;
1356
+ }
1357
+ const set = settled();
1358
+ if (stored !== undefined) {
1359
+ transfers.delete(room.locator);
1360
+ catching.delete(room.locator);
1361
+ }
711
1362
  for (const batch of accepted)
712
1363
  pendings.delete(batch.id);
713
1364
  stale.delete(room.locator);
1365
+ unindexable.delete(room.locator);
714
1366
  if (unreadable.delete(room.locator))
715
1367
  scopedRefusals.delete(room.locator);
716
1368
  sourceRefusals.delete(room.locator);
717
1369
  declarationRefusals.delete(room.locator);
718
1370
  list(room, true);
1371
+ if (prior === undefined)
1372
+ prepared.add(room.locator);
719
1373
  publishChangeSet(set);
720
1374
  moveTo({ ...status, ...attention() });
1375
+ options.adapter.roomStream?.published(room.locator, roomVersion);
721
1376
  return true;
722
1377
  });
723
1378
  }
724
1379
  async function saveRoomDrafts(locator, named, writtenBy) {
725
1380
  const local = await openLocal();
1381
+ await referenced(locator);
726
1382
  const replica = replicas.get(locator);
727
1383
  const room = rooms.get(locator);
728
1384
  if (replica === undefined || room === undefined) {
729
- throw new Error(`No Local Replica for ${locator}`);
1385
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
730
1386
  }
731
1387
  const saved = [];
732
1388
  for (const { path, revision } of named) {
733
1389
  const record = roomDrafts.get(locator)?.get(path);
734
1390
  if (record === undefined || record.revision !== revision) {
735
- return { outcome: 'refused', rule: 'stale', file: path };
1391
+ return { outcome: 'refused', code: 'stale', retryable: false, file: path };
736
1392
  }
737
1393
  if (!saved.some((draft) => draft.path === path))
738
1394
  saved.push({ path, parts: record.parts });
739
1395
  }
740
1396
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
741
1397
  if (changed.length > 0)
742
- return { outcome: 'refused', rule: 'changed-underneath', parts: changed };
1398
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
743
1399
  const parts = saved.map(({ path, parts }) => changesOf(path, parts));
744
- const built = buildBatch({
1400
+ const changes = parts.flatMap(({ changes }) => changes);
1401
+ const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
1402
+ if (requeue.kind === 'refused')
1403
+ return { outcome: 'refused', ...requeue.refusal };
1404
+ const foreign = [...requeue.withdrawn.keys()].some((origin) => {
1405
+ const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
1406
+ return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
1407
+ });
1408
+ if (foreign) {
1409
+ return {
1410
+ outcome: 'refused',
1411
+ code: 'unknown-revision',
1412
+ retryable: false,
1413
+ file: [...requeue.queued].at(-1),
1414
+ };
1415
+ }
1416
+ const withdrawing = requeue.withdrawn.size > 0;
1417
+ const input = {
1418
+ local: localSaves,
745
1419
  baseRoomVersion: replica.confirmed.record.roomVersion,
746
1420
  composition: compositionFor(replica.confirmed.record),
747
1421
  operations: room.operations,
@@ -749,29 +1423,93 @@ export function composeRuntime(options) {
749
1423
  providerLimits: limits,
750
1424
  handle: room.handle,
751
1425
  visible: replica.visible,
752
- changes: parts.flatMap(({ changes }) => changes),
1426
+ changes,
753
1427
  bases: parts.flatMap(({ bases }) => bases),
754
1428
  writtenBy,
1429
+ clean: {
1430
+ text: replica.confirmed.record.documents,
1431
+ binaries: replica.confirmed.record.binaries,
1432
+ composition: compositionOf(replica.confirmed.record),
1433
+ },
1434
+ };
1435
+ const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
1436
+ if (checked?.kind === 'refused')
1437
+ return { outcome: 'refused', ...checked.refusal };
1438
+ const owners = [...requeue.owners].flatMap((origin) => {
1439
+ const batch = requeue.withdrawn.get(origin);
1440
+ return batch === undefined || batch === null ? [] : [{ origin, batch }];
755
1441
  });
756
- if (built.kind === 'refused')
1442
+ if (owners.length > 1) {
1443
+ return {
1444
+ outcome: 'refused',
1445
+ code: 'unknown-revision',
1446
+ retryable: false,
1447
+ file: [...requeue.queued].at(-1),
1448
+ };
1449
+ }
1450
+ const built = !withdrawing
1451
+ ? buildBatch(input)
1452
+ : requeue.changes.length === 0
1453
+ ? null
1454
+ : buildBatch({
1455
+ ...input,
1456
+ visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
1457
+ changes: requeue.changes,
1458
+ });
1459
+ if (built?.kind === 'refused')
757
1460
  return { outcome: 'refused', ...built.refusal };
758
- const batches = [...replica.queue, built.batch];
1461
+ const [owner] = owners;
1462
+ const join = built === null || owner === undefined
1463
+ ? null
1464
+ : joined(requeue.queue, owner.batch, built.batch);
1465
+ if (join?.kind === 'refused')
1466
+ return { outcome: 'refused', ...join.refusal };
1467
+ const withdrawn = new Map(requeue.withdrawn);
1468
+ if (join !== null && owner !== undefined)
1469
+ withdrawn.set(owner.origin, join.batch);
1470
+ const drafted = new Map();
1471
+ for (const [origin, replaced] of withdrawn) {
1472
+ if (replaced === null)
1473
+ continue;
1474
+ const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
1475
+ drafted.set(replaced.id, replaced === join?.batch
1476
+ ? [
1477
+ ...carried.map((held) => joinedDraft(held, saved)),
1478
+ ...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
1479
+ ]
1480
+ : carried);
1481
+ }
1482
+ if (built !== null && join === null)
1483
+ drafted.set(built.batch.id, saved);
1484
+ const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
1485
+ for (const replaced of withdrawn.values()) {
1486
+ if (replaced === null || !batches.includes(replaced))
1487
+ continue;
1488
+ const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
1489
+ if (limited !== null)
1490
+ return { outcome: 'refused', ...limited };
1491
+ }
759
1492
  const consumed = saved.map(({ path }) => path);
760
1493
  try {
761
- await local.write({
762
- next: replica.confirmed.record,
763
- previous: replica.confirmed.record,
1494
+ await local.keepAuthored({
1495
+ locator,
764
1496
  queue: batches,
765
- previousQueue: replica.queue,
766
- saved,
1497
+ saved: drafted,
767
1498
  consumedRoomDrafts: consumed,
768
1499
  });
769
1500
  }
770
1501
  catch {
771
- return { outcome: 'refused', rule: 'durable-write' };
1502
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
1503
+ }
1504
+ if (fences.has(locator))
1505
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1506
+ for (const origin of withdrawn.keys())
1507
+ pendings.delete(origin);
1508
+ for (const [id, drafts] of drafted) {
1509
+ pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
772
1510
  }
773
- pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
774
- recorded = false;
1511
+ if (!localSaves)
1512
+ recorded = false;
775
1513
  for (const path of consumed)
776
1514
  forgetRoomDraft(locator, path);
777
1515
  replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
@@ -779,9 +1517,12 @@ export function composeRuntime(options) {
779
1517
  queue: true,
780
1518
  drafts: consumed,
781
1519
  }));
782
- moveTo({ ...saving(status), ...attention() });
1520
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
783
1521
  submitLater(locator);
784
- return { outcome: 'saved', identifier: built.batch.id };
1522
+ const identifier = join?.batch.id ??
1523
+ built?.batch.id ??
1524
+ [...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
1525
+ return { outcome: 'saved', identifier };
785
1526
  }
786
1527
  async function mark(local, locator, identifier, submission, lost) {
787
1528
  const state = pendings.get(identifier);
@@ -791,6 +1532,8 @@ export function composeRuntime(options) {
791
1532
  return;
792
1533
  }
793
1534
  await local.submit(locator, identifier, submission, lost);
1535
+ if (fences.has(locator))
1536
+ return;
794
1537
  pendings.set(identifier, { ...state, submission, lost });
795
1538
  const replica = replicas.get(locator);
796
1539
  if (replica !== undefined)
@@ -801,22 +1544,24 @@ export function composeRuntime(options) {
801
1544
  if (!continuing())
802
1545
  throw new Error('connection refused');
803
1546
  await local.keepUpload(locator, identifier, upload);
1547
+ if (fences.has(locator))
1548
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
804
1549
  pendings.set(identifier, { ...pendings.get(identifier), upload });
805
1550
  if (!continuing())
806
1551
  throw new Error('connection refused');
807
1552
  }
808
1553
  async function readHistory(locator, path, query = {}) {
809
1554
  if (closed || refusal !== null)
810
- return { kind: 'transport-uncertainty' };
1555
+ return UNCERTAIN;
811
1556
  try {
812
1557
  await ensureAuthorization();
813
1558
  }
814
1559
  catch {
815
- return { kind: 'transport-uncertainty' };
816
- }
817
- if (closed || authorization === undefined || refusal !== null) {
818
- return { kind: 'transport-uncertainty' };
1560
+ return UNCERTAIN;
819
1561
  }
1562
+ if (closed || authorization === undefined || refusal !== null)
1563
+ return UNCERTAIN;
1564
+ const since = fencings;
820
1565
  const transfer = ['transfer', 'history', locator, path];
821
1566
  if (query.revision !== undefined)
822
1567
  transfer.push(query.revision);
@@ -831,6 +1576,8 @@ export function composeRuntime(options) {
831
1576
  ...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
832
1577
  });
833
1578
  journal.record(`outcome history ${result.kind}`);
1579
+ if (fencedSince(locator, since))
1580
+ return failure({ kind: 'room-not-readable', locator });
834
1581
  if (result.kind === 'scoped-refusal') {
835
1582
  const room = replicas.get(result.locator)?.confirmed.record;
836
1583
  if (room !== undefined)
@@ -838,13 +1585,107 @@ export function composeRuntime(options) {
838
1585
  }
839
1586
  if (result.kind === 'revoked-authorization')
840
1587
  await refuse('revoked-authorization');
841
- return result;
1588
+ return historyAnswer(result);
842
1589
  }
843
1590
  catch {
844
1591
  journal.record('outcome history transport-uncertainty');
845
- return { kind: 'transport-uncertainty' };
1592
+ return UNCERTAIN;
1593
+ }
1594
+ }
1595
+ function queuedBlob(locator, path, localId) {
1596
+ for (const batch of replicas.get(locator)?.queue ?? []) {
1597
+ const change = batch.changes[queuedFile(batch, path, localId)];
1598
+ if (change?.kind === 'binary')
1599
+ return change.blob;
1600
+ }
1601
+ return null;
1602
+ }
1603
+ function binaryRoom(request) {
1604
+ if (request.kind === 'path')
1605
+ return request.locator;
1606
+ const scope = options.adapter.referrerScope(request.referrer);
1607
+ return scope?.kind === 'room' ? scope.locator : null;
1608
+ }
1609
+ async function readBinary(request) {
1610
+ if (closed || refusal !== null)
1611
+ return UNCERTAIN;
1612
+ try {
1613
+ await ensureAuthorization();
1614
+ }
1615
+ catch {
1616
+ return UNCERTAIN;
1617
+ }
1618
+ if (closed || authorization === undefined || refusal !== null)
1619
+ return UNCERTAIN;
1620
+ const since = fencings;
1621
+ journal.record(request.kind === 'path'
1622
+ ? `transfer binary ${request.locator} ${request.path}`
1623
+ : `transfer binary ${request.fileRef}`);
1624
+ try {
1625
+ const result = await options.adapter.binary(authorization, request);
1626
+ journal.record(`outcome binary ${result.kind}`);
1627
+ const locator = binaryRoom(request);
1628
+ if (locator !== null && fencedSince(locator, since))
1629
+ return failure({ kind: 'room-not-readable', locator });
1630
+ if (result.kind === 'scoped-refusal') {
1631
+ const room = replicas.get(result.locator)?.confirmed.record;
1632
+ if (room !== undefined)
1633
+ await scoped(result, room, await openLocal());
1634
+ }
1635
+ if (result.kind === 'revoked-authorization')
1636
+ await refuse('revoked-authorization');
1637
+ return binaryAnswer(result);
1638
+ }
1639
+ catch {
1640
+ journal.record('outcome binary transport-uncertainty');
1641
+ return UNCERTAIN;
846
1642
  }
847
1643
  }
1644
+ async function readBinaryPath(locator, path, revision) {
1645
+ await ready;
1646
+ if (revision !== undefined)
1647
+ return readBinary({ kind: 'path', locator, path, revision });
1648
+ const visible = replicas.get(locator)?.visible;
1649
+ const held = visible?.binaries[path];
1650
+ if (visible === undefined || held === undefined)
1651
+ return absentBinary(path);
1652
+ if (held.state === 'pending') {
1653
+ const blob = queuedBlob(locator, path, held.localId);
1654
+ return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1655
+ }
1656
+ const record = binaryRecord(path, held, visible.revisions[path]);
1657
+ if (record.state !== 'accepted')
1658
+ return absentBinary(path);
1659
+ const replica = replicas.get(locator);
1660
+ const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
1661
+ const holds = (at) => {
1662
+ const kept = replica?.confirmed.record.binaries[at];
1663
+ return (kept?.state === 'accepted' &&
1664
+ kept.fileRef === record.fileRef &&
1665
+ replica?.confirmed.record.revisions[at] === record.revision);
1666
+ };
1667
+ let confirmed = path;
1668
+ for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
1669
+ const change = queued[index];
1670
+ if (change.kind === 'move' && change.to === confirmed)
1671
+ confirmed = change.path;
1672
+ }
1673
+ return readBinary({
1674
+ kind: 'path',
1675
+ locator,
1676
+ path: confirmed,
1677
+ revision: record.revision,
1678
+ fileRef: record.fileRef,
1679
+ });
1680
+ }
1681
+ async function readBinaryReference(reference) {
1682
+ await ready;
1683
+ return readBinary({
1684
+ kind: 'reference',
1685
+ fileRef: reference.fileRef,
1686
+ referrer: reference.referrer,
1687
+ });
1688
+ }
848
1689
  function declaredAgents() {
849
1690
  for (const capability of capabilities) {
850
1691
  if (capability.conversationAgents !== undefined)
@@ -864,18 +1705,17 @@ export function composeRuntime(options) {
864
1705
  async function invokeAction(name, input) {
865
1706
  await ready;
866
1707
  if (closed || refusal !== null)
867
- return { kind: 'transport-uncertainty' };
1708
+ return UNCERTAIN;
868
1709
  if (declaredAction(name) === undefined)
869
- return { kind: 'undeclared' };
1710
+ return undeclared(name);
870
1711
  try {
871
1712
  await ensureAuthorization();
872
1713
  }
873
1714
  catch {
874
- return { kind: 'transport-uncertainty' };
875
- }
876
- if (closed || authorization === undefined || refusal !== null) {
877
- return { kind: 'transport-uncertainty' };
1715
+ return UNCERTAIN;
878
1716
  }
1717
+ if (closed || authorization === undefined || refusal !== null)
1718
+ return UNCERTAIN;
879
1719
  journal.record(`transfer action ${name}`);
880
1720
  try {
881
1721
  const result = await options.adapter.action(authorization, { name, input });
@@ -889,11 +1729,11 @@ export function composeRuntime(options) {
889
1729
  journal.record(`outcome action ${result.kind}`);
890
1730
  if (result.kind === 'rejected')
891
1731
  journal.record(`refusal rejected ${result.code}`);
892
- return result;
1732
+ return calledResult(result);
893
1733
  }
894
1734
  catch {
895
1735
  journal.record('outcome action transport-uncertainty');
896
- return { kind: 'transport-uncertainty' };
1736
+ return UNCERTAIN;
897
1737
  }
898
1738
  }
899
1739
  function continuing() {
@@ -910,11 +1750,11 @@ export function composeRuntime(options) {
910
1750
  options.adapter.reached(locator, written, roomVersion) &&
911
1751
  !options.adapter.reached(locator, roomVersion, written));
912
1752
  }
913
- async function pullRefresh(authorized, confirmed) {
914
- if (!continuing())
1753
+ async function pullRefresh(authorized, confirmed, live) {
1754
+ if (!live())
915
1755
  return unconfirmed;
916
1756
  try {
917
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
1757
+ const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
918
1758
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
919
1759
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
920
1760
  return result;
@@ -924,28 +1764,80 @@ export function composeRuntime(options) {
924
1764
  throw error;
925
1765
  }
926
1766
  }
927
- async function pullBootstrap(authorized, room) {
928
- if (!continuing())
1767
+ function staging(local, live) {
1768
+ return {
1769
+ held: (locator) => transfers.get(locator) ?? null,
1770
+ staged: (record) => local.staged(record),
1771
+ stage: (record, page) => exclusive(async () => {
1772
+ if (!live())
1773
+ return false;
1774
+ const bytes = layerBytes(page);
1775
+ if (!(await admitted(local, record.locator, bytes)))
1776
+ return false;
1777
+ try {
1778
+ await local.stage(record, page);
1779
+ }
1780
+ catch (error) {
1781
+ if (!exceeded(error))
1782
+ throw error;
1783
+ await exceededOn(record.locator, bytes);
1784
+ return false;
1785
+ }
1786
+ transfers.set(record.locator, record);
1787
+ readSide.recover();
1788
+ return true;
1789
+ }),
1790
+ abandon: (locator) => exclusive(async () => {
1791
+ if (!live())
1792
+ return;
1793
+ await local.abandon(locator);
1794
+ transfers.delete(locator);
1795
+ readSide.recover();
1796
+ }),
1797
+ };
1798
+ }
1799
+ function reusable(room) {
1800
+ const record = replicas.get(room.locator)?.confirmed.record;
1801
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1802
+ return null;
1803
+ return { documents: record.documents, revisions: record.revisions };
1804
+ }
1805
+ async function pullBootstrap(authorized, room, local, live) {
1806
+ if (!live())
929
1807
  return unconfirmed;
1808
+ if (replicas.has(room.locator)) {
1809
+ catching.add(room.locator);
1810
+ readSide.recover();
1811
+ }
930
1812
  try {
931
1813
  const result = await bootstrapRoom(options.adapter, authorized, room, {
932
1814
  collection: modelOf(room.locator),
933
1815
  selection: modelOf(room.locator, 'typed-selection'),
934
- }, (models, held) => remodel(room.locator, models, held), continuing);
1816
+ }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
935
1817
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
936
1818
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1819
+ if (result.kind !== 'staged')
1820
+ caughtUpTo(room.locator);
937
1821
  return result;
938
1822
  }
939
1823
  catch (error) {
940
1824
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
1825
+ caughtUpTo(room.locator);
941
1826
  throw error;
942
1827
  }
943
1828
  }
944
- async function advance(room, local, authorized) {
1829
+ function caughtUpTo(locator) {
1830
+ if (catching.delete(locator))
1831
+ readSide.recover();
1832
+ }
1833
+ async function advance(room, local, authorized, live) {
1834
+ await referenced(room.locator);
1835
+ if (!live() || paused.has(room.locator))
1836
+ return unconfirmed;
945
1837
  const held = replicas.get(room.locator);
946
1838
  if (held !== undefined) {
947
- const refreshed = await pullRefresh(authorized, held.confirmed);
948
- if (!continuing())
1839
+ const refreshed = await pullRefresh(authorized, held.confirmed, live);
1840
+ if (!live())
949
1841
  return unconfirmed;
950
1842
  if (refreshed.kind === 'still-current') {
951
1843
  stale.delete(room.locator);
@@ -959,31 +1851,45 @@ export function composeRuntime(options) {
959
1851
  return null;
960
1852
  }
961
1853
  if (refreshed.kind === 'advanced') {
962
- if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
1854
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
963
1855
  const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
964
1856
  const reached = now !== undefined &&
965
1857
  options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
966
- return reached ? null : advance(room, local, authorized);
1858
+ return reached ? null : advance(room, local, authorized, live);
967
1859
  }
968
1860
  await retainModel(local, room, 'collection', modelOf(room.locator));
969
1861
  return null;
970
1862
  }
971
1863
  if (refreshed.kind !== 'beyond-horizon')
972
1864
  return refreshed;
1865
+ if (opened !== null) {
1866
+ owed.add(room.locator);
1867
+ readSide.recover();
1868
+ return unconfirmed;
1869
+ }
973
1870
  }
974
- const bootstrapped = await pullBootstrap(authorized, room);
975
- if (!continuing())
1871
+ const bootstrapped = await pullBootstrap(authorized, room, local, live);
1872
+ if (!live())
976
1873
  return unconfirmed;
977
1874
  if (bootstrapped.kind !== 'staged')
978
1875
  return bootstrapped;
979
- await publish(local, room, bootstrapped.confirmed);
1876
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
980
1877
  return null;
981
1878
  }
982
1879
  async function withhold(withheld, room, local, progress) {
1880
+ if (withheld.kind === 'unconfirmed')
1881
+ return;
983
1882
  if (withheld.kind === 'refused')
984
1883
  stale.delete(room.locator);
985
1884
  else
986
1885
  stale.add(room.locator);
1886
+ if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
1887
+ const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
1888
+ repairable.set(room.locator, reset);
1889
+ repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
1890
+ }
1891
+ else
1892
+ repairable.delete(room.locator);
987
1893
  if (withheld.kind === 'scoped-refusal') {
988
1894
  await scoped(withheld, room, local);
989
1895
  }
@@ -1008,6 +1914,7 @@ export function composeRuntime(options) {
1008
1914
  function refusedSource(room, { refusal }) {
1009
1915
  sourceRefusals.set(room.locator, {
1010
1916
  code: refusal.rule,
1917
+ retryable: false,
1011
1918
  fields: {
1012
1919
  file: refusal.file,
1013
1920
  ...(refusal.location === undefined ? {} : { location: refusal.location }),
@@ -1018,12 +1925,21 @@ export function composeRuntime(options) {
1018
1925
  unreadable.add(room.locator);
1019
1926
  list(room, false);
1020
1927
  emitRooms();
1928
+ moveTo({ ...status, ...attention() });
1021
1929
  journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
1022
1930
  }
1023
1931
  function leaving(batch, result) {
1024
1932
  if (result.kind === 'expired')
1025
- return { batch, returned: { kind: 'expired' } };
1026
- return { batch, returned: { kind: result.kind, code: result.code, fields: result.fields } };
1933
+ return { batch, returned: returnedBy('expired') };
1934
+ return {
1935
+ batch,
1936
+ returned: {
1937
+ kind: result.kind,
1938
+ code: result.code,
1939
+ fields: result.fields,
1940
+ retryable: result.retryable,
1941
+ },
1942
+ };
1027
1943
  }
1028
1944
  function logAccepted(locator, submitted, accepted) {
1029
1945
  journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
@@ -1031,6 +1947,7 @@ export function composeRuntime(options) {
1031
1947
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1032
1948
  }
1033
1949
  async function adoptAccepted(room, local, submitted, accepted) {
1950
+ await referenced(room.locator);
1034
1951
  if (!continuing())
1035
1952
  return unconfirmed;
1036
1953
  const current = replicas.get(room.locator);
@@ -1041,22 +1958,23 @@ export function composeRuntime(options) {
1041
1958
  logAccepted(room.locator, submitted, accepted);
1042
1959
  const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1043
1960
  ? { 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)
1961
+ : applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
1045
1962
  ? accepted.roomVersion
1046
1963
  : held, accepted.roomVersion);
1047
1964
  if (applied.kind !== 'staged') {
1048
1965
  const state = pendings.get(submitted.id);
1049
1966
  await local.accept(room.locator, submitted.id, accepted.roomVersion);
1050
- if (state !== undefined)
1967
+ if (state !== undefined && !fences.has(room.locator))
1051
1968
  pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1052
1969
  emitQueue(room.locator);
1053
1970
  return applied;
1054
1971
  }
1055
- const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1972
+ const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1056
1973
  pendings.delete(submitted.id);
1057
1974
  unreadable.delete(room.locator);
1058
1975
  sourceRefusals.delete(room.locator);
1059
1976
  declarationRefusals.delete(room.locator);
1977
+ const set = settled();
1060
1978
  list(room, true);
1061
1979
  publishChangeSet(set);
1062
1980
  moveTo({ ...status, ...attention() });
@@ -1070,14 +1988,14 @@ export function composeRuntime(options) {
1070
1988
  refusedSource(room, failed);
1071
1989
  return false;
1072
1990
  }
1073
- async function submitRoom(room, local, authorized, progress) {
1074
- while (continuing() && authorization !== undefined) {
1991
+ async function submitRoom(room, local, authorized, progress, signal) {
1992
+ while (continuing() && authorization !== undefined && !signal.aborted) {
1075
1993
  const replica = replicas.get(room.locator);
1076
1994
  const head = replica?.queue[0];
1077
1995
  if (replica === undefined || head === undefined)
1078
- return;
1996
+ return 'done';
1079
1997
  if (pendings.get(head.id)?.accepted !== undefined)
1080
- return;
1998
+ return 'done';
1081
1999
  const state = pendings.get(head.id);
1082
2000
  const { submission, lost: earlier } = state ?? {
1083
2001
  submission: { state: 'waiting' },
@@ -1085,24 +2003,30 @@ export function composeRuntime(options) {
1085
2003
  };
1086
2004
  if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
1087
2005
  progress.complete = false;
1088
- return;
2006
+ return { at: Date.parse(submission.reset) };
1089
2007
  }
1090
2008
  if (writesHeld()) {
1091
2009
  progress.complete = false;
1092
2010
  journal.record(`submission ${room.locator} ${head.id} held`);
1093
- return;
2011
+ return 'done';
1094
2012
  }
1095
2013
  const lost = earlier || submission.state === 'submitted';
2014
+ if (lost && pastHorizon(head.id)) {
2015
+ await discardPending(room, local, signal);
2016
+ return 'done';
2017
+ }
1096
2018
  let result;
1097
2019
  journal.record(`submission ${room.locator} ${head.id}`);
1098
2020
  await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1099
- if (discoveryPaused)
1100
- return;
1101
- if (writesHeld()) {
2021
+ if (discoveryPaused || fences.has(room.locator))
2022
+ return 'done';
2023
+ if (signal.aborted || writesHeld()) {
1102
2024
  await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
2025
+ if (signal.aborted)
2026
+ return 'done';
1103
2027
  progress.complete = false;
1104
2028
  journal.record(`submission ${room.locator} ${head.id} held`);
1105
- return;
2029
+ return 'done';
1106
2030
  }
1107
2031
  const opensBefore = openSignals;
1108
2032
  const answered = awaitingAnswer(room.locator);
@@ -1118,32 +2042,36 @@ export function composeRuntime(options) {
1118
2042
  progress.complete = false;
1119
2043
  if (continuing())
1120
2044
  progress.reachable = false;
1121
- return;
2045
+ return 'again';
2046
+ }
2047
+ if (signal.aborted) {
2048
+ answered();
2049
+ return 'done';
1122
2050
  }
1123
2051
  if (result.kind !== 'accepted' || !continuing())
1124
2052
  answered();
1125
2053
  if (!continuing())
1126
- return;
2054
+ return 'done';
1127
2055
  if (result.kind === 'transport-uncertainty') {
1128
2056
  progress.complete = false;
1129
2057
  progress.reachable = false;
1130
- return;
2058
+ return 'again';
1131
2059
  }
1132
2060
  if (result.kind === 'room-not-readable') {
1133
2061
  progress.complete = false;
1134
2062
  unreadable.add(room.locator);
1135
2063
  list(room, false);
1136
2064
  emitRooms();
1137
- return;
2065
+ return 'done';
1138
2066
  }
1139
2067
  if (result.kind === 'revoked-authorization') {
1140
2068
  await refuse('revoked-authorization');
1141
- return;
2069
+ return 'done';
1142
2070
  }
1143
2071
  if (result.kind === 'scoped-refusal') {
1144
2072
  await scoped(result, room, local);
1145
2073
  progress.complete = false;
1146
- return;
2074
+ return 'done';
1147
2075
  }
1148
2076
  if (result.kind === 'write-halted') {
1149
2077
  journal.record(`outcome ${room.locator} ${head.id} write-halted`);
@@ -1152,16 +2080,24 @@ export function composeRuntime(options) {
1152
2080
  continue;
1153
2081
  haltedRefusal = true;
1154
2082
  progress.complete = false;
1155
- return;
2083
+ return 'done';
1156
2084
  }
1157
2085
  if (result.kind === 'conflicted' ||
1158
2086
  result.kind === 'rejected' ||
1159
2087
  result.kind === 'expired' ||
1160
2088
  result.kind === 'retryable-refusal') {
2089
+ if (result.kind === 'expired' && lost && result.restored !== true) {
2090
+ journal.record(`outcome ${room.locator} ${head.id} expired`);
2091
+ await discardPending(room, local, signal);
2092
+ return 'done';
2093
+ }
1161
2094
  if (result.kind === 'expired') {
1162
- const withheld = await advance(room, local, authorized);
1163
- if (withheld !== null)
1164
- return withhold(withheld, room, local, progress);
2095
+ await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
2096
+ const withheld = await advance(room, local, authorized, continuing);
2097
+ if (withheld !== null) {
2098
+ await withhold(withheld, room, local, progress);
2099
+ return 'done';
2100
+ }
1165
2101
  }
1166
2102
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
1167
2103
  (result.kind === 'conflicted' && result.roomVersion !== undefined
@@ -1174,8 +2110,7 @@ export function composeRuntime(options) {
1174
2110
  const reset = result.reset;
1175
2111
  progress.complete = false;
1176
2112
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1177
- armTimer();
1178
- return;
2113
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
1179
2114
  }
1180
2115
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
1181
2116
  const left = leaving(head, result);
@@ -1186,37 +2121,82 @@ export function composeRuntime(options) {
1186
2121
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
1187
2122
  return;
1188
2123
  }
1189
- const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
2124
+ const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1190
2125
  leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1191
2126
  (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1192
2127
  ? { ...left, returned: null }
1193
2128
  : left,
1194
2129
  });
1195
- publishChangeSet(set);
2130
+ publishChangeSet(settled());
1196
2131
  moveTo({ ...status, ...attention() });
1197
2132
  });
1198
2133
  if (result.kind === 'expired')
1199
2134
  continue;
1200
- const withheld = await advance(room, local, authorized);
1201
- if (withheld !== null)
1202
- return withhold(withheld, room, local, progress);
2135
+ const withheld = await advance(room, local, authorized, continuing);
2136
+ if (withheld !== null) {
2137
+ await withhold(withheld, room, local, progress);
2138
+ return 'done';
2139
+ }
1203
2140
  continue;
1204
2141
  }
1205
2142
  const kept = await adopted(room, local, head, result).finally(answered);
1206
2143
  if (!kept) {
1207
2144
  progress.complete = false;
1208
- return;
2145
+ return 'done';
1209
2146
  }
1210
2147
  }
2148
+ return 'done';
2149
+ }
2150
+ function pastHorizon(identity) {
2151
+ return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
2152
+ }
2153
+ async function discardPending(room, local, signal) {
2154
+ const { locator } = room;
2155
+ const discarded = await exclusive(async () => {
2156
+ if (fences.has(locator))
2157
+ return false;
2158
+ const before = replicas.get(locator);
2159
+ const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
2160
+ fences.set(locator, { at: discoveries, erased: false });
2161
+ lastFenced.set(locator, ++fencings);
2162
+ try {
2163
+ await local.discard(locator);
2164
+ }
2165
+ finally {
2166
+ fences.delete(locator);
2167
+ }
2168
+ for (const batch of queue)
2169
+ pendings.delete(batch.id);
2170
+ queuesWithoutReplica.delete(locator);
2171
+ const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
2172
+ if (after !== null)
2173
+ replicas.set(locator, after);
2174
+ journal.record(`room discarded ${locator}`);
2175
+ publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
2176
+ moveTo({ ...status, ...attention() });
2177
+ return true;
2178
+ });
2179
+ if (!discarded)
2180
+ return;
2181
+ const progress = { complete: true, reachable: true };
2182
+ await roomTask(locator, async (live) => {
2183
+ await Promise.race([
2184
+ reload(room, local, progress, () => live() && !signal.aborted),
2185
+ abandonedOn(signal),
2186
+ ]);
2187
+ });
2188
+ if (signal.aborted)
2189
+ return;
2190
+ await rest(progress);
1211
2191
  }
1212
2192
  function awaitingAnswer(locator) {
1213
2193
  let release = () => { };
1214
2194
  const awaited = new Promise((resolve) => {
1215
2195
  release = resolve;
1216
2196
  });
1217
- answering.set(locator, awaited);
2197
+ answering.set(locator, { generation, answered: awaited });
1218
2198
  return () => {
1219
- if (answering.get(locator) === awaited)
2199
+ if (answering.get(locator)?.answered === awaited)
1220
2200
  answering.delete(locator);
1221
2201
  release();
1222
2202
  };
@@ -1249,7 +2229,7 @@ export function composeRuntime(options) {
1249
2229
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1250
2230
  };
1251
2231
  const reclassified = sameMembership(declared, held.confirmed.record)
1252
- ? readConfirmed(declared, held.confirmed)
2232
+ ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
1253
2233
  : readReclassified(declared, held.confirmed);
1254
2234
  const record = reclassified.kind === 'staged'
1255
2235
  ? declared
@@ -1257,12 +2237,10 @@ export function composeRuntime(options) {
1257
2237
  ...declared,
1258
2238
  typedSelection: held.confirmed.record.typedSelection,
1259
2239
  };
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
- });
2240
+ const read = reclassified.kind === 'deferred'
2241
+ ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
2242
+ : reclassified;
2243
+ await local.write({ next: record, previous: held.confirmed.record });
1266
2244
  if (read.kind === 'staged') {
1267
2245
  replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1268
2246
  if (declarationRefusals.delete(room.locator)) {
@@ -1289,7 +2267,11 @@ export function composeRuntime(options) {
1289
2267
  });
1290
2268
  }
1291
2269
  async function scoped(result, room, local) {
1292
- scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
2270
+ scopedRefusals.set(room.locator, {
2271
+ code: result.code,
2272
+ fields: result.fields,
2273
+ retryable: result.retryable,
2274
+ });
1293
2275
  let scopedAway = room;
1294
2276
  if (result.scope === 'model')
1295
2277
  await retainModel(local, room, 'collection', {
@@ -1326,24 +2308,27 @@ export function composeRuntime(options) {
1326
2308
  roomVersion: record.roomVersion,
1327
2309
  documents: record.documents,
1328
2310
  revisions: record.revisions,
1329
- attachments: record.attachments,
1330
2311
  },
1331
2312
  ];
1332
2313
  });
1333
2314
  }
1334
- async function synchronize(listed, local, progress, attempt) {
1335
- const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1336
- const ready = listed.filter((_, index) => {
2315
+ async function synchronize(listed, local, progress, attempt, live) {
2316
+ const acquired = listed.filter(acquires);
2317
+ const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
2318
+ if (!live())
2319
+ return;
2320
+ const ready = acquired.filter((_, index) => {
1337
2321
  const outcome = prepared[index];
1338
2322
  return outcome?.status === 'fulfilled' && outcome.value;
1339
2323
  });
1340
2324
  if (attempt === 'opened') {
1341
- options.adapter.roomStream?.hold(heldRooms(ready));
2325
+ options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
1342
2326
  handed = true;
1343
2327
  }
2328
+ discovering?.release();
1344
2329
  const loaded = await Promise.allSettled(ready
1345
2330
  .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1346
- .map((room) => loadRoom(room, local, progress)));
2331
+ .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
1347
2332
  const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1348
2333
  if (failed !== undefined)
1349
2334
  throw failed.reason;
@@ -1372,14 +2357,14 @@ export function composeRuntime(options) {
1372
2357
  }
1373
2358
  return true;
1374
2359
  }
1375
- async function caughtUp(room, local, progress) {
2360
+ async function caughtUp(room, local, progress, live) {
1376
2361
  const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1377
- if (held === undefined || authorization === undefined)
2362
+ if (held === undefined || authorization === undefined || !live())
1378
2363
  return;
1379
2364
  const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1380
2365
  if (!behind)
1381
2366
  return;
1382
- const withheld = await advance(room, local, authorization);
2367
+ const withheld = await advance(room, local, authorization, live);
1383
2368
  if (withheld !== null)
1384
2369
  await withhold(withheld, room, local, progress);
1385
2370
  }
@@ -1389,39 +2374,46 @@ export function composeRuntime(options) {
1389
2374
  return model.status === 'unavailable' && model.reason === 'revoked';
1390
2375
  });
1391
2376
  }
1392
- async function loadRoom(room, local, progress) {
1393
- if (discoveryPaused || authorization === undefined)
2377
+ async function loadRoom(room, local, progress, live) {
2378
+ if (!live() || authorization === undefined)
1394
2379
  return;
1395
2380
  if (replicas.has(room.locator) && revoked(room.locator)) {
1396
- await reload(room, local, progress);
2381
+ await reload(room, local, progress, live);
1397
2382
  return;
1398
2383
  }
1399
- const withheld = await advance(room, local, authorization);
2384
+ const withheld = await advance(room, local, authorization, live);
1400
2385
  if (withheld !== null) {
1401
2386
  await withhold(withheld, room, local, progress);
1402
2387
  return;
1403
2388
  }
1404
- await caughtUp(room, local, progress);
2389
+ await caughtUp(room, local, progress, live);
1405
2390
  }
1406
- async function reload(room, local, progress) {
1407
- if (discoveryPaused || authorization === undefined)
1408
- return;
1409
- const bootstrapped = await pullBootstrap(authorization, room);
1410
- if (!continuing())
1411
- return;
1412
- if (bootstrapped.kind !== 'staged') {
1413
- await withhold(bootstrapped, room, local, progress);
1414
- return;
2391
+ async function reload(room, local, progress, live) {
2392
+ try {
2393
+ if (!live() || authorization === undefined)
2394
+ return;
2395
+ const bootstrapped = await pullBootstrap(authorization, room, local, live);
2396
+ if (!live())
2397
+ return;
2398
+ if (bootstrapped.kind !== 'staged') {
2399
+ await withhold(bootstrapped, room, local, progress);
2400
+ return;
2401
+ }
2402
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
2403
+ await caughtUp(room, local, progress, live);
2404
+ }
2405
+ finally {
2406
+ caughtUpTo(room.locator);
1415
2407
  }
1416
- await publish(local, room, bootstrapped.confirmed);
1417
- await caughtUp(room, local, progress);
1418
2408
  }
1419
- async function discoverRooms(local, progress, attempt) {
2409
+ async function discoverRooms(local, progress, attempt, live) {
1420
2410
  if (closed || authorization === undefined) {
1421
2411
  notifyReady();
1422
2412
  return;
1423
2413
  }
1424
2414
  moveTo(transferring(status));
2415
+ discoveries += 1;
2416
+ const started = discoveries;
1425
2417
  let result;
1426
2418
  try {
1427
2419
  result = await options.adapter.discover(authorization);
@@ -1429,7 +2421,8 @@ export function composeRuntime(options) {
1429
2421
  catch {
1430
2422
  result = { kind: 'transport-uncertainty' };
1431
2423
  }
1432
- if (closed) {
2424
+ if (!live()) {
2425
+ progress.complete = false;
1433
2426
  notifyReady();
1434
2427
  return;
1435
2428
  }
@@ -1448,7 +2441,11 @@ export function composeRuntime(options) {
1448
2441
  }
1449
2442
  if (result.kind === 'contract-unavailable') {
1450
2443
  journal.record(`connection contract unavailable ${result.code}`);
1451
- await refuse('missing-required-guarantee', { code: result.code, fields: result.fields });
2444
+ await refuse('missing-required-guarantee', {
2445
+ code: result.code,
2446
+ fields: result.fields,
2447
+ retryable: result.retryable,
2448
+ });
1452
2449
  notifyReady();
1453
2450
  return;
1454
2451
  }
@@ -1483,14 +2480,20 @@ export function composeRuntime(options) {
1483
2480
  }
1484
2481
  const previousCapabilities = capabilities;
1485
2482
  capabilities = result.capabilities;
2483
+ outcomeRetention = result.retention.outcome;
1486
2484
  notifyReady();
1487
2485
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1488
2486
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
1489
2487
  limits = result.providerLimits;
1490
2488
  await local.declare(limits, capabilities);
1491
2489
  }
1492
- await applyDiscovery(result.rooms, local);
1493
- await synchronize(result.rooms, local, progress, attempt);
2490
+ const signedIn = result.member ?? null;
2491
+ if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
2492
+ await local.keepMember(signedIn);
2493
+ member = signedIn;
2494
+ emitMember();
2495
+ }
2496
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
1494
2497
  }
1495
2498
  async function ensureAuthorization() {
1496
2499
  if (authorization !== undefined)
@@ -1504,32 +2507,64 @@ export function composeRuntime(options) {
1504
2507
  throw error;
1505
2508
  }
1506
2509
  }
1507
- function task(work) {
2510
+ async function admittedRead(work) {
2511
+ if (roomReads.generation !== generation) {
2512
+ roomReads = { generation, free: ROOM_READS, waiting: [] };
2513
+ }
2514
+ const reads = roomReads;
2515
+ while (reads.free === 0)
2516
+ await new Promise((admit) => reads.waiting.push(admit));
2517
+ reads.free -= 1;
2518
+ try {
2519
+ await work();
2520
+ }
2521
+ finally {
2522
+ reads.free += 1;
2523
+ reads.waiting.shift()?.();
2524
+ }
2525
+ }
2526
+ function roomTask(locator, work) {
2527
+ return task(roomLane(locator), (live) => admittedRead(() => work(live)));
2528
+ }
2529
+ function task(lane, work) {
1508
2530
  if (stopping)
1509
- return chain;
1510
- chain = chain.then(work, work).catch(() => undefined);
1511
- return chain;
2531
+ return Promise.resolve();
2532
+ const held = lanes.get(lane);
2533
+ const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
2534
+ const turn = current.generation;
2535
+ const live = () => turn === generation && continuing();
2536
+ const run = current.chain.then(() => work(live)).catch(() => undefined);
2537
+ current.chain = run;
2538
+ lanes.set(lane, current);
2539
+ working.add(run);
2540
+ void run.finally(() => working.delete(run));
2541
+ return run;
1512
2542
  }
1513
2543
  function sync(attempt) {
1514
- return task(async () => {
1515
- if (closed || stopping || discoveryPaused) {
2544
+ return task(OPENING_LANE, async (live) => {
2545
+ if (!live()) {
1516
2546
  notifyReady();
1517
2547
  return;
1518
2548
  }
1519
2549
  const progress = { complete: true, reachable: true };
1520
- underway = attempt === 'opened' ? progress : null;
2550
+ const gate = discover();
2551
+ if (attempt === 'opened')
2552
+ opened = generation;
2553
+ underway = attempt === 'opened' ? { generation, progress } : null;
2554
+ if (attempt === 'opened')
2555
+ streamed = progress;
1521
2556
  complete = false;
1522
2557
  recorded = false;
1523
2558
  moveTo(transferring(status));
1524
2559
  handed = false;
1525
2560
  try {
1526
2561
  const local = await openLocal();
1527
- if (closed)
2562
+ if (!live())
1528
2563
  return;
1529
2564
  await ensureAuthorization();
1530
- if (closed)
2565
+ if (!live())
1531
2566
  return;
1532
- await discoverRooms(local, progress, attempt);
2567
+ await discoverRooms(local, progress, attempt, live);
1533
2568
  }
1534
2569
  catch {
1535
2570
  progress.complete = false;
@@ -1537,17 +2572,21 @@ export function composeRuntime(options) {
1537
2572
  progress.reachable = false;
1538
2573
  }
1539
2574
  finally {
2575
+ gate.release();
1540
2576
  notifyReady();
1541
2577
  }
2578
+ if (!live())
2579
+ return;
1542
2580
  if (attempt !== 'opened') {
1543
- await finishOpening(progress);
2581
+ await finishOpening(progress, live);
1544
2582
  return;
1545
2583
  }
1546
2584
  if (handed || closed || progress.reachable)
1547
2585
  return;
1548
2586
  underway = null;
2587
+ opened = null;
1549
2588
  journal.record('connection reopen');
1550
- await rest(progress);
2589
+ await rest(progress, live);
1551
2590
  options.adapter.roomStream?.reopen();
1552
2591
  });
1553
2592
  }
@@ -1558,29 +2597,51 @@ export function composeRuntime(options) {
1558
2597
  void refuse('revoked-authorization');
1559
2598
  return;
1560
2599
  }
1561
- if (opened || status.providerUnreachable)
1562
- return;
1563
- moveTo({ ...status, providerUnreachable: true });
2600
+ complete = false;
2601
+ if (streamed !== null)
2602
+ streamed.complete = false;
2603
+ moveTo({
2604
+ ...status,
2605
+ state: status.state === 'synced' ? 'syncing' : status.state,
2606
+ providerUnreachable: status.providerUnreachable || !opened,
2607
+ });
1564
2608
  }
1565
2609
  function applyOpening(expired) {
1566
- return task(async () => {
1567
- const progress = underway;
2610
+ const progress = underway?.generation === generation ? underway.progress : null;
2611
+ const cycle = progress ?? { complete: true, reachable: true };
2612
+ const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
2613
+ if (opened === generation)
2614
+ opened = null;
2615
+ return task(OPENING_LANE, async (live) => {
1568
2616
  underway = null;
1569
- if (closed || stopping)
2617
+ if (!live())
1570
2618
  return;
1571
2619
  if (progress === null) {
1572
- await reloadExpired(expired);
2620
+ await replacing;
2621
+ await rest(cycle, live);
1573
2622
  return;
1574
2623
  }
2624
+ await roomsCarried(expired);
2625
+ if (!live())
2626
+ return;
2627
+ const replaced = new Set(owed);
2628
+ owed.clear();
1575
2629
  try {
1576
2630
  const local = await openLocal();
1577
2631
  const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1578
- const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1579
- const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
2632
+ const reloading = held.filter((room) => !expired.includes(room.locator) &&
2633
+ (replaced.has(room.locator) || revoked(room.locator)));
2634
+ for (const room of reloading)
2635
+ catching.add(room.locator);
2636
+ readSide.recover();
2637
+ const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
2638
+ const reloaded = [...(await reloads), ...(await replacing)];
1580
2639
  if (progress.reachable) {
1581
- for (const room of held)
1582
- if (!reloading.includes(room))
2640
+ for (const room of held) {
2641
+ if (!expired.includes(room.locator) && !reloading.includes(room)) {
1583
2642
  await confirmModels(room, local);
2643
+ }
2644
+ }
1584
2645
  }
1585
2646
  const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1586
2647
  if (failed !== undefined)
@@ -1590,21 +2651,21 @@ export function composeRuntime(options) {
1590
2651
  progress.complete = false;
1591
2652
  progress.reachable = false;
1592
2653
  }
1593
- await finishOpening(progress);
2654
+ if (live())
2655
+ await finishOpening(progress, live);
1594
2656
  });
1595
2657
  }
1596
- async function reloadExpired(expired) {
1597
- const progress = { complete: true, reachable: true };
2658
+ async function reloadNow(replaced, progress) {
1598
2659
  try {
1599
2660
  const local = await openLocal();
1600
- await Promise.allSettled([...discovered.values()]
1601
- .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1602
- .map((room) => reload(room, local, progress)));
2661
+ return await Promise.allSettled([...discovered.values()]
2662
+ .filter((room) => replicas.has(room.locator) && replaced(room))
2663
+ .map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
1603
2664
  }
1604
2665
  catch {
1605
2666
  progress.reachable = false;
2667
+ return [];
1606
2668
  }
1607
- await rest(progress);
1608
2669
  }
1609
2670
  async function confirmModels(room, local) {
1610
2671
  for (const attached of ['collection', 'typed-selection']) {
@@ -1615,38 +2676,96 @@ export function composeRuntime(options) {
1615
2676
  }
1616
2677
  }
1617
2678
  function submitLater(locator) {
1618
- const running = submitting.get(locator);
1619
- if (running !== undefined) {
1620
- running.again = true;
2679
+ if (localSaves)
1621
2680
  return;
2681
+ void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
2682
+ }
2683
+ async function submitPass(locator, signal) {
2684
+ const room = discovered.get(locator);
2685
+ if (closed || stopping || discoveryPaused || room === undefined)
2686
+ return 'done';
2687
+ const progress = { complete: true, reachable: true };
2688
+ let next;
2689
+ try {
2690
+ await ensureAuthorization();
2691
+ if (authorization === undefined)
2692
+ return 'done';
2693
+ next = await submitRoom(room, await openLocal(), authorization, progress, signal);
1622
2694
  }
1623
- const run = { again: false, done: Promise.resolve() };
1624
- submitting.set(locator, run);
1625
- run.done = (async () => {
1626
- try {
1627
- do {
1628
- run.again = false;
1629
- const room = discovered.get(locator);
1630
- if (closed || stopping || discoveryPaused || room === undefined)
1631
- return;
1632
- const progress = { complete: true, reachable: true };
1633
- try {
1634
- await ensureAuthorization();
1635
- if (authorization === undefined)
1636
- return;
1637
- await submitRoom(room, await openLocal(), authorization, progress);
1638
- }
1639
- catch {
1640
- progress.reachable = false;
1641
- }
1642
- armTimer();
1643
- await rest(progress);
1644
- } while (run.again);
1645
- }
1646
- finally {
1647
- submitting.delete(locator);
1648
- }
1649
- })();
2695
+ catch {
2696
+ progress.reachable = false;
2697
+ next = 'again';
2698
+ }
2699
+ if (signal.aborted)
2700
+ return 'done';
2701
+ await rest(progress);
2702
+ return next === 'again' && !socketOpen ? 'done' : next;
2703
+ }
2704
+ function repairLater(locator, when) {
2705
+ if (!socketOpen || repairing.has(locator))
2706
+ return;
2707
+ void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
2708
+ }
2709
+ async function repairPass(locator, signal) {
2710
+ const room = discovered.get(locator);
2711
+ if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
2712
+ return 'done';
2713
+ if (room === undefined || room.replica.admission !== 'admitted')
2714
+ return 'done';
2715
+ const progress = { complete: true, reachable: true };
2716
+ repairing.add(locator);
2717
+ try {
2718
+ const local = await openLocal();
2719
+ await roomTask(locator, async (live) => {
2720
+ const current = () => live() && !signal.aborted;
2721
+ if (!current() || authorization === undefined)
2722
+ return;
2723
+ const withheld = await Promise.race([
2724
+ advance(room, local, authorization, current),
2725
+ abandonedOn(signal),
2726
+ ]);
2727
+ if (withheld !== null && current())
2728
+ await withhold(withheld, room, local, progress);
2729
+ });
2730
+ }
2731
+ catch {
2732
+ stale.add(locator);
2733
+ progress.reachable = false;
2734
+ }
2735
+ finally {
2736
+ repairing.delete(locator);
2737
+ }
2738
+ if (signal.aborted)
2739
+ return 'done';
2740
+ await rest(progress);
2741
+ if (!stale.has(locator)) {
2742
+ if ((replicas.get(locator)?.queue.length ?? 0) > 0)
2743
+ submitLater(locator);
2744
+ return 'done';
2745
+ }
2746
+ if (!repairable.has(locator))
2747
+ return 'done';
2748
+ const reset = repairable.get(locator);
2749
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
2750
+ }
2751
+ function resendLater() {
2752
+ if (!socketOpen)
2753
+ return;
2754
+ void recovery.attempt('resend', resendPass, { again: true });
2755
+ }
2756
+ async function resendPass(signal) {
2757
+ if (!socketOpen || closed || stopping || discoveryPaused)
2758
+ return 'done';
2759
+ const reading = { complete: true, reachable: true };
2760
+ try {
2761
+ await conversations.resend(reading);
2762
+ }
2763
+ catch {
2764
+ reading.reachable = false;
2765
+ }
2766
+ if (signal.aborted)
2767
+ return 'done';
2768
+ return reading.reachable && reading.complete ? 'done' : 'again';
1650
2769
  }
1651
2770
  function submitAll() {
1652
2771
  for (const [locator, replica] of replicas) {
@@ -1654,31 +2773,41 @@ export function composeRuntime(options) {
1654
2773
  submitLater(locator);
1655
2774
  }
1656
2775
  }
1657
- async function finishOpening(progress) {
2776
+ async function finishOpening(progress, live) {
1658
2777
  try {
1659
- if (closed || discoveryPaused) {
2778
+ if (!live()) {
1660
2779
  progress.complete = false;
1661
2780
  return;
1662
2781
  }
1663
2782
  submitAll();
2783
+ for (const locator of stale)
2784
+ if (repairable.has(locator))
2785
+ repairLater(locator, {});
1664
2786
  await conversations.reconcile();
1665
2787
  if (status.providerUnreachable)
1666
2788
  conversations.reconnected();
1667
2789
  const reading = { complete: true, reachable: true };
1668
- await conversations.catchUp(reading);
1669
- if (!reading.reachable)
2790
+ if (demand.conversations)
2791
+ await conversations.catchUp(reading);
2792
+ if (progress.reachable && reading.reachable)
2793
+ await conversations.resend(reading);
2794
+ if (!reading.reachable) {
1670
2795
  progress.reachable = false;
2796
+ resendLater();
2797
+ }
1671
2798
  }
1672
2799
  catch {
1673
2800
  progress.complete = false;
1674
2801
  }
1675
2802
  finally {
1676
- complete = progress.complete;
1677
- await rest(progress);
2803
+ if (live()) {
2804
+ complete = progress.complete;
2805
+ await rest(progress, live);
2806
+ }
1678
2807
  }
1679
2808
  }
1680
- async function rest(progress) {
1681
- if (closed)
2809
+ async function rest(progress, live = () => !closed) {
2810
+ if (!live())
1682
2811
  return;
1683
2812
  let lastCompleted = status.lastCompleted;
1684
2813
  let synced = complete && stale.size === 0 && !pending();
@@ -1686,6 +2815,8 @@ export function composeRuntime(options) {
1686
2815
  const completedAt = new Date().toISOString();
1687
2816
  try {
1688
2817
  await (await openLocal()).complete(completedAt);
2818
+ if (!live())
2819
+ return;
1689
2820
  recorded = true;
1690
2821
  lastCompleted = completedAt;
1691
2822
  }
@@ -1694,46 +2825,74 @@ export function composeRuntime(options) {
1694
2825
  }
1695
2826
  }
1696
2827
  moveTo({
1697
- state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
2828
+ state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
1698
2829
  lastCompleted,
1699
2830
  providerUnreachable: !progress.reachable,
1700
2831
  ...attention(),
1701
2832
  });
1702
2833
  }
2834
+ function discover() {
2835
+ let release = () => { };
2836
+ const done = new Promise((resolve) => {
2837
+ release = resolve;
2838
+ });
2839
+ discovering = { generation, done, release };
2840
+ return discovering;
2841
+ }
1703
2842
  function applyRoom(locator, roomVersion) {
1704
2843
  announced.set(locator, roomVersion);
1705
- return task(async () => {
1706
- await answering.get(locator);
1707
- if (closed || stopping || discoveryPaused || authorization === undefined)
2844
+ return task(roomLane(locator), async (live) => {
2845
+ if (discovering?.generation === generation)
2846
+ await discovering.done;
2847
+ const saving = answering.get(locator);
2848
+ if (saving?.generation === generation)
2849
+ await saving.answered;
2850
+ if (!live() || authorization === undefined)
1708
2851
  return;
1709
2852
  const room = discovered.get(locator);
1710
2853
  const held = replicas.get(locator);
1711
- if (room === undefined || room.replica.admission !== 'admitted')
2854
+ if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
1712
2855
  return;
1713
2856
  if (held !== undefined &&
1714
2857
  (held.confirmed.record.replica.selection !== room.replica.selection ||
1715
2858
  options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
1716
2859
  return;
1717
2860
  }
1718
- const during = underway;
2861
+ const during = underway?.generation === generation ? underway.progress : null;
1719
2862
  const progress = during ?? { complete: true, reachable: true };
1720
2863
  if (during === null)
1721
2864
  moveTo(transferring(status));
1722
2865
  try {
1723
2866
  const local = await openLocal();
1724
- const withheld = await advance(room, local, authorization);
1725
- if (withheld !== null)
1726
- await withhold(withheld, room, local, progress);
2867
+ await admittedRead(async () => {
2868
+ if (!live() || authorization === undefined)
2869
+ return;
2870
+ const withheld = await advance(room, local, authorization, live);
2871
+ if (withheld !== null)
2872
+ await withhold(withheld, room, local, progress);
2873
+ });
1727
2874
  }
1728
2875
  catch {
1729
2876
  stale.add(locator);
1730
2877
  }
1731
2878
  finally {
1732
2879
  if (during === null)
1733
- await rest(progress);
2880
+ await rest(progress, live);
1734
2881
  }
1735
2882
  });
1736
2883
  }
2884
+ function roomsCarried(replacing) {
2885
+ const replaced = new Set(replacing.map(roomLane));
2886
+ return Promise.all([...lanes]
2887
+ .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
2888
+ .map(([, held]) => held.chain));
2889
+ }
2890
+ const receipts = {
2891
+ heads: async () => (await openLocal()).receivedHeads(),
2892
+ parts: async (key, count) => (await openLocal()).receivedParts(key, count),
2893
+ keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
2894
+ drop: async (keys) => (await openLocal()).dropReceived(keys),
2895
+ };
1737
2896
  function observeStream() {
1738
2897
  const stream = options.adapter.roomStream;
1739
2898
  if (stream === undefined || withdrawStream !== undefined)
@@ -1743,6 +2902,9 @@ export function composeRuntime(options) {
1743
2902
  return;
1744
2903
  if (event.kind === 'opened') {
1745
2904
  socketOpen = true;
2905
+ generation += 1;
2906
+ conversations.reopened();
2907
+ recovery.resume();
1746
2908
  void sync('opened');
1747
2909
  }
1748
2910
  else if (event.kind === 'closed')
@@ -1751,7 +2913,7 @@ export function composeRuntime(options) {
1751
2913
  void applyOpening(event.expired);
1752
2914
  else
1753
2915
  void applyRoom(event.locator, event.roomVersion);
1754
- });
2916
+ }, receipts);
1755
2917
  }
1756
2918
  function writesHeld() {
1757
2919
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -1766,6 +2928,7 @@ export function composeRuntime(options) {
1766
2928
  haltedRefusal = false;
1767
2929
  if (mode !== serviceMode) {
1768
2930
  serviceMode = mode;
2931
+ queueReadiness();
1769
2932
  journal.record(`service mode ${mode}`);
1770
2933
  for (const listener of serviceModeListeners)
1771
2934
  listener(mode);
@@ -1778,6 +2941,8 @@ export function composeRuntime(options) {
1778
2941
  if (stream === undefined || withdrawConversations !== undefined)
1779
2942
  return;
1780
2943
  withdrawConversations = stream.observe((event) => {
2944
+ if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
2945
+ queueReadiness();
1781
2946
  if (conversations.receive(event)) {
1782
2947
  void ready.then(scheduleCurrency);
1783
2948
  return;
@@ -1789,46 +2954,47 @@ export function composeRuntime(options) {
1789
2954
  });
1790
2955
  }
1791
2956
  function scheduleCurrency() {
1792
- if (currencyQueued || closed || stopping)
2957
+ if (currencyQueued === generation || closed || stopping)
1793
2958
  return;
1794
- currencyQueued = true;
1795
- void task(runCurrency);
2959
+ currencyQueued = generation;
2960
+ void task(CONVERSATION_LANE, runCurrency);
1796
2961
  }
1797
- async function runCurrency() {
1798
- currencyQueued = false;
1799
- if (closed || stopping || discoveryPaused)
2962
+ async function runCurrency(live) {
2963
+ if (currencyQueued === generation)
2964
+ currencyQueued = null;
2965
+ if (!live())
1800
2966
  return;
1801
2967
  await conversations.reconcile();
1802
- if (conversations.current()) {
2968
+ if (conversations.current() || !demand.conversations) {
1803
2969
  show();
1804
2970
  return;
1805
2971
  }
1806
- const resting = status;
1807
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1808
- moveTo(transferring(resting));
2972
+ const progress = { complete: false, reachable: !status.providerUnreachable };
2973
+ moveTo(transferring(status));
1809
2974
  try {
1810
2975
  await conversations.catchUp(progress);
1811
2976
  }
1812
2977
  catch {
1813
2978
  progress.reachable = false;
1814
2979
  }
1815
- finally {
1816
- if (!closed) {
1817
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1818
- }
1819
- }
2980
+ await rest(progress, live);
1820
2981
  }
1821
2982
  function close() {
1822
2983
  if (closed)
1823
2984
  return;
1824
2985
  closed = true;
2986
+ if (narrowing !== null)
2987
+ clearTimeout(narrowing);
2988
+ if (rechecking !== null)
2989
+ clearInterval(rechecking);
2990
+ readinessWatches.clear();
1825
2991
  withdrawStream?.();
1826
2992
  withdrawStream = undefined;
1827
2993
  withdrawConversations?.();
1828
2994
  withdrawConversations = undefined;
1829
2995
  withdrawServiceMode?.();
1830
2996
  conversations.close();
1831
- stopTimer();
2997
+ void recovery.retire();
1832
2998
  store?.close();
1833
2999
  void opening?.then((opened) => {
1834
3000
  opened.close();
@@ -1838,6 +3004,7 @@ export function composeRuntime(options) {
1838
3004
  storageLossListeners.clear();
1839
3005
  statusListeners.clear();
1840
3006
  serviceModeListeners.clear();
3007
+ memberListeners.clear();
1841
3008
  readSide.close();
1842
3009
  queueSubscriptions.clear();
1843
3010
  roomDraftListeners.clear();
@@ -1845,9 +3012,8 @@ export function composeRuntime(options) {
1845
3012
  }
1846
3013
  async function stop() {
1847
3014
  stopping = true;
1848
- stopTimer();
1849
- await chain.catch(() => undefined);
1850
- await Promise.all([...submitting.values()].map((run) => run.done));
3015
+ await recovery.retire();
3016
+ await Promise.all(working);
1851
3017
  await mutations;
1852
3018
  close();
1853
3019
  }
@@ -1869,14 +3035,69 @@ export function composeRuntime(options) {
1869
3035
  return planOf(input, roomFacts(reach), planReach(reach));
1870
3036
  }
1871
3037
  function watchPlan(plan, deliver) {
1872
- return readSide.watch(plan, identityOf(plan), deliver);
3038
+ return readSide.watch(plan, identityOf(plan), () => {
3039
+ const cover = covered(plan);
3040
+ awaitPreparation(cover);
3041
+ return cover;
3042
+ }, deliver);
1873
3043
  }
1874
3044
  async function searched(input, reach) {
1875
3045
  const plan = searchPlanOf(input);
1876
- const selected = plan.documents === undefined && reach === null
1877
- ? null
1878
- : (await readSide.query(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
1879
- return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
3046
+ const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
3047
+ for (;;) {
3048
+ const cover = covered(filter);
3049
+ awaitPreparation(cover);
3050
+ const pending = 'pending' in cover ? [...cover.pending] : [];
3051
+ const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3052
+ const held = covered({
3053
+ ...filter,
3054
+ rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
3055
+ });
3056
+ const covering = 'pending' in held ? [] : held.coverage.rooms;
3057
+ const unindexed = covering
3058
+ .filter(({ room }) => !searchIndex.rooms().has(room))
3059
+ .map(({ room }) => room);
3060
+ for (const room of unindexed)
3061
+ pending.push({ room, awaits: 'index' });
3062
+ const listed = conversations.searchable();
3063
+ const named = reach !== null && !reach.conversations
3064
+ ? []
3065
+ : (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
3066
+ const waiting = named.filter((one) => !searchIndex.conversations().has(one));
3067
+ for (const conversation of waiting)
3068
+ pending.push({ conversation, awaits: 'index' });
3069
+ if (unindexed.length > 0 || waiting.length > 0)
3070
+ index(unindexed, waiting);
3071
+ const complete = pending.length === 0 && !('pending' in cover);
3072
+ const answering = complete
3073
+ ? filter
3074
+ : {
3075
+ ...filter,
3076
+ rooms: covering
3077
+ .filter(({ room }) => searchIndex.rooms().has(room))
3078
+ .map(({ room }) => room),
3079
+ };
3080
+ const answered = covered(answering);
3081
+ if ('pending' in answered)
3082
+ continue;
3083
+ const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
3084
+ const seen = searchIndex.changes();
3085
+ await searchIndex.caughtUp();
3086
+ const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(answering);
3087
+ if (selected !== null && 'pending' in selected)
3088
+ continue;
3089
+ if (searchIndex.changes() !== seen ||
3090
+ JSON.stringify(covered(answering)) !== JSON.stringify(answered))
3091
+ continue;
3092
+ const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, complete ? null : new Set(answering.rooms));
3093
+ const coverage = {
3094
+ rooms: answered.coverage.rooms,
3095
+ conversations: searchedConversations,
3096
+ index: searchIndex.generation(),
3097
+ overlay: answered.coverage.overlay,
3098
+ };
3099
+ return complete ? { ...hits, coverage } : { pending, ...hits, coverage };
3100
+ }
1880
3101
  }
1881
3102
  const watch = (topic, input, deliver) => {
1882
3103
  if (topic === 'room-drafts') {
@@ -1901,6 +3122,8 @@ export function composeRuntime(options) {
1901
3122
  }
1902
3123
  if (topic === 'conversations')
1903
3124
  return conversations.watchList(deliver);
3125
+ if (topic === 'agents')
3126
+ return conversations.watchAgents(deliver);
1904
3127
  if (topic === 'presentation') {
1905
3128
  const listener = {
1906
3129
  app: input[0],
@@ -1920,6 +3143,9 @@ export function composeRuntime(options) {
1920
3143
  presentationListeners.delete(listener);
1921
3144
  };
1922
3145
  }
3146
+ if (topic === 'readiness') {
3147
+ return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
3148
+ }
1923
3149
  if (topic === 'rooms') {
1924
3150
  roomListeners.add(deliver);
1925
3151
  deliver(listed());
@@ -1955,6 +3181,13 @@ export function composeRuntime(options) {
1955
3181
  serviceModeListeners.delete(deliver);
1956
3182
  };
1957
3183
  }
3184
+ if (topic === 'member') {
3185
+ memberListeners.add(deliver);
3186
+ deliver(member);
3187
+ return () => {
3188
+ memberListeners.delete(deliver);
3189
+ };
3190
+ }
1958
3191
  if (topic === 'queue') {
1959
3192
  const queue = { locator: input[0], notify: deliver };
1960
3193
  queueSubscriptions.add(queue);
@@ -1972,7 +3205,7 @@ export function composeRuntime(options) {
1972
3205
  rooms() {
1973
3206
  return listed();
1974
3207
  },
1975
- query: ((input) => readSide.query(planned(input, null))),
3208
+ query: ((input) => rowsQuery(planned(input, null))),
1976
3209
  search: (input) => searched(input, null),
1977
3210
  types(locator) {
1978
3211
  return {
@@ -2001,20 +3234,20 @@ export function composeRuntime(options) {
2001
3234
  return exclusive(async () => {
2002
3235
  const parts = readParts(input);
2003
3236
  if (path === '' || normalize(path) !== path)
2004
- throw new Error('room-draft-malformed');
3237
+ throw runtimeError('room-draft-malformed');
2005
3238
  const local = await openLocal();
2006
3239
  const current = roomDrafts.get(room)?.get(path);
2007
3240
  if ((current?.revision ?? null) !== revision) {
2008
- return {
2009
- outcome: 'stale',
2010
- current: current === undefined ? null : roomDraftOf(current),
2011
- };
3241
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2012
3242
  }
3243
+ const file = parts.file === undefined
3244
+ ? undefined
3245
+ : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
2013
3246
  const record = {
2014
3247
  locator: room,
2015
3248
  path,
2016
3249
  revision: roomDraftRevision(),
2017
- parts: held(current?.parts, parts),
3250
+ parts: held(current?.parts, parts, file),
2018
3251
  returned: current?.returned ?? null,
2019
3252
  };
2020
3253
  await local.keepRoomDraft(record);
@@ -2028,10 +3261,7 @@ export function composeRuntime(options) {
2028
3261
  const local = await openLocal();
2029
3262
  const current = roomDrafts.get(room)?.get(path);
2030
3263
  if (current === undefined || current.revision !== revision) {
2031
- return {
2032
- outcome: 'stale',
2033
- current: current === undefined ? null : roomDraftOf(current),
2034
- };
3264
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2035
3265
  }
2036
3266
  await local.discardRoomDraft(room, path);
2037
3267
  forgetRoomDraft(room, path);
@@ -2066,6 +3296,10 @@ export function composeRuntime(options) {
2066
3296
  history(locator, path, query) {
2067
3297
  return readHistory(locator, path, query);
2068
3298
  },
3299
+ binaryFile: {
3300
+ read: readBinaryPath,
3301
+ readReference: readBinaryReference,
3302
+ },
2069
3303
  call(name, input) {
2070
3304
  return invokeAction(name, input);
2071
3305
  },
@@ -2078,30 +3312,54 @@ export function composeRuntime(options) {
2078
3312
  return journal.export();
2079
3313
  },
2080
3314
  persistence() {
2081
- return options.shell.persistentStorage;
3315
+ return persistence === 'granted';
3316
+ },
3317
+ async retry() {
3318
+ journal.record('retry');
3319
+ unindexable.clear();
3320
+ queueReadiness();
3321
+ resume();
2082
3322
  },
2083
3323
  observe: observing(watch),
2084
3324
  async reauthorize() {
2085
3325
  if (refusal === null || !refusal.reauthorizationOffered)
2086
3326
  return;
2087
3327
  journal.record('connection reauthorize');
2088
- await chain.catch(() => undefined);
3328
+ await Promise.all([...lanes.values()]
3329
+ .filter((held) => held.generation === generation)
3330
+ .map((held) => held.chain));
2089
3331
  await mutations;
2090
3332
  authorization = undefined;
2091
3333
  discoveryPaused = false;
2092
3334
  if (options.adapter.roomStream === undefined)
2093
3335
  await sync('unstreamed');
2094
3336
  else
2095
- await task(ensureAuthorization);
3337
+ await task(OPENING_LANE, ensureAuthorization);
2096
3338
  },
2097
3339
  };
2098
3340
  const reachable = {
2099
3341
  md,
2100
3342
  watch,
3343
+ referrerScope: (referrer) => options.adapter.referrerScope(referrer),
2101
3344
  plan: planned,
2102
- query: (plan) => readSide.query(plan),
3345
+ query: (plan) => rowsQuery(plan),
2103
3346
  watchPlan,
2104
3347
  search: searched,
3348
+ watchReadiness,
3349
+ };
3350
+ return {
3351
+ md,
3352
+ stop,
3353
+ lose: loseRooms,
3354
+ watch,
3355
+ acquire,
3356
+ persisted(answer) {
3357
+ if (answer === persistence)
3358
+ return;
3359
+ persistence = answer;
3360
+ journal.record(`persistence ${answer}`);
3361
+ queueReadiness();
3362
+ },
3363
+ within: (reach, port) => narrowed(reachable, reach, port),
2105
3364
  };
2106
- return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2107
3365
  }