@agentshouse/mdruntime 0.1.0-ca1eff92bbe73 → 0.1.0-ca4dc698bd0c4

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 -209
  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 +76 -17
  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 +109 -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 +48 -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 +60 -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 +26 -0
  39. package/dist/chat/list.d.ts.map +1 -0
  40. package/dist/chat/list.js +62 -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 +21 -0
  48. package/dist/chat/messages.d.ts.map +1 -0
  49. package/dist/chat/messages.js +151 -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 +620 -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 +33 -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 +23 -11
  87. package/dist/client.d.ts.map +1 -1
  88. package/dist/client.js +67 -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 +41 -15
  93. package/dist/connection.d.ts.map +1 -1
  94. package/dist/connection.js +1631 -378
  95. package/dist/conversation.d.ts +24 -10
  96. package/dist/conversation.d.ts.map +1 -1
  97. package/dist/conversation.js +307 -136
  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 +13 -2
  113. package/dist/host.d.ts.map +1 -1
  114. package/dist/host.js +48 -14
  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 +6 -3
  122. package/dist/memory.d.ts.map +1 -1
  123. package/dist/memory.js +70 -22
  124. package/dist/model.d.ts +4 -2
  125. package/dist/model.d.ts.map +1 -1
  126. package/dist/model.js +9 -0
  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 +15 -3
  137. package/dist/overlay.d.ts.map +1 -1
  138. package/dist/overlay.js +94 -77
  139. package/dist/query.d.ts +32 -3
  140. package/dist/query.d.ts.map +1 -1
  141. package/dist/query.js +5 -31
  142. package/dist/reach.d.ts +8 -2
  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 +291 -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 +43 -38
  160. package/dist/replica.d.ts +8 -5
  161. package/dist/replica.d.ts.map +1 -1
  162. package/dist/replica.js +115 -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 +8 -13
  167. package/dist/rows.d.ts.map +1 -1
  168. package/dist/rows.js +12 -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 +31 -8
  173. package/dist/search.d.ts.map +1 -1
  174. package/dist/search.js +108 -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 +73 -17
  182. package/dist/store.d.ts.map +1 -1
  183. package/dist/store.js +531 -198
  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 +9 -3
  188. package/dist/wire.d.ts.map +1 -1
  189. package/dist/wire.js +17 -2
  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,486 @@ 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 target = targets[port.target];
647
+ const search = target?.search === true;
648
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
649
+ const building = (state) => coverage
650
+ .filter((room) => room.state === state && !unindexable.has(room.room))
651
+ .map(({ room }) => room);
652
+ const preparing = building('preparing');
653
+ if (preparing.length > 0)
654
+ prepare(preparing);
655
+ const indexing = building('indexing');
656
+ if (indexing.length > 0)
657
+ index(indexing, []);
658
+ return readinessOf({
659
+ code: port.code,
660
+ coverage: {
661
+ listed: required.listed,
662
+ rooms: coverage,
663
+ conversations: conversationCoverage(reach, search),
664
+ search: search ? 'required' : 'unused',
665
+ },
666
+ persistence,
667
+ connection: connectionOf(),
668
+ house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
669
+ capacity: capacity !== null &&
670
+ coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
671
+ ? capacity
672
+ : port.capacity,
673
+ refused: refusedIn(required.rooms),
674
+ lost: storageLoss !== null,
675
+ established: target?.established === true,
676
+ });
677
+ }
678
+ function keepTarget(name, change) {
679
+ const held = targets[name] ?? { established: false, search: false };
680
+ const next = { ...held, ...change };
681
+ if (next.established === held.established && next.search === held.search)
682
+ return;
683
+ targets = { ...targets, [name]: next };
684
+ queueReadiness();
685
+ void durably((local) => local.keepTargets(targets)).catch(() => {
686
+ journal.record(`target record failed ${name}`);
687
+ });
688
+ }
689
+ function deliverReadiness(watch) {
690
+ if (!readinessWatches.has(watch))
691
+ return;
692
+ const readiness = readinessFor(watch.port, watch.reach);
693
+ if (readiness.ready && targets[watch.port.target]?.established !== true) {
694
+ keepTarget(watch.port.target, { established: true });
695
+ }
696
+ const shown = JSON.stringify(readiness);
697
+ if (shown === watch.last)
698
+ return;
699
+ watch.last = shown;
700
+ watch.deliver(structuredClone(readiness));
701
+ }
702
+ function queueReadiness() {
703
+ if (readinessQueued || closed || readinessWatches.size === 0)
704
+ return;
705
+ readinessQueued = true;
706
+ queueMicrotask(() => {
707
+ readinessQueued = false;
708
+ if (closed)
709
+ return;
710
+ for (const watch of [...readinessWatches])
711
+ deliverReadiness(watch);
712
+ });
713
+ }
714
+ function watchReadiness(port, reach, deliver) {
715
+ const watch = { port, reach, deliver, last: null };
716
+ readinessWatches.add(watch);
717
+ void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
718
+ return () => {
719
+ readinessWatches.delete(watch);
720
+ };
721
+ }
722
+ function searching(target) {
723
+ if (targets[target]?.search !== true)
724
+ keepTarget(target, { search: true });
725
+ }
726
+ function layerBytes(layer) {
727
+ let bytes = 0;
728
+ for (const [path, text] of Object.entries(layer.documents))
729
+ bytes += (path.length + text.length) * 2;
730
+ return bytes;
731
+ }
732
+ function grownBy(next, previous) {
733
+ if (previous === undefined)
734
+ return layerBytes(next);
735
+ const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
736
+ return layerBytes({ ...next, documents });
737
+ }
738
+ function exceeded(error) {
739
+ return (typeof error === 'object' &&
740
+ error !== null &&
741
+ error.name === 'QuotaExceededError');
742
+ }
743
+ async function estimated() {
744
+ const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
745
+ return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
746
+ }
747
+ function pause(locator, cause) {
748
+ paused.add(locator);
749
+ capacity = cause;
750
+ journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
751
+ queueReadiness();
752
+ if (rechecking !== null)
753
+ return;
754
+ // A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
755
+ rechecking = setInterval(() => {
756
+ void recheck();
757
+ }, 60_000);
758
+ }
759
+ async function recheck() {
760
+ const held = capacity;
761
+ if (held === null || closed)
762
+ return;
763
+ const available = await estimated().catch(() => null);
764
+ if (available === null || available < (held.needed ?? 0))
765
+ return;
766
+ if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
767
+ return;
768
+ resume();
769
+ }
770
+ function resume() {
771
+ if (rechecking !== null)
772
+ clearInterval(rechecking);
773
+ rechecking = null;
774
+ if (paused.size === 0 && capacity === null)
775
+ return;
776
+ paused.clear();
777
+ capacity = null;
778
+ journal.record('capacity resumed');
779
+ queueReadiness();
780
+ if (!closed && authorization !== undefined)
781
+ void sync('unstreamed');
782
+ }
783
+ async function reclaimed(local) {
784
+ const unreferenced = [...transfers.keys()].filter((locator) => {
785
+ const room = discovered.get(locator);
786
+ return room === undefined || !acquires(room);
787
+ });
788
+ for (const locator of unreferenced) {
789
+ await local.abandon(locator);
790
+ transfers.delete(locator);
791
+ }
792
+ if (unreferenced.length > 0)
793
+ journal.record(`capacity reclaimed ${unreferenced.length}`);
794
+ return unreferenced.length > 0;
795
+ }
796
+ async function admitted(local, locator, needed) {
797
+ if (paused.has(locator))
798
+ return false;
799
+ let available = await estimated().catch(() => null);
800
+ if (available === null || available >= needed)
801
+ return true;
802
+ if (await reclaimed(local))
803
+ available = await estimated().catch(() => null);
804
+ if (available === null || available >= needed)
805
+ return true;
806
+ pause(locator, { cause: 'insufficient-space', needed, available });
807
+ return false;
808
+ }
809
+ async function exceededOn(locator, bytes) {
810
+ const available = await estimated().catch(() => null);
811
+ pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
812
+ }
813
+ async function rowsQuery(plan) {
814
+ const before = covered(plan);
815
+ awaitPreparation(before);
816
+ if ('pending' in before)
817
+ return before;
818
+ const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
819
+ awaitPreparation(cover);
820
+ if ('pending' in cover)
821
+ return cover;
822
+ return 'groups' in rows
823
+ ? { groups: rows.groups, coverage: cover.coverage }
824
+ : { documents: rows.documents, coverage: cover.coverage };
825
+ }
826
+ async function indexConversation(conversation) {
827
+ searchIndex.indexMessages(conversation, [], false);
828
+ let after = null;
829
+ for (;;) {
830
+ const batch = await durably((local) => local.messagesAfter(conversation, after));
831
+ if (closed)
832
+ return;
833
+ searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
834
+ if (batch.length === 0)
835
+ return;
836
+ after = batch[batch.length - 1].identity;
837
+ }
838
+ }
839
+ function indexNext() {
840
+ const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
841
+ if (room !== undefined) {
842
+ roomsToIndex.delete(room);
843
+ return () => prepared.has(room)
844
+ ? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
845
+ : undefined;
846
+ }
847
+ const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
848
+ if (conversation === undefined)
849
+ return null;
850
+ conversationsToIndex.delete(conversation);
851
+ return () => indexConversation(conversation);
852
+ }
853
+ function index(rooms, named) {
854
+ for (const room of rooms)
855
+ roomsToIndex.add(room);
856
+ for (const conversation of named)
857
+ conversationsToIndex.add(conversation);
858
+ if (indexing)
859
+ return;
860
+ indexing = true;
861
+ void (async () => {
862
+ try {
863
+ for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
864
+ await yielded();
865
+ await unit();
866
+ queueReadiness();
867
+ }
868
+ }
869
+ finally {
870
+ indexing = false;
871
+ }
872
+ })().catch(() => { });
289
873
  }
290
874
  function publishChangeSet(set) {
291
875
  applyChangeSet(set);
292
- if (set.rooms.length > 0)
876
+ if (set.rooms.length > 0 ||
877
+ [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
293
878
  emitRooms();
294
879
  for (const locator of set.queues)
295
880
  emitQueue(locator);
@@ -301,34 +886,7 @@ export function composeRuntime(options) {
301
886
  emitPresentation(app);
302
887
  conversations.changed(set);
303
888
  }
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
889
  async function refuse(reason, provider = null) {
331
- stopTimer();
332
890
  discoveryPaused = true;
333
891
  try {
334
892
  if (reason === 'revoked-authorization') {
@@ -336,10 +894,7 @@ export function composeRuntime(options) {
336
894
  const revoked = { status: 'unavailable', reason: 'revoked' };
337
895
  try {
338
896
  const local = await openLocal();
339
- await local.revokeModels([...replicas.values()].map((replica) => ({
340
- record: replica.confirmed.record,
341
- queue: replica.queue,
342
- })));
897
+ await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
343
898
  }
344
899
  catch {
345
900
  journal.record('model revocation persistence failed');
@@ -363,10 +918,7 @@ export function composeRuntime(options) {
363
918
  model: revoked,
364
919
  selectionModel: revoked,
365
920
  };
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 } });
921
+ replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
370
922
  }
371
923
  rooms.set(locator, {
372
924
  ...room,
@@ -383,7 +935,8 @@ export function composeRuntime(options) {
383
935
  }
384
936
  finally {
385
937
  refusal = {
386
- reason,
938
+ code: reason,
939
+ retryable: false,
387
940
  reauthorizationOffered: reason === 'revoked-authorization',
388
941
  refusal: provider,
389
942
  };
@@ -408,19 +961,28 @@ export function composeRuntime(options) {
408
961
  },
409
962
  providerOwnedPaths: room.providerOwnedPaths,
410
963
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
411
- attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
412
964
  synchronizationAvailable,
413
965
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
414
966
  });
415
967
  }
416
- async function applyDiscovery(listed, local) {
968
+ async function applyDiscovery(found, started) {
969
+ for (const room of found) {
970
+ const fence = fences.get(room.locator);
971
+ if (fence !== undefined && fence.erased && fence.at < started)
972
+ fences.delete(room.locator);
973
+ }
974
+ const listed = found.filter((room) => !fences.has(room.locator));
417
975
  const listedLocators = new Set(listed.map((room) => room.locator));
418
976
  for (const locator of listedLocators)
419
977
  scopedRefusals.delete(locator);
420
978
  discovered.clear();
421
979
  for (const room of listed)
422
980
  discovered.set(room.locator, room);
423
- await dropUnlisted(listedLocators, local);
981
+ const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
982
+ if (options.adapter.exhaustiveDiscovery === true)
983
+ await loseRooms(unlisted);
984
+ else
985
+ unlistedKept(unlisted);
424
986
  for (const room of listed) {
425
987
  const held = replicas.get(room.locator)?.confirmed.record;
426
988
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -431,43 +993,85 @@ export function composeRuntime(options) {
431
993
  }
432
994
  roomsListed = true;
433
995
  emitRooms();
996
+ return listed;
434
997
  }
435
998
  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);
999
+ return [
1000
+ ...new Set([
1001
+ ...rooms.keys(),
1002
+ ...roomDrafts.keys(),
1003
+ ...transfers.keys(),
1004
+ ...queuesWithoutReplica.keys(),
1005
+ ]),
1006
+ ].filter((locator) => !listed.has(locator));
1007
+ }
1008
+ function unlistedKept(unlisted) {
1009
+ for (const locator of unlisted) {
1010
+ const held = replicas.get(locator)?.confirmed.record;
1011
+ if (held !== undefined)
1012
+ list(held, false);
1013
+ else
453
1014
  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}`);
1015
+ }
1016
+ }
1017
+ function fence(locators) {
1018
+ const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1019
+ const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
1020
+ room: true,
1021
+ queue: true,
1022
+ drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
1023
+ }));
1024
+ for (const locator of fresh) {
1025
+ fences.set(locator, { at: discoveries, erased: false });
1026
+ lastFenced.set(locator, ++fencings);
1027
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1028
+ pendings.delete(batch.id);
1029
+ replicas.delete(locator);
1030
+ queuesWithoutReplica.delete(locator);
1031
+ rooms.delete(locator);
1032
+ roomDrafts.delete(locator);
1033
+ transfers.delete(locator);
1034
+ catching.delete(locator);
1035
+ discovered.delete(locator);
1036
+ announced.delete(locator);
1037
+ unreadable.delete(locator);
1038
+ stale.delete(locator);
1039
+ repairable.delete(locator);
1040
+ unindexable.delete(locator);
1041
+ paused.delete(locator);
1042
+ scopedRefusals.delete(locator);
1043
+ sourceRefusals.delete(locator);
1044
+ declarationRefusals.delete(locator);
1045
+ for (const attached of ['collection', 'typed-selection']) {
1046
+ const key = modelKey(locator, attached);
1047
+ models.delete(key);
1048
+ liveModels.delete(key);
1049
+ invalidModels.delete(key);
467
1050
  }
468
- publishChangeSet(mergeChangeSets(removed));
469
- moveTo({ ...status, ...attention() });
470
- });
1051
+ journal.record(`room lost ${locator}`);
1052
+ }
1053
+ if (fresh.length === 0)
1054
+ return fresh;
1055
+ publishChangeSet(mergeChangeSets(removed));
1056
+ for (const locator of fresh)
1057
+ prepared.delete(locator);
1058
+ searchIndex.forget(fresh);
1059
+ readSide.recover();
1060
+ moveTo({ ...status, ...attention() });
1061
+ return fresh;
1062
+ }
1063
+ async function erased(local, locators) {
1064
+ await local.erase(locators);
1065
+ for (const locator of locators) {
1066
+ const held = fences.get(locator);
1067
+ if (held !== undefined)
1068
+ held.erased = true;
1069
+ }
1070
+ }
1071
+ async function loseRooms(locators) {
1072
+ const fresh = fence(locators);
1073
+ if (fresh.length > 0)
1074
+ await durably((local) => erased(local, fresh));
471
1075
  }
472
1076
  function restored(key, persisted) {
473
1077
  const prior = models.get(key);
@@ -488,7 +1092,7 @@ export function composeRuntime(options) {
488
1092
  return model;
489
1093
  }
490
1094
  async function hydrate(opened) {
491
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
1095
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
492
1096
  opened.all(),
493
1097
  opened.queues(),
494
1098
  opened.roomDrafts(),
@@ -498,9 +1102,17 @@ export function composeRuntime(options) {
498
1102
  opened.lastCompleted(),
499
1103
  opened.providerLimits(),
500
1104
  opened.capabilities(),
1105
+ opened.member(),
1106
+ opened.transfers(),
1107
+ opened.targets(),
501
1108
  ]);
1109
+ targets = heldTargets;
1110
+ transfers.clear();
1111
+ for (const record of staged)
1112
+ transfers.set(record.locator, record);
502
1113
  limits = declared;
503
1114
  capabilities = named;
1115
+ member = known;
504
1116
  roomDrafts.clear();
505
1117
  for (const record of kept)
506
1118
  rememberRoomDraft(record);
@@ -516,18 +1128,17 @@ export function composeRuntime(options) {
516
1128
  }
517
1129
  const previousReplicas = new Map(replicas);
518
1130
  replicas.clear();
1131
+ queuesWithoutReplica.clear();
1132
+ const replicated = new Set(records.map((record) => record.locator));
1133
+ for (const [locator, queue] of queuedBatches) {
1134
+ if (!replicated.has(locator))
1135
+ queuesWithoutReplica.set(locator, queue);
1136
+ }
519
1137
  for (const record of records) {
520
1138
  const model = restored(modelKey(record.locator, 'collection'), record.model);
521
1139
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
522
1140
  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
- };
1141
+ const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
531
1142
  replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
532
1143
  const existing = rooms.get(record.locator);
533
1144
  if (existing === undefined)
@@ -536,7 +1147,6 @@ export function composeRuntime(options) {
536
1147
  rooms.set(record.locator, {
537
1148
  ...existing,
538
1149
  roomVersion: record.roomVersion,
539
- attachments: record.attachments,
540
1150
  operations: record.operations,
541
1151
  replica: record.replica,
542
1152
  model: modelTypes(model),
@@ -548,10 +1158,6 @@ export function composeRuntime(options) {
548
1158
  },
549
1159
  });
550
1160
  }
551
- if (read.kind === 'refused') {
552
- refusedSource(record, read);
553
- declarationRefusals.add(record.locator);
554
- }
555
1161
  }
556
1162
  return {
557
1163
  lastCompleted,
@@ -562,16 +1168,26 @@ export function composeRuntime(options) {
562
1168
  async function readState(opened) {
563
1169
  const { lastCompleted, locators, read } = await hydrate(opened);
564
1170
  applyChangeSet(read);
1171
+ prepare();
565
1172
  if (replicas.size > 0)
566
1173
  emitRooms();
1174
+ if (member !== null)
1175
+ emitMember();
567
1176
  for (const locator of locators)
568
1177
  emitQueue(locator);
569
1178
  publishRoomDrafts(false);
570
1179
  return lastCompleted;
571
1180
  }
572
1181
  async function open() {
573
- const opened = await openRuntimeStore();
1182
+ if (closed)
1183
+ throw new Error('runtime closed');
1184
+ const opened = await openRuntimeStore(isFenced);
574
1185
  store = opened;
1186
+ const interrupted = await opened.fences();
1187
+ for (const locator of interrupted) {
1188
+ fences.set(locator, { at: discoveries, erased: false });
1189
+ lastFenced.set(locator, ++fencings);
1190
+ }
575
1191
  const kept = options.shell.generation;
576
1192
  const storedGeneration = await opened.generation();
577
1193
  const loss = detectStorageLoss(storedGeneration, kept);
@@ -585,6 +1201,8 @@ export function composeRuntime(options) {
585
1201
  emitStorageLoss();
586
1202
  }
587
1203
  const lastCompleted = await readState(opened);
1204
+ if (interrupted.length > 0)
1205
+ await erased(opened, interrupted);
588
1206
  moveTo({ ...status, lastCompleted, ...attention() });
589
1207
  return opened;
590
1208
  }
@@ -611,11 +1229,12 @@ export function composeRuntime(options) {
611
1229
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
612
1230
  }
613
1231
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
614
- const withdrawn = withdrawals(confirmed, queue, withdrawable);
1232
+ const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
1233
+ const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
615
1234
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
616
1235
  const unqueued = withdrawn.map((batch) => ({
617
1236
  batch,
618
- returned: landed(batch, confirmed) ? null : { kind: 'withdrawn' },
1237
+ returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
619
1238
  }));
620
1239
  const returning = [
621
1240
  ...[...unqueued].reverse(),
@@ -636,28 +1255,48 @@ export function composeRuntime(options) {
636
1255
  });
637
1256
  }
638
1257
  }
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 ?? []) {
1258
+ await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
1259
+ for (const batch of previousQueue) {
647
1260
  if (!kept.includes(batch))
648
1261
  pendings.delete(batch.id);
649
1262
  }
650
1263
  for (const record of written.values())
651
1264
  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
- });
1265
+ try {
1266
+ await local.write({
1267
+ next: confirmed.record,
1268
+ previous: before?.confirmed.record ?? null,
1269
+ ...(settling.stored === undefined ? {} : { stored: settling.stored }),
1270
+ });
1271
+ }
1272
+ catch (error) {
1273
+ const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
1274
+ if (held !== null)
1275
+ replicas.set(locator, held);
1276
+ else if (kept.length > 0)
1277
+ queuesWithoutReplica.set(locator, kept);
1278
+ else
1279
+ queuesWithoutReplica.delete(locator);
1280
+ publishChangeSet(changeSet(locator, before ?? null, held, {
1281
+ queue: kept.length !== previousQueue.length,
1282
+ drafts: [...written.keys()],
1283
+ }));
1284
+ moveTo({ ...status, ...attention() });
1285
+ throw error;
1286
+ }
1287
+ return () => {
1288
+ queuesWithoutReplica.delete(locator);
1289
+ for (const { batch, returned } of unqueued) {
1290
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
1291
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1292
+ }
1293
+ replicas.set(locator, replicaOf(confirmed, kept, before));
1294
+ return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
1295
+ room: settling.room,
1296
+ queue: kept.length !== previousQueue.length,
1297
+ drafts: [...written.keys()],
1298
+ });
1299
+ };
661
1300
  }
662
1301
  function adoptModels(locator, record) {
663
1302
  for (const [attached, model] of [
@@ -690,10 +1329,16 @@ export function composeRuntime(options) {
690
1329
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
691
1330
  };
692
1331
  }
693
- function publish(local, room, confirmed, base) {
1332
+ function publish(local, room, confirmed, landing, live) {
1333
+ const { base, stored } = landing;
694
1334
  return exclusive(async () => {
695
- if (!continuing())
1335
+ if (stored !== undefined) {
1336
+ await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
1337
+ }
1338
+ if (!live()) {
1339
+ caughtUpTo(room.locator);
696
1340
  return true;
1341
+ }
697
1342
  const prior = replicas.get(room.locator);
698
1343
  if (base !== undefined &&
699
1344
  (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
@@ -701,47 +1346,85 @@ export function composeRuntime(options) {
701
1346
  return false;
702
1347
  }
703
1348
  adoptModels(room.locator, confirmed.record);
704
- const queue = prior?.queue ?? [];
1349
+ const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
705
1350
  const roomVersion = confirmed.record.roomVersion;
706
1351
  const accepted = queue.filter((batch) => {
707
1352
  const accepted = pendings.get(batch.id)?.accepted;
708
1353
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
709
1354
  });
710
- const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
1355
+ let settled;
1356
+ try {
1357
+ settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
1358
+ }
1359
+ catch (error) {
1360
+ if (!exceeded(error))
1361
+ throw error;
1362
+ await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
1363
+ caughtUpTo(room.locator);
1364
+ return true;
1365
+ }
1366
+ const set = settled();
1367
+ if (stored !== undefined) {
1368
+ transfers.delete(room.locator);
1369
+ catching.delete(room.locator);
1370
+ }
711
1371
  for (const batch of accepted)
712
1372
  pendings.delete(batch.id);
713
1373
  stale.delete(room.locator);
1374
+ unindexable.delete(room.locator);
714
1375
  if (unreadable.delete(room.locator))
715
1376
  scopedRefusals.delete(room.locator);
716
1377
  sourceRefusals.delete(room.locator);
717
1378
  declarationRefusals.delete(room.locator);
718
1379
  list(room, true);
1380
+ if (prior === undefined)
1381
+ prepared.add(room.locator);
719
1382
  publishChangeSet(set);
720
1383
  moveTo({ ...status, ...attention() });
1384
+ options.adapter.roomStream?.published(room.locator, roomVersion);
721
1385
  return true;
722
1386
  });
723
1387
  }
724
1388
  async function saveRoomDrafts(locator, named, writtenBy) {
725
1389
  const local = await openLocal();
1390
+ await referenced(locator);
726
1391
  const replica = replicas.get(locator);
727
1392
  const room = rooms.get(locator);
728
1393
  if (replica === undefined || room === undefined) {
729
- throw new Error(`No Local Replica for ${locator}`);
1394
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
730
1395
  }
731
1396
  const saved = [];
732
1397
  for (const { path, revision } of named) {
733
1398
  const record = roomDrafts.get(locator)?.get(path);
734
1399
  if (record === undefined || record.revision !== revision) {
735
- return { outcome: 'refused', rule: 'stale', file: path };
1400
+ return { outcome: 'refused', code: 'stale', retryable: false, file: path };
736
1401
  }
737
1402
  if (!saved.some((draft) => draft.path === path))
738
1403
  saved.push({ path, parts: record.parts });
739
1404
  }
740
1405
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
741
1406
  if (changed.length > 0)
742
- return { outcome: 'refused', rule: 'changed-underneath', parts: changed };
1407
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
743
1408
  const parts = saved.map(({ path, parts }) => changesOf(path, parts));
744
- const built = buildBatch({
1409
+ const changes = parts.flatMap(({ changes }) => changes);
1410
+ const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
1411
+ if (requeue.kind === 'refused')
1412
+ return { outcome: 'refused', ...requeue.refusal };
1413
+ const foreign = [...requeue.withdrawn.keys()].some((origin) => {
1414
+ const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
1415
+ return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
1416
+ });
1417
+ if (foreign) {
1418
+ return {
1419
+ outcome: 'refused',
1420
+ code: 'unknown-revision',
1421
+ retryable: false,
1422
+ file: [...requeue.queued].at(-1),
1423
+ };
1424
+ }
1425
+ const withdrawing = requeue.withdrawn.size > 0;
1426
+ const input = {
1427
+ local: localSaves,
745
1428
  baseRoomVersion: replica.confirmed.record.roomVersion,
746
1429
  composition: compositionFor(replica.confirmed.record),
747
1430
  operations: room.operations,
@@ -749,29 +1432,93 @@ export function composeRuntime(options) {
749
1432
  providerLimits: limits,
750
1433
  handle: room.handle,
751
1434
  visible: replica.visible,
752
- changes: parts.flatMap(({ changes }) => changes),
1435
+ changes,
753
1436
  bases: parts.flatMap(({ bases }) => bases),
754
1437
  writtenBy,
1438
+ clean: {
1439
+ text: replica.confirmed.record.documents,
1440
+ binaries: replica.confirmed.record.binaries,
1441
+ composition: compositionOf(replica.confirmed.record),
1442
+ },
1443
+ };
1444
+ const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
1445
+ if (checked?.kind === 'refused')
1446
+ return { outcome: 'refused', ...checked.refusal };
1447
+ const owners = [...requeue.owners].flatMap((origin) => {
1448
+ const batch = requeue.withdrawn.get(origin);
1449
+ return batch === undefined || batch === null ? [] : [{ origin, batch }];
755
1450
  });
756
- if (built.kind === 'refused')
1451
+ if (owners.length > 1) {
1452
+ return {
1453
+ outcome: 'refused',
1454
+ code: 'unknown-revision',
1455
+ retryable: false,
1456
+ file: [...requeue.queued].at(-1),
1457
+ };
1458
+ }
1459
+ const built = !withdrawing
1460
+ ? buildBatch(input)
1461
+ : requeue.changes.length === 0
1462
+ ? null
1463
+ : buildBatch({
1464
+ ...input,
1465
+ visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
1466
+ changes: requeue.changes,
1467
+ });
1468
+ if (built?.kind === 'refused')
757
1469
  return { outcome: 'refused', ...built.refusal };
758
- const batches = [...replica.queue, built.batch];
1470
+ const [owner] = owners;
1471
+ const join = built === null || owner === undefined
1472
+ ? null
1473
+ : joined(requeue.queue, owner.batch, built.batch);
1474
+ if (join?.kind === 'refused')
1475
+ return { outcome: 'refused', ...join.refusal };
1476
+ const withdrawn = new Map(requeue.withdrawn);
1477
+ if (join !== null && owner !== undefined)
1478
+ withdrawn.set(owner.origin, join.batch);
1479
+ const drafted = new Map();
1480
+ for (const [origin, replaced] of withdrawn) {
1481
+ if (replaced === null)
1482
+ continue;
1483
+ const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
1484
+ drafted.set(replaced.id, replaced === join?.batch
1485
+ ? [
1486
+ ...carried.map((held) => joinedDraft(held, saved)),
1487
+ ...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
1488
+ ]
1489
+ : carried);
1490
+ }
1491
+ if (built !== null && join === null)
1492
+ drafted.set(built.batch.id, saved);
1493
+ const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
1494
+ for (const replaced of withdrawn.values()) {
1495
+ if (replaced === null || !batches.includes(replaced))
1496
+ continue;
1497
+ const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
1498
+ if (limited !== null)
1499
+ return { outcome: 'refused', ...limited };
1500
+ }
759
1501
  const consumed = saved.map(({ path }) => path);
760
1502
  try {
761
- await local.write({
762
- next: replica.confirmed.record,
763
- previous: replica.confirmed.record,
1503
+ await local.keepAuthored({
1504
+ locator,
764
1505
  queue: batches,
765
- previousQueue: replica.queue,
766
- saved,
1506
+ saved: drafted,
767
1507
  consumedRoomDrafts: consumed,
768
1508
  });
769
1509
  }
770
1510
  catch {
771
- return { outcome: 'refused', rule: 'durable-write' };
1511
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
1512
+ }
1513
+ if (fences.has(locator))
1514
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1515
+ for (const origin of withdrawn.keys())
1516
+ pendings.delete(origin);
1517
+ for (const [id, drafts] of drafted) {
1518
+ pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
772
1519
  }
773
- pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
774
- recorded = false;
1520
+ if (!localSaves)
1521
+ recorded = false;
775
1522
  for (const path of consumed)
776
1523
  forgetRoomDraft(locator, path);
777
1524
  replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
@@ -779,9 +1526,12 @@ export function composeRuntime(options) {
779
1526
  queue: true,
780
1527
  drafts: consumed,
781
1528
  }));
782
- moveTo({ ...saving(status), ...attention() });
1529
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
783
1530
  submitLater(locator);
784
- return { outcome: 'saved', identifier: built.batch.id };
1531
+ const identifier = join?.batch.id ??
1532
+ built?.batch.id ??
1533
+ [...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
1534
+ return { outcome: 'saved', identifier };
785
1535
  }
786
1536
  async function mark(local, locator, identifier, submission, lost) {
787
1537
  const state = pendings.get(identifier);
@@ -791,6 +1541,8 @@ export function composeRuntime(options) {
791
1541
  return;
792
1542
  }
793
1543
  await local.submit(locator, identifier, submission, lost);
1544
+ if (fences.has(locator))
1545
+ return;
794
1546
  pendings.set(identifier, { ...state, submission, lost });
795
1547
  const replica = replicas.get(locator);
796
1548
  if (replica !== undefined)
@@ -801,22 +1553,24 @@ export function composeRuntime(options) {
801
1553
  if (!continuing())
802
1554
  throw new Error('connection refused');
803
1555
  await local.keepUpload(locator, identifier, upload);
1556
+ if (fences.has(locator))
1557
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
804
1558
  pendings.set(identifier, { ...pendings.get(identifier), upload });
805
1559
  if (!continuing())
806
1560
  throw new Error('connection refused');
807
1561
  }
808
1562
  async function readHistory(locator, path, query = {}) {
809
1563
  if (closed || refusal !== null)
810
- return { kind: 'transport-uncertainty' };
1564
+ return UNCERTAIN;
811
1565
  try {
812
1566
  await ensureAuthorization();
813
1567
  }
814
1568
  catch {
815
- return { kind: 'transport-uncertainty' };
816
- }
817
- if (closed || authorization === undefined || refusal !== null) {
818
- return { kind: 'transport-uncertainty' };
1569
+ return UNCERTAIN;
819
1570
  }
1571
+ if (closed || authorization === undefined || refusal !== null)
1572
+ return UNCERTAIN;
1573
+ const since = fencings;
820
1574
  const transfer = ['transfer', 'history', locator, path];
821
1575
  if (query.revision !== undefined)
822
1576
  transfer.push(query.revision);
@@ -831,6 +1585,8 @@ export function composeRuntime(options) {
831
1585
  ...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
832
1586
  });
833
1587
  journal.record(`outcome history ${result.kind}`);
1588
+ if (fencedSince(locator, since))
1589
+ return failure({ kind: 'room-not-readable', locator });
834
1590
  if (result.kind === 'scoped-refusal') {
835
1591
  const room = replicas.get(result.locator)?.confirmed.record;
836
1592
  if (room !== undefined)
@@ -838,13 +1594,107 @@ export function composeRuntime(options) {
838
1594
  }
839
1595
  if (result.kind === 'revoked-authorization')
840
1596
  await refuse('revoked-authorization');
841
- return result;
1597
+ return historyAnswer(result);
842
1598
  }
843
1599
  catch {
844
1600
  journal.record('outcome history transport-uncertainty');
845
- return { kind: 'transport-uncertainty' };
1601
+ return UNCERTAIN;
1602
+ }
1603
+ }
1604
+ function queuedBlob(locator, path, localId) {
1605
+ for (const batch of replicas.get(locator)?.queue ?? []) {
1606
+ const change = batch.changes[queuedFile(batch, path, localId)];
1607
+ if (change?.kind === 'binary')
1608
+ return change.blob;
1609
+ }
1610
+ return null;
1611
+ }
1612
+ function binaryRoom(request) {
1613
+ if (request.kind === 'path')
1614
+ return request.locator;
1615
+ const scope = options.adapter.referrerScope(request.referrer);
1616
+ return scope?.kind === 'room' ? scope.locator : null;
1617
+ }
1618
+ async function readBinary(request) {
1619
+ if (closed || refusal !== null)
1620
+ return UNCERTAIN;
1621
+ try {
1622
+ await ensureAuthorization();
1623
+ }
1624
+ catch {
1625
+ return UNCERTAIN;
1626
+ }
1627
+ if (closed || authorization === undefined || refusal !== null)
1628
+ return UNCERTAIN;
1629
+ const since = fencings;
1630
+ journal.record(request.kind === 'path'
1631
+ ? `transfer binary ${request.locator} ${request.path}`
1632
+ : `transfer binary ${request.fileRef}`);
1633
+ try {
1634
+ const result = await options.adapter.binary(authorization, request);
1635
+ journal.record(`outcome binary ${result.kind}`);
1636
+ const locator = binaryRoom(request);
1637
+ if (locator !== null && fencedSince(locator, since))
1638
+ return failure({ kind: 'room-not-readable', locator });
1639
+ if (result.kind === 'scoped-refusal') {
1640
+ const room = replicas.get(result.locator)?.confirmed.record;
1641
+ if (room !== undefined)
1642
+ await scoped(result, room, await openLocal());
1643
+ }
1644
+ if (result.kind === 'revoked-authorization')
1645
+ await refuse('revoked-authorization');
1646
+ return binaryAnswer(result);
1647
+ }
1648
+ catch {
1649
+ journal.record('outcome binary transport-uncertainty');
1650
+ return UNCERTAIN;
846
1651
  }
847
1652
  }
1653
+ async function readBinaryPath(locator, path, revision) {
1654
+ await ready;
1655
+ if (revision !== undefined)
1656
+ return readBinary({ kind: 'path', locator, path, revision });
1657
+ const visible = replicas.get(locator)?.visible;
1658
+ const held = visible?.binaries[path];
1659
+ if (visible === undefined || held === undefined)
1660
+ return absentBinary(path);
1661
+ if (held.state === 'pending') {
1662
+ const blob = queuedBlob(locator, path, held.localId);
1663
+ return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1664
+ }
1665
+ const record = binaryRecord(path, held, visible.revisions[path]);
1666
+ if (record.state !== 'accepted')
1667
+ return absentBinary(path);
1668
+ const replica = replicas.get(locator);
1669
+ const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
1670
+ const holds = (at) => {
1671
+ const kept = replica?.confirmed.record.binaries[at];
1672
+ return (kept?.state === 'accepted' &&
1673
+ kept.fileRef === record.fileRef &&
1674
+ replica?.confirmed.record.revisions[at] === record.revision);
1675
+ };
1676
+ let confirmed = path;
1677
+ for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
1678
+ const change = queued[index];
1679
+ if (change.kind === 'move' && change.to === confirmed)
1680
+ confirmed = change.path;
1681
+ }
1682
+ return readBinary({
1683
+ kind: 'path',
1684
+ locator,
1685
+ path: confirmed,
1686
+ revision: record.revision,
1687
+ fileRef: record.fileRef,
1688
+ });
1689
+ }
1690
+ async function readBinaryReference(reference) {
1691
+ await ready;
1692
+ return readBinary({
1693
+ kind: 'reference',
1694
+ fileRef: reference.fileRef,
1695
+ referrer: reference.referrer,
1696
+ });
1697
+ }
848
1698
  function declaredAgents() {
849
1699
  for (const capability of capabilities) {
850
1700
  if (capability.conversationAgents !== undefined)
@@ -864,18 +1714,17 @@ export function composeRuntime(options) {
864
1714
  async function invokeAction(name, input) {
865
1715
  await ready;
866
1716
  if (closed || refusal !== null)
867
- return { kind: 'transport-uncertainty' };
1717
+ return UNCERTAIN;
868
1718
  if (declaredAction(name) === undefined)
869
- return { kind: 'undeclared' };
1719
+ return undeclared(name);
870
1720
  try {
871
1721
  await ensureAuthorization();
872
1722
  }
873
1723
  catch {
874
- return { kind: 'transport-uncertainty' };
875
- }
876
- if (closed || authorization === undefined || refusal !== null) {
877
- return { kind: 'transport-uncertainty' };
1724
+ return UNCERTAIN;
878
1725
  }
1726
+ if (closed || authorization === undefined || refusal !== null)
1727
+ return UNCERTAIN;
879
1728
  journal.record(`transfer action ${name}`);
880
1729
  try {
881
1730
  const result = await options.adapter.action(authorization, { name, input });
@@ -889,11 +1738,11 @@ export function composeRuntime(options) {
889
1738
  journal.record(`outcome action ${result.kind}`);
890
1739
  if (result.kind === 'rejected')
891
1740
  journal.record(`refusal rejected ${result.code}`);
892
- return result;
1741
+ return calledResult(result);
893
1742
  }
894
1743
  catch {
895
1744
  journal.record('outcome action transport-uncertainty');
896
- return { kind: 'transport-uncertainty' };
1745
+ return UNCERTAIN;
897
1746
  }
898
1747
  }
899
1748
  function continuing() {
@@ -910,11 +1759,11 @@ export function composeRuntime(options) {
910
1759
  options.adapter.reached(locator, written, roomVersion) &&
911
1760
  !options.adapter.reached(locator, roomVersion, written));
912
1761
  }
913
- async function pullRefresh(authorized, confirmed) {
914
- if (!continuing())
1762
+ async function pullRefresh(authorized, confirmed, live) {
1763
+ if (!live())
915
1764
  return unconfirmed;
916
1765
  try {
917
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
1766
+ const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
918
1767
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
919
1768
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
920
1769
  return result;
@@ -924,28 +1773,80 @@ export function composeRuntime(options) {
924
1773
  throw error;
925
1774
  }
926
1775
  }
927
- async function pullBootstrap(authorized, room) {
928
- if (!continuing())
1776
+ function staging(local, live) {
1777
+ return {
1778
+ held: (locator) => transfers.get(locator) ?? null,
1779
+ staged: (record) => local.staged(record),
1780
+ stage: (record, page) => exclusive(async () => {
1781
+ if (!live())
1782
+ return false;
1783
+ const bytes = layerBytes(page);
1784
+ if (!(await admitted(local, record.locator, bytes)))
1785
+ return false;
1786
+ try {
1787
+ await local.stage(record, page);
1788
+ }
1789
+ catch (error) {
1790
+ if (!exceeded(error))
1791
+ throw error;
1792
+ await exceededOn(record.locator, bytes);
1793
+ return false;
1794
+ }
1795
+ transfers.set(record.locator, record);
1796
+ readSide.recover();
1797
+ return true;
1798
+ }),
1799
+ abandon: (locator) => exclusive(async () => {
1800
+ if (!live())
1801
+ return;
1802
+ await local.abandon(locator);
1803
+ transfers.delete(locator);
1804
+ readSide.recover();
1805
+ }),
1806
+ };
1807
+ }
1808
+ function reusable(room) {
1809
+ const record = replicas.get(room.locator)?.confirmed.record;
1810
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1811
+ return null;
1812
+ return { documents: record.documents, revisions: record.revisions };
1813
+ }
1814
+ async function pullBootstrap(authorized, room, local, live) {
1815
+ if (!live())
929
1816
  return unconfirmed;
1817
+ if (replicas.has(room.locator)) {
1818
+ catching.add(room.locator);
1819
+ readSide.recover();
1820
+ }
930
1821
  try {
931
1822
  const result = await bootstrapRoom(options.adapter, authorized, room, {
932
1823
  collection: modelOf(room.locator),
933
1824
  selection: modelOf(room.locator, 'typed-selection'),
934
- }, (models, held) => remodel(room.locator, models, held), continuing);
1825
+ }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
935
1826
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
936
1827
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1828
+ if (result.kind !== 'staged')
1829
+ caughtUpTo(room.locator);
937
1830
  return result;
938
1831
  }
939
1832
  catch (error) {
940
1833
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
1834
+ caughtUpTo(room.locator);
941
1835
  throw error;
942
1836
  }
943
1837
  }
944
- async function advance(room, local, authorized) {
1838
+ function caughtUpTo(locator) {
1839
+ if (catching.delete(locator))
1840
+ readSide.recover();
1841
+ }
1842
+ async function advance(room, local, authorized, live) {
1843
+ await referenced(room.locator);
1844
+ if (!live() || paused.has(room.locator))
1845
+ return unconfirmed;
945
1846
  const held = replicas.get(room.locator);
946
1847
  if (held !== undefined) {
947
- const refreshed = await pullRefresh(authorized, held.confirmed);
948
- if (!continuing())
1848
+ const refreshed = await pullRefresh(authorized, held.confirmed, live);
1849
+ if (!live())
949
1850
  return unconfirmed;
950
1851
  if (refreshed.kind === 'still-current') {
951
1852
  stale.delete(room.locator);
@@ -959,31 +1860,45 @@ export function composeRuntime(options) {
959
1860
  return null;
960
1861
  }
961
1862
  if (refreshed.kind === 'advanced') {
962
- if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
1863
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
963
1864
  const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
964
1865
  const reached = now !== undefined &&
965
1866
  options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
966
- return reached ? null : advance(room, local, authorized);
1867
+ return reached ? null : advance(room, local, authorized, live);
967
1868
  }
968
1869
  await retainModel(local, room, 'collection', modelOf(room.locator));
969
1870
  return null;
970
1871
  }
971
1872
  if (refreshed.kind !== 'beyond-horizon')
972
1873
  return refreshed;
1874
+ if (opened !== null) {
1875
+ owed.add(room.locator);
1876
+ readSide.recover();
1877
+ return unconfirmed;
1878
+ }
973
1879
  }
974
- const bootstrapped = await pullBootstrap(authorized, room);
975
- if (!continuing())
1880
+ const bootstrapped = await pullBootstrap(authorized, room, local, live);
1881
+ if (!live())
976
1882
  return unconfirmed;
977
1883
  if (bootstrapped.kind !== 'staged')
978
1884
  return bootstrapped;
979
- await publish(local, room, bootstrapped.confirmed);
1885
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
980
1886
  return null;
981
1887
  }
982
1888
  async function withhold(withheld, room, local, progress) {
1889
+ if (withheld.kind === 'unconfirmed')
1890
+ return;
983
1891
  if (withheld.kind === 'refused')
984
1892
  stale.delete(room.locator);
985
1893
  else
986
1894
  stale.add(room.locator);
1895
+ if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
1896
+ const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
1897
+ repairable.set(room.locator, reset);
1898
+ repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
1899
+ }
1900
+ else
1901
+ repairable.delete(room.locator);
987
1902
  if (withheld.kind === 'scoped-refusal') {
988
1903
  await scoped(withheld, room, local);
989
1904
  }
@@ -1008,6 +1923,7 @@ export function composeRuntime(options) {
1008
1923
  function refusedSource(room, { refusal }) {
1009
1924
  sourceRefusals.set(room.locator, {
1010
1925
  code: refusal.rule,
1926
+ retryable: false,
1011
1927
  fields: {
1012
1928
  file: refusal.file,
1013
1929
  ...(refusal.location === undefined ? {} : { location: refusal.location }),
@@ -1018,12 +1934,21 @@ export function composeRuntime(options) {
1018
1934
  unreadable.add(room.locator);
1019
1935
  list(room, false);
1020
1936
  emitRooms();
1937
+ moveTo({ ...status, ...attention() });
1021
1938
  journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
1022
1939
  }
1023
1940
  function leaving(batch, result) {
1024
1941
  if (result.kind === 'expired')
1025
- return { batch, returned: { kind: 'expired' } };
1026
- return { batch, returned: { kind: result.kind, code: result.code, fields: result.fields } };
1942
+ return { batch, returned: returnedBy('expired') };
1943
+ return {
1944
+ batch,
1945
+ returned: {
1946
+ kind: result.kind,
1947
+ code: result.code,
1948
+ fields: result.fields,
1949
+ retryable: result.retryable,
1950
+ },
1951
+ };
1027
1952
  }
1028
1953
  function logAccepted(locator, submitted, accepted) {
1029
1954
  journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
@@ -1031,6 +1956,7 @@ export function composeRuntime(options) {
1031
1956
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1032
1957
  }
1033
1958
  async function adoptAccepted(room, local, submitted, accepted) {
1959
+ await referenced(room.locator);
1034
1960
  if (!continuing())
1035
1961
  return unconfirmed;
1036
1962
  const current = replicas.get(room.locator);
@@ -1041,22 +1967,23 @@ export function composeRuntime(options) {
1041
1967
  logAccepted(room.locator, submitted, accepted);
1042
1968
  const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1043
1969
  ? { 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)
1970
+ : applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
1045
1971
  ? accepted.roomVersion
1046
1972
  : held, accepted.roomVersion);
1047
1973
  if (applied.kind !== 'staged') {
1048
1974
  const state = pendings.get(submitted.id);
1049
1975
  await local.accept(room.locator, submitted.id, accepted.roomVersion);
1050
- if (state !== undefined)
1976
+ if (state !== undefined && !fences.has(room.locator))
1051
1977
  pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1052
1978
  emitQueue(room.locator);
1053
1979
  return applied;
1054
1980
  }
1055
- const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1981
+ const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1056
1982
  pendings.delete(submitted.id);
1057
1983
  unreadable.delete(room.locator);
1058
1984
  sourceRefusals.delete(room.locator);
1059
1985
  declarationRefusals.delete(room.locator);
1986
+ const set = settled();
1060
1987
  list(room, true);
1061
1988
  publishChangeSet(set);
1062
1989
  moveTo({ ...status, ...attention() });
@@ -1070,14 +1997,14 @@ export function composeRuntime(options) {
1070
1997
  refusedSource(room, failed);
1071
1998
  return false;
1072
1999
  }
1073
- async function submitRoom(room, local, authorized, progress) {
1074
- while (continuing() && authorization !== undefined) {
2000
+ async function submitRoom(room, local, authorized, progress, signal) {
2001
+ while (continuing() && authorization !== undefined && !signal.aborted) {
1075
2002
  const replica = replicas.get(room.locator);
1076
2003
  const head = replica?.queue[0];
1077
2004
  if (replica === undefined || head === undefined)
1078
- return;
2005
+ return 'done';
1079
2006
  if (pendings.get(head.id)?.accepted !== undefined)
1080
- return;
2007
+ return 'done';
1081
2008
  const state = pendings.get(head.id);
1082
2009
  const { submission, lost: earlier } = state ?? {
1083
2010
  submission: { state: 'waiting' },
@@ -1085,24 +2012,30 @@ export function composeRuntime(options) {
1085
2012
  };
1086
2013
  if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
1087
2014
  progress.complete = false;
1088
- return;
2015
+ return { at: Date.parse(submission.reset) };
1089
2016
  }
1090
2017
  if (writesHeld()) {
1091
2018
  progress.complete = false;
1092
2019
  journal.record(`submission ${room.locator} ${head.id} held`);
1093
- return;
2020
+ return 'done';
1094
2021
  }
1095
2022
  const lost = earlier || submission.state === 'submitted';
2023
+ if (lost && pastHorizon(head.id)) {
2024
+ await discardPending(room, local, signal);
2025
+ return 'done';
2026
+ }
1096
2027
  let result;
1097
2028
  journal.record(`submission ${room.locator} ${head.id}`);
1098
2029
  await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1099
- if (discoveryPaused)
1100
- return;
1101
- if (writesHeld()) {
2030
+ if (discoveryPaused || fences.has(room.locator))
2031
+ return 'done';
2032
+ if (signal.aborted || writesHeld()) {
1102
2033
  await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
2034
+ if (signal.aborted)
2035
+ return 'done';
1103
2036
  progress.complete = false;
1104
2037
  journal.record(`submission ${room.locator} ${head.id} held`);
1105
- return;
2038
+ return 'done';
1106
2039
  }
1107
2040
  const opensBefore = openSignals;
1108
2041
  const answered = awaitingAnswer(room.locator);
@@ -1118,32 +2051,36 @@ export function composeRuntime(options) {
1118
2051
  progress.complete = false;
1119
2052
  if (continuing())
1120
2053
  progress.reachable = false;
1121
- return;
2054
+ return 'again';
2055
+ }
2056
+ if (signal.aborted) {
2057
+ answered();
2058
+ return 'done';
1122
2059
  }
1123
2060
  if (result.kind !== 'accepted' || !continuing())
1124
2061
  answered();
1125
2062
  if (!continuing())
1126
- return;
2063
+ return 'done';
1127
2064
  if (result.kind === 'transport-uncertainty') {
1128
2065
  progress.complete = false;
1129
2066
  progress.reachable = false;
1130
- return;
2067
+ return 'again';
1131
2068
  }
1132
2069
  if (result.kind === 'room-not-readable') {
1133
2070
  progress.complete = false;
1134
2071
  unreadable.add(room.locator);
1135
2072
  list(room, false);
1136
2073
  emitRooms();
1137
- return;
2074
+ return 'done';
1138
2075
  }
1139
2076
  if (result.kind === 'revoked-authorization') {
1140
2077
  await refuse('revoked-authorization');
1141
- return;
2078
+ return 'done';
1142
2079
  }
1143
2080
  if (result.kind === 'scoped-refusal') {
1144
2081
  await scoped(result, room, local);
1145
2082
  progress.complete = false;
1146
- return;
2083
+ return 'done';
1147
2084
  }
1148
2085
  if (result.kind === 'write-halted') {
1149
2086
  journal.record(`outcome ${room.locator} ${head.id} write-halted`);
@@ -1152,16 +2089,24 @@ export function composeRuntime(options) {
1152
2089
  continue;
1153
2090
  haltedRefusal = true;
1154
2091
  progress.complete = false;
1155
- return;
2092
+ return 'done';
1156
2093
  }
1157
2094
  if (result.kind === 'conflicted' ||
1158
2095
  result.kind === 'rejected' ||
1159
2096
  result.kind === 'expired' ||
1160
2097
  result.kind === 'retryable-refusal') {
2098
+ if (result.kind === 'expired' && lost && result.restored !== true) {
2099
+ journal.record(`outcome ${room.locator} ${head.id} expired`);
2100
+ await discardPending(room, local, signal);
2101
+ return 'done';
2102
+ }
1161
2103
  if (result.kind === 'expired') {
1162
- const withheld = await advance(room, local, authorized);
1163
- if (withheld !== null)
1164
- return withhold(withheld, room, local, progress);
2104
+ await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
2105
+ const withheld = await advance(room, local, authorized, continuing);
2106
+ if (withheld !== null) {
2107
+ await withhold(withheld, room, local, progress);
2108
+ return 'done';
2109
+ }
1165
2110
  }
1166
2111
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
1167
2112
  (result.kind === 'conflicted' && result.roomVersion !== undefined
@@ -1174,8 +2119,7 @@ export function composeRuntime(options) {
1174
2119
  const reset = result.reset;
1175
2120
  progress.complete = false;
1176
2121
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1177
- armTimer();
1178
- return;
2122
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
1179
2123
  }
1180
2124
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
1181
2125
  const left = leaving(head, result);
@@ -1186,37 +2130,82 @@ export function composeRuntime(options) {
1186
2130
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
1187
2131
  return;
1188
2132
  }
1189
- const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
2133
+ const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1190
2134
  leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1191
2135
  (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1192
2136
  ? { ...left, returned: null }
1193
2137
  : left,
1194
2138
  });
1195
- publishChangeSet(set);
2139
+ publishChangeSet(settled());
1196
2140
  moveTo({ ...status, ...attention() });
1197
2141
  });
1198
2142
  if (result.kind === 'expired')
1199
2143
  continue;
1200
- const withheld = await advance(room, local, authorized);
1201
- if (withheld !== null)
1202
- return withhold(withheld, room, local, progress);
2144
+ const withheld = await advance(room, local, authorized, continuing);
2145
+ if (withheld !== null) {
2146
+ await withhold(withheld, room, local, progress);
2147
+ return 'done';
2148
+ }
1203
2149
  continue;
1204
2150
  }
1205
2151
  const kept = await adopted(room, local, head, result).finally(answered);
1206
2152
  if (!kept) {
1207
2153
  progress.complete = false;
1208
- return;
2154
+ return 'done';
1209
2155
  }
1210
2156
  }
2157
+ return 'done';
2158
+ }
2159
+ function pastHorizon(identity) {
2160
+ return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
2161
+ }
2162
+ async function discardPending(room, local, signal) {
2163
+ const { locator } = room;
2164
+ const discarded = await exclusive(async () => {
2165
+ if (fences.has(locator))
2166
+ return false;
2167
+ const before = replicas.get(locator);
2168
+ const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
2169
+ fences.set(locator, { at: discoveries, erased: false });
2170
+ lastFenced.set(locator, ++fencings);
2171
+ try {
2172
+ await local.discard(locator);
2173
+ }
2174
+ finally {
2175
+ fences.delete(locator);
2176
+ }
2177
+ for (const batch of queue)
2178
+ pendings.delete(batch.id);
2179
+ queuesWithoutReplica.delete(locator);
2180
+ const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
2181
+ if (after !== null)
2182
+ replicas.set(locator, after);
2183
+ journal.record(`room discarded ${locator}`);
2184
+ publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
2185
+ moveTo({ ...status, ...attention() });
2186
+ return true;
2187
+ });
2188
+ if (!discarded)
2189
+ return;
2190
+ const progress = { complete: true, reachable: true };
2191
+ await roomTask(locator, async (live) => {
2192
+ await Promise.race([
2193
+ reload(room, local, progress, () => live() && !signal.aborted),
2194
+ abandonedOn(signal),
2195
+ ]);
2196
+ });
2197
+ if (signal.aborted)
2198
+ return;
2199
+ await rest(progress);
1211
2200
  }
1212
2201
  function awaitingAnswer(locator) {
1213
2202
  let release = () => { };
1214
2203
  const awaited = new Promise((resolve) => {
1215
2204
  release = resolve;
1216
2205
  });
1217
- answering.set(locator, awaited);
2206
+ answering.set(locator, { generation, answered: awaited });
1218
2207
  return () => {
1219
- if (answering.get(locator) === awaited)
2208
+ if (answering.get(locator)?.answered === awaited)
1220
2209
  answering.delete(locator);
1221
2210
  release();
1222
2211
  };
@@ -1249,7 +2238,7 @@ export function composeRuntime(options) {
1249
2238
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1250
2239
  };
1251
2240
  const reclassified = sameMembership(declared, held.confirmed.record)
1252
- ? readConfirmed(declared, held.confirmed)
2241
+ ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
1253
2242
  : readReclassified(declared, held.confirmed);
1254
2243
  const record = reclassified.kind === 'staged'
1255
2244
  ? declared
@@ -1257,12 +2246,10 @@ export function composeRuntime(options) {
1257
2246
  ...declared,
1258
2247
  typedSelection: held.confirmed.record.typedSelection,
1259
2248
  };
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
- });
2249
+ const read = reclassified.kind === 'deferred'
2250
+ ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
2251
+ : reclassified;
2252
+ await local.write({ next: record, previous: held.confirmed.record });
1266
2253
  if (read.kind === 'staged') {
1267
2254
  replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1268
2255
  if (declarationRefusals.delete(room.locator)) {
@@ -1289,7 +2276,11 @@ export function composeRuntime(options) {
1289
2276
  });
1290
2277
  }
1291
2278
  async function scoped(result, room, local) {
1292
- scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
2279
+ scopedRefusals.set(room.locator, {
2280
+ code: result.code,
2281
+ fields: result.fields,
2282
+ retryable: result.retryable,
2283
+ });
1293
2284
  let scopedAway = room;
1294
2285
  if (result.scope === 'model')
1295
2286
  await retainModel(local, room, 'collection', {
@@ -1326,24 +2317,27 @@ export function composeRuntime(options) {
1326
2317
  roomVersion: record.roomVersion,
1327
2318
  documents: record.documents,
1328
2319
  revisions: record.revisions,
1329
- attachments: record.attachments,
1330
2320
  },
1331
2321
  ];
1332
2322
  });
1333
2323
  }
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) => {
2324
+ async function synchronize(listed, local, progress, attempt, live) {
2325
+ const acquired = listed.filter(acquires);
2326
+ const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
2327
+ if (!live())
2328
+ return;
2329
+ const ready = acquired.filter((_, index) => {
1337
2330
  const outcome = prepared[index];
1338
2331
  return outcome?.status === 'fulfilled' && outcome.value;
1339
2332
  });
1340
2333
  if (attempt === 'opened') {
1341
- options.adapter.roomStream?.hold(heldRooms(ready));
2334
+ options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
1342
2335
  handed = true;
1343
2336
  }
2337
+ discovering?.release();
1344
2338
  const loaded = await Promise.allSettled(ready
1345
2339
  .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1346
- .map((room) => loadRoom(room, local, progress)));
2340
+ .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
1347
2341
  const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1348
2342
  if (failed !== undefined)
1349
2343
  throw failed.reason;
@@ -1372,14 +2366,14 @@ export function composeRuntime(options) {
1372
2366
  }
1373
2367
  return true;
1374
2368
  }
1375
- async function caughtUp(room, local, progress) {
2369
+ async function caughtUp(room, local, progress, live) {
1376
2370
  const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1377
- if (held === undefined || authorization === undefined)
2371
+ if (held === undefined || authorization === undefined || !live())
1378
2372
  return;
1379
2373
  const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1380
2374
  if (!behind)
1381
2375
  return;
1382
- const withheld = await advance(room, local, authorization);
2376
+ const withheld = await advance(room, local, authorization, live);
1383
2377
  if (withheld !== null)
1384
2378
  await withhold(withheld, room, local, progress);
1385
2379
  }
@@ -1389,39 +2383,46 @@ export function composeRuntime(options) {
1389
2383
  return model.status === 'unavailable' && model.reason === 'revoked';
1390
2384
  });
1391
2385
  }
1392
- async function loadRoom(room, local, progress) {
1393
- if (discoveryPaused || authorization === undefined)
2386
+ async function loadRoom(room, local, progress, live) {
2387
+ if (!live() || authorization === undefined)
1394
2388
  return;
1395
2389
  if (replicas.has(room.locator) && revoked(room.locator)) {
1396
- await reload(room, local, progress);
2390
+ await reload(room, local, progress, live);
1397
2391
  return;
1398
2392
  }
1399
- const withheld = await advance(room, local, authorization);
2393
+ const withheld = await advance(room, local, authorization, live);
1400
2394
  if (withheld !== null) {
1401
2395
  await withhold(withheld, room, local, progress);
1402
2396
  return;
1403
2397
  }
1404
- await caughtUp(room, local, progress);
2398
+ await caughtUp(room, local, progress, live);
1405
2399
  }
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;
2400
+ async function reload(room, local, progress, live) {
2401
+ try {
2402
+ if (!live() || authorization === undefined)
2403
+ return;
2404
+ const bootstrapped = await pullBootstrap(authorization, room, local, live);
2405
+ if (!live())
2406
+ return;
2407
+ if (bootstrapped.kind !== 'staged') {
2408
+ await withhold(bootstrapped, room, local, progress);
2409
+ return;
2410
+ }
2411
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
2412
+ await caughtUp(room, local, progress, live);
2413
+ }
2414
+ finally {
2415
+ caughtUpTo(room.locator);
1415
2416
  }
1416
- await publish(local, room, bootstrapped.confirmed);
1417
- await caughtUp(room, local, progress);
1418
2417
  }
1419
- async function discoverRooms(local, progress, attempt) {
2418
+ async function discoverRooms(local, progress, attempt, live) {
1420
2419
  if (closed || authorization === undefined) {
1421
2420
  notifyReady();
1422
2421
  return;
1423
2422
  }
1424
2423
  moveTo(transferring(status));
2424
+ discoveries += 1;
2425
+ const started = discoveries;
1425
2426
  let result;
1426
2427
  try {
1427
2428
  result = await options.adapter.discover(authorization);
@@ -1429,7 +2430,8 @@ export function composeRuntime(options) {
1429
2430
  catch {
1430
2431
  result = { kind: 'transport-uncertainty' };
1431
2432
  }
1432
- if (closed) {
2433
+ if (!live()) {
2434
+ progress.complete = false;
1433
2435
  notifyReady();
1434
2436
  return;
1435
2437
  }
@@ -1448,7 +2450,11 @@ export function composeRuntime(options) {
1448
2450
  }
1449
2451
  if (result.kind === 'contract-unavailable') {
1450
2452
  journal.record(`connection contract unavailable ${result.code}`);
1451
- await refuse('missing-required-guarantee', { code: result.code, fields: result.fields });
2453
+ await refuse('missing-required-guarantee', {
2454
+ code: result.code,
2455
+ fields: result.fields,
2456
+ retryable: result.retryable,
2457
+ });
1452
2458
  notifyReady();
1453
2459
  return;
1454
2460
  }
@@ -1483,14 +2489,20 @@ export function composeRuntime(options) {
1483
2489
  }
1484
2490
  const previousCapabilities = capabilities;
1485
2491
  capabilities = result.capabilities;
2492
+ outcomeRetention = result.retention.outcome;
1486
2493
  notifyReady();
1487
2494
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1488
2495
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
1489
2496
  limits = result.providerLimits;
1490
2497
  await local.declare(limits, capabilities);
1491
2498
  }
1492
- await applyDiscovery(result.rooms, local);
1493
- await synchronize(result.rooms, local, progress, attempt);
2499
+ const signedIn = result.member ?? null;
2500
+ if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
2501
+ await local.keepMember(signedIn);
2502
+ member = signedIn;
2503
+ emitMember();
2504
+ }
2505
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
1494
2506
  }
1495
2507
  async function ensureAuthorization() {
1496
2508
  if (authorization !== undefined)
@@ -1504,32 +2516,64 @@ export function composeRuntime(options) {
1504
2516
  throw error;
1505
2517
  }
1506
2518
  }
1507
- function task(work) {
2519
+ async function admittedRead(work) {
2520
+ if (roomReads.generation !== generation) {
2521
+ roomReads = { generation, free: ROOM_READS, waiting: [] };
2522
+ }
2523
+ const reads = roomReads;
2524
+ while (reads.free === 0)
2525
+ await new Promise((admit) => reads.waiting.push(admit));
2526
+ reads.free -= 1;
2527
+ try {
2528
+ await work();
2529
+ }
2530
+ finally {
2531
+ reads.free += 1;
2532
+ reads.waiting.shift()?.();
2533
+ }
2534
+ }
2535
+ function roomTask(locator, work) {
2536
+ return task(roomLane(locator), (live) => admittedRead(() => work(live)));
2537
+ }
2538
+ function task(lane, work) {
1508
2539
  if (stopping)
1509
- return chain;
1510
- chain = chain.then(work, work).catch(() => undefined);
1511
- return chain;
2540
+ return Promise.resolve();
2541
+ const held = lanes.get(lane);
2542
+ const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
2543
+ const turn = current.generation;
2544
+ const live = () => turn === generation && continuing();
2545
+ const run = current.chain.then(() => work(live)).catch(() => undefined);
2546
+ current.chain = run;
2547
+ lanes.set(lane, current);
2548
+ working.add(run);
2549
+ void run.finally(() => working.delete(run));
2550
+ return run;
1512
2551
  }
1513
2552
  function sync(attempt) {
1514
- return task(async () => {
1515
- if (closed || stopping || discoveryPaused) {
2553
+ return task(OPENING_LANE, async (live) => {
2554
+ if (!live()) {
1516
2555
  notifyReady();
1517
2556
  return;
1518
2557
  }
1519
2558
  const progress = { complete: true, reachable: true };
1520
- underway = attempt === 'opened' ? progress : null;
2559
+ const gate = discover();
2560
+ if (attempt === 'opened')
2561
+ opened = generation;
2562
+ underway = attempt === 'opened' ? { generation, progress } : null;
2563
+ if (attempt === 'opened')
2564
+ streamed = progress;
1521
2565
  complete = false;
1522
2566
  recorded = false;
1523
2567
  moveTo(transferring(status));
1524
2568
  handed = false;
1525
2569
  try {
1526
2570
  const local = await openLocal();
1527
- if (closed)
2571
+ if (!live())
1528
2572
  return;
1529
2573
  await ensureAuthorization();
1530
- if (closed)
2574
+ if (!live())
1531
2575
  return;
1532
- await discoverRooms(local, progress, attempt);
2576
+ await discoverRooms(local, progress, attempt, live);
1533
2577
  }
1534
2578
  catch {
1535
2579
  progress.complete = false;
@@ -1537,17 +2581,21 @@ export function composeRuntime(options) {
1537
2581
  progress.reachable = false;
1538
2582
  }
1539
2583
  finally {
2584
+ gate.release();
1540
2585
  notifyReady();
1541
2586
  }
2587
+ if (!live())
2588
+ return;
1542
2589
  if (attempt !== 'opened') {
1543
- await finishOpening(progress);
2590
+ await finishOpening(progress, live);
1544
2591
  return;
1545
2592
  }
1546
2593
  if (handed || closed || progress.reachable)
1547
2594
  return;
1548
2595
  underway = null;
2596
+ opened = null;
1549
2597
  journal.record('connection reopen');
1550
- await rest(progress);
2598
+ await rest(progress, live);
1551
2599
  options.adapter.roomStream?.reopen();
1552
2600
  });
1553
2601
  }
@@ -1558,29 +2606,51 @@ export function composeRuntime(options) {
1558
2606
  void refuse('revoked-authorization');
1559
2607
  return;
1560
2608
  }
1561
- if (opened || status.providerUnreachable)
1562
- return;
1563
- moveTo({ ...status, providerUnreachable: true });
2609
+ complete = false;
2610
+ if (streamed !== null)
2611
+ streamed.complete = false;
2612
+ moveTo({
2613
+ ...status,
2614
+ state: status.state === 'synced' ? 'syncing' : status.state,
2615
+ providerUnreachable: status.providerUnreachable || !opened,
2616
+ });
1564
2617
  }
1565
2618
  function applyOpening(expired) {
1566
- return task(async () => {
1567
- const progress = underway;
2619
+ const progress = underway?.generation === generation ? underway.progress : null;
2620
+ const cycle = progress ?? { complete: true, reachable: true };
2621
+ const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
2622
+ if (opened === generation)
2623
+ opened = null;
2624
+ return task(OPENING_LANE, async (live) => {
1568
2625
  underway = null;
1569
- if (closed || stopping)
2626
+ if (!live())
1570
2627
  return;
1571
2628
  if (progress === null) {
1572
- await reloadExpired(expired);
2629
+ await replacing;
2630
+ await rest(cycle, live);
1573
2631
  return;
1574
2632
  }
2633
+ await roomsCarried(expired);
2634
+ if (!live())
2635
+ return;
2636
+ const replaced = new Set(owed);
2637
+ owed.clear();
1575
2638
  try {
1576
2639
  const local = await openLocal();
1577
2640
  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)));
2641
+ const reloading = held.filter((room) => !expired.includes(room.locator) &&
2642
+ (replaced.has(room.locator) || revoked(room.locator)));
2643
+ for (const room of reloading)
2644
+ catching.add(room.locator);
2645
+ readSide.recover();
2646
+ const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
2647
+ const reloaded = [...(await reloads), ...(await replacing)];
1580
2648
  if (progress.reachable) {
1581
- for (const room of held)
1582
- if (!reloading.includes(room))
2649
+ for (const room of held) {
2650
+ if (!expired.includes(room.locator) && !reloading.includes(room)) {
1583
2651
  await confirmModels(room, local);
2652
+ }
2653
+ }
1584
2654
  }
1585
2655
  const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1586
2656
  if (failed !== undefined)
@@ -1590,21 +2660,21 @@ export function composeRuntime(options) {
1590
2660
  progress.complete = false;
1591
2661
  progress.reachable = false;
1592
2662
  }
1593
- await finishOpening(progress);
2663
+ if (live())
2664
+ await finishOpening(progress, live);
1594
2665
  });
1595
2666
  }
1596
- async function reloadExpired(expired) {
1597
- const progress = { complete: true, reachable: true };
2667
+ async function reloadNow(replaced, progress) {
1598
2668
  try {
1599
2669
  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)));
2670
+ return await Promise.allSettled([...discovered.values()]
2671
+ .filter((room) => replicas.has(room.locator) && replaced(room))
2672
+ .map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
1603
2673
  }
1604
2674
  catch {
1605
2675
  progress.reachable = false;
2676
+ return [];
1606
2677
  }
1607
- await rest(progress);
1608
2678
  }
1609
2679
  async function confirmModels(room, local) {
1610
2680
  for (const attached of ['collection', 'typed-selection']) {
@@ -1615,38 +2685,96 @@ export function composeRuntime(options) {
1615
2685
  }
1616
2686
  }
1617
2687
  function submitLater(locator) {
1618
- const running = submitting.get(locator);
1619
- if (running !== undefined) {
1620
- running.again = true;
2688
+ if (localSaves)
1621
2689
  return;
2690
+ void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
2691
+ }
2692
+ async function submitPass(locator, signal) {
2693
+ const room = discovered.get(locator);
2694
+ if (closed || stopping || discoveryPaused || room === undefined)
2695
+ return 'done';
2696
+ const progress = { complete: true, reachable: true };
2697
+ let next;
2698
+ try {
2699
+ await ensureAuthorization();
2700
+ if (authorization === undefined)
2701
+ return 'done';
2702
+ next = await submitRoom(room, await openLocal(), authorization, progress, signal);
1622
2703
  }
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
- })();
2704
+ catch {
2705
+ progress.reachable = false;
2706
+ next = 'again';
2707
+ }
2708
+ if (signal.aborted)
2709
+ return 'done';
2710
+ await rest(progress);
2711
+ return next === 'again' && !socketOpen ? 'done' : next;
2712
+ }
2713
+ function repairLater(locator, when) {
2714
+ if (!socketOpen || repairing.has(locator))
2715
+ return;
2716
+ void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
2717
+ }
2718
+ async function repairPass(locator, signal) {
2719
+ const room = discovered.get(locator);
2720
+ if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
2721
+ return 'done';
2722
+ if (room === undefined || room.replica.admission !== 'admitted')
2723
+ return 'done';
2724
+ const progress = { complete: true, reachable: true };
2725
+ repairing.add(locator);
2726
+ try {
2727
+ const local = await openLocal();
2728
+ await roomTask(locator, async (live) => {
2729
+ const current = () => live() && !signal.aborted;
2730
+ if (!current() || authorization === undefined)
2731
+ return;
2732
+ const withheld = await Promise.race([
2733
+ advance(room, local, authorization, current),
2734
+ abandonedOn(signal),
2735
+ ]);
2736
+ if (withheld !== null && current())
2737
+ await withhold(withheld, room, local, progress);
2738
+ });
2739
+ }
2740
+ catch {
2741
+ stale.add(locator);
2742
+ progress.reachable = false;
2743
+ }
2744
+ finally {
2745
+ repairing.delete(locator);
2746
+ }
2747
+ if (signal.aborted)
2748
+ return 'done';
2749
+ await rest(progress);
2750
+ if (!stale.has(locator)) {
2751
+ if ((replicas.get(locator)?.queue.length ?? 0) > 0)
2752
+ submitLater(locator);
2753
+ return 'done';
2754
+ }
2755
+ if (!repairable.has(locator))
2756
+ return 'done';
2757
+ const reset = repairable.get(locator);
2758
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
2759
+ }
2760
+ function resendLater() {
2761
+ if (!socketOpen)
2762
+ return;
2763
+ void recovery.attempt('resend', resendPass, { again: true });
2764
+ }
2765
+ async function resendPass(signal) {
2766
+ if (!socketOpen || closed || stopping || discoveryPaused)
2767
+ return 'done';
2768
+ const reading = { complete: true, reachable: true };
2769
+ try {
2770
+ await conversations.resend(reading);
2771
+ }
2772
+ catch {
2773
+ reading.reachable = false;
2774
+ }
2775
+ if (signal.aborted)
2776
+ return 'done';
2777
+ return reading.reachable && reading.complete ? 'done' : 'again';
1650
2778
  }
1651
2779
  function submitAll() {
1652
2780
  for (const [locator, replica] of replicas) {
@@ -1654,31 +2782,41 @@ export function composeRuntime(options) {
1654
2782
  submitLater(locator);
1655
2783
  }
1656
2784
  }
1657
- async function finishOpening(progress) {
2785
+ async function finishOpening(progress, live) {
1658
2786
  try {
1659
- if (closed || discoveryPaused) {
2787
+ if (!live()) {
1660
2788
  progress.complete = false;
1661
2789
  return;
1662
2790
  }
1663
2791
  submitAll();
2792
+ for (const locator of stale)
2793
+ if (repairable.has(locator))
2794
+ repairLater(locator, {});
1664
2795
  await conversations.reconcile();
1665
2796
  if (status.providerUnreachable)
1666
2797
  conversations.reconnected();
1667
2798
  const reading = { complete: true, reachable: true };
1668
- await conversations.catchUp(reading);
1669
- if (!reading.reachable)
2799
+ if (demand.conversations)
2800
+ await conversations.catchUp(reading);
2801
+ if (progress.reachable && reading.reachable)
2802
+ await conversations.resend(reading);
2803
+ if (!reading.reachable) {
1670
2804
  progress.reachable = false;
2805
+ resendLater();
2806
+ }
1671
2807
  }
1672
2808
  catch {
1673
2809
  progress.complete = false;
1674
2810
  }
1675
2811
  finally {
1676
- complete = progress.complete;
1677
- await rest(progress);
2812
+ if (live()) {
2813
+ complete = progress.complete;
2814
+ await rest(progress, live);
2815
+ }
1678
2816
  }
1679
2817
  }
1680
- async function rest(progress) {
1681
- if (closed)
2818
+ async function rest(progress, live = () => !closed) {
2819
+ if (!live())
1682
2820
  return;
1683
2821
  let lastCompleted = status.lastCompleted;
1684
2822
  let synced = complete && stale.size === 0 && !pending();
@@ -1686,6 +2824,8 @@ export function composeRuntime(options) {
1686
2824
  const completedAt = new Date().toISOString();
1687
2825
  try {
1688
2826
  await (await openLocal()).complete(completedAt);
2827
+ if (!live())
2828
+ return;
1689
2829
  recorded = true;
1690
2830
  lastCompleted = completedAt;
1691
2831
  }
@@ -1694,46 +2834,74 @@ export function composeRuntime(options) {
1694
2834
  }
1695
2835
  }
1696
2836
  moveTo({
1697
- state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
2837
+ state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
1698
2838
  lastCompleted,
1699
2839
  providerUnreachable: !progress.reachable,
1700
2840
  ...attention(),
1701
2841
  });
1702
2842
  }
2843
+ function discover() {
2844
+ let release = () => { };
2845
+ const done = new Promise((resolve) => {
2846
+ release = resolve;
2847
+ });
2848
+ discovering = { generation, done, release };
2849
+ return discovering;
2850
+ }
1703
2851
  function applyRoom(locator, roomVersion) {
1704
2852
  announced.set(locator, roomVersion);
1705
- return task(async () => {
1706
- await answering.get(locator);
1707
- if (closed || stopping || discoveryPaused || authorization === undefined)
2853
+ return task(roomLane(locator), async (live) => {
2854
+ if (discovering?.generation === generation)
2855
+ await discovering.done;
2856
+ const saving = answering.get(locator);
2857
+ if (saving?.generation === generation)
2858
+ await saving.answered;
2859
+ if (!live() || authorization === undefined)
1708
2860
  return;
1709
2861
  const room = discovered.get(locator);
1710
2862
  const held = replicas.get(locator);
1711
- if (room === undefined || room.replica.admission !== 'admitted')
2863
+ if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
1712
2864
  return;
1713
2865
  if (held !== undefined &&
1714
2866
  (held.confirmed.record.replica.selection !== room.replica.selection ||
1715
2867
  options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
1716
2868
  return;
1717
2869
  }
1718
- const during = underway;
2870
+ const during = underway?.generation === generation ? underway.progress : null;
1719
2871
  const progress = during ?? { complete: true, reachable: true };
1720
2872
  if (during === null)
1721
2873
  moveTo(transferring(status));
1722
2874
  try {
1723
2875
  const local = await openLocal();
1724
- const withheld = await advance(room, local, authorization);
1725
- if (withheld !== null)
1726
- await withhold(withheld, room, local, progress);
2876
+ await admittedRead(async () => {
2877
+ if (!live() || authorization === undefined)
2878
+ return;
2879
+ const withheld = await advance(room, local, authorization, live);
2880
+ if (withheld !== null)
2881
+ await withhold(withheld, room, local, progress);
2882
+ });
1727
2883
  }
1728
2884
  catch {
1729
2885
  stale.add(locator);
1730
2886
  }
1731
2887
  finally {
1732
2888
  if (during === null)
1733
- await rest(progress);
2889
+ await rest(progress, live);
1734
2890
  }
1735
2891
  });
1736
2892
  }
2893
+ function roomsCarried(replacing) {
2894
+ const replaced = new Set(replacing.map(roomLane));
2895
+ return Promise.all([...lanes]
2896
+ .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
2897
+ .map(([, held]) => held.chain));
2898
+ }
2899
+ const receipts = {
2900
+ heads: async () => (await openLocal()).receivedHeads(),
2901
+ parts: async (key, count) => (await openLocal()).receivedParts(key, count),
2902
+ keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
2903
+ drop: async (keys) => (await openLocal()).dropReceived(keys),
2904
+ };
1737
2905
  function observeStream() {
1738
2906
  const stream = options.adapter.roomStream;
1739
2907
  if (stream === undefined || withdrawStream !== undefined)
@@ -1743,6 +2911,9 @@ export function composeRuntime(options) {
1743
2911
  return;
1744
2912
  if (event.kind === 'opened') {
1745
2913
  socketOpen = true;
2914
+ generation += 1;
2915
+ conversations.reopened();
2916
+ recovery.resume();
1746
2917
  void sync('opened');
1747
2918
  }
1748
2919
  else if (event.kind === 'closed')
@@ -1751,7 +2922,7 @@ export function composeRuntime(options) {
1751
2922
  void applyOpening(event.expired);
1752
2923
  else
1753
2924
  void applyRoom(event.locator, event.roomVersion);
1754
- });
2925
+ }, receipts);
1755
2926
  }
1756
2927
  function writesHeld() {
1757
2928
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -1766,6 +2937,7 @@ export function composeRuntime(options) {
1766
2937
  haltedRefusal = false;
1767
2938
  if (mode !== serviceMode) {
1768
2939
  serviceMode = mode;
2940
+ queueReadiness();
1769
2941
  journal.record(`service mode ${mode}`);
1770
2942
  for (const listener of serviceModeListeners)
1771
2943
  listener(mode);
@@ -1778,6 +2950,8 @@ export function composeRuntime(options) {
1778
2950
  if (stream === undefined || withdrawConversations !== undefined)
1779
2951
  return;
1780
2952
  withdrawConversations = stream.observe((event) => {
2953
+ if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
2954
+ queueReadiness();
1781
2955
  if (conversations.receive(event)) {
1782
2956
  void ready.then(scheduleCurrency);
1783
2957
  return;
@@ -1789,46 +2963,47 @@ export function composeRuntime(options) {
1789
2963
  });
1790
2964
  }
1791
2965
  function scheduleCurrency() {
1792
- if (currencyQueued || closed || stopping)
2966
+ if (currencyQueued === generation || closed || stopping)
1793
2967
  return;
1794
- currencyQueued = true;
1795
- void task(runCurrency);
2968
+ currencyQueued = generation;
2969
+ void task(CONVERSATION_LANE, runCurrency);
1796
2970
  }
1797
- async function runCurrency() {
1798
- currencyQueued = false;
1799
- if (closed || stopping || discoveryPaused)
2971
+ async function runCurrency(live) {
2972
+ if (currencyQueued === generation)
2973
+ currencyQueued = null;
2974
+ if (!live())
1800
2975
  return;
1801
2976
  await conversations.reconcile();
1802
- if (conversations.current()) {
2977
+ if (conversations.current() || !demand.conversations) {
1803
2978
  show();
1804
2979
  return;
1805
2980
  }
1806
- const resting = status;
1807
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1808
- moveTo(transferring(resting));
2981
+ const progress = { complete: false, reachable: !status.providerUnreachable };
2982
+ moveTo(transferring(status));
1809
2983
  try {
1810
2984
  await conversations.catchUp(progress);
1811
2985
  }
1812
2986
  catch {
1813
2987
  progress.reachable = false;
1814
2988
  }
1815
- finally {
1816
- if (!closed) {
1817
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1818
- }
1819
- }
2989
+ await rest(progress, live);
1820
2990
  }
1821
2991
  function close() {
1822
2992
  if (closed)
1823
2993
  return;
1824
2994
  closed = true;
2995
+ if (narrowing !== null)
2996
+ clearTimeout(narrowing);
2997
+ if (rechecking !== null)
2998
+ clearInterval(rechecking);
2999
+ readinessWatches.clear();
1825
3000
  withdrawStream?.();
1826
3001
  withdrawStream = undefined;
1827
3002
  withdrawConversations?.();
1828
3003
  withdrawConversations = undefined;
1829
3004
  withdrawServiceMode?.();
1830
3005
  conversations.close();
1831
- stopTimer();
3006
+ void recovery.retire();
1832
3007
  store?.close();
1833
3008
  void opening?.then((opened) => {
1834
3009
  opened.close();
@@ -1838,6 +3013,7 @@ export function composeRuntime(options) {
1838
3013
  storageLossListeners.clear();
1839
3014
  statusListeners.clear();
1840
3015
  serviceModeListeners.clear();
3016
+ memberListeners.clear();
1841
3017
  readSide.close();
1842
3018
  queueSubscriptions.clear();
1843
3019
  roomDraftListeners.clear();
@@ -1845,9 +3021,8 @@ export function composeRuntime(options) {
1845
3021
  }
1846
3022
  async function stop() {
1847
3023
  stopping = true;
1848
- stopTimer();
1849
- await chain.catch(() => undefined);
1850
- await Promise.all([...submitting.values()].map((run) => run.done));
3024
+ await recovery.retire();
3025
+ await Promise.all(working);
1851
3026
  await mutations;
1852
3027
  close();
1853
3028
  }
@@ -1869,14 +3044,54 @@ export function composeRuntime(options) {
1869
3044
  return planOf(input, roomFacts(reach), planReach(reach));
1870
3045
  }
1871
3046
  function watchPlan(plan, deliver) {
1872
- return readSide.watch(plan, identityOf(plan), deliver);
3047
+ return readSide.watch(plan, identityOf(plan), () => {
3048
+ const cover = covered(plan);
3049
+ awaitPreparation(cover);
3050
+ return cover;
3051
+ }, deliver);
1873
3052
  }
1874
3053
  async function searched(input, reach) {
1875
3054
  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);
3055
+ const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
3056
+ for (;;) {
3057
+ const cover = covered(filter);
3058
+ awaitPreparation(cover);
3059
+ const pending = 'pending' in cover ? [...cover.pending] : [];
3060
+ const covering = 'pending' in cover ? [] : cover.coverage.rooms;
3061
+ const unindexed = covering
3062
+ .filter(({ room }) => !searchIndex.rooms().has(room))
3063
+ .map(({ room }) => room);
3064
+ for (const room of unindexed)
3065
+ pending.push({ room, awaits: 'index' });
3066
+ const held = conversations.searchable();
3067
+ const named = reach !== null && !reach.conversations
3068
+ ? []
3069
+ : (plan.conversations ?? held).filter((one) => held.includes(one) && conversations.acquired(one));
3070
+ const waiting = named.filter((one) => !searchIndex.conversations().has(one));
3071
+ for (const conversation of waiting)
3072
+ pending.push({ conversation, awaits: 'index' });
3073
+ if (unindexed.length > 0 || waiting.length > 0)
3074
+ index(unindexed, waiting);
3075
+ if (pending.length > 0 || 'pending' in cover)
3076
+ return { pending };
3077
+ const seen = searchIndex.changes();
3078
+ await searchIndex.caughtUp();
3079
+ const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(filter);
3080
+ if (selected !== null && 'pending' in selected)
3081
+ return selected;
3082
+ if (searchIndex.changes() !== seen ||
3083
+ JSON.stringify(covered(filter)) !== JSON.stringify(cover))
3084
+ continue;
3085
+ return {
3086
+ ...searchIndex.search(plan, selected === null ? null : selected.documents, named),
3087
+ coverage: {
3088
+ rooms: covering,
3089
+ conversations: named,
3090
+ index: searchIndex.generation(),
3091
+ overlay: cover.coverage.overlay,
3092
+ },
3093
+ };
3094
+ }
1880
3095
  }
1881
3096
  const watch = (topic, input, deliver) => {
1882
3097
  if (topic === 'room-drafts') {
@@ -1901,6 +3116,8 @@ export function composeRuntime(options) {
1901
3116
  }
1902
3117
  if (topic === 'conversations')
1903
3118
  return conversations.watchList(deliver);
3119
+ if (topic === 'agents')
3120
+ return conversations.watchAgents(deliver);
1904
3121
  if (topic === 'presentation') {
1905
3122
  const listener = {
1906
3123
  app: input[0],
@@ -1920,6 +3137,9 @@ export function composeRuntime(options) {
1920
3137
  presentationListeners.delete(listener);
1921
3138
  };
1922
3139
  }
3140
+ if (topic === 'readiness') {
3141
+ return watchReadiness({ target: '', code: 'stored', capacity: null }, null, deliver);
3142
+ }
1923
3143
  if (topic === 'rooms') {
1924
3144
  roomListeners.add(deliver);
1925
3145
  deliver(listed());
@@ -1955,6 +3175,13 @@ export function composeRuntime(options) {
1955
3175
  serviceModeListeners.delete(deliver);
1956
3176
  };
1957
3177
  }
3178
+ if (topic === 'member') {
3179
+ memberListeners.add(deliver);
3180
+ deliver(member);
3181
+ return () => {
3182
+ memberListeners.delete(deliver);
3183
+ };
3184
+ }
1958
3185
  if (topic === 'queue') {
1959
3186
  const queue = { locator: input[0], notify: deliver };
1960
3187
  queueSubscriptions.add(queue);
@@ -1972,7 +3199,7 @@ export function composeRuntime(options) {
1972
3199
  rooms() {
1973
3200
  return listed();
1974
3201
  },
1975
- query: ((input) => readSide.query(planned(input, null))),
3202
+ query: ((input) => rowsQuery(planned(input, null))),
1976
3203
  search: (input) => searched(input, null),
1977
3204
  types(locator) {
1978
3205
  return {
@@ -2001,20 +3228,20 @@ export function composeRuntime(options) {
2001
3228
  return exclusive(async () => {
2002
3229
  const parts = readParts(input);
2003
3230
  if (path === '' || normalize(path) !== path)
2004
- throw new Error('room-draft-malformed');
3231
+ throw runtimeError('room-draft-malformed');
2005
3232
  const local = await openLocal();
2006
3233
  const current = roomDrafts.get(room)?.get(path);
2007
3234
  if ((current?.revision ?? null) !== revision) {
2008
- return {
2009
- outcome: 'stale',
2010
- current: current === undefined ? null : roomDraftOf(current),
2011
- };
3235
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2012
3236
  }
3237
+ const file = parts.file === undefined
3238
+ ? undefined
3239
+ : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
2013
3240
  const record = {
2014
3241
  locator: room,
2015
3242
  path,
2016
3243
  revision: roomDraftRevision(),
2017
- parts: held(current?.parts, parts),
3244
+ parts: held(current?.parts, parts, file),
2018
3245
  returned: current?.returned ?? null,
2019
3246
  };
2020
3247
  await local.keepRoomDraft(record);
@@ -2028,10 +3255,7 @@ export function composeRuntime(options) {
2028
3255
  const local = await openLocal();
2029
3256
  const current = roomDrafts.get(room)?.get(path);
2030
3257
  if (current === undefined || current.revision !== revision) {
2031
- return {
2032
- outcome: 'stale',
2033
- current: current === undefined ? null : roomDraftOf(current),
2034
- };
3258
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2035
3259
  }
2036
3260
  await local.discardRoomDraft(room, path);
2037
3261
  forgetRoomDraft(room, path);
@@ -2066,6 +3290,10 @@ export function composeRuntime(options) {
2066
3290
  history(locator, path, query) {
2067
3291
  return readHistory(locator, path, query);
2068
3292
  },
3293
+ binaryFile: {
3294
+ read: readBinaryPath,
3295
+ readReference: readBinaryReference,
3296
+ },
2069
3297
  call(name, input) {
2070
3298
  return invokeAction(name, input);
2071
3299
  },
@@ -2078,30 +3306,55 @@ export function composeRuntime(options) {
2078
3306
  return journal.export();
2079
3307
  },
2080
3308
  persistence() {
2081
- return options.shell.persistentStorage;
3309
+ return persistence === 'granted';
3310
+ },
3311
+ async retry() {
3312
+ journal.record('retry');
3313
+ unindexable.clear();
3314
+ queueReadiness();
3315
+ resume();
2082
3316
  },
2083
3317
  observe: observing(watch),
2084
3318
  async reauthorize() {
2085
3319
  if (refusal === null || !refusal.reauthorizationOffered)
2086
3320
  return;
2087
3321
  journal.record('connection reauthorize');
2088
- await chain.catch(() => undefined);
3322
+ await Promise.all([...lanes.values()]
3323
+ .filter((held) => held.generation === generation)
3324
+ .map((held) => held.chain));
2089
3325
  await mutations;
2090
3326
  authorization = undefined;
2091
3327
  discoveryPaused = false;
2092
3328
  if (options.adapter.roomStream === undefined)
2093
3329
  await sync('unstreamed');
2094
3330
  else
2095
- await task(ensureAuthorization);
3331
+ await task(OPENING_LANE, ensureAuthorization);
2096
3332
  },
2097
3333
  };
2098
3334
  const reachable = {
2099
3335
  md,
2100
3336
  watch,
3337
+ referrerScope: (referrer) => options.adapter.referrerScope(referrer),
2101
3338
  plan: planned,
2102
- query: (plan) => readSide.query(plan),
3339
+ query: (plan) => rowsQuery(plan),
2103
3340
  watchPlan,
2104
3341
  search: searched,
3342
+ searching,
3343
+ watchReadiness,
3344
+ };
3345
+ return {
3346
+ md,
3347
+ stop,
3348
+ lose: loseRooms,
3349
+ watch,
3350
+ acquire,
3351
+ persisted(answer) {
3352
+ if (answer === persistence)
3353
+ return;
3354
+ persistence = answer;
3355
+ journal.record(`persistence ${answer}`);
3356
+ queueReadiness();
3357
+ },
3358
+ within: (reach, port) => narrowed(reachable, reach, port),
2105
3359
  };
2106
- return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2107
3360
  }