@agentshouse/mdruntime 0.1.0-cebcd51f70ed1 → 0.1.0-cede5a74284be

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 (166) hide show
  1. package/AGENTS.md +109 -35
  2. package/README.md +13 -3
  3. package/dist/adapter.d.ts +47 -14
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/attached-model.d.ts.map +1 -1
  6. package/dist/attached-model.js +15 -2
  7. package/dist/attempt.d.ts +16 -0
  8. package/dist/attempt.d.ts.map +1 -0
  9. package/dist/attempt.js +57 -0
  10. package/dist/binary.d.ts +23 -0
  11. package/dist/binary.d.ts.map +1 -0
  12. package/dist/binary.js +8 -0
  13. package/dist/bootstrap.d.ts +17 -5
  14. package/dist/bootstrap.d.ts.map +1 -1
  15. package/dist/bootstrap.js +88 -38
  16. package/dist/change-set.d.ts.map +1 -1
  17. package/dist/change-set.js +19 -12
  18. package/dist/change.d.ts +15 -0
  19. package/dist/change.d.ts.map +1 -1
  20. package/dist/change.js +26 -1
  21. package/dist/chat/acts.d.ts +34 -0
  22. package/dist/chat/acts.d.ts.map +1 -0
  23. package/dist/chat/acts.js +36 -0
  24. package/dist/chat/context.d.ts +3 -0
  25. package/dist/chat/context.d.ts.map +1 -0
  26. package/dist/chat/context.js +10 -0
  27. package/dist/chat/input.d.ts +9 -0
  28. package/dist/chat/input.d.ts.map +1 -0
  29. package/dist/chat/input.js +99 -0
  30. package/dist/chat/kit.d.ts +60 -0
  31. package/dist/chat/kit.d.ts.map +1 -0
  32. package/dist/chat/kit.js +123 -0
  33. package/dist/chat/lasting.d.ts +6 -0
  34. package/dist/chat/lasting.d.ts.map +1 -0
  35. package/dist/chat/lasting.js +19 -0
  36. package/dist/chat/list.d.ts +20 -0
  37. package/dist/chat/list.d.ts.map +1 -0
  38. package/dist/chat/list.js +59 -0
  39. package/dist/chat/markdown.d.ts +19 -0
  40. package/dist/chat/markdown.d.ts.map +1 -0
  41. package/dist/chat/markdown.js +187 -0
  42. package/dist/chat/message.d.ts +6 -0
  43. package/dist/chat/message.d.ts.map +1 -0
  44. package/dist/chat/message.js +192 -0
  45. package/dist/chat/messages.d.ts +21 -0
  46. package/dist/chat/messages.d.ts.map +1 -0
  47. package/dist/chat/messages.js +151 -0
  48. package/dist/chat/name.d.ts +6 -0
  49. package/dist/chat/name.d.ts.map +1 -0
  50. package/dist/chat/name.js +37 -0
  51. package/dist/chat/opened.d.ts +11 -0
  52. package/dist/chat/opened.d.ts.map +1 -0
  53. package/dist/chat/opened.js +17 -0
  54. package/dist/chat/overlay.d.ts +31 -0
  55. package/dist/chat/overlay.d.ts.map +1 -0
  56. package/dist/chat/overlay.js +97 -0
  57. package/dist/chat/parts.d.ts +57 -0
  58. package/dist/chat/parts.d.ts.map +1 -0
  59. package/dist/chat/parts.js +119 -0
  60. package/dist/chat/process.d.ts +20 -0
  61. package/dist/chat/process.d.ts.map +1 -0
  62. package/dist/chat/process.js +32 -0
  63. package/dist/chat/provider.d.ts +63 -0
  64. package/dist/chat/provider.d.ts.map +1 -0
  65. package/dist/chat/provider.js +645 -0
  66. package/dist/chat/question.d.ts +9 -0
  67. package/dist/chat/question.d.ts.map +1 -0
  68. package/dist/chat/question.js +80 -0
  69. package/dist/chat/questions.d.ts +31 -0
  70. package/dist/chat/questions.d.ts.map +1 -0
  71. package/dist/chat/questions.js +111 -0
  72. package/dist/chat/read.d.ts +5 -0
  73. package/dist/chat/read.d.ts.map +1 -0
  74. package/dist/chat/read.js +13 -0
  75. package/dist/chat/ready.d.ts +33 -0
  76. package/dist/chat/ready.d.ts.map +1 -0
  77. package/dist/chat/ready.js +91 -0
  78. package/dist/chat/style.d.ts +4 -0
  79. package/dist/chat/style.d.ts.map +1 -0
  80. package/dist/chat/style.js +302 -0
  81. package/dist/chat.d.ts +14 -0
  82. package/dist/chat.d.ts.map +1 -0
  83. package/dist/chat.js +8 -0
  84. package/dist/client.d.ts +14 -5
  85. package/dist/client.d.ts.map +1 -1
  86. package/dist/client.js +65 -15
  87. package/dist/collection.d.ts +6 -11
  88. package/dist/collection.d.ts.map +1 -1
  89. package/dist/collection.js +56 -19
  90. package/dist/connection.d.ts +21 -9
  91. package/dist/connection.d.ts.map +1 -1
  92. package/dist/connection.js +470 -142
  93. package/dist/conversation.d.ts +11 -3
  94. package/dist/conversation.d.ts.map +1 -1
  95. package/dist/conversation.js +47 -7
  96. package/dist/expectation.d.ts +21 -0
  97. package/dist/expectation.d.ts.map +1 -0
  98. package/dist/expectation.js +70 -0
  99. package/dist/failure.d.ts +13 -0
  100. package/dist/failure.d.ts.map +1 -0
  101. package/dist/failure.js +19 -0
  102. package/dist/host.d.ts +1 -0
  103. package/dist/host.d.ts.map +1 -1
  104. package/dist/host.js +9 -8
  105. package/dist/identity.d.ts +3 -0
  106. package/dist/identity.d.ts.map +1 -1
  107. package/dist/identity.js +13 -1
  108. package/dist/index.d.ts +9 -4
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +3 -0
  111. package/dist/memory.d.ts +4 -3
  112. package/dist/memory.d.ts.map +1 -1
  113. package/dist/memory.js +60 -20
  114. package/dist/model.d.ts +10 -2
  115. package/dist/model.d.ts.map +1 -1
  116. package/dist/model.js +2 -0
  117. package/dist/open.d.ts +2 -0
  118. package/dist/open.d.ts.map +1 -0
  119. package/dist/open.js +1 -0
  120. package/dist/outcome.d.ts +28 -3
  121. package/dist/outcome.d.ts.map +1 -1
  122. package/dist/outcome.js +41 -1
  123. package/dist/overlay.d.ts +14 -3
  124. package/dist/overlay.d.ts.map +1 -1
  125. package/dist/overlay.js +75 -37
  126. package/dist/query.d.ts +4 -0
  127. package/dist/query.d.ts.map +1 -1
  128. package/dist/query.js +5 -31
  129. package/dist/reach.d.ts +2 -0
  130. package/dist/reach.d.ts.map +1 -1
  131. package/dist/reach.js +43 -19
  132. package/dist/react.d.ts +25 -16
  133. package/dist/react.d.ts.map +1 -1
  134. package/dist/react.js +8 -6
  135. package/dist/read-side.d.ts.map +1 -1
  136. package/dist/read-side.js +9 -2
  137. package/dist/refresh.d.ts +10 -0
  138. package/dist/refresh.d.ts.map +1 -1
  139. package/dist/refresh.js +42 -38
  140. package/dist/replica.d.ts +8 -5
  141. package/dist/replica.d.ts.map +1 -1
  142. package/dist/replica.js +76 -56
  143. package/dist/room-draft.d.ts +26 -9
  144. package/dist/room-draft.d.ts.map +1 -1
  145. package/dist/room-draft.js +132 -23
  146. package/dist/rows.d.ts +5 -1
  147. package/dist/rows.d.ts.map +1 -1
  148. package/dist/rows.js +10 -2
  149. package/dist/save.d.ts +27 -1
  150. package/dist/save.d.ts.map +1 -1
  151. package/dist/save.js +340 -33
  152. package/dist/search.d.ts +3 -2
  153. package/dist/search.d.ts.map +1 -1
  154. package/dist/status.d.ts +2 -2
  155. package/dist/status.d.ts.map +1 -1
  156. package/dist/status.js +0 -8
  157. package/dist/store.d.ts +33 -9
  158. package/dist/store.d.ts.map +1 -1
  159. package/dist/store.js +273 -164
  160. package/dist/wire.d.ts +7 -3
  161. package/dist/wire.d.ts.map +1 -1
  162. package/dist/wire.js +15 -2
  163. package/package.json +22 -14
  164. package/dist/attachment.d.ts +0 -3
  165. package/dist/attachment.d.ts.map +0 -1
  166. 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,8 @@ export function composeRuntime(options) {
82
90
  let withdrawStream;
83
91
  const announced = new Map();
84
92
  let underway = null;
93
+ let streamed = null;
94
+ let handed = false;
85
95
  const submitting = new Map();
86
96
  const answering = new Map();
87
97
  let complete = false;
@@ -104,9 +114,14 @@ export function composeRuntime(options) {
104
114
  let haltedRefusal = false;
105
115
  let openSignals = 0;
106
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();
107
122
  const conversations = createConversationStore({
108
123
  agents: declaredAgents,
109
- documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
124
+ documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
110
125
  call: (name, input) => invokeAction(name, input),
111
126
  durably,
112
127
  stream: () => options.adapter.conversations,
@@ -182,7 +197,9 @@ export function composeRuntime(options) {
182
197
  }
183
198
  }
184
199
  function pending() {
185
- 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)));
186
203
  }
187
204
  function attention() {
188
205
  const attentionRequired = [];
@@ -211,8 +228,24 @@ export function composeRuntime(options) {
211
228
  }
212
229
  return { attentionRequired, reset };
213
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
+ }
214
243
  function listed() {
215
- 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
+ }));
216
249
  }
217
250
  function roomFacts(reach = null) {
218
251
  const held = [...rooms.values()]
@@ -244,6 +277,10 @@ export function composeRuntime(options) {
244
277
  for (const listener of storageLossListeners)
245
278
  listener(storageLoss);
246
279
  }
280
+ function emitMember() {
281
+ for (const listener of memberListeners)
282
+ listener(member);
283
+ }
247
284
  function moveTo(next) {
248
285
  if (closed)
249
286
  return;
@@ -263,6 +300,8 @@ export function composeRuntime(options) {
263
300
  listener(shown);
264
301
  }
265
302
  function entryOf(batch) {
303
+ if (localSaves)
304
+ return Object.freeze({ batch, state: 'local' });
266
305
  const state = pendings.get(batch.id);
267
306
  if (state?.accepted !== undefined) {
268
307
  return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
@@ -270,10 +309,7 @@ export function composeRuntime(options) {
270
309
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
271
310
  }
272
311
  function entriesOf(locator) {
273
- const replica = replicas.get(locator);
274
- if (replica === undefined)
275
- return [];
276
- return replica.queue.map(entryOf);
312
+ return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
277
313
  }
278
314
  function emitQueue(locator) {
279
315
  const entries = entriesOf(locator);
@@ -283,12 +319,47 @@ export function composeRuntime(options) {
283
319
  }
284
320
  }
285
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);
286
345
  readSide.apply(set);
287
346
  searchIndex.changed(set);
288
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
+ }
289
359
  function publishChangeSet(set) {
290
360
  applyChangeSet(set);
291
- 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)))
292
363
  emitRooms();
293
364
  for (const locator of set.queues)
294
365
  emitQueue(locator);
@@ -335,10 +406,7 @@ export function composeRuntime(options) {
335
406
  const revoked = { status: 'unavailable', reason: 'revoked' };
336
407
  try {
337
408
  const local = await openLocal();
338
- await local.revokeModels([...replicas.values()].map((replica) => ({
339
- record: replica.confirmed.record,
340
- queue: replica.queue,
341
- })));
409
+ await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
342
410
  }
343
411
  catch {
344
412
  journal.record('model revocation persistence failed');
@@ -362,10 +430,7 @@ export function composeRuntime(options) {
362
430
  model: revoked,
363
431
  selectionModel: revoked,
364
432
  };
365
- const read = readConfirmed(record, before.confirmed);
366
- replicas.set(locator, read.kind === 'staged'
367
- ? replicaOf(read.confirmed, before.queue, before)
368
- : { ...before, confirmed: { ...before.confirmed, record } });
433
+ replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
369
434
  }
370
435
  rooms.set(locator, {
371
436
  ...room,
@@ -382,7 +447,8 @@ export function composeRuntime(options) {
382
447
  }
383
448
  finally {
384
449
  refusal = {
385
- reason,
450
+ code: reason,
451
+ retryable: false,
386
452
  reauthorizationOffered: reason === 'revoked-authorization',
387
453
  refusal: provider,
388
454
  };
@@ -407,7 +473,6 @@ export function composeRuntime(options) {
407
473
  },
408
474
  providerOwnedPaths: room.providerOwnedPaths,
409
475
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
410
- attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
411
476
  synchronizationAvailable,
412
477
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
413
478
  });
@@ -432,7 +497,14 @@ export function composeRuntime(options) {
432
497
  emitRooms();
433
498
  }
434
499
  function unlistedHeld(listed) {
435
- 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));
436
508
  }
437
509
  async function dropUnlisted(listed, local) {
438
510
  if (unlistedHeld(listed).length === 0)
@@ -446,11 +518,13 @@ export function composeRuntime(options) {
446
518
  drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
447
519
  }));
448
520
  for (const locator of unlisted) {
449
- for (const batch of replicas.get(locator)?.queue ?? [])
521
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
450
522
  pendings.delete(batch.id);
451
523
  replicas.delete(locator);
524
+ queuesWithoutReplica.delete(locator);
452
525
  rooms.delete(locator);
453
526
  roomDrafts.delete(locator);
527
+ transfers.delete(locator);
454
528
  unreadable.delete(locator);
455
529
  stale.delete(locator);
456
530
  scopedRefusals.delete(locator);
@@ -487,7 +561,7 @@ export function composeRuntime(options) {
487
561
  return model;
488
562
  }
489
563
  async function hydrate(opened) {
490
- 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([
491
565
  opened.all(),
492
566
  opened.queues(),
493
567
  opened.roomDrafts(),
@@ -497,9 +571,15 @@ export function composeRuntime(options) {
497
571
  opened.lastCompleted(),
498
572
  opened.providerLimits(),
499
573
  opened.capabilities(),
574
+ opened.member(),
575
+ opened.transfers(),
500
576
  ]);
577
+ transfers.clear();
578
+ for (const record of staged)
579
+ transfers.set(record.locator, record);
501
580
  limits = declared;
502
581
  capabilities = named;
582
+ member = known;
503
583
  roomDrafts.clear();
504
584
  for (const record of kept)
505
585
  rememberRoomDraft(record);
@@ -515,18 +595,17 @@ export function composeRuntime(options) {
515
595
  }
516
596
  const previousReplicas = new Map(replicas);
517
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
+ }
518
604
  for (const record of records) {
519
605
  const model = restored(modelKey(record.locator, 'collection'), record.model);
520
606
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
521
607
  const prior = previousReplicas.get(record.locator);
522
- const retyped = { ...record, model, selectionModel };
523
- const read = readConfirmed(retyped, prior?.confirmed);
524
- const confirmed = read.kind === 'staged'
525
- ? read.confirmed
526
- : {
527
- record: retyped,
528
- collection: parseCollection(retyped.documents, compositionOf(retyped)),
529
- };
608
+ const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
530
609
  replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
531
610
  const existing = rooms.get(record.locator);
532
611
  if (existing === undefined)
@@ -535,7 +614,6 @@ export function composeRuntime(options) {
535
614
  rooms.set(record.locator, {
536
615
  ...existing,
537
616
  roomVersion: record.roomVersion,
538
- attachments: record.attachments,
539
617
  operations: record.operations,
540
618
  replica: record.replica,
541
619
  model: modelTypes(model),
@@ -547,10 +625,6 @@ export function composeRuntime(options) {
547
625
  },
548
626
  });
549
627
  }
550
- if (read.kind === 'refused') {
551
- refusedSource(record, read);
552
- declarationRefusals.add(record.locator);
553
- }
554
628
  }
555
629
  return {
556
630
  lastCompleted,
@@ -563,6 +637,8 @@ export function composeRuntime(options) {
563
637
  applyChangeSet(read);
564
638
  if (replicas.size > 0)
565
639
  emitRooms();
640
+ if (member !== null)
641
+ emitMember();
566
642
  for (const locator of locators)
567
643
  emitQueue(locator);
568
644
  publishRoomDrafts(false);
@@ -610,10 +686,15 @@ export function composeRuntime(options) {
610
686
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
611
687
  }
612
688
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
613
- const withdrawn = withdrawals(confirmed, queue, withdrawable);
689
+ const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
690
+ const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
614
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
+ }));
615
696
  const returning = [
616
- ...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
697
+ ...[...unqueued].reverse(),
617
698
  ...(settling.leaving === undefined ? [] : [settling.leaving]),
618
699
  ];
619
700
  const written = new Map();
@@ -631,25 +712,44 @@ export function composeRuntime(options) {
631
712
  });
632
713
  }
633
714
  }
634
- await local.write({
635
- next: confirmed.record,
636
- previous: before?.confirmed.record ?? null,
637
- queue: kept,
638
- previousQueue: before?.queue ?? [],
639
- roomDrafts: [...written.values()],
640
- });
641
- for (const batch of before?.queue ?? []) {
715
+ await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
716
+ for (const batch of previousQueue) {
642
717
  if (!kept.includes(batch))
643
718
  pendings.delete(batch.id);
644
719
  }
645
720
  for (const record of written.values())
646
721
  rememberRoomDraft(record);
647
- for (const batch of withdrawn)
648
- journal.record(`outcome ${locator} ${batch.id} withdrawn`);
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
+ }
649
749
  replicas.set(locator, replicaOf(confirmed, kept, before));
650
750
  return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
651
751
  room: settling.room,
652
- queue: kept.length !== (before?.queue.length ?? 0),
752
+ queue: kept.length !== previousQueue.length,
653
753
  drafts: [...written.keys()],
654
754
  });
655
755
  }
@@ -684,7 +784,8 @@ export function composeRuntime(options) {
684
784
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
685
785
  };
686
786
  }
687
- function publish(local, room, confirmed, base) {
787
+ function publish(local, room, confirmed, landing = {}) {
788
+ const { base, stored } = landing;
688
789
  return exclusive(async () => {
689
790
  if (!continuing())
690
791
  return true;
@@ -695,13 +796,15 @@ export function composeRuntime(options) {
695
796
  return false;
696
797
  }
697
798
  adoptModels(room.locator, confirmed.record);
698
- const queue = prior?.queue ?? [];
799
+ const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
699
800
  const roomVersion = confirmed.record.roomVersion;
700
801
  const accepted = queue.filter((batch) => {
701
802
  const accepted = pendings.get(batch.id)?.accepted;
702
803
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
703
804
  });
704
- 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);
705
808
  for (const batch of accepted)
706
809
  pendings.delete(batch.id);
707
810
  stale.delete(room.locator);
@@ -720,22 +823,40 @@ export function composeRuntime(options) {
720
823
  const replica = replicas.get(locator);
721
824
  const room = rooms.get(locator);
722
825
  if (replica === undefined || room === undefined) {
723
- throw new Error(`No Local Replica for ${locator}`);
826
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
724
827
  }
725
828
  const saved = [];
726
829
  for (const { path, revision } of named) {
727
830
  const record = roomDrafts.get(locator)?.get(path);
728
831
  if (record === undefined || record.revision !== revision) {
729
- return { outcome: 'refused', rule: 'stale', file: path };
832
+ return { outcome: 'refused', code: 'stale', retryable: false, file: path };
730
833
  }
731
834
  if (!saved.some((draft) => draft.path === path))
732
835
  saved.push({ path, parts: record.parts });
733
836
  }
734
837
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
735
838
  if (changed.length > 0)
736
- return { outcome: 'refused', rule: 'changed-underneath', parts: changed };
839
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
737
840
  const parts = saved.map(({ path, parts }) => changesOf(path, parts));
738
- 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,
739
860
  baseRoomVersion: replica.confirmed.record.roomVersion,
740
861
  composition: compositionFor(replica.confirmed.record),
741
862
  operations: room.operations,
@@ -743,29 +864,86 @@ export function composeRuntime(options) {
743
864
  providerLimits: limits,
744
865
  handle: room.handle,
745
866
  visible: replica.visible,
746
- changes: parts.flatMap(({ changes }) => changes),
867
+ changes,
747
868
  bases: parts.flatMap(({ bases }) => bases),
748
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 }];
749
877
  });
750
- 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')
751
896
  return { outcome: 'refused', ...built.refusal };
752
- 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
+ }
753
928
  const consumed = saved.map(({ path }) => path);
754
929
  try {
755
- await local.write({
756
- next: replica.confirmed.record,
757
- previous: replica.confirmed.record,
930
+ await local.keepAuthored({
931
+ locator,
758
932
  queue: batches,
759
- previousQueue: replica.queue,
760
- saved,
933
+ saved: drafted,
761
934
  consumedRoomDrafts: consumed,
762
935
  });
763
936
  }
764
937
  catch {
765
- 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 });
766
944
  }
767
- pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
768
- recorded = false;
945
+ if (!localSaves)
946
+ recorded = false;
769
947
  for (const path of consumed)
770
948
  forgetRoomDraft(locator, path);
771
949
  replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
@@ -773,9 +951,12 @@ export function composeRuntime(options) {
773
951
  queue: true,
774
952
  drafts: consumed,
775
953
  }));
776
- moveTo({ ...saving(status), ...attention() });
954
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
777
955
  submitLater(locator);
778
- 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 };
779
960
  }
780
961
  async function mark(local, locator, identifier, submission, lost) {
781
962
  const state = pendings.get(identifier);
@@ -801,16 +982,15 @@ export function composeRuntime(options) {
801
982
  }
802
983
  async function readHistory(locator, path, query = {}) {
803
984
  if (closed || refusal !== null)
804
- return { kind: 'transport-uncertainty' };
985
+ return UNCERTAIN;
805
986
  try {
806
987
  await ensureAuthorization();
807
988
  }
808
989
  catch {
809
- return { kind: 'transport-uncertainty' };
810
- }
811
- if (closed || authorization === undefined || refusal !== null) {
812
- return { kind: 'transport-uncertainty' };
990
+ return UNCERTAIN;
813
991
  }
992
+ if (closed || authorization === undefined || refusal !== null)
993
+ return UNCERTAIN;
814
994
  const transfer = ['transfer', 'history', locator, path];
815
995
  if (query.revision !== undefined)
816
996
  transfer.push(query.revision);
@@ -832,13 +1012,97 @@ export function composeRuntime(options) {
832
1012
  }
833
1013
  if (result.kind === 'revoked-authorization')
834
1014
  await refuse('revoked-authorization');
835
- return result;
1015
+ return historyAnswer(result);
836
1016
  }
837
1017
  catch {
838
1018
  journal.record('outcome history transport-uncertainty');
839
- return { kind: 'transport-uncertainty' };
1019
+ return UNCERTAIN;
840
1020
  }
841
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);
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
+ });
1105
+ }
842
1106
  function declaredAgents() {
843
1107
  for (const capability of capabilities) {
844
1108
  if (capability.conversationAgents !== undefined)
@@ -858,18 +1122,17 @@ export function composeRuntime(options) {
858
1122
  async function invokeAction(name, input) {
859
1123
  await ready;
860
1124
  if (closed || refusal !== null)
861
- return { kind: 'transport-uncertainty' };
1125
+ return UNCERTAIN;
862
1126
  if (declaredAction(name) === undefined)
863
- return { kind: 'undeclared' };
1127
+ return undeclared(name);
864
1128
  try {
865
1129
  await ensureAuthorization();
866
1130
  }
867
1131
  catch {
868
- return { kind: 'transport-uncertainty' };
869
- }
870
- if (closed || authorization === undefined || refusal !== null) {
871
- return { kind: 'transport-uncertainty' };
1132
+ return UNCERTAIN;
872
1133
  }
1134
+ if (closed || authorization === undefined || refusal !== null)
1135
+ return UNCERTAIN;
873
1136
  journal.record(`transfer action ${name}`);
874
1137
  try {
875
1138
  const result = await options.adapter.action(authorization, { name, input });
@@ -883,11 +1146,11 @@ export function composeRuntime(options) {
883
1146
  journal.record(`outcome action ${result.kind}`);
884
1147
  if (result.kind === 'rejected')
885
1148
  journal.record(`refusal rejected ${result.code}`);
886
- return result;
1149
+ return calledResult(result);
887
1150
  }
888
1151
  catch {
889
1152
  journal.record('outcome action transport-uncertainty');
890
- return { kind: 'transport-uncertainty' };
1153
+ return UNCERTAIN;
891
1154
  }
892
1155
  }
893
1156
  function continuing() {
@@ -918,14 +1181,33 @@ export function composeRuntime(options) {
918
1181
  throw error;
919
1182
  }
920
1183
  }
921
- 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) {
922
1204
  if (!continuing())
923
1205
  return unconfirmed;
924
1206
  try {
925
1207
  const result = await bootstrapRoom(options.adapter, authorized, room, {
926
1208
  collection: modelOf(room.locator),
927
1209
  selection: modelOf(room.locator, 'typed-selection'),
928
- }, (models, held) => remodel(room.locator, models, held), continuing);
1210
+ }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
929
1211
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
930
1212
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
931
1213
  return result;
@@ -953,7 +1235,7 @@ export function composeRuntime(options) {
953
1235
  return null;
954
1236
  }
955
1237
  if (refreshed.kind === 'advanced') {
956
- if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
1238
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
957
1239
  const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
958
1240
  const reached = now !== undefined &&
959
1241
  options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
@@ -965,12 +1247,12 @@ export function composeRuntime(options) {
965
1247
  if (refreshed.kind !== 'beyond-horizon')
966
1248
  return refreshed;
967
1249
  }
968
- const bootstrapped = await pullBootstrap(authorized, room);
1250
+ const bootstrapped = await pullBootstrap(authorized, room, local);
969
1251
  if (!continuing())
970
1252
  return unconfirmed;
971
1253
  if (bootstrapped.kind !== 'staged')
972
1254
  return bootstrapped;
973
- await publish(local, room, bootstrapped.confirmed);
1255
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
974
1256
  return null;
975
1257
  }
976
1258
  async function withhold(withheld, room, local, progress) {
@@ -1002,6 +1284,7 @@ export function composeRuntime(options) {
1002
1284
  function refusedSource(room, { refusal }) {
1003
1285
  sourceRefusals.set(room.locator, {
1004
1286
  code: refusal.rule,
1287
+ retryable: false,
1005
1288
  fields: {
1006
1289
  file: refusal.file,
1007
1290
  ...(refusal.location === undefined ? {} : { location: refusal.location }),
@@ -1016,8 +1299,16 @@ export function composeRuntime(options) {
1016
1299
  }
1017
1300
  function leaving(batch, result) {
1018
1301
  if (result.kind === 'expired')
1019
- return { batch, returned: { kind: 'expired' } };
1020
- 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
+ };
1021
1312
  }
1022
1313
  function logAccepted(locator, submitted, accepted) {
1023
1314
  journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
@@ -1035,7 +1326,7 @@ export function composeRuntime(options) {
1035
1326
  logAccepted(room.locator, submitted, accepted);
1036
1327
  const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1037
1328
  ? { kind: 'staged', confirmed: current.confirmed }
1038
- : 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)
1039
1330
  ? accepted.roomVersion
1040
1331
  : held, accepted.roomVersion);
1041
1332
  if (applied.kind !== 'staged') {
@@ -1181,7 +1472,8 @@ export function composeRuntime(options) {
1181
1472
  return;
1182
1473
  }
1183
1474
  const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1184
- 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))
1185
1477
  ? { ...left, returned: null }
1186
1478
  : left,
1187
1479
  });
@@ -1242,7 +1534,7 @@ export function composeRuntime(options) {
1242
1534
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1243
1535
  };
1244
1536
  const reclassified = sameMembership(declared, held.confirmed.record)
1245
- ? readConfirmed(declared, held.confirmed)
1537
+ ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
1246
1538
  : readReclassified(declared, held.confirmed);
1247
1539
  const record = reclassified.kind === 'staged'
1248
1540
  ? declared
@@ -1250,12 +1542,10 @@ export function composeRuntime(options) {
1250
1542
  ...declared,
1251
1543
  typedSelection: held.confirmed.record.typedSelection,
1252
1544
  };
1253
- const read = reclassified.kind === 'deferred' ? readConfirmed(record, held.confirmed) : reclassified;
1254
- await local.write({
1255
- next: record,
1256
- previous: held.confirmed.record,
1257
- queue: held.queue,
1258
- });
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 });
1259
1549
  if (read.kind === 'staged') {
1260
1550
  replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1261
1551
  if (declarationRefusals.delete(room.locator)) {
@@ -1282,7 +1572,11 @@ export function composeRuntime(options) {
1282
1572
  });
1283
1573
  }
1284
1574
  async function scoped(result, room, local) {
1285
- 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
+ });
1286
1580
  let scopedAway = room;
1287
1581
  if (result.scope === 'model')
1288
1582
  await retainModel(local, room, 'collection', {
@@ -1319,7 +1613,6 @@ export function composeRuntime(options) {
1319
1613
  roomVersion: record.roomVersion,
1320
1614
  documents: record.documents,
1321
1615
  revisions: record.revisions,
1322
- attachments: record.attachments,
1323
1616
  },
1324
1617
  ];
1325
1618
  });
@@ -1330,8 +1623,10 @@ export function composeRuntime(options) {
1330
1623
  const outcome = prepared[index];
1331
1624
  return outcome?.status === 'fulfilled' && outcome.value;
1332
1625
  });
1333
- if (attempt === 'opened')
1626
+ if (attempt === 'opened') {
1334
1627
  options.adapter.roomStream?.hold(heldRooms(ready));
1628
+ handed = true;
1629
+ }
1335
1630
  const loaded = await Promise.allSettled(ready
1336
1631
  .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1337
1632
  .map((room) => loadRoom(room, local, progress)));
@@ -1397,14 +1692,14 @@ export function composeRuntime(options) {
1397
1692
  async function reload(room, local, progress) {
1398
1693
  if (discoveryPaused || authorization === undefined)
1399
1694
  return;
1400
- const bootstrapped = await pullBootstrap(authorization, room);
1695
+ const bootstrapped = await pullBootstrap(authorization, room, local);
1401
1696
  if (!continuing())
1402
1697
  return;
1403
1698
  if (bootstrapped.kind !== 'staged') {
1404
1699
  await withhold(bootstrapped, room, local, progress);
1405
1700
  return;
1406
1701
  }
1407
- await publish(local, room, bootstrapped.confirmed);
1702
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1408
1703
  await caughtUp(room, local, progress);
1409
1704
  }
1410
1705
  async function discoverRooms(local, progress, attempt) {
@@ -1434,15 +1729,16 @@ export function composeRuntime(options) {
1434
1729
  }
1435
1730
  progress.reachable = false;
1436
1731
  progress.complete = false;
1437
- if (attempt === 'opened') {
1438
- options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1439
- }
1440
1732
  notifyReady();
1441
1733
  return;
1442
1734
  }
1443
1735
  if (result.kind === 'contract-unavailable') {
1444
1736
  journal.record(`connection contract unavailable ${result.code}`);
1445
- 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
+ });
1446
1742
  notifyReady();
1447
1743
  return;
1448
1744
  }
@@ -1483,6 +1779,12 @@ export function composeRuntime(options) {
1483
1779
  limits = result.providerLimits;
1484
1780
  await local.declare(limits, capabilities);
1485
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
+ }
1486
1788
  await applyDiscovery(result.rooms, local);
1487
1789
  await synchronize(result.rooms, local, progress, attempt);
1488
1790
  }
@@ -1512,9 +1814,12 @@ export function composeRuntime(options) {
1512
1814
  }
1513
1815
  const progress = { complete: true, reachable: true };
1514
1816
  underway = attempt === 'opened' ? progress : null;
1817
+ if (attempt === 'opened')
1818
+ streamed = progress;
1515
1819
  complete = false;
1516
1820
  recorded = false;
1517
1821
  moveTo(transferring(status));
1822
+ handed = false;
1518
1823
  try {
1519
1824
  const local = await openLocal();
1520
1825
  if (closed)
@@ -1526,16 +1831,22 @@ export function composeRuntime(options) {
1526
1831
  }
1527
1832
  catch {
1528
1833
  progress.complete = false;
1529
- if (attempt === 'opened') {
1530
- options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1531
- }
1834
+ if (attempt === 'opened' && !handed)
1835
+ progress.reachable = false;
1532
1836
  }
1533
1837
  finally {
1534
1838
  notifyReady();
1535
1839
  }
1536
- if (attempt === 'opened')
1840
+ if (attempt !== 'opened') {
1841
+ await finishOpening(progress);
1537
1842
  return;
1538
- 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();
1539
1850
  });
1540
1851
  }
1541
1852
  function closedSocket(revoked) {
@@ -1545,9 +1856,14 @@ export function composeRuntime(options) {
1545
1856
  void refuse('revoked-authorization');
1546
1857
  return;
1547
1858
  }
1548
- if (opened || status.providerUnreachable)
1549
- return;
1550
- 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
+ });
1551
1867
  }
1552
1868
  function applyOpening(expired) {
1553
1869
  return task(async () => {
@@ -1602,6 +1918,8 @@ export function composeRuntime(options) {
1602
1918
  }
1603
1919
  }
1604
1920
  function submitLater(locator) {
1921
+ if (localSaves)
1922
+ return;
1605
1923
  const running = submitting.get(locator);
1606
1924
  if (running !== undefined) {
1607
1925
  running.again = true;
@@ -1681,7 +1999,7 @@ export function composeRuntime(options) {
1681
1999
  }
1682
2000
  }
1683
2001
  moveTo({
1684
- state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
2002
+ state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
1685
2003
  lastCompleted,
1686
2004
  providerUnreachable: !progress.reachable,
1687
2005
  ...attention(),
@@ -1790,20 +2108,15 @@ export function composeRuntime(options) {
1790
2108
  show();
1791
2109
  return;
1792
2110
  }
1793
- const resting = status;
1794
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1795
- moveTo(transferring(resting));
2111
+ const progress = { complete: false, reachable: !status.providerUnreachable };
2112
+ moveTo(transferring(status));
1796
2113
  try {
1797
2114
  await conversations.catchUp(progress);
1798
2115
  }
1799
2116
  catch {
1800
2117
  progress.reachable = false;
1801
2118
  }
1802
- finally {
1803
- if (!closed) {
1804
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1805
- }
1806
- }
2119
+ await rest(progress);
1807
2120
  }
1808
2121
  function close() {
1809
2122
  if (closed)
@@ -1825,6 +2138,7 @@ export function composeRuntime(options) {
1825
2138
  storageLossListeners.clear();
1826
2139
  statusListeners.clear();
1827
2140
  serviceModeListeners.clear();
2141
+ memberListeners.clear();
1828
2142
  readSide.close();
1829
2143
  queueSubscriptions.clear();
1830
2144
  roomDraftListeners.clear();
@@ -1856,13 +2170,16 @@ export function composeRuntime(options) {
1856
2170
  return planOf(input, roomFacts(reach), planReach(reach));
1857
2171
  }
1858
2172
  function watchPlan(plan, deliver) {
2173
+ materialize(reachedRooms(plan));
1859
2174
  return readSide.watch(plan, identityOf(plan), deliver);
1860
2175
  }
1861
2176
  async function searched(input, reach) {
1862
2177
  const plan = searchPlanOf(input);
1863
2178
  const selected = plan.documents === undefined && reach === null
1864
2179
  ? null
1865
- : (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);
1866
2183
  return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
1867
2184
  }
1868
2185
  const watch = (topic, input, deliver) => {
@@ -1888,6 +2205,8 @@ export function composeRuntime(options) {
1888
2205
  }
1889
2206
  if (topic === 'conversations')
1890
2207
  return conversations.watchList(deliver);
2208
+ if (topic === 'agents')
2209
+ return conversations.watchAgents(deliver);
1891
2210
  if (topic === 'presentation') {
1892
2211
  const listener = {
1893
2212
  app: input[0],
@@ -1942,6 +2261,13 @@ export function composeRuntime(options) {
1942
2261
  serviceModeListeners.delete(deliver);
1943
2262
  };
1944
2263
  }
2264
+ if (topic === 'member') {
2265
+ memberListeners.add(deliver);
2266
+ deliver(member);
2267
+ return () => {
2268
+ memberListeners.delete(deliver);
2269
+ };
2270
+ }
1945
2271
  if (topic === 'queue') {
1946
2272
  const queue = { locator: input[0], notify: deliver };
1947
2273
  queueSubscriptions.add(queue);
@@ -1959,7 +2285,7 @@ export function composeRuntime(options) {
1959
2285
  rooms() {
1960
2286
  return listed();
1961
2287
  },
1962
- query: ((input) => readSide.query(planned(input, null))),
2288
+ query: ((input) => rowsQuery(planned(input, null))),
1963
2289
  search: (input) => searched(input, null),
1964
2290
  types(locator) {
1965
2291
  return {
@@ -1988,20 +2314,20 @@ export function composeRuntime(options) {
1988
2314
  return exclusive(async () => {
1989
2315
  const parts = readParts(input);
1990
2316
  if (path === '' || normalize(path) !== path)
1991
- throw new Error('room-draft-malformed');
2317
+ throw runtimeError('room-draft-malformed');
1992
2318
  const local = await openLocal();
1993
2319
  const current = roomDrafts.get(room)?.get(path);
1994
2320
  if ((current?.revision ?? null) !== revision) {
1995
- return {
1996
- outcome: 'stale',
1997
- current: current === undefined ? null : roomDraftOf(current),
1998
- };
2321
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
1999
2322
  }
2323
+ const file = parts.file === undefined
2324
+ ? undefined
2325
+ : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
2000
2326
  const record = {
2001
2327
  locator: room,
2002
2328
  path,
2003
2329
  revision: roomDraftRevision(),
2004
- parts: held(current?.parts, parts),
2330
+ parts: held(current?.parts, parts, file),
2005
2331
  returned: current?.returned ?? null,
2006
2332
  };
2007
2333
  await local.keepRoomDraft(record);
@@ -2015,10 +2341,7 @@ export function composeRuntime(options) {
2015
2341
  const local = await openLocal();
2016
2342
  const current = roomDrafts.get(room)?.get(path);
2017
2343
  if (current === undefined || current.revision !== revision) {
2018
- return {
2019
- outcome: 'stale',
2020
- current: current === undefined ? null : roomDraftOf(current),
2021
- };
2344
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2022
2345
  }
2023
2346
  await local.discardRoomDraft(room, path);
2024
2347
  forgetRoomDraft(room, path);
@@ -2053,6 +2376,10 @@ export function composeRuntime(options) {
2053
2376
  history(locator, path, query) {
2054
2377
  return readHistory(locator, path, query);
2055
2378
  },
2379
+ binaryFile: {
2380
+ read: readBinaryPath,
2381
+ readReference: readBinaryReference,
2382
+ },
2056
2383
  call(name, input) {
2057
2384
  return invokeAction(name, input);
2058
2385
  },
@@ -2085,8 +2412,9 @@ export function composeRuntime(options) {
2085
2412
  const reachable = {
2086
2413
  md,
2087
2414
  watch,
2415
+ referrerScope: (referrer) => options.adapter.referrerScope(referrer),
2088
2416
  plan: planned,
2089
- query: (plan) => readSide.query(plan),
2417
+ query: (plan) => rowsQuery(plan),
2090
2418
  watchPlan,
2091
2419
  search: searched,
2092
2420
  };