@agentshouse/mdruntime 0.1.0-ca7c26c6bc27e → 0.1.0-ca9afacd5aebd

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 (170) hide show
  1. package/AGENTS.md +144 -41
  2. package/README.md +21 -11
  3. package/dist/adapter.d.ts +71 -20
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/adapter.js +1 -9
  6. package/dist/attached-model.d.ts.map +1 -1
  7. package/dist/attached-model.js +15 -2
  8. package/dist/attempt.d.ts +16 -0
  9. package/dist/attempt.d.ts.map +1 -0
  10. package/dist/attempt.js +57 -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 +89 -38
  17. package/dist/change-set.d.ts.map +1 -1
  18. package/dist/change-set.js +19 -12
  19. package/dist/change.d.ts +15 -0
  20. package/dist/change.d.ts.map +1 -1
  21. package/dist/change.js +26 -1
  22. package/dist/chat/acts.d.ts +34 -0
  23. package/dist/chat/acts.d.ts.map +1 -0
  24. package/dist/chat/acts.js +36 -0
  25. package/dist/chat/context.d.ts +3 -0
  26. package/dist/chat/context.d.ts.map +1 -0
  27. package/dist/chat/context.js +10 -0
  28. package/dist/chat/input.d.ts +9 -0
  29. package/dist/chat/input.d.ts.map +1 -0
  30. package/dist/chat/input.js +99 -0
  31. package/dist/chat/kit.d.ts +60 -0
  32. package/dist/chat/kit.d.ts.map +1 -0
  33. package/dist/chat/kit.js +123 -0
  34. package/dist/chat/lasting.d.ts +6 -0
  35. package/dist/chat/lasting.d.ts.map +1 -0
  36. package/dist/chat/lasting.js +19 -0
  37. package/dist/chat/list.d.ts +20 -0
  38. package/dist/chat/list.d.ts.map +1 -0
  39. package/dist/chat/list.js +59 -0
  40. package/dist/chat/markdown.d.ts +19 -0
  41. package/dist/chat/markdown.d.ts.map +1 -0
  42. package/dist/chat/markdown.js +187 -0
  43. package/dist/chat/message.d.ts +6 -0
  44. package/dist/chat/message.d.ts.map +1 -0
  45. package/dist/chat/message.js +192 -0
  46. package/dist/chat/messages.d.ts +21 -0
  47. package/dist/chat/messages.d.ts.map +1 -0
  48. package/dist/chat/messages.js +151 -0
  49. package/dist/chat/name.d.ts +6 -0
  50. package/dist/chat/name.d.ts.map +1 -0
  51. package/dist/chat/name.js +37 -0
  52. package/dist/chat/opened.d.ts +11 -0
  53. package/dist/chat/opened.d.ts.map +1 -0
  54. package/dist/chat/opened.js +17 -0
  55. package/dist/chat/overlay.d.ts +31 -0
  56. package/dist/chat/overlay.d.ts.map +1 -0
  57. package/dist/chat/overlay.js +97 -0
  58. package/dist/chat/parts.d.ts +57 -0
  59. package/dist/chat/parts.d.ts.map +1 -0
  60. package/dist/chat/parts.js +119 -0
  61. package/dist/chat/process.d.ts +20 -0
  62. package/dist/chat/process.d.ts.map +1 -0
  63. package/dist/chat/process.js +32 -0
  64. package/dist/chat/provider.d.ts +63 -0
  65. package/dist/chat/provider.d.ts.map +1 -0
  66. package/dist/chat/provider.js +645 -0
  67. package/dist/chat/question.d.ts +9 -0
  68. package/dist/chat/question.d.ts.map +1 -0
  69. package/dist/chat/question.js +80 -0
  70. package/dist/chat/questions.d.ts +31 -0
  71. package/dist/chat/questions.d.ts.map +1 -0
  72. package/dist/chat/questions.js +111 -0
  73. package/dist/chat/read.d.ts +5 -0
  74. package/dist/chat/read.d.ts.map +1 -0
  75. package/dist/chat/read.js +13 -0
  76. package/dist/chat/ready.d.ts +33 -0
  77. package/dist/chat/ready.d.ts.map +1 -0
  78. package/dist/chat/ready.js +91 -0
  79. package/dist/chat/style.d.ts +4 -0
  80. package/dist/chat/style.d.ts.map +1 -0
  81. package/dist/chat/style.js +302 -0
  82. package/dist/chat.d.ts +14 -0
  83. package/dist/chat.d.ts.map +1 -0
  84. package/dist/chat.js +8 -0
  85. package/dist/client.d.ts +17 -7
  86. package/dist/client.d.ts.map +1 -1
  87. package/dist/client.js +68 -25
  88. package/dist/collection.d.ts +6 -11
  89. package/dist/collection.d.ts.map +1 -1
  90. package/dist/collection.js +56 -19
  91. package/dist/connection.d.ts +25 -12
  92. package/dist/connection.d.ts.map +1 -1
  93. package/dist/connection.js +492 -185
  94. package/dist/conversation.d.ts +27 -8
  95. package/dist/conversation.d.ts.map +1 -1
  96. package/dist/conversation.js +210 -81
  97. package/dist/expectation.d.ts +21 -0
  98. package/dist/expectation.d.ts.map +1 -0
  99. package/dist/expectation.js +70 -0
  100. package/dist/failure.d.ts +13 -0
  101. package/dist/failure.d.ts.map +1 -0
  102. package/dist/failure.js +19 -0
  103. package/dist/host.d.ts +1 -0
  104. package/dist/host.d.ts.map +1 -1
  105. package/dist/host.js +12 -19
  106. package/dist/identity.d.ts +3 -0
  107. package/dist/identity.d.ts.map +1 -1
  108. package/dist/identity.js +13 -1
  109. package/dist/index.d.ts +9 -4
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js +3 -0
  112. package/dist/memory.d.ts +5 -5
  113. package/dist/memory.d.ts.map +1 -1
  114. package/dist/memory.js +73 -27
  115. package/dist/model.d.ts +13 -8
  116. package/dist/model.d.ts.map +1 -1
  117. package/dist/model.js +6 -3
  118. package/dist/open.d.ts +2 -0
  119. package/dist/open.d.ts.map +1 -0
  120. package/dist/open.js +1 -0
  121. package/dist/outcome.d.ts +28 -3
  122. package/dist/outcome.d.ts.map +1 -1
  123. package/dist/outcome.js +41 -1
  124. package/dist/overlay.d.ts +14 -3
  125. package/dist/overlay.d.ts.map +1 -1
  126. package/dist/overlay.js +75 -37
  127. package/dist/paths.d.ts +0 -1
  128. package/dist/paths.d.ts.map +1 -1
  129. package/dist/paths.js +0 -16
  130. package/dist/query.d.ts +5 -1
  131. package/dist/query.d.ts.map +1 -1
  132. package/dist/query.js +74 -33
  133. package/dist/reach.d.ts +2 -0
  134. package/dist/reach.d.ts.map +1 -1
  135. package/dist/reach.js +51 -34
  136. package/dist/react.d.ts +41 -0
  137. package/dist/react.d.ts.map +1 -0
  138. package/dist/react.js +64 -0
  139. package/dist/read-side.d.ts.map +1 -1
  140. package/dist/read-side.js +111 -48
  141. package/dist/refresh.d.ts +10 -0
  142. package/dist/refresh.d.ts.map +1 -1
  143. package/dist/refresh.js +42 -38
  144. package/dist/replica.d.ts +9 -7
  145. package/dist/replica.d.ts.map +1 -1
  146. package/dist/replica.js +90 -64
  147. package/dist/room-draft.d.ts +26 -9
  148. package/dist/room-draft.d.ts.map +1 -1
  149. package/dist/room-draft.js +132 -23
  150. package/dist/rows.d.ts +7 -2
  151. package/dist/rows.d.ts.map +1 -1
  152. package/dist/rows.js +85 -40
  153. package/dist/save.d.ts +28 -3
  154. package/dist/save.d.ts.map +1 -1
  155. package/dist/save.js +429 -80
  156. package/dist/search.d.ts +3 -2
  157. package/dist/search.d.ts.map +1 -1
  158. package/dist/status.d.ts +2 -2
  159. package/dist/status.d.ts.map +1 -1
  160. package/dist/status.js +0 -8
  161. package/dist/store.d.ts +34 -9
  162. package/dist/store.d.ts.map +1 -1
  163. package/dist/store.js +273 -164
  164. package/dist/wire.d.ts +8 -15
  165. package/dist/wire.d.ts.map +1 -1
  166. package/dist/wire.js +16 -17
  167. package/package.json +23 -3
  168. package/dist/attachment.d.ts +0 -3
  169. package/dist/attachment.d.ts.map +0 -1
  170. package/dist/attachment.js +0 -8
@@ -3,25 +3,28 @@ import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
3
3
  import { observing } from './observation.js';
4
4
  import { filterOf, identityOf, planOf, } from './query.js';
5
5
  import { createReadSide } from './read-side.js';
6
+ import { markdownPath, roomOfKey } from './rows.js';
6
7
  import { createSearchIndex, searchPlanOf, } from './search.js';
7
- import { ROOM_CONTRACT_VERSION, VERSION_1_CAPABILITIES, } from './adapter.js';
8
8
  import { bootstrapRoom } from './bootstrap.js';
9
9
  import { changeSet, mergeChangeSets } from './change-set.js';
10
10
  import { createConversationStore, } from './conversation.js';
11
- import { MD_MODEL, parseCollection, validRoot } from './collection.js';
11
+ import { queuedFile } from './change.js';
12
+ import { validRoot } from './collection.js';
13
+ import { runtimeError } from './failure.js';
12
14
  import { createLog } from './log.js';
15
+ import { binaryRecord } from './binary.js';
16
+ import { absentBinary, binaryAnswer, calledResult, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
13
17
  import { refreshRoom } from './refresh.js';
14
- import { buildBatch } from './save.js';
15
- import { applyReturned, compositionOf, landed, readConfirmed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
16
- import { changedParts, changesOf, held, readParts, returnedInto, underneath, } from './room-draft.js';
17
- import { settled, same, saving, STARTING, transferring, } from './status.js';
18
+ import { buildBatch, joined, limitedIn, requeued } from './save.js';
19
+ import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
20
+ import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
21
+ import { same, saving, STARTING, transferring, } from './status.js';
18
22
  import { detectStorageLoss, nextGeneration } from './durability.js';
19
23
  import { roomDraftRevision } from './identity.js';
20
24
  import { beneath, normalize } from './paths.js';
21
25
  import { openRuntimeStore, } from './store.js';
22
26
  import { unconfirmed } from './transfer.js';
23
27
  import { governs, holds, narrowed, planReach, } from './reach.js';
24
- const knownCapabilities = new Set(VERSION_1_CAPABILITIES);
25
28
  function within(root, path) {
26
29
  return root === '' || beneath(root, path);
27
30
  }
@@ -36,21 +39,19 @@ function declaredSelection(room) {
36
39
  selection.roots.every((other, at) => at === index || !within(other, root)));
37
40
  }
38
41
  export function composeRuntime(options) {
42
+ const localSaves = options.adapter.localSaves === true;
39
43
  let authorization = options.authorization;
40
44
  let opening;
41
45
  let store;
42
46
  const replicas = new Map();
47
+ const queuesWithoutReplica = new Map();
43
48
  const pendings = new Map();
44
49
  const roomDrafts = new Map();
45
50
  const presentations = new Map();
46
51
  let limits = [];
47
52
  let capabilities = [];
48
- function rewriting() {
49
- return capabilities.some((capability) => capability.name === 'move-reference-rewrites')
50
- ? 'provider'
51
- : 'runtime';
52
- }
53
53
  const rooms = new Map();
54
+ const counted = new Map();
54
55
  const unreadable = new Set();
55
56
  const models = new Map();
56
57
  const liveModels = new Map();
@@ -89,6 +90,8 @@ export function composeRuntime(options) {
89
90
  let withdrawStream;
90
91
  const announced = new Map();
91
92
  let underway = null;
93
+ let streamed = null;
94
+ let handed = false;
92
95
  const submitting = new Map();
93
96
  const answering = new Map();
94
97
  let complete = false;
@@ -111,9 +114,14 @@ export function composeRuntime(options) {
111
114
  let haltedRefusal = false;
112
115
  let openSignals = 0;
113
116
  const serviceModeListeners = new Set();
117
+ let member = null;
118
+ const memberListeners = new Set();
119
+ const materialized = new Set();
120
+ let everyRoom = false;
121
+ const transfers = new Map();
114
122
  const conversations = createConversationStore({
115
- cards: declaredCards,
116
- documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
123
+ agents: declaredAgents,
124
+ documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
117
125
  call: (name, input) => invokeAction(name, input),
118
126
  durably,
119
127
  stream: () => options.adapter.conversations,
@@ -189,7 +197,9 @@ export function composeRuntime(options) {
189
197
  }
190
198
  }
191
199
  function pending() {
192
- return [...replicas.values()].some((replica) => replica.queue.length > 0);
200
+ return (!localSaves &&
201
+ (queuesWithoutReplica.size > 0 ||
202
+ [...replicas.values()].some((replica) => replica.queue.length > 0)));
193
203
  }
194
204
  function attention() {
195
205
  const attentionRequired = [];
@@ -218,8 +228,24 @@ export function composeRuntime(options) {
218
228
  }
219
229
  return { attentionRequired, reset };
220
230
  }
231
+ function documentsIn(locator) {
232
+ const replica = replicas.get(locator);
233
+ if (replica === undefined)
234
+ return 0;
235
+ const roots = replica.confirmed.record.typedSelection?.roots ?? [];
236
+ let count = 0;
237
+ for (const path of Object.keys(replica.visible.documents)) {
238
+ if (markdownPath(path) && !roots.some((root) => beneath(root, path)))
239
+ count += 1;
240
+ }
241
+ return count;
242
+ }
221
243
  function listed() {
222
- return structuredClone([...rooms.values()]);
244
+ return structuredClone([...rooms.values()].map((room) => {
245
+ const documents = documentsIn(room.locator);
246
+ counted.set(room.locator, documents);
247
+ return { ...room, documents };
248
+ }));
223
249
  }
224
250
  function roomFacts(reach = null) {
225
251
  const held = [...rooms.values()]
@@ -251,6 +277,10 @@ export function composeRuntime(options) {
251
277
  for (const listener of storageLossListeners)
252
278
  listener(storageLoss);
253
279
  }
280
+ function emitMember() {
281
+ for (const listener of memberListeners)
282
+ listener(member);
283
+ }
254
284
  function moveTo(next) {
255
285
  if (closed)
256
286
  return;
@@ -270,6 +300,8 @@ export function composeRuntime(options) {
270
300
  listener(shown);
271
301
  }
272
302
  function entryOf(batch) {
303
+ if (localSaves)
304
+ return Object.freeze({ batch, state: 'local' });
273
305
  const state = pendings.get(batch.id);
274
306
  if (state?.accepted !== undefined) {
275
307
  return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
@@ -277,10 +309,7 @@ export function composeRuntime(options) {
277
309
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
278
310
  }
279
311
  function entriesOf(locator) {
280
- const replica = replicas.get(locator);
281
- if (replica === undefined)
282
- return [];
283
- return replica.queue.map(entryOf);
312
+ return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
284
313
  }
285
314
  function emitQueue(locator) {
286
315
  const entries = entriesOf(locator);
@@ -290,12 +319,47 @@ export function composeRuntime(options) {
290
319
  }
291
320
  }
292
321
  function applyChangeSet(set) {
322
+ const held = everyRoom
323
+ ? set
324
+ : { ...set, documents: set.documents.filter(({ locator }) => materialized.has(locator)) };
325
+ readSide.apply(held);
326
+ searchIndex.changed(held);
327
+ }
328
+ function materialize(locators) {
329
+ if (everyRoom)
330
+ return;
331
+ if (locators === null)
332
+ everyRoom = true;
333
+ const sets = [];
334
+ for (const locator of locators ?? [...replicas.keys()]) {
335
+ if (materialized.has(locator))
336
+ continue;
337
+ materialized.add(locator);
338
+ const replica = replicas.get(locator);
339
+ if (replica !== undefined)
340
+ sets.push(changeSet(locator, null, replica));
341
+ }
342
+ if (sets.length === 0)
343
+ return;
344
+ const set = mergeChangeSets(sets);
293
345
  readSide.apply(set);
294
346
  searchIndex.changed(set);
295
347
  }
348
+ function reachedRooms(plan) {
349
+ if (plan.rooms !== null)
350
+ return plan.rooms;
351
+ if (plan.reach === null)
352
+ return null;
353
+ return [...plan.reach.rooms, ...plan.reach.documents.map(roomOfKey)];
354
+ }
355
+ function rowsQuery(plan) {
356
+ materialize(reachedRooms(plan));
357
+ return readSide.query(plan);
358
+ }
296
359
  function publishChangeSet(set) {
297
360
  applyChangeSet(set);
298
- if (set.rooms.length > 0)
361
+ if (set.rooms.length > 0 ||
362
+ [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
299
363
  emitRooms();
300
364
  for (const locator of set.queues)
301
365
  emitQueue(locator);
@@ -306,10 +370,6 @@ export function composeRuntime(options) {
306
370
  for (const app of set.presentations)
307
371
  emitPresentation(app);
308
372
  conversations.changed(set);
309
- const cards = declaredCards();
310
- if (cards !== undefined && set.documents.some((change) => change.locator === cards.locator)) {
311
- scheduleCurrency();
312
- }
313
373
  }
314
374
  function stopTimer() {
315
375
  if (resetTimer === undefined)
@@ -346,10 +406,7 @@ export function composeRuntime(options) {
346
406
  const revoked = { status: 'unavailable', reason: 'revoked' };
347
407
  try {
348
408
  const local = await openLocal();
349
- await local.revokeModels([...replicas.values()].map((replica) => ({
350
- record: replica.confirmed.record,
351
- queue: replica.queue,
352
- })));
409
+ await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
353
410
  }
354
411
  catch {
355
412
  journal.record('model revocation persistence failed');
@@ -373,10 +430,7 @@ export function composeRuntime(options) {
373
430
  model: revoked,
374
431
  selectionModel: revoked,
375
432
  };
376
- const read = readConfirmed(record, before.confirmed);
377
- replicas.set(locator, read.kind === 'staged'
378
- ? replicaOf(read.confirmed, before.queue, rewriting(), before)
379
- : { ...before, confirmed: { ...before.confirmed, record } });
433
+ replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
380
434
  }
381
435
  rooms.set(locator, {
382
436
  ...room,
@@ -393,7 +447,8 @@ export function composeRuntime(options) {
393
447
  }
394
448
  finally {
395
449
  refusal = {
396
- reason,
450
+ code: reason,
451
+ retryable: false,
397
452
  reauthorizationOffered: reason === 'revoked-authorization',
398
453
  refusal: provider,
399
454
  };
@@ -406,6 +461,7 @@ export function composeRuntime(options) {
406
461
  rooms.set(room.locator, {
407
462
  name: room.name,
408
463
  locator: room.locator,
464
+ handle: room.handle,
409
465
  operations: room.operations,
410
466
  replica: room.replica,
411
467
  model: modelTypes(modelOf(room.locator)),
@@ -417,7 +473,6 @@ export function composeRuntime(options) {
417
473
  },
418
474
  providerOwnedPaths: room.providerOwnedPaths,
419
475
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
420
- attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
421
476
  synchronizationAvailable,
422
477
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
423
478
  });
@@ -442,7 +497,14 @@ export function composeRuntime(options) {
442
497
  emitRooms();
443
498
  }
444
499
  function unlistedHeld(listed) {
445
- return [...new Set([...rooms.keys(), ...roomDrafts.keys()])].filter((locator) => !listed.has(locator));
500
+ return [
501
+ ...new Set([
502
+ ...rooms.keys(),
503
+ ...roomDrafts.keys(),
504
+ ...transfers.keys(),
505
+ ...queuesWithoutReplica.keys(),
506
+ ]),
507
+ ].filter((locator) => !listed.has(locator));
446
508
  }
447
509
  async function dropUnlisted(listed, local) {
448
510
  if (unlistedHeld(listed).length === 0)
@@ -456,11 +518,13 @@ export function composeRuntime(options) {
456
518
  drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
457
519
  }));
458
520
  for (const locator of unlisted) {
459
- for (const batch of replicas.get(locator)?.queue ?? [])
521
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
460
522
  pendings.delete(batch.id);
461
523
  replicas.delete(locator);
524
+ queuesWithoutReplica.delete(locator);
462
525
  rooms.delete(locator);
463
526
  roomDrafts.delete(locator);
527
+ transfers.delete(locator);
464
528
  unreadable.delete(locator);
465
529
  stale.delete(locator);
466
530
  scopedRefusals.delete(locator);
@@ -497,7 +561,7 @@ export function composeRuntime(options) {
497
561
  return model;
498
562
  }
499
563
  async function hydrate(opened) {
500
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
564
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
501
565
  opened.all(),
502
566
  opened.queues(),
503
567
  opened.roomDrafts(),
@@ -507,9 +571,15 @@ export function composeRuntime(options) {
507
571
  opened.lastCompleted(),
508
572
  opened.providerLimits(),
509
573
  opened.capabilities(),
574
+ opened.member(),
575
+ opened.transfers(),
510
576
  ]);
577
+ transfers.clear();
578
+ for (const record of staged)
579
+ transfers.set(record.locator, record);
511
580
  limits = declared;
512
581
  capabilities = named;
582
+ member = known;
513
583
  roomDrafts.clear();
514
584
  for (const record of kept)
515
585
  rememberRoomDraft(record);
@@ -525,19 +595,18 @@ export function composeRuntime(options) {
525
595
  }
526
596
  const previousReplicas = new Map(replicas);
527
597
  replicas.clear();
598
+ queuesWithoutReplica.clear();
599
+ const replicated = new Set(records.map((record) => record.locator));
600
+ for (const [locator, queue] of queuedBatches) {
601
+ if (!replicated.has(locator))
602
+ queuesWithoutReplica.set(locator, queue);
603
+ }
528
604
  for (const record of records) {
529
605
  const model = restored(modelKey(record.locator, 'collection'), record.model);
530
606
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
531
607
  const prior = previousReplicas.get(record.locator);
532
- const retyped = { ...record, model, selectionModel };
533
- const read = readConfirmed(retyped, prior?.confirmed);
534
- const confirmed = read.kind === 'staged'
535
- ? read.confirmed
536
- : {
537
- record: retyped,
538
- collection: parseCollection(retyped.documents, compositionOf(retyped)),
539
- };
540
- replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], rewriting(), prior));
608
+ const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
609
+ replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
541
610
  const existing = rooms.get(record.locator);
542
611
  if (existing === undefined)
543
612
  list(record, false);
@@ -545,7 +614,6 @@ export function composeRuntime(options) {
545
614
  rooms.set(record.locator, {
546
615
  ...existing,
547
616
  roomVersion: record.roomVersion,
548
- attachments: record.attachments,
549
617
  operations: record.operations,
550
618
  replica: record.replica,
551
619
  model: modelTypes(model),
@@ -557,10 +625,6 @@ export function composeRuntime(options) {
557
625
  },
558
626
  });
559
627
  }
560
- if (read.kind === 'refused') {
561
- refusedSource(record, read);
562
- declarationRefusals.add(record.locator);
563
- }
564
628
  }
565
629
  return {
566
630
  lastCompleted,
@@ -573,6 +637,8 @@ export function composeRuntime(options) {
573
637
  applyChangeSet(read);
574
638
  if (replicas.size > 0)
575
639
  emitRooms();
640
+ if (member !== null)
641
+ emitMember();
576
642
  for (const locator of locators)
577
643
  emitQueue(locator);
578
644
  publishRoomDrafts(false);
@@ -620,10 +686,15 @@ export function composeRuntime(options) {
620
686
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
621
687
  }
622
688
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
623
- const withdrawn = withdrawals(confirmed, queue, withdrawable, rewriting());
689
+ const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
690
+ const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
624
691
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
692
+ const unqueued = withdrawn.map((batch) => ({
693
+ batch,
694
+ returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
695
+ }));
625
696
  const returning = [
626
- ...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
697
+ ...[...unqueued].reverse(),
627
698
  ...(settling.leaving === undefined ? [] : [settling.leaving]),
628
699
  ];
629
700
  const written = new Map();
@@ -641,25 +712,44 @@ export function composeRuntime(options) {
641
712
  });
642
713
  }
643
714
  }
644
- await local.write({
645
- next: confirmed.record,
646
- previous: before?.confirmed.record ?? null,
647
- queue: kept,
648
- previousQueue: before?.queue ?? [],
649
- roomDrafts: [...written.values()],
650
- });
651
- for (const batch of before?.queue ?? []) {
715
+ await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
716
+ for (const batch of previousQueue) {
652
717
  if (!kept.includes(batch))
653
718
  pendings.delete(batch.id);
654
719
  }
655
720
  for (const record of written.values())
656
721
  rememberRoomDraft(record);
657
- for (const batch of withdrawn)
658
- journal.record(`outcome ${locator} ${batch.id} withdrawn`);
659
- replicas.set(locator, replicaOf(confirmed, kept, rewriting(), before));
722
+ try {
723
+ await local.write({
724
+ next: confirmed.record,
725
+ previous: before?.confirmed.record ?? null,
726
+ ...(settling.stored === undefined ? {} : { stored: settling.stored }),
727
+ });
728
+ }
729
+ catch (error) {
730
+ const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
731
+ if (held !== null)
732
+ replicas.set(locator, held);
733
+ else if (kept.length > 0)
734
+ queuesWithoutReplica.set(locator, kept);
735
+ else
736
+ queuesWithoutReplica.delete(locator);
737
+ publishChangeSet(changeSet(locator, before ?? null, held, {
738
+ queue: kept.length !== previousQueue.length,
739
+ drafts: [...written.keys()],
740
+ }));
741
+ moveTo({ ...status, ...attention() });
742
+ throw error;
743
+ }
744
+ queuesWithoutReplica.delete(locator);
745
+ for (const { batch, returned } of unqueued) {
746
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
747
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
748
+ }
749
+ replicas.set(locator, replicaOf(confirmed, kept, before));
660
750
  return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
661
751
  room: settling.room,
662
- queue: kept.length !== (before?.queue.length ?? 0),
752
+ queue: kept.length !== previousQueue.length,
663
753
  drafts: [...written.keys()],
664
754
  });
665
755
  }
@@ -694,7 +784,8 @@ export function composeRuntime(options) {
694
784
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
695
785
  };
696
786
  }
697
- function publish(local, room, confirmed, base) {
787
+ function publish(local, room, confirmed, landing = {}) {
788
+ const { base, stored } = landing;
698
789
  return exclusive(async () => {
699
790
  if (!continuing())
700
791
  return true;
@@ -705,13 +796,15 @@ export function composeRuntime(options) {
705
796
  return false;
706
797
  }
707
798
  adoptModels(room.locator, confirmed.record);
708
- const queue = prior?.queue ?? [];
799
+ const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
709
800
  const roomVersion = confirmed.record.roomVersion;
710
801
  const accepted = queue.filter((batch) => {
711
802
  const accepted = pendings.get(batch.id)?.accepted;
712
803
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
713
804
  });
714
- const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
805
+ const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
806
+ if (stored !== undefined)
807
+ transfers.delete(room.locator);
715
808
  for (const batch of accepted)
716
809
  pendings.delete(batch.id);
717
810
  stale.delete(room.locator);
@@ -730,62 +823,140 @@ export function composeRuntime(options) {
730
823
  const replica = replicas.get(locator);
731
824
  const room = rooms.get(locator);
732
825
  if (replica === undefined || room === undefined) {
733
- throw new Error(`No Local Replica for ${locator}`);
826
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
734
827
  }
735
828
  const saved = [];
736
829
  for (const { path, revision } of named) {
737
830
  const record = roomDrafts.get(locator)?.get(path);
738
831
  if (record === undefined || record.revision !== revision) {
739
- return { outcome: 'refused', rule: 'stale', file: path };
832
+ return { outcome: 'refused', code: 'stale', retryable: false, file: path };
740
833
  }
741
834
  if (!saved.some((draft) => draft.path === path))
742
835
  saved.push({ path, parts: record.parts });
743
836
  }
744
837
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
745
838
  if (changed.length > 0)
746
- return { outcome: 'refused', rule: 'changed-underneath', parts: changed };
839
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
747
840
  const parts = saved.map(({ path, parts }) => changesOf(path, parts));
748
- const built = buildBatch({
841
+ const changes = parts.flatMap(({ changes }) => changes);
842
+ const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
843
+ if (requeue.kind === 'refused')
844
+ return { outcome: 'refused', ...requeue.refusal };
845
+ const foreign = [...requeue.withdrawn.keys()].some((origin) => {
846
+ const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
847
+ return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
848
+ });
849
+ if (foreign) {
850
+ return {
851
+ outcome: 'refused',
852
+ code: 'unknown-revision',
853
+ retryable: false,
854
+ file: [...requeue.queued].at(-1),
855
+ };
856
+ }
857
+ const withdrawing = requeue.withdrawn.size > 0;
858
+ const input = {
859
+ local: localSaves,
749
860
  baseRoomVersion: replica.confirmed.record.roomVersion,
750
861
  composition: compositionFor(replica.confirmed.record),
751
862
  operations: room.operations,
752
863
  providerOwnedPaths: room.providerOwnedPaths,
753
864
  providerLimits: limits,
754
- rewriting: rewriting(),
865
+ handle: room.handle,
755
866
  visible: replica.visible,
756
- changes: parts.flatMap(({ changes }) => changes),
867
+ changes,
757
868
  bases: parts.flatMap(({ bases }) => bases),
758
869
  writtenBy,
870
+ };
871
+ const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
872
+ if (checked?.kind === 'refused')
873
+ return { outcome: 'refused', ...checked.refusal };
874
+ const owners = [...requeue.owners].flatMap((origin) => {
875
+ const batch = requeue.withdrawn.get(origin);
876
+ return batch === undefined || batch === null ? [] : [{ origin, batch }];
759
877
  });
760
- if (built.kind === 'refused')
878
+ if (owners.length > 1) {
879
+ return {
880
+ outcome: 'refused',
881
+ code: 'unknown-revision',
882
+ retryable: false,
883
+ file: [...requeue.queued].at(-1),
884
+ };
885
+ }
886
+ const built = !withdrawing
887
+ ? buildBatch(input)
888
+ : requeue.changes.length === 0
889
+ ? null
890
+ : buildBatch({
891
+ ...input,
892
+ visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
893
+ changes: requeue.changes,
894
+ });
895
+ if (built?.kind === 'refused')
761
896
  return { outcome: 'refused', ...built.refusal };
762
- const batches = [...replica.queue, built.batch];
897
+ const [owner] = owners;
898
+ const join = built === null || owner === undefined
899
+ ? null
900
+ : joined(requeue.queue, owner.batch, built.batch);
901
+ if (join?.kind === 'refused')
902
+ return { outcome: 'refused', ...join.refusal };
903
+ const withdrawn = new Map(requeue.withdrawn);
904
+ if (join !== null && owner !== undefined)
905
+ withdrawn.set(owner.origin, join.batch);
906
+ const drafted = new Map();
907
+ for (const [origin, replaced] of withdrawn) {
908
+ if (replaced === null)
909
+ continue;
910
+ const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
911
+ drafted.set(replaced.id, replaced === join?.batch
912
+ ? [
913
+ ...carried.map((held) => joinedDraft(held, saved)),
914
+ ...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
915
+ ]
916
+ : carried);
917
+ }
918
+ if (built !== null && join === null)
919
+ drafted.set(built.batch.id, saved);
920
+ const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
921
+ for (const replaced of withdrawn.values()) {
922
+ if (replaced === null || !batches.includes(replaced))
923
+ continue;
924
+ const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
925
+ if (limited !== null)
926
+ return { outcome: 'refused', ...limited };
927
+ }
763
928
  const consumed = saved.map(({ path }) => path);
764
929
  try {
765
- await local.write({
766
- next: replica.confirmed.record,
767
- previous: replica.confirmed.record,
930
+ await local.keepAuthored({
931
+ locator,
768
932
  queue: batches,
769
- previousQueue: replica.queue,
770
- saved,
933
+ saved: drafted,
771
934
  consumedRoomDrafts: consumed,
772
935
  });
773
936
  }
774
937
  catch {
775
- return { outcome: 'refused', rule: 'durable-write' };
938
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
939
+ }
940
+ for (const origin of withdrawn.keys())
941
+ pendings.delete(origin);
942
+ for (const [id, drafts] of drafted) {
943
+ pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
776
944
  }
777
- pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
778
- recorded = false;
945
+ if (!localSaves)
946
+ recorded = false;
779
947
  for (const path of consumed)
780
948
  forgetRoomDraft(locator, path);
781
- replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting(), replica));
949
+ replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
782
950
  publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
783
951
  queue: true,
784
952
  drafts: consumed,
785
953
  }));
786
- moveTo({ ...saving(status), ...attention() });
954
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
787
955
  submitLater(locator);
788
- return { outcome: 'saved', identifier: built.batch.id };
956
+ const identifier = join?.batch.id ??
957
+ built?.batch.id ??
958
+ [...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
959
+ return { outcome: 'saved', identifier };
789
960
  }
790
961
  async function mark(local, locator, identifier, submission, lost) {
791
962
  const state = pendings.get(identifier);
@@ -811,16 +982,15 @@ export function composeRuntime(options) {
811
982
  }
812
983
  async function readHistory(locator, path, query = {}) {
813
984
  if (closed || refusal !== null)
814
- return { kind: 'transport-uncertainty' };
985
+ return UNCERTAIN;
815
986
  try {
816
987
  await ensureAuthorization();
817
988
  }
818
989
  catch {
819
- return { kind: 'transport-uncertainty' };
820
- }
821
- if (closed || authorization === undefined || refusal !== null) {
822
- return { kind: 'transport-uncertainty' };
990
+ return UNCERTAIN;
823
991
  }
992
+ if (closed || authorization === undefined || refusal !== null)
993
+ return UNCERTAIN;
824
994
  const transfer = ['transfer', 'history', locator, path];
825
995
  if (query.revision !== undefined)
826
996
  transfer.push(query.revision);
@@ -842,17 +1012,101 @@ export function composeRuntime(options) {
842
1012
  }
843
1013
  if (result.kind === 'revoked-authorization')
844
1014
  await refuse('revoked-authorization');
845
- return result;
1015
+ return historyAnswer(result);
846
1016
  }
847
1017
  catch {
848
1018
  journal.record('outcome history transport-uncertainty');
849
- return { kind: 'transport-uncertainty' };
1019
+ return UNCERTAIN;
1020
+ }
1021
+ }
1022
+ function queuedBlob(locator, path, localId) {
1023
+ for (const batch of replicas.get(locator)?.queue ?? []) {
1024
+ const change = batch.changes[queuedFile(batch, path, localId)];
1025
+ if (change?.kind === 'binary')
1026
+ return change.blob;
1027
+ }
1028
+ return null;
1029
+ }
1030
+ async function readBinary(request) {
1031
+ if (closed || refusal !== null)
1032
+ return UNCERTAIN;
1033
+ try {
1034
+ await ensureAuthorization();
1035
+ }
1036
+ catch {
1037
+ return UNCERTAIN;
1038
+ }
1039
+ if (closed || authorization === undefined || refusal !== null)
1040
+ return UNCERTAIN;
1041
+ journal.record(request.kind === 'path'
1042
+ ? `transfer binary ${request.locator} ${request.path}`
1043
+ : `transfer binary ${request.fileRef}`);
1044
+ try {
1045
+ const result = await options.adapter.binary(authorization, request);
1046
+ journal.record(`outcome binary ${result.kind}`);
1047
+ if (result.kind === 'scoped-refusal') {
1048
+ const room = replicas.get(result.locator)?.confirmed.record;
1049
+ if (room !== undefined)
1050
+ await scoped(result, room, await openLocal());
1051
+ }
1052
+ if (result.kind === 'revoked-authorization')
1053
+ await refuse('revoked-authorization');
1054
+ return binaryAnswer(result);
850
1055
  }
1056
+ catch {
1057
+ journal.record('outcome binary transport-uncertainty');
1058
+ return UNCERTAIN;
1059
+ }
1060
+ }
1061
+ async function readBinaryPath(locator, path, revision) {
1062
+ await ready;
1063
+ if (revision !== undefined)
1064
+ return readBinary({ kind: 'path', locator, path, revision });
1065
+ const visible = replicas.get(locator)?.visible;
1066
+ const held = visible?.binaries[path];
1067
+ if (visible === undefined || held === undefined)
1068
+ return absentBinary(path);
1069
+ if (held.state === 'pending') {
1070
+ const blob = queuedBlob(locator, path, held.localId);
1071
+ return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1072
+ }
1073
+ const record = binaryRecord(path, held, visible.revisions[path]);
1074
+ if (record.state !== 'accepted')
1075
+ return absentBinary(path);
1076
+ const replica = replicas.get(locator);
1077
+ const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
1078
+ const holds = (at) => {
1079
+ const kept = replica?.confirmed.record.binaries[at];
1080
+ return (kept?.state === 'accepted' &&
1081
+ kept.fileRef === record.fileRef &&
1082
+ replica?.confirmed.record.revisions[at] === record.revision);
1083
+ };
1084
+ let confirmed = path;
1085
+ for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
1086
+ const change = queued[index];
1087
+ if (change.kind === 'move' && change.to === confirmed)
1088
+ confirmed = change.path;
1089
+ }
1090
+ return readBinary({
1091
+ kind: 'path',
1092
+ locator,
1093
+ path: confirmed,
1094
+ revision: record.revision,
1095
+ fileRef: record.fileRef,
1096
+ });
851
1097
  }
852
- function declaredCards() {
1098
+ async function readBinaryReference(reference) {
1099
+ await ready;
1100
+ return readBinary({
1101
+ kind: 'reference',
1102
+ fileRef: reference.fileRef,
1103
+ referrer: reference.referrer,
1104
+ });
1105
+ }
1106
+ function declaredAgents() {
853
1107
  for (const capability of capabilities) {
854
- if (capability.conversationCards !== undefined)
855
- return capability.conversationCards;
1108
+ if (capability.conversationAgents !== undefined)
1109
+ return capability.conversationAgents;
856
1110
  }
857
1111
  return undefined;
858
1112
  }
@@ -868,18 +1122,17 @@ export function composeRuntime(options) {
868
1122
  async function invokeAction(name, input) {
869
1123
  await ready;
870
1124
  if (closed || refusal !== null)
871
- return { kind: 'transport-uncertainty' };
1125
+ return UNCERTAIN;
872
1126
  if (declaredAction(name) === undefined)
873
- return { kind: 'undeclared' };
1127
+ return undeclared(name);
874
1128
  try {
875
1129
  await ensureAuthorization();
876
1130
  }
877
1131
  catch {
878
- return { kind: 'transport-uncertainty' };
879
- }
880
- if (closed || authorization === undefined || refusal !== null) {
881
- return { kind: 'transport-uncertainty' };
1132
+ return UNCERTAIN;
882
1133
  }
1134
+ if (closed || authorization === undefined || refusal !== null)
1135
+ return UNCERTAIN;
883
1136
  journal.record(`transfer action ${name}`);
884
1137
  try {
885
1138
  const result = await options.adapter.action(authorization, { name, input });
@@ -893,11 +1146,11 @@ export function composeRuntime(options) {
893
1146
  journal.record(`outcome action ${result.kind}`);
894
1147
  if (result.kind === 'rejected')
895
1148
  journal.record(`refusal rejected ${result.code}`);
896
- return result;
1149
+ return calledResult(result);
897
1150
  }
898
1151
  catch {
899
1152
  journal.record('outcome action transport-uncertainty');
900
- return { kind: 'transport-uncertainty' };
1153
+ return UNCERTAIN;
901
1154
  }
902
1155
  }
903
1156
  function continuing() {
@@ -928,14 +1181,33 @@ export function composeRuntime(options) {
928
1181
  throw error;
929
1182
  }
930
1183
  }
931
- async function pullBootstrap(authorized, room) {
1184
+ function staging(local) {
1185
+ return {
1186
+ held: (locator) => transfers.get(locator) ?? null,
1187
+ staged: (record) => local.staged(record),
1188
+ stage: (record, page) => exclusive(async () => {
1189
+ if (!continuing())
1190
+ return false;
1191
+ await local.stage(record, page);
1192
+ transfers.set(record.locator, record);
1193
+ return true;
1194
+ }),
1195
+ abandon: (locator) => exclusive(async () => {
1196
+ if (!continuing())
1197
+ return;
1198
+ await local.abandon(locator);
1199
+ transfers.delete(locator);
1200
+ }),
1201
+ };
1202
+ }
1203
+ async function pullBootstrap(authorized, room, local) {
932
1204
  if (!continuing())
933
1205
  return unconfirmed;
934
1206
  try {
935
1207
  const result = await bootstrapRoom(options.adapter, authorized, room, {
936
1208
  collection: modelOf(room.locator),
937
1209
  selection: modelOf(room.locator, 'typed-selection'),
938
- }, (models, held) => remodel(room.locator, models, held), continuing);
1210
+ }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
939
1211
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
940
1212
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
941
1213
  return result;
@@ -952,9 +1224,9 @@ export function composeRuntime(options) {
952
1224
  if (!continuing())
953
1225
  return unconfirmed;
954
1226
  if (refreshed.kind === 'still-current') {
955
- if (sourceRefusals.has(room.locator))
956
- return unconfirmed;
957
1227
  stale.delete(room.locator);
1228
+ if (sourceRefusals.has(room.locator))
1229
+ return null;
958
1230
  if (unreadable.delete(room.locator)) {
959
1231
  scopedRefusals.delete(room.locator);
960
1232
  list(room, true);
@@ -963,7 +1235,7 @@ export function composeRuntime(options) {
963
1235
  return null;
964
1236
  }
965
1237
  if (refreshed.kind === 'advanced') {
966
- if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
1238
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
967
1239
  const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
968
1240
  const reached = now !== undefined &&
969
1241
  options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
@@ -975,16 +1247,19 @@ export function composeRuntime(options) {
975
1247
  if (refreshed.kind !== 'beyond-horizon')
976
1248
  return refreshed;
977
1249
  }
978
- const bootstrapped = await pullBootstrap(authorized, room);
1250
+ const bootstrapped = await pullBootstrap(authorized, room, local);
979
1251
  if (!continuing())
980
1252
  return unconfirmed;
981
1253
  if (bootstrapped.kind !== 'staged')
982
1254
  return bootstrapped;
983
- await publish(local, room, bootstrapped.confirmed);
1255
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
984
1256
  return null;
985
1257
  }
986
1258
  async function withhold(withheld, room, local, progress) {
987
- stale.add(room.locator);
1259
+ if (withheld.kind === 'refused')
1260
+ stale.delete(room.locator);
1261
+ else
1262
+ stale.add(room.locator);
988
1263
  if (withheld.kind === 'scoped-refusal') {
989
1264
  await scoped(withheld, room, local);
990
1265
  }
@@ -1009,6 +1284,7 @@ export function composeRuntime(options) {
1009
1284
  function refusedSource(room, { refusal }) {
1010
1285
  sourceRefusals.set(room.locator, {
1011
1286
  code: refusal.rule,
1287
+ retryable: false,
1012
1288
  fields: {
1013
1289
  file: refusal.file,
1014
1290
  ...(refusal.location === undefined ? {} : { location: refusal.location }),
@@ -1023,8 +1299,16 @@ export function composeRuntime(options) {
1023
1299
  }
1024
1300
  function leaving(batch, result) {
1025
1301
  if (result.kind === 'expired')
1026
- return { batch, returned: { kind: 'expired' } };
1027
- return { batch, returned: { kind: result.kind, code: result.code, fields: result.fields } };
1302
+ return { batch, returned: returnedBy('expired') };
1303
+ return {
1304
+ batch,
1305
+ returned: {
1306
+ kind: result.kind,
1307
+ code: result.code,
1308
+ fields: result.fields,
1309
+ retryable: result.retryable,
1310
+ },
1311
+ };
1028
1312
  }
1029
1313
  function logAccepted(locator, submitted, accepted) {
1030
1314
  journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
@@ -1042,7 +1326,7 @@ export function composeRuntime(options) {
1042
1326
  logAccepted(room.locator, submitted, accepted);
1043
1327
  const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1044
1328
  ? { kind: 'staged', confirmed: current.confirmed }
1045
- : applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
1329
+ : applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
1046
1330
  ? accepted.roomVersion
1047
1331
  : held, accepted.roomVersion);
1048
1332
  if (applied.kind !== 'staged') {
@@ -1188,7 +1472,8 @@ export function composeRuntime(options) {
1188
1472
  return;
1189
1473
  }
1190
1474
  const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1191
- leaving: result.kind === 'expired' && landed(head, current.confirmed)
1475
+ leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1476
+ (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1192
1477
  ? { ...left, returned: null }
1193
1478
  : left,
1194
1479
  });
@@ -1249,7 +1534,7 @@ export function composeRuntime(options) {
1249
1534
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1250
1535
  };
1251
1536
  const reclassified = sameMembership(declared, held.confirmed.record)
1252
- ? readConfirmed(declared, held.confirmed)
1537
+ ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
1253
1538
  : readReclassified(declared, held.confirmed);
1254
1539
  const record = reclassified.kind === 'staged'
1255
1540
  ? declared
@@ -1257,14 +1542,12 @@ export function composeRuntime(options) {
1257
1542
  ...declared,
1258
1543
  typedSelection: held.confirmed.record.typedSelection,
1259
1544
  };
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
- });
1545
+ const read = reclassified.kind === 'deferred'
1546
+ ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
1547
+ : reclassified;
1548
+ await local.write({ next: record, previous: held.confirmed.record });
1266
1549
  if (read.kind === 'staged') {
1267
- replicas.set(room.locator, replicaOf(read.confirmed, held.queue, rewriting(), held));
1550
+ replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1268
1551
  if (declarationRefusals.delete(room.locator)) {
1269
1552
  sourceRefusals.delete(room.locator);
1270
1553
  unreadable.delete(room.locator);
@@ -1289,7 +1572,11 @@ export function composeRuntime(options) {
1289
1572
  });
1290
1573
  }
1291
1574
  async function scoped(result, room, local) {
1292
- scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
1575
+ scopedRefusals.set(room.locator, {
1576
+ code: result.code,
1577
+ fields: result.fields,
1578
+ retryable: result.retryable,
1579
+ });
1293
1580
  let scopedAway = room;
1294
1581
  if (result.scope === 'model')
1295
1582
  await retainModel(local, room, 'collection', {
@@ -1326,7 +1613,6 @@ export function composeRuntime(options) {
1326
1613
  roomVersion: record.roomVersion,
1327
1614
  documents: record.documents,
1328
1615
  revisions: record.revisions,
1329
- attachments: record.attachments,
1330
1616
  },
1331
1617
  ];
1332
1618
  });
@@ -1337,8 +1623,10 @@ export function composeRuntime(options) {
1337
1623
  const outcome = prepared[index];
1338
1624
  return outcome?.status === 'fulfilled' && outcome.value;
1339
1625
  });
1340
- if (attempt === 'opened')
1626
+ if (attempt === 'opened') {
1341
1627
  options.adapter.roomStream?.hold(heldRooms(ready));
1628
+ handed = true;
1629
+ }
1342
1630
  const loaded = await Promise.allSettled(ready
1343
1631
  .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1344
1632
  .map((room) => loadRoom(room, local, progress)));
@@ -1404,14 +1692,14 @@ export function composeRuntime(options) {
1404
1692
  async function reload(room, local, progress) {
1405
1693
  if (discoveryPaused || authorization === undefined)
1406
1694
  return;
1407
- const bootstrapped = await pullBootstrap(authorization, room);
1695
+ const bootstrapped = await pullBootstrap(authorization, room, local);
1408
1696
  if (!continuing())
1409
1697
  return;
1410
1698
  if (bootstrapped.kind !== 'staged') {
1411
1699
  await withhold(bootstrapped, room, local, progress);
1412
1700
  return;
1413
1701
  }
1414
- await publish(local, room, bootstrapped.confirmed);
1702
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1415
1703
  await caughtUp(room, local, progress);
1416
1704
  }
1417
1705
  async function discoverRooms(local, progress, attempt) {
@@ -1441,15 +1729,16 @@ export function composeRuntime(options) {
1441
1729
  }
1442
1730
  progress.reachable = false;
1443
1731
  progress.complete = false;
1444
- if (attempt === 'opened') {
1445
- options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1446
- }
1447
1732
  notifyReady();
1448
1733
  return;
1449
1734
  }
1450
1735
  if (result.kind === 'contract-unavailable') {
1451
1736
  journal.record(`connection contract unavailable ${result.code}`);
1452
- await refuse('missing-required-guarantee', { code: result.code, fields: result.fields });
1737
+ await refuse('missing-required-guarantee', {
1738
+ code: result.code,
1739
+ fields: result.fields,
1740
+ retryable: result.retryable,
1741
+ });
1453
1742
  notifyReady();
1454
1743
  return;
1455
1744
  }
@@ -1458,17 +1747,6 @@ export function composeRuntime(options) {
1458
1747
  notifyReady();
1459
1748
  return;
1460
1749
  }
1461
- if (!result.supportedVersions.includes(ROOM_CONTRACT_VERSION)) {
1462
- await refuse('no-matching-version');
1463
- notifyReady();
1464
- return;
1465
- }
1466
- if (result.mdModel.identity !== MD_MODEL.identity ||
1467
- result.mdModel.version !== MD_MODEL.version) {
1468
- await refuse('md-model-version-mismatch');
1469
- notifyReady();
1470
- return;
1471
- }
1472
1750
  for (const room of result.rooms) {
1473
1751
  if (room.operations === undefined ||
1474
1752
  !['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
@@ -1489,14 +1767,6 @@ export function composeRuntime(options) {
1489
1767
  return;
1490
1768
  }
1491
1769
  }
1492
- const declared = new Set(result.capabilities.map((capability) => capability.name));
1493
- for (const required of options.requiredGuarantees) {
1494
- if (!knownCapabilities.has(required) || !declared.has(required)) {
1495
- await refuse('missing-required-guarantee');
1496
- notifyReady();
1497
- return;
1498
- }
1499
- }
1500
1770
  if (refusal !== null) {
1501
1771
  refusal = null;
1502
1772
  emitRefusal();
@@ -1509,6 +1779,12 @@ export function composeRuntime(options) {
1509
1779
  limits = result.providerLimits;
1510
1780
  await local.declare(limits, capabilities);
1511
1781
  }
1782
+ const signedIn = result.member ?? null;
1783
+ if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
1784
+ await local.keepMember(signedIn);
1785
+ member = signedIn;
1786
+ emitMember();
1787
+ }
1512
1788
  await applyDiscovery(result.rooms, local);
1513
1789
  await synchronize(result.rooms, local, progress, attempt);
1514
1790
  }
@@ -1538,9 +1814,12 @@ export function composeRuntime(options) {
1538
1814
  }
1539
1815
  const progress = { complete: true, reachable: true };
1540
1816
  underway = attempt === 'opened' ? progress : null;
1817
+ if (attempt === 'opened')
1818
+ streamed = progress;
1541
1819
  complete = false;
1542
1820
  recorded = false;
1543
1821
  moveTo(transferring(status));
1822
+ handed = false;
1544
1823
  try {
1545
1824
  const local = await openLocal();
1546
1825
  if (closed)
@@ -1552,16 +1831,22 @@ export function composeRuntime(options) {
1552
1831
  }
1553
1832
  catch {
1554
1833
  progress.complete = false;
1555
- if (attempt === 'opened') {
1556
- options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1557
- }
1834
+ if (attempt === 'opened' && !handed)
1835
+ progress.reachable = false;
1558
1836
  }
1559
1837
  finally {
1560
1838
  notifyReady();
1561
1839
  }
1562
- if (attempt === 'opened')
1840
+ if (attempt !== 'opened') {
1841
+ await finishOpening(progress);
1563
1842
  return;
1564
- await finishOpening(progress);
1843
+ }
1844
+ if (handed || closed || progress.reachable)
1845
+ return;
1846
+ underway = null;
1847
+ journal.record('connection reopen');
1848
+ await rest(progress);
1849
+ options.adapter.roomStream?.reopen();
1565
1850
  });
1566
1851
  }
1567
1852
  function closedSocket(revoked) {
@@ -1571,9 +1856,14 @@ export function composeRuntime(options) {
1571
1856
  void refuse('revoked-authorization');
1572
1857
  return;
1573
1858
  }
1574
- if (opened || status.providerUnreachable)
1575
- return;
1576
- moveTo({ ...status, providerUnreachable: true });
1859
+ complete = false;
1860
+ if (streamed !== null)
1861
+ streamed.complete = false;
1862
+ moveTo({
1863
+ ...status,
1864
+ state: status.state === 'synced' ? 'syncing' : status.state,
1865
+ providerUnreachable: status.providerUnreachable || !opened,
1866
+ });
1577
1867
  }
1578
1868
  function applyOpening(expired) {
1579
1869
  return task(async () => {
@@ -1628,6 +1918,8 @@ export function composeRuntime(options) {
1628
1918
  }
1629
1919
  }
1630
1920
  function submitLater(locator) {
1921
+ if (localSaves)
1922
+ return;
1631
1923
  const running = submitting.get(locator);
1632
1924
  if (running !== undefined) {
1633
1925
  running.again = true;
@@ -1707,7 +1999,7 @@ export function composeRuntime(options) {
1707
1999
  }
1708
2000
  }
1709
2001
  moveTo({
1710
- state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
2002
+ state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
1711
2003
  lastCompleted,
1712
2004
  providerUnreachable: !progress.reachable,
1713
2005
  ...attention(),
@@ -1791,7 +2083,10 @@ export function composeRuntime(options) {
1791
2083
  if (stream === undefined || withdrawConversations !== undefined)
1792
2084
  return;
1793
2085
  withdrawConversations = stream.observe((event) => {
1794
- conversations.receive(event);
2086
+ if (conversations.receive(event)) {
2087
+ void ready.then(scheduleCurrency);
2088
+ return;
2089
+ }
1795
2090
  if (event.kind !== 'gap' || conversations.current())
1796
2091
  return;
1797
2092
  show();
@@ -1813,20 +2108,15 @@ export function composeRuntime(options) {
1813
2108
  show();
1814
2109
  return;
1815
2110
  }
1816
- const resting = status;
1817
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1818
- moveTo(transferring(resting));
2111
+ const progress = { complete: false, reachable: !status.providerUnreachable };
2112
+ moveTo(transferring(status));
1819
2113
  try {
1820
2114
  await conversations.catchUp(progress);
1821
2115
  }
1822
2116
  catch {
1823
2117
  progress.reachable = false;
1824
2118
  }
1825
- finally {
1826
- if (!closed) {
1827
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1828
- }
1829
- }
2119
+ await rest(progress);
1830
2120
  }
1831
2121
  function close() {
1832
2122
  if (closed)
@@ -1848,6 +2138,7 @@ export function composeRuntime(options) {
1848
2138
  storageLossListeners.clear();
1849
2139
  statusListeners.clear();
1850
2140
  serviceModeListeners.clear();
2141
+ memberListeners.clear();
1851
2142
  readSide.close();
1852
2143
  queueSubscriptions.clear();
1853
2144
  roomDraftListeners.clear();
@@ -1879,13 +2170,16 @@ export function composeRuntime(options) {
1879
2170
  return planOf(input, roomFacts(reach), planReach(reach));
1880
2171
  }
1881
2172
  function watchPlan(plan, deliver) {
2173
+ materialize(reachedRooms(plan));
1882
2174
  return readSide.watch(plan, identityOf(plan), deliver);
1883
2175
  }
1884
2176
  async function searched(input, reach) {
1885
2177
  const plan = searchPlanOf(input);
1886
2178
  const selected = plan.documents === undefined && reach === null
1887
2179
  ? null
1888
- : (await readSide.query(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
2180
+ : (await rowsQuery(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
2181
+ if (selected === null)
2182
+ materialize(null);
1889
2183
  return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
1890
2184
  }
1891
2185
  const watch = (topic, input, deliver) => {
@@ -1909,6 +2203,10 @@ export function composeRuntime(options) {
1909
2203
  if (topic === 'conversation') {
1910
2204
  return conversations.watch(input[0], input[1], deliver);
1911
2205
  }
2206
+ if (topic === 'conversations')
2207
+ return conversations.watchList(deliver);
2208
+ if (topic === 'agents')
2209
+ return conversations.watchAgents(deliver);
1912
2210
  if (topic === 'presentation') {
1913
2211
  const listener = {
1914
2212
  app: input[0],
@@ -1963,6 +2261,13 @@ export function composeRuntime(options) {
1963
2261
  serviceModeListeners.delete(deliver);
1964
2262
  };
1965
2263
  }
2264
+ if (topic === 'member') {
2265
+ memberListeners.add(deliver);
2266
+ deliver(member);
2267
+ return () => {
2268
+ memberListeners.delete(deliver);
2269
+ };
2270
+ }
1966
2271
  if (topic === 'queue') {
1967
2272
  const queue = { locator: input[0], notify: deliver };
1968
2273
  queueSubscriptions.add(queue);
@@ -1980,7 +2285,7 @@ export function composeRuntime(options) {
1980
2285
  rooms() {
1981
2286
  return listed();
1982
2287
  },
1983
- query: ((input) => readSide.query(planned(input, null))),
2288
+ query: ((input) => rowsQuery(planned(input, null))),
1984
2289
  search: (input) => searched(input, null),
1985
2290
  types(locator) {
1986
2291
  return {
@@ -2009,20 +2314,20 @@ export function composeRuntime(options) {
2009
2314
  return exclusive(async () => {
2010
2315
  const parts = readParts(input);
2011
2316
  if (path === '' || normalize(path) !== path)
2012
- throw new Error('room-draft-malformed');
2317
+ throw runtimeError('room-draft-malformed');
2013
2318
  const local = await openLocal();
2014
2319
  const current = roomDrafts.get(room)?.get(path);
2015
2320
  if ((current?.revision ?? null) !== revision) {
2016
- return {
2017
- outcome: 'stale',
2018
- current: current === undefined ? null : roomDraftOf(current),
2019
- };
2321
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2020
2322
  }
2323
+ const file = parts.file === undefined
2324
+ ? undefined
2325
+ : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
2021
2326
  const record = {
2022
2327
  locator: room,
2023
2328
  path,
2024
2329
  revision: roomDraftRevision(),
2025
- parts: held(current?.parts, parts),
2330
+ parts: held(current?.parts, parts, file),
2026
2331
  returned: current?.returned ?? null,
2027
2332
  };
2028
2333
  await local.keepRoomDraft(record);
@@ -2036,10 +2341,7 @@ export function composeRuntime(options) {
2036
2341
  const local = await openLocal();
2037
2342
  const current = roomDrafts.get(room)?.get(path);
2038
2343
  if (current === undefined || current.revision !== revision) {
2039
- return {
2040
- outcome: 'stale',
2041
- current: current === undefined ? null : roomDraftOf(current),
2042
- };
2344
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2043
2345
  }
2044
2346
  await local.discardRoomDraft(room, path);
2045
2347
  forgetRoomDraft(room, path);
@@ -2074,6 +2376,10 @@ export function composeRuntime(options) {
2074
2376
  history(locator, path, query) {
2075
2377
  return readHistory(locator, path, query);
2076
2378
  },
2379
+ binaryFile: {
2380
+ read: readBinaryPath,
2381
+ readReference: readBinaryReference,
2382
+ },
2077
2383
  call(name, input) {
2078
2384
  return invokeAction(name, input);
2079
2385
  },
@@ -2106,8 +2412,9 @@ export function composeRuntime(options) {
2106
2412
  const reachable = {
2107
2413
  md,
2108
2414
  watch,
2415
+ referrerScope: (referrer) => options.adapter.referrerScope(referrer),
2109
2416
  plan: planned,
2110
- query: (plan) => readSide.query(plan),
2417
+ query: (plan) => rowsQuery(plan),
2111
2418
  watchPlan,
2112
2419
  search: searched,
2113
2420
  };