@agentshouse/mdruntime 0.1.0-c1cc928d1d256 → 0.1.0-c2aaa9c0e3ca4

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 (163) hide show
  1. package/AGENTS.md +147 -101
  2. package/README.md +13 -3
  3. package/dist/adapter.d.ts +42 -14
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/attempt.d.ts +16 -0
  6. package/dist/attempt.d.ts.map +1 -0
  7. package/dist/attempt.js +57 -0
  8. package/dist/binary.d.ts +23 -0
  9. package/dist/binary.d.ts.map +1 -0
  10. package/dist/binary.js +8 -0
  11. package/dist/bootstrap.d.ts +17 -5
  12. package/dist/bootstrap.d.ts.map +1 -1
  13. package/dist/bootstrap.js +88 -38
  14. package/dist/change-set.d.ts.map +1 -1
  15. package/dist/change-set.js +19 -12
  16. package/dist/change.d.ts +15 -0
  17. package/dist/change.d.ts.map +1 -1
  18. package/dist/change.js +26 -1
  19. package/dist/chat/acts.d.ts +34 -0
  20. package/dist/chat/acts.d.ts.map +1 -0
  21. package/dist/chat/acts.js +36 -0
  22. package/dist/chat/context.d.ts +3 -0
  23. package/dist/chat/context.d.ts.map +1 -0
  24. package/dist/chat/context.js +10 -0
  25. package/dist/chat/input.d.ts +9 -0
  26. package/dist/chat/input.d.ts.map +1 -0
  27. package/dist/chat/input.js +99 -0
  28. package/dist/chat/kit.d.ts +60 -0
  29. package/dist/chat/kit.d.ts.map +1 -0
  30. package/dist/chat/kit.js +123 -0
  31. package/dist/chat/lasting.d.ts +6 -0
  32. package/dist/chat/lasting.d.ts.map +1 -0
  33. package/dist/chat/lasting.js +19 -0
  34. package/dist/chat/list.d.ts +20 -0
  35. package/dist/chat/list.d.ts.map +1 -0
  36. package/dist/chat/list.js +59 -0
  37. package/dist/chat/markdown.d.ts +19 -0
  38. package/dist/chat/markdown.d.ts.map +1 -0
  39. package/dist/chat/markdown.js +187 -0
  40. package/dist/chat/message.d.ts +6 -0
  41. package/dist/chat/message.d.ts.map +1 -0
  42. package/dist/chat/message.js +192 -0
  43. package/dist/chat/messages.d.ts +21 -0
  44. package/dist/chat/messages.d.ts.map +1 -0
  45. package/dist/chat/messages.js +151 -0
  46. package/dist/chat/name.d.ts +6 -0
  47. package/dist/chat/name.d.ts.map +1 -0
  48. package/dist/chat/name.js +37 -0
  49. package/dist/chat/opened.d.ts +11 -0
  50. package/dist/chat/opened.d.ts.map +1 -0
  51. package/dist/chat/opened.js +17 -0
  52. package/dist/chat/overlay.d.ts +31 -0
  53. package/dist/chat/overlay.d.ts.map +1 -0
  54. package/dist/chat/overlay.js +97 -0
  55. package/dist/chat/parts.d.ts +57 -0
  56. package/dist/chat/parts.d.ts.map +1 -0
  57. package/dist/chat/parts.js +119 -0
  58. package/dist/chat/process.d.ts +20 -0
  59. package/dist/chat/process.d.ts.map +1 -0
  60. package/dist/chat/process.js +32 -0
  61. package/dist/chat/provider.d.ts +63 -0
  62. package/dist/chat/provider.d.ts.map +1 -0
  63. package/dist/chat/provider.js +645 -0
  64. package/dist/chat/question.d.ts +9 -0
  65. package/dist/chat/question.d.ts.map +1 -0
  66. package/dist/chat/question.js +80 -0
  67. package/dist/chat/questions.d.ts +31 -0
  68. package/dist/chat/questions.d.ts.map +1 -0
  69. package/dist/chat/questions.js +111 -0
  70. package/dist/chat/read.d.ts +5 -0
  71. package/dist/chat/read.d.ts.map +1 -0
  72. package/dist/chat/read.js +13 -0
  73. package/dist/chat/ready.d.ts +33 -0
  74. package/dist/chat/ready.d.ts.map +1 -0
  75. package/dist/chat/ready.js +91 -0
  76. package/dist/chat/style.d.ts +4 -0
  77. package/dist/chat/style.d.ts.map +1 -0
  78. package/dist/chat/style.js +302 -0
  79. package/dist/chat.d.ts +14 -0
  80. package/dist/chat.d.ts.map +1 -0
  81. package/dist/chat.js +8 -0
  82. package/dist/client.d.ts +12 -5
  83. package/dist/client.d.ts.map +1 -1
  84. package/dist/client.js +30 -15
  85. package/dist/collection.d.ts +6 -11
  86. package/dist/collection.d.ts.map +1 -1
  87. package/dist/collection.js +56 -19
  88. package/dist/connection.d.ts +20 -9
  89. package/dist/connection.d.ts.map +1 -1
  90. package/dist/connection.js +422 -131
  91. package/dist/conversation.d.ts +11 -3
  92. package/dist/conversation.d.ts.map +1 -1
  93. package/dist/conversation.js +47 -7
  94. package/dist/expectation.d.ts +21 -0
  95. package/dist/expectation.d.ts.map +1 -0
  96. package/dist/expectation.js +70 -0
  97. package/dist/failure.d.ts +13 -0
  98. package/dist/failure.d.ts.map +1 -0
  99. package/dist/failure.js +19 -0
  100. package/dist/host.d.ts +1 -1
  101. package/dist/host.d.ts.map +1 -1
  102. package/dist/host.js +9 -9
  103. package/dist/identity.d.ts +3 -0
  104. package/dist/identity.d.ts.map +1 -1
  105. package/dist/identity.js +13 -1
  106. package/dist/index.d.ts +9 -4
  107. package/dist/index.d.ts.map +1 -1
  108. package/dist/index.js +3 -0
  109. package/dist/memory.d.ts +3 -3
  110. package/dist/memory.d.ts.map +1 -1
  111. package/dist/memory.js +53 -20
  112. package/dist/model.d.ts +3 -2
  113. package/dist/model.d.ts.map +1 -1
  114. package/dist/open.d.ts +2 -0
  115. package/dist/open.d.ts.map +1 -0
  116. package/dist/open.js +1 -0
  117. package/dist/outcome.d.ts +28 -3
  118. package/dist/outcome.d.ts.map +1 -1
  119. package/dist/outcome.js +41 -1
  120. package/dist/overlay.d.ts +14 -3
  121. package/dist/overlay.d.ts.map +1 -1
  122. package/dist/overlay.js +75 -37
  123. package/dist/query.d.ts +4 -0
  124. package/dist/query.d.ts.map +1 -1
  125. package/dist/query.js +3 -0
  126. package/dist/reach.d.ts +2 -0
  127. package/dist/reach.d.ts.map +1 -1
  128. package/dist/reach.js +43 -19
  129. package/dist/react.d.ts +4 -2
  130. package/dist/react.d.ts.map +1 -1
  131. package/dist/react.js +3 -2
  132. package/dist/read-side.d.ts.map +1 -1
  133. package/dist/read-side.js +9 -2
  134. package/dist/refresh.d.ts +10 -0
  135. package/dist/refresh.d.ts.map +1 -1
  136. package/dist/refresh.js +42 -38
  137. package/dist/replica.d.ts +7 -4
  138. package/dist/replica.d.ts.map +1 -1
  139. package/dist/replica.js +74 -54
  140. package/dist/room-draft.d.ts +26 -9
  141. package/dist/room-draft.d.ts.map +1 -1
  142. package/dist/room-draft.js +132 -23
  143. package/dist/rows.d.ts +5 -1
  144. package/dist/rows.d.ts.map +1 -1
  145. package/dist/rows.js +10 -2
  146. package/dist/save.d.ts +27 -1
  147. package/dist/save.d.ts.map +1 -1
  148. package/dist/save.js +340 -33
  149. package/dist/search.d.ts +3 -2
  150. package/dist/search.d.ts.map +1 -1
  151. package/dist/status.d.ts +2 -2
  152. package/dist/status.d.ts.map +1 -1
  153. package/dist/status.js +0 -8
  154. package/dist/store.d.ts +31 -10
  155. package/dist/store.d.ts.map +1 -1
  156. package/dist/store.js +261 -163
  157. package/dist/wire.d.ts +6 -3
  158. package/dist/wire.d.ts.map +1 -1
  159. package/dist/wire.js +14 -2
  160. package/package.json +22 -14
  161. package/dist/attachment.d.ts +0 -3
  162. package/dist/attachment.d.ts.map +0 -1
  163. package/dist/attachment.js +0 -8
@@ -3,17 +3,22 @@ import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
3
3
  import { observing } from './observation.js';
4
4
  import { filterOf, identityOf, planOf, } from './query.js';
5
5
  import { createReadSide } from './read-side.js';
6
+ import { markdownPath, roomOfKey } from './rows.js';
6
7
  import { createSearchIndex, searchPlanOf, } from './search.js';
7
8
  import { bootstrapRoom } from './bootstrap.js';
8
9
  import { changeSet, mergeChangeSets } from './change-set.js';
9
10
  import { createConversationStore, } from './conversation.js';
10
- import { parseCollection, validRoot } from './collection.js';
11
+ import { queuedFile } from './change.js';
12
+ import { validRoot } from './collection.js';
13
+ import { runtimeError } from './failure.js';
11
14
  import { createLog } from './log.js';
15
+ import { binaryRecord } from './binary.js';
16
+ import { absentBinary, binaryAnswer, calledResult, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
12
17
  import { refreshRoom } from './refresh.js';
13
- import { buildBatch } from './save.js';
14
- import { applyReturned, compositionOf, landed, readConfirmed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
15
- import { changedParts, changesOf, held, readParts, returnedInto, underneath, } from './room-draft.js';
16
- import { settled, same, saving, STARTING, transferring, } from './status.js';
18
+ import { buildBatch, joined, limitedIn, requeued } from './save.js';
19
+ import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
20
+ import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
21
+ import { same, saving, STARTING, transferring, } from './status.js';
17
22
  import { detectStorageLoss, nextGeneration } from './durability.js';
18
23
  import { roomDraftRevision } from './identity.js';
19
24
  import { beneath, normalize } from './paths.js';
@@ -34,16 +39,19 @@ function declaredSelection(room) {
34
39
  selection.roots.every((other, at) => at === index || !within(other, root)));
35
40
  }
36
41
  export function composeRuntime(options) {
42
+ const localSaves = options.adapter.localSaves === true;
37
43
  let authorization = options.authorization;
38
44
  let opening;
39
45
  let store;
40
46
  const replicas = new Map();
47
+ const queuesWithoutReplica = new Map();
41
48
  const pendings = new Map();
42
49
  const roomDrafts = new Map();
43
50
  const presentations = new Map();
44
51
  let limits = [];
45
52
  let capabilities = [];
46
53
  const rooms = new Map();
54
+ const counted = new Map();
47
55
  const unreadable = new Set();
48
56
  const models = new Map();
49
57
  const liveModels = new Map();
@@ -82,6 +90,7 @@ export function composeRuntime(options) {
82
90
  let withdrawStream;
83
91
  const announced = new Map();
84
92
  let underway = null;
93
+ let streamed = null;
85
94
  let handed = false;
86
95
  const submitting = new Map();
87
96
  const answering = new Map();
@@ -107,9 +116,12 @@ export function composeRuntime(options) {
107
116
  const serviceModeListeners = new Set();
108
117
  let member = null;
109
118
  const memberListeners = new Set();
119
+ const materialized = new Set();
120
+ let everyRoom = false;
121
+ const transfers = new Map();
110
122
  const conversations = createConversationStore({
111
123
  agents: declaredAgents,
112
- documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
124
+ documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
113
125
  call: (name, input) => invokeAction(name, input),
114
126
  durably,
115
127
  stream: () => options.adapter.conversations,
@@ -185,7 +197,9 @@ export function composeRuntime(options) {
185
197
  }
186
198
  }
187
199
  function pending() {
188
- 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)));
189
203
  }
190
204
  function attention() {
191
205
  const attentionRequired = [];
@@ -214,8 +228,24 @@ export function composeRuntime(options) {
214
228
  }
215
229
  return { attentionRequired, reset };
216
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
+ }
217
243
  function listed() {
218
- 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
+ }));
219
249
  }
220
250
  function roomFacts(reach = null) {
221
251
  const held = [...rooms.values()]
@@ -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);
@@ -342,10 +406,7 @@ export function composeRuntime(options) {
342
406
  const revoked = { status: 'unavailable', reason: 'revoked' };
343
407
  try {
344
408
  const local = await openLocal();
345
- await local.revokeModels([...replicas.values()].map((replica) => ({
346
- record: replica.confirmed.record,
347
- queue: replica.queue,
348
- })));
409
+ await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
349
410
  }
350
411
  catch {
351
412
  journal.record('model revocation persistence failed');
@@ -369,10 +430,7 @@ export function composeRuntime(options) {
369
430
  model: revoked,
370
431
  selectionModel: revoked,
371
432
  };
372
- const read = readConfirmed(record, before.confirmed);
373
- replicas.set(locator, read.kind === 'staged'
374
- ? replicaOf(read.confirmed, before.queue, before)
375
- : { ...before, confirmed: { ...before.confirmed, record } });
433
+ replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
376
434
  }
377
435
  rooms.set(locator, {
378
436
  ...room,
@@ -389,7 +447,8 @@ export function composeRuntime(options) {
389
447
  }
390
448
  finally {
391
449
  refusal = {
392
- reason,
450
+ code: reason,
451
+ retryable: false,
393
452
  reauthorizationOffered: reason === 'revoked-authorization',
394
453
  refusal: provider,
395
454
  };
@@ -414,7 +473,6 @@ export function composeRuntime(options) {
414
473
  },
415
474
  providerOwnedPaths: room.providerOwnedPaths,
416
475
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
417
- attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
418
476
  synchronizationAvailable,
419
477
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
420
478
  });
@@ -439,7 +497,14 @@ export function composeRuntime(options) {
439
497
  emitRooms();
440
498
  }
441
499
  function unlistedHeld(listed) {
442
- 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));
443
508
  }
444
509
  async function dropUnlisted(listed, local) {
445
510
  if (unlistedHeld(listed).length === 0)
@@ -453,11 +518,13 @@ export function composeRuntime(options) {
453
518
  drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
454
519
  }));
455
520
  for (const locator of unlisted) {
456
- for (const batch of replicas.get(locator)?.queue ?? [])
521
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
457
522
  pendings.delete(batch.id);
458
523
  replicas.delete(locator);
524
+ queuesWithoutReplica.delete(locator);
459
525
  rooms.delete(locator);
460
526
  roomDrafts.delete(locator);
527
+ transfers.delete(locator);
461
528
  unreadable.delete(locator);
462
529
  stale.delete(locator);
463
530
  scopedRefusals.delete(locator);
@@ -494,7 +561,7 @@ export function composeRuntime(options) {
494
561
  return model;
495
562
  }
496
563
  async function hydrate(opened) {
497
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known,] = await Promise.all([
564
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
498
565
  opened.all(),
499
566
  opened.queues(),
500
567
  opened.roomDrafts(),
@@ -505,7 +572,11 @@ export function composeRuntime(options) {
505
572
  opened.providerLimits(),
506
573
  opened.capabilities(),
507
574
  opened.member(),
575
+ opened.transfers(),
508
576
  ]);
577
+ transfers.clear();
578
+ for (const record of staged)
579
+ transfers.set(record.locator, record);
509
580
  limits = declared;
510
581
  capabilities = named;
511
582
  member = known;
@@ -524,18 +595,17 @@ export function composeRuntime(options) {
524
595
  }
525
596
  const previousReplicas = new Map(replicas);
526
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
+ }
527
604
  for (const record of records) {
528
605
  const model = restored(modelKey(record.locator, 'collection'), record.model);
529
606
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
530
607
  const prior = previousReplicas.get(record.locator);
531
- const retyped = { ...record, model, selectionModel };
532
- const read = readConfirmed(retyped, prior?.confirmed);
533
- const confirmed = read.kind === 'staged'
534
- ? read.confirmed
535
- : {
536
- record: retyped,
537
- collection: parseCollection(retyped.documents, compositionOf(retyped)),
538
- };
608
+ const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
539
609
  replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
540
610
  const existing = rooms.get(record.locator);
541
611
  if (existing === undefined)
@@ -544,7 +614,6 @@ export function composeRuntime(options) {
544
614
  rooms.set(record.locator, {
545
615
  ...existing,
546
616
  roomVersion: record.roomVersion,
547
- attachments: record.attachments,
548
617
  operations: record.operations,
549
618
  replica: record.replica,
550
619
  model: modelTypes(model),
@@ -556,10 +625,6 @@ export function composeRuntime(options) {
556
625
  },
557
626
  });
558
627
  }
559
- if (read.kind === 'refused') {
560
- refusedSource(record, read);
561
- declarationRefusals.add(record.locator);
562
- }
563
628
  }
564
629
  return {
565
630
  lastCompleted,
@@ -621,11 +686,12 @@ export function composeRuntime(options) {
621
686
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
622
687
  }
623
688
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
624
- const withdrawn = withdrawals(confirmed, queue, withdrawable);
689
+ const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
690
+ const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
625
691
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
626
692
  const unqueued = withdrawn.map((batch) => ({
627
693
  batch,
628
- returned: landed(batch, confirmed) ? null : { kind: 'withdrawn' },
694
+ returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
629
695
  }));
630
696
  const returning = [
631
697
  ...[...unqueued].reverse(),
@@ -646,26 +712,44 @@ export function composeRuntime(options) {
646
712
  });
647
713
  }
648
714
  }
649
- await local.write({
650
- next: confirmed.record,
651
- previous: before?.confirmed.record ?? null,
652
- queue: kept,
653
- previousQueue: before?.queue ?? [],
654
- roomDrafts: [...written.values()],
655
- });
656
- for (const batch of before?.queue ?? []) {
715
+ await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
716
+ for (const batch of previousQueue) {
657
717
  if (!kept.includes(batch))
658
718
  pendings.delete(batch.id);
659
719
  }
660
720
  for (const record of written.values())
661
721
  rememberRoomDraft(record);
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);
662
745
  for (const { batch, returned } of unqueued) {
663
- journal.record(`outcome ${locator} ${batch.id} ${returned === null ? 'landed' : 'withdrawn'}`);
746
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
747
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
664
748
  }
665
749
  replicas.set(locator, replicaOf(confirmed, kept, before));
666
750
  return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
667
751
  room: settling.room,
668
- queue: kept.length !== (before?.queue.length ?? 0),
752
+ queue: kept.length !== previousQueue.length,
669
753
  drafts: [...written.keys()],
670
754
  });
671
755
  }
@@ -700,7 +784,8 @@ export function composeRuntime(options) {
700
784
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
701
785
  };
702
786
  }
703
- function publish(local, room, confirmed, base) {
787
+ function publish(local, room, confirmed, landing = {}) {
788
+ const { base, stored } = landing;
704
789
  return exclusive(async () => {
705
790
  if (!continuing())
706
791
  return true;
@@ -711,13 +796,15 @@ export function composeRuntime(options) {
711
796
  return false;
712
797
  }
713
798
  adoptModels(room.locator, confirmed.record);
714
- const queue = prior?.queue ?? [];
799
+ const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
715
800
  const roomVersion = confirmed.record.roomVersion;
716
801
  const accepted = queue.filter((batch) => {
717
802
  const accepted = pendings.get(batch.id)?.accepted;
718
803
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
719
804
  });
720
- 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);
721
808
  for (const batch of accepted)
722
809
  pendings.delete(batch.id);
723
810
  stale.delete(room.locator);
@@ -736,22 +823,40 @@ export function composeRuntime(options) {
736
823
  const replica = replicas.get(locator);
737
824
  const room = rooms.get(locator);
738
825
  if (replica === undefined || room === undefined) {
739
- throw new Error(`No Local Replica for ${locator}`);
826
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
740
827
  }
741
828
  const saved = [];
742
829
  for (const { path, revision } of named) {
743
830
  const record = roomDrafts.get(locator)?.get(path);
744
831
  if (record === undefined || record.revision !== revision) {
745
- return { outcome: 'refused', rule: 'stale', file: path };
832
+ return { outcome: 'refused', code: 'stale', retryable: false, file: path };
746
833
  }
747
834
  if (!saved.some((draft) => draft.path === path))
748
835
  saved.push({ path, parts: record.parts });
749
836
  }
750
837
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
751
838
  if (changed.length > 0)
752
- return { outcome: 'refused', rule: 'changed-underneath', parts: changed };
839
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
753
840
  const parts = saved.map(({ path, parts }) => changesOf(path, parts));
754
- 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,
755
860
  baseRoomVersion: replica.confirmed.record.roomVersion,
756
861
  composition: compositionFor(replica.confirmed.record),
757
862
  operations: room.operations,
@@ -759,29 +864,86 @@ export function composeRuntime(options) {
759
864
  providerLimits: limits,
760
865
  handle: room.handle,
761
866
  visible: replica.visible,
762
- changes: parts.flatMap(({ changes }) => changes),
867
+ changes,
763
868
  bases: parts.flatMap(({ bases }) => bases),
764
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 }];
765
877
  });
766
- 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')
767
896
  return { outcome: 'refused', ...built.refusal };
768
- 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
+ }
769
928
  const consumed = saved.map(({ path }) => path);
770
929
  try {
771
- await local.write({
772
- next: replica.confirmed.record,
773
- previous: replica.confirmed.record,
930
+ await local.keepAuthored({
931
+ locator,
774
932
  queue: batches,
775
- previousQueue: replica.queue,
776
- saved,
933
+ saved: drafted,
777
934
  consumedRoomDrafts: consumed,
778
935
  });
779
936
  }
780
937
  catch {
781
- return { outcome: 'refused', rule: 'durable-write' };
938
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
782
939
  }
783
- pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
784
- recorded = false;
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 });
944
+ }
945
+ if (!localSaves)
946
+ recorded = false;
785
947
  for (const path of consumed)
786
948
  forgetRoomDraft(locator, path);
787
949
  replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
@@ -789,9 +951,12 @@ export function composeRuntime(options) {
789
951
  queue: true,
790
952
  drafts: consumed,
791
953
  }));
792
- moveTo({ ...saving(status), ...attention() });
954
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
793
955
  submitLater(locator);
794
- 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 };
795
960
  }
796
961
  async function mark(local, locator, identifier, submission, lost) {
797
962
  const state = pendings.get(identifier);
@@ -817,16 +982,15 @@ export function composeRuntime(options) {
817
982
  }
818
983
  async function readHistory(locator, path, query = {}) {
819
984
  if (closed || refusal !== null)
820
- return { kind: 'transport-uncertainty' };
985
+ return UNCERTAIN;
821
986
  try {
822
987
  await ensureAuthorization();
823
988
  }
824
989
  catch {
825
- return { kind: 'transport-uncertainty' };
826
- }
827
- if (closed || authorization === undefined || refusal !== null) {
828
- return { kind: 'transport-uncertainty' };
990
+ return UNCERTAIN;
829
991
  }
992
+ if (closed || authorization === undefined || refusal !== null)
993
+ return UNCERTAIN;
830
994
  const transfer = ['transfer', 'history', locator, path];
831
995
  if (query.revision !== undefined)
832
996
  transfer.push(query.revision);
@@ -848,12 +1012,96 @@ export function composeRuntime(options) {
848
1012
  }
849
1013
  if (result.kind === 'revoked-authorization')
850
1014
  await refuse('revoked-authorization');
851
- return result;
1015
+ return historyAnswer(result);
852
1016
  }
853
1017
  catch {
854
1018
  journal.record('outcome history transport-uncertainty');
855
- 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;
856
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);
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
+ });
1097
+ }
1098
+ async function readBinaryReference(reference) {
1099
+ await ready;
1100
+ return readBinary({
1101
+ kind: 'reference',
1102
+ fileRef: reference.fileRef,
1103
+ referrer: reference.referrer,
1104
+ });
857
1105
  }
858
1106
  function declaredAgents() {
859
1107
  for (const capability of capabilities) {
@@ -874,18 +1122,17 @@ export function composeRuntime(options) {
874
1122
  async function invokeAction(name, input) {
875
1123
  await ready;
876
1124
  if (closed || refusal !== null)
877
- return { kind: 'transport-uncertainty' };
1125
+ return UNCERTAIN;
878
1126
  if (declaredAction(name) === undefined)
879
- return { kind: 'undeclared' };
1127
+ return undeclared(name);
880
1128
  try {
881
1129
  await ensureAuthorization();
882
1130
  }
883
1131
  catch {
884
- return { kind: 'transport-uncertainty' };
885
- }
886
- if (closed || authorization === undefined || refusal !== null) {
887
- return { kind: 'transport-uncertainty' };
1132
+ return UNCERTAIN;
888
1133
  }
1134
+ if (closed || authorization === undefined || refusal !== null)
1135
+ return UNCERTAIN;
889
1136
  journal.record(`transfer action ${name}`);
890
1137
  try {
891
1138
  const result = await options.adapter.action(authorization, { name, input });
@@ -899,11 +1146,11 @@ export function composeRuntime(options) {
899
1146
  journal.record(`outcome action ${result.kind}`);
900
1147
  if (result.kind === 'rejected')
901
1148
  journal.record(`refusal rejected ${result.code}`);
902
- return result;
1149
+ return calledResult(result);
903
1150
  }
904
1151
  catch {
905
1152
  journal.record('outcome action transport-uncertainty');
906
- return { kind: 'transport-uncertainty' };
1153
+ return UNCERTAIN;
907
1154
  }
908
1155
  }
909
1156
  function continuing() {
@@ -934,14 +1181,33 @@ export function composeRuntime(options) {
934
1181
  throw error;
935
1182
  }
936
1183
  }
937
- 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) {
938
1204
  if (!continuing())
939
1205
  return unconfirmed;
940
1206
  try {
941
1207
  const result = await bootstrapRoom(options.adapter, authorized, room, {
942
1208
  collection: modelOf(room.locator),
943
1209
  selection: modelOf(room.locator, 'typed-selection'),
944
- }, (models, held) => remodel(room.locator, models, held), continuing);
1210
+ }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
945
1211
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
946
1212
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
947
1213
  return result;
@@ -969,7 +1235,7 @@ export function composeRuntime(options) {
969
1235
  return null;
970
1236
  }
971
1237
  if (refreshed.kind === 'advanced') {
972
- if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
1238
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
973
1239
  const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
974
1240
  const reached = now !== undefined &&
975
1241
  options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
@@ -981,12 +1247,12 @@ export function composeRuntime(options) {
981
1247
  if (refreshed.kind !== 'beyond-horizon')
982
1248
  return refreshed;
983
1249
  }
984
- const bootstrapped = await pullBootstrap(authorized, room);
1250
+ const bootstrapped = await pullBootstrap(authorized, room, local);
985
1251
  if (!continuing())
986
1252
  return unconfirmed;
987
1253
  if (bootstrapped.kind !== 'staged')
988
1254
  return bootstrapped;
989
- await publish(local, room, bootstrapped.confirmed);
1255
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
990
1256
  return null;
991
1257
  }
992
1258
  async function withhold(withheld, room, local, progress) {
@@ -1018,6 +1284,7 @@ export function composeRuntime(options) {
1018
1284
  function refusedSource(room, { refusal }) {
1019
1285
  sourceRefusals.set(room.locator, {
1020
1286
  code: refusal.rule,
1287
+ retryable: false,
1021
1288
  fields: {
1022
1289
  file: refusal.file,
1023
1290
  ...(refusal.location === undefined ? {} : { location: refusal.location }),
@@ -1032,8 +1299,16 @@ export function composeRuntime(options) {
1032
1299
  }
1033
1300
  function leaving(batch, result) {
1034
1301
  if (result.kind === 'expired')
1035
- return { batch, returned: { kind: 'expired' } };
1036
- 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
+ };
1037
1312
  }
1038
1313
  function logAccepted(locator, submitted, accepted) {
1039
1314
  journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
@@ -1051,7 +1326,7 @@ export function composeRuntime(options) {
1051
1326
  logAccepted(room.locator, submitted, accepted);
1052
1327
  const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1053
1328
  ? { kind: 'staged', confirmed: current.confirmed }
1054
- : 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)
1055
1330
  ? accepted.roomVersion
1056
1331
  : held, accepted.roomVersion);
1057
1332
  if (applied.kind !== 'staged') {
@@ -1259,7 +1534,7 @@ export function composeRuntime(options) {
1259
1534
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1260
1535
  };
1261
1536
  const reclassified = sameMembership(declared, held.confirmed.record)
1262
- ? readConfirmed(declared, held.confirmed)
1537
+ ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
1263
1538
  : readReclassified(declared, held.confirmed);
1264
1539
  const record = reclassified.kind === 'staged'
1265
1540
  ? declared
@@ -1267,12 +1542,10 @@ export function composeRuntime(options) {
1267
1542
  ...declared,
1268
1543
  typedSelection: held.confirmed.record.typedSelection,
1269
1544
  };
1270
- const read = reclassified.kind === 'deferred' ? readConfirmed(record, held.confirmed) : reclassified;
1271
- await local.write({
1272
- next: record,
1273
- previous: held.confirmed.record,
1274
- queue: held.queue,
1275
- });
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 });
1276
1549
  if (read.kind === 'staged') {
1277
1550
  replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1278
1551
  if (declarationRefusals.delete(room.locator)) {
@@ -1299,7 +1572,11 @@ export function composeRuntime(options) {
1299
1572
  });
1300
1573
  }
1301
1574
  async function scoped(result, room, local) {
1302
- 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
+ });
1303
1580
  let scopedAway = room;
1304
1581
  if (result.scope === 'model')
1305
1582
  await retainModel(local, room, 'collection', {
@@ -1336,7 +1613,6 @@ export function composeRuntime(options) {
1336
1613
  roomVersion: record.roomVersion,
1337
1614
  documents: record.documents,
1338
1615
  revisions: record.revisions,
1339
- attachments: record.attachments,
1340
1616
  },
1341
1617
  ];
1342
1618
  });
@@ -1416,14 +1692,14 @@ export function composeRuntime(options) {
1416
1692
  async function reload(room, local, progress) {
1417
1693
  if (discoveryPaused || authorization === undefined)
1418
1694
  return;
1419
- const bootstrapped = await pullBootstrap(authorization, room);
1695
+ const bootstrapped = await pullBootstrap(authorization, room, local);
1420
1696
  if (!continuing())
1421
1697
  return;
1422
1698
  if (bootstrapped.kind !== 'staged') {
1423
1699
  await withhold(bootstrapped, room, local, progress);
1424
1700
  return;
1425
1701
  }
1426
- await publish(local, room, bootstrapped.confirmed);
1702
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1427
1703
  await caughtUp(room, local, progress);
1428
1704
  }
1429
1705
  async function discoverRooms(local, progress, attempt) {
@@ -1458,7 +1734,11 @@ export function composeRuntime(options) {
1458
1734
  }
1459
1735
  if (result.kind === 'contract-unavailable') {
1460
1736
  journal.record(`connection contract unavailable ${result.code}`);
1461
- 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
+ });
1462
1742
  notifyReady();
1463
1743
  return;
1464
1744
  }
@@ -1534,6 +1814,8 @@ export function composeRuntime(options) {
1534
1814
  }
1535
1815
  const progress = { complete: true, reachable: true };
1536
1816
  underway = attempt === 'opened' ? progress : null;
1817
+ if (attempt === 'opened')
1818
+ streamed = progress;
1537
1819
  complete = false;
1538
1820
  recorded = false;
1539
1821
  moveTo(transferring(status));
@@ -1574,9 +1856,14 @@ export function composeRuntime(options) {
1574
1856
  void refuse('revoked-authorization');
1575
1857
  return;
1576
1858
  }
1577
- if (opened || status.providerUnreachable)
1578
- return;
1579
- 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
+ });
1580
1867
  }
1581
1868
  function applyOpening(expired) {
1582
1869
  return task(async () => {
@@ -1631,6 +1918,8 @@ export function composeRuntime(options) {
1631
1918
  }
1632
1919
  }
1633
1920
  function submitLater(locator) {
1921
+ if (localSaves)
1922
+ return;
1634
1923
  const running = submitting.get(locator);
1635
1924
  if (running !== undefined) {
1636
1925
  running.again = true;
@@ -1710,7 +1999,7 @@ export function composeRuntime(options) {
1710
1999
  }
1711
2000
  }
1712
2001
  moveTo({
1713
- state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
2002
+ state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
1714
2003
  lastCompleted,
1715
2004
  providerUnreachable: !progress.reachable,
1716
2005
  ...attention(),
@@ -1819,20 +2108,15 @@ export function composeRuntime(options) {
1819
2108
  show();
1820
2109
  return;
1821
2110
  }
1822
- const resting = status;
1823
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1824
- moveTo(transferring(resting));
2111
+ const progress = { complete: false, reachable: !status.providerUnreachable };
2112
+ moveTo(transferring(status));
1825
2113
  try {
1826
2114
  await conversations.catchUp(progress);
1827
2115
  }
1828
2116
  catch {
1829
2117
  progress.reachable = false;
1830
2118
  }
1831
- finally {
1832
- if (!closed) {
1833
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1834
- }
1835
- }
2119
+ await rest(progress);
1836
2120
  }
1837
2121
  function close() {
1838
2122
  if (closed)
@@ -1886,13 +2170,16 @@ export function composeRuntime(options) {
1886
2170
  return planOf(input, roomFacts(reach), planReach(reach));
1887
2171
  }
1888
2172
  function watchPlan(plan, deliver) {
2173
+ materialize(reachedRooms(plan));
1889
2174
  return readSide.watch(plan, identityOf(plan), deliver);
1890
2175
  }
1891
2176
  async function searched(input, reach) {
1892
2177
  const plan = searchPlanOf(input);
1893
2178
  const selected = plan.documents === undefined && reach === null
1894
2179
  ? null
1895
- : (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);
1896
2183
  return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
1897
2184
  }
1898
2185
  const watch = (topic, input, deliver) => {
@@ -1918,6 +2205,8 @@ export function composeRuntime(options) {
1918
2205
  }
1919
2206
  if (topic === 'conversations')
1920
2207
  return conversations.watchList(deliver);
2208
+ if (topic === 'agents')
2209
+ return conversations.watchAgents(deliver);
1921
2210
  if (topic === 'presentation') {
1922
2211
  const listener = {
1923
2212
  app: input[0],
@@ -1996,7 +2285,7 @@ export function composeRuntime(options) {
1996
2285
  rooms() {
1997
2286
  return listed();
1998
2287
  },
1999
- query: ((input) => readSide.query(planned(input, null))),
2288
+ query: ((input) => rowsQuery(planned(input, null))),
2000
2289
  search: (input) => searched(input, null),
2001
2290
  types(locator) {
2002
2291
  return {
@@ -2025,20 +2314,20 @@ export function composeRuntime(options) {
2025
2314
  return exclusive(async () => {
2026
2315
  const parts = readParts(input);
2027
2316
  if (path === '' || normalize(path) !== path)
2028
- throw new Error('room-draft-malformed');
2317
+ throw runtimeError('room-draft-malformed');
2029
2318
  const local = await openLocal();
2030
2319
  const current = roomDrafts.get(room)?.get(path);
2031
2320
  if ((current?.revision ?? null) !== revision) {
2032
- return {
2033
- outcome: 'stale',
2034
- current: current === undefined ? null : roomDraftOf(current),
2035
- };
2321
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2036
2322
  }
2323
+ const file = parts.file === undefined
2324
+ ? undefined
2325
+ : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
2037
2326
  const record = {
2038
2327
  locator: room,
2039
2328
  path,
2040
2329
  revision: roomDraftRevision(),
2041
- parts: held(current?.parts, parts),
2330
+ parts: held(current?.parts, parts, file),
2042
2331
  returned: current?.returned ?? null,
2043
2332
  };
2044
2333
  await local.keepRoomDraft(record);
@@ -2052,10 +2341,7 @@ export function composeRuntime(options) {
2052
2341
  const local = await openLocal();
2053
2342
  const current = roomDrafts.get(room)?.get(path);
2054
2343
  if (current === undefined || current.revision !== revision) {
2055
- return {
2056
- outcome: 'stale',
2057
- current: current === undefined ? null : roomDraftOf(current),
2058
- };
2344
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2059
2345
  }
2060
2346
  await local.discardRoomDraft(room, path);
2061
2347
  forgetRoomDraft(room, path);
@@ -2090,6 +2376,10 @@ export function composeRuntime(options) {
2090
2376
  history(locator, path, query) {
2091
2377
  return readHistory(locator, path, query);
2092
2378
  },
2379
+ binaryFile: {
2380
+ read: readBinaryPath,
2381
+ readReference: readBinaryReference,
2382
+ },
2093
2383
  call(name, input) {
2094
2384
  return invokeAction(name, input);
2095
2385
  },
@@ -2122,8 +2412,9 @@ export function composeRuntime(options) {
2122
2412
  const reachable = {
2123
2413
  md,
2124
2414
  watch,
2415
+ referrerScope: (referrer) => options.adapter.referrerScope(referrer),
2125
2416
  plan: planned,
2126
- query: (plan) => readSide.query(plan),
2417
+ query: (plan) => rowsQuery(plan),
2127
2418
  watchPlan,
2128
2419
  search: searched,
2129
2420
  };