@agentshouse/mdruntime 0.1.0-c6e4c19c13638 → 0.1.0-c70658e32ec08

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 +149 -91
  2. package/README.md +20 -10
  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 +70 -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 +26 -0
  37. package/dist/chat/list.d.ts.map +1 -0
  38. package/dist/chat/list.js +62 -0
  39. package/dist/chat/markdown.d.ts +20 -0
  40. package/dist/chat/markdown.d.ts.map +1 -0
  41. package/dist/chat/markdown.js +188 -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 +469 -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 +2 -1
  103. package/dist/host.d.ts.map +1 -1
  104. package/dist/host.js +12 -11
  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 +26 -16
  133. package/dist/react.d.ts.map +1 -1
  134. package/dist/react.js +10 -8
  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 +26 -1
  150. package/dist/save.d.ts.map +1 -1
  151. package/dist/save.js +345 -37
  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 +36 -10
  158. package/dist/store.d.ts.map +1 -1
  159. package/dist/store.js +310 -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,23 @@ 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
+ const paths = [
237
+ ...Object.keys(replica.visible.documents).filter(markdownPath),
238
+ ...Object.keys(replica.visible.binaries),
239
+ ];
240
+ return paths.filter((path) => !roots.some((root) => beneath(root, path))).length;
241
+ }
214
242
  function listed() {
215
- return structuredClone([...rooms.values()]);
243
+ return structuredClone([...rooms.values()].map((room) => {
244
+ const documents = documentsIn(room.locator);
245
+ counted.set(room.locator, documents);
246
+ return { ...room, documents };
247
+ }));
216
248
  }
217
249
  function roomFacts(reach = null) {
218
250
  const held = [...rooms.values()]
@@ -244,6 +276,10 @@ export function composeRuntime(options) {
244
276
  for (const listener of storageLossListeners)
245
277
  listener(storageLoss);
246
278
  }
279
+ function emitMember() {
280
+ for (const listener of memberListeners)
281
+ listener(member);
282
+ }
247
283
  function moveTo(next) {
248
284
  if (closed)
249
285
  return;
@@ -263,6 +299,8 @@ export function composeRuntime(options) {
263
299
  listener(shown);
264
300
  }
265
301
  function entryOf(batch) {
302
+ if (localSaves)
303
+ return Object.freeze({ batch, state: 'local' });
266
304
  const state = pendings.get(batch.id);
267
305
  if (state?.accepted !== undefined) {
268
306
  return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
@@ -270,10 +308,7 @@ export function composeRuntime(options) {
270
308
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
271
309
  }
272
310
  function entriesOf(locator) {
273
- const replica = replicas.get(locator);
274
- if (replica === undefined)
275
- return [];
276
- return replica.queue.map(entryOf);
311
+ return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
277
312
  }
278
313
  function emitQueue(locator) {
279
314
  const entries = entriesOf(locator);
@@ -283,12 +318,47 @@ export function composeRuntime(options) {
283
318
  }
284
319
  }
285
320
  function applyChangeSet(set) {
321
+ const held = everyRoom
322
+ ? set
323
+ : { ...set, documents: set.documents.filter(({ locator }) => materialized.has(locator)) };
324
+ readSide.apply(held);
325
+ searchIndex.changed(held);
326
+ }
327
+ function materialize(locators) {
328
+ if (everyRoom)
329
+ return;
330
+ if (locators === null)
331
+ everyRoom = true;
332
+ const sets = [];
333
+ for (const locator of locators ?? [...replicas.keys()]) {
334
+ if (materialized.has(locator))
335
+ continue;
336
+ materialized.add(locator);
337
+ const replica = replicas.get(locator);
338
+ if (replica !== undefined)
339
+ sets.push(changeSet(locator, null, replica));
340
+ }
341
+ if (sets.length === 0)
342
+ return;
343
+ const set = mergeChangeSets(sets);
286
344
  readSide.apply(set);
287
345
  searchIndex.changed(set);
288
346
  }
347
+ function reachedRooms(plan) {
348
+ if (plan.rooms !== null)
349
+ return plan.rooms;
350
+ if (plan.reach === null)
351
+ return null;
352
+ return [...plan.reach.rooms, ...plan.reach.documents.map(roomOfKey)];
353
+ }
354
+ function rowsQuery(plan) {
355
+ materialize(reachedRooms(plan));
356
+ return readSide.query(plan);
357
+ }
289
358
  function publishChangeSet(set) {
290
359
  applyChangeSet(set);
291
- if (set.rooms.length > 0)
360
+ if (set.rooms.length > 0 ||
361
+ [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
292
362
  emitRooms();
293
363
  for (const locator of set.queues)
294
364
  emitQueue(locator);
@@ -335,10 +405,7 @@ export function composeRuntime(options) {
335
405
  const revoked = { status: 'unavailable', reason: 'revoked' };
336
406
  try {
337
407
  const local = await openLocal();
338
- await local.revokeModels([...replicas.values()].map((replica) => ({
339
- record: replica.confirmed.record,
340
- queue: replica.queue,
341
- })));
408
+ await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
342
409
  }
343
410
  catch {
344
411
  journal.record('model revocation persistence failed');
@@ -362,10 +429,7 @@ export function composeRuntime(options) {
362
429
  model: revoked,
363
430
  selectionModel: revoked,
364
431
  };
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 } });
432
+ replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
369
433
  }
370
434
  rooms.set(locator, {
371
435
  ...room,
@@ -382,7 +446,8 @@ export function composeRuntime(options) {
382
446
  }
383
447
  finally {
384
448
  refusal = {
385
- reason,
449
+ code: reason,
450
+ retryable: false,
386
451
  reauthorizationOffered: reason === 'revoked-authorization',
387
452
  refusal: provider,
388
453
  };
@@ -407,7 +472,6 @@ export function composeRuntime(options) {
407
472
  },
408
473
  providerOwnedPaths: room.providerOwnedPaths,
409
474
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
410
- attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
411
475
  synchronizationAvailable,
412
476
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
413
477
  });
@@ -432,7 +496,14 @@ export function composeRuntime(options) {
432
496
  emitRooms();
433
497
  }
434
498
  function unlistedHeld(listed) {
435
- return [...new Set([...rooms.keys(), ...roomDrafts.keys()])].filter((locator) => !listed.has(locator));
499
+ return [
500
+ ...new Set([
501
+ ...rooms.keys(),
502
+ ...roomDrafts.keys(),
503
+ ...transfers.keys(),
504
+ ...queuesWithoutReplica.keys(),
505
+ ]),
506
+ ].filter((locator) => !listed.has(locator));
436
507
  }
437
508
  async function dropUnlisted(listed, local) {
438
509
  if (unlistedHeld(listed).length === 0)
@@ -446,11 +517,13 @@ export function composeRuntime(options) {
446
517
  drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
447
518
  }));
448
519
  for (const locator of unlisted) {
449
- for (const batch of replicas.get(locator)?.queue ?? [])
520
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
450
521
  pendings.delete(batch.id);
451
522
  replicas.delete(locator);
523
+ queuesWithoutReplica.delete(locator);
452
524
  rooms.delete(locator);
453
525
  roomDrafts.delete(locator);
526
+ transfers.delete(locator);
454
527
  unreadable.delete(locator);
455
528
  stale.delete(locator);
456
529
  scopedRefusals.delete(locator);
@@ -487,7 +560,7 @@ export function composeRuntime(options) {
487
560
  return model;
488
561
  }
489
562
  async function hydrate(opened) {
490
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
563
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
491
564
  opened.all(),
492
565
  opened.queues(),
493
566
  opened.roomDrafts(),
@@ -497,9 +570,15 @@ export function composeRuntime(options) {
497
570
  opened.lastCompleted(),
498
571
  opened.providerLimits(),
499
572
  opened.capabilities(),
573
+ opened.member(),
574
+ opened.transfers(),
500
575
  ]);
576
+ transfers.clear();
577
+ for (const record of staged)
578
+ transfers.set(record.locator, record);
501
579
  limits = declared;
502
580
  capabilities = named;
581
+ member = known;
503
582
  roomDrafts.clear();
504
583
  for (const record of kept)
505
584
  rememberRoomDraft(record);
@@ -515,18 +594,17 @@ export function composeRuntime(options) {
515
594
  }
516
595
  const previousReplicas = new Map(replicas);
517
596
  replicas.clear();
597
+ queuesWithoutReplica.clear();
598
+ const replicated = new Set(records.map((record) => record.locator));
599
+ for (const [locator, queue] of queuedBatches) {
600
+ if (!replicated.has(locator))
601
+ queuesWithoutReplica.set(locator, queue);
602
+ }
518
603
  for (const record of records) {
519
604
  const model = restored(modelKey(record.locator, 'collection'), record.model);
520
605
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
521
606
  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
- };
607
+ const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
530
608
  replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
531
609
  const existing = rooms.get(record.locator);
532
610
  if (existing === undefined)
@@ -535,7 +613,6 @@ export function composeRuntime(options) {
535
613
  rooms.set(record.locator, {
536
614
  ...existing,
537
615
  roomVersion: record.roomVersion,
538
- attachments: record.attachments,
539
616
  operations: record.operations,
540
617
  replica: record.replica,
541
618
  model: modelTypes(model),
@@ -547,10 +624,6 @@ export function composeRuntime(options) {
547
624
  },
548
625
  });
549
626
  }
550
- if (read.kind === 'refused') {
551
- refusedSource(record, read);
552
- declarationRefusals.add(record.locator);
553
- }
554
627
  }
555
628
  return {
556
629
  lastCompleted,
@@ -563,6 +636,8 @@ export function composeRuntime(options) {
563
636
  applyChangeSet(read);
564
637
  if (replicas.size > 0)
565
638
  emitRooms();
639
+ if (member !== null)
640
+ emitMember();
566
641
  for (const locator of locators)
567
642
  emitQueue(locator);
568
643
  publishRoomDrafts(false);
@@ -610,10 +685,15 @@ export function composeRuntime(options) {
610
685
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
611
686
  }
612
687
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
613
- const withdrawn = withdrawals(confirmed, queue, withdrawable);
688
+ const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
689
+ const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
614
690
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
691
+ const unqueued = withdrawn.map((batch) => ({
692
+ batch,
693
+ returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
694
+ }));
615
695
  const returning = [
616
- ...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
696
+ ...[...unqueued].reverse(),
617
697
  ...(settling.leaving === undefined ? [] : [settling.leaving]),
618
698
  ];
619
699
  const written = new Map();
@@ -631,25 +711,44 @@ export function composeRuntime(options) {
631
711
  });
632
712
  }
633
713
  }
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 ?? []) {
714
+ await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
715
+ for (const batch of previousQueue) {
642
716
  if (!kept.includes(batch))
643
717
  pendings.delete(batch.id);
644
718
  }
645
719
  for (const record of written.values())
646
720
  rememberRoomDraft(record);
647
- for (const batch of withdrawn)
648
- journal.record(`outcome ${locator} ${batch.id} withdrawn`);
721
+ try {
722
+ await local.write({
723
+ next: confirmed.record,
724
+ previous: before?.confirmed.record ?? null,
725
+ ...(settling.stored === undefined ? {} : { stored: settling.stored }),
726
+ });
727
+ }
728
+ catch (error) {
729
+ const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
730
+ if (held !== null)
731
+ replicas.set(locator, held);
732
+ else if (kept.length > 0)
733
+ queuesWithoutReplica.set(locator, kept);
734
+ else
735
+ queuesWithoutReplica.delete(locator);
736
+ publishChangeSet(changeSet(locator, before ?? null, held, {
737
+ queue: kept.length !== previousQueue.length,
738
+ drafts: [...written.keys()],
739
+ }));
740
+ moveTo({ ...status, ...attention() });
741
+ throw error;
742
+ }
743
+ queuesWithoutReplica.delete(locator);
744
+ for (const { batch, returned } of unqueued) {
745
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
746
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
747
+ }
649
748
  replicas.set(locator, replicaOf(confirmed, kept, before));
650
749
  return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
651
750
  room: settling.room,
652
- queue: kept.length !== (before?.queue.length ?? 0),
751
+ queue: kept.length !== previousQueue.length,
653
752
  drafts: [...written.keys()],
654
753
  });
655
754
  }
@@ -684,7 +783,8 @@ export function composeRuntime(options) {
684
783
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
685
784
  };
686
785
  }
687
- function publish(local, room, confirmed, base) {
786
+ function publish(local, room, confirmed, landing = {}) {
787
+ const { base, stored } = landing;
688
788
  return exclusive(async () => {
689
789
  if (!continuing())
690
790
  return true;
@@ -695,13 +795,15 @@ export function composeRuntime(options) {
695
795
  return false;
696
796
  }
697
797
  adoptModels(room.locator, confirmed.record);
698
- const queue = prior?.queue ?? [];
798
+ const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
699
799
  const roomVersion = confirmed.record.roomVersion;
700
800
  const accepted = queue.filter((batch) => {
701
801
  const accepted = pendings.get(batch.id)?.accepted;
702
802
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
703
803
  });
704
- const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
804
+ const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
805
+ if (stored !== undefined)
806
+ transfers.delete(room.locator);
705
807
  for (const batch of accepted)
706
808
  pendings.delete(batch.id);
707
809
  stale.delete(room.locator);
@@ -720,22 +822,40 @@ export function composeRuntime(options) {
720
822
  const replica = replicas.get(locator);
721
823
  const room = rooms.get(locator);
722
824
  if (replica === undefined || room === undefined) {
723
- throw new Error(`No Local Replica for ${locator}`);
825
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
724
826
  }
725
827
  const saved = [];
726
828
  for (const { path, revision } of named) {
727
829
  const record = roomDrafts.get(locator)?.get(path);
728
830
  if (record === undefined || record.revision !== revision) {
729
- return { outcome: 'refused', rule: 'stale', file: path };
831
+ return { outcome: 'refused', code: 'stale', retryable: false, file: path };
730
832
  }
731
833
  if (!saved.some((draft) => draft.path === path))
732
834
  saved.push({ path, parts: record.parts });
733
835
  }
734
836
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
735
837
  if (changed.length > 0)
736
- return { outcome: 'refused', rule: 'changed-underneath', parts: changed };
838
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
737
839
  const parts = saved.map(({ path, parts }) => changesOf(path, parts));
738
- const built = buildBatch({
840
+ const changes = parts.flatMap(({ changes }) => changes);
841
+ const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
842
+ if (requeue.kind === 'refused')
843
+ return { outcome: 'refused', ...requeue.refusal };
844
+ const foreign = [...requeue.withdrawn.keys()].some((origin) => {
845
+ const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
846
+ return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
847
+ });
848
+ if (foreign) {
849
+ return {
850
+ outcome: 'refused',
851
+ code: 'unknown-revision',
852
+ retryable: false,
853
+ file: [...requeue.queued].at(-1),
854
+ };
855
+ }
856
+ const withdrawing = requeue.withdrawn.size > 0;
857
+ const input = {
858
+ local: localSaves,
739
859
  baseRoomVersion: replica.confirmed.record.roomVersion,
740
860
  composition: compositionFor(replica.confirmed.record),
741
861
  operations: room.operations,
@@ -743,29 +863,86 @@ export function composeRuntime(options) {
743
863
  providerLimits: limits,
744
864
  handle: room.handle,
745
865
  visible: replica.visible,
746
- changes: parts.flatMap(({ changes }) => changes),
866
+ changes,
747
867
  bases: parts.flatMap(({ bases }) => bases),
748
868
  writtenBy,
869
+ };
870
+ const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
871
+ if (checked?.kind === 'refused')
872
+ return { outcome: 'refused', ...checked.refusal };
873
+ const owners = [...requeue.owners].flatMap((origin) => {
874
+ const batch = requeue.withdrawn.get(origin);
875
+ return batch === undefined || batch === null ? [] : [{ origin, batch }];
749
876
  });
750
- if (built.kind === 'refused')
877
+ if (owners.length > 1) {
878
+ return {
879
+ outcome: 'refused',
880
+ code: 'unknown-revision',
881
+ retryable: false,
882
+ file: [...requeue.queued].at(-1),
883
+ };
884
+ }
885
+ const built = !withdrawing
886
+ ? buildBatch(input)
887
+ : requeue.changes.length === 0
888
+ ? null
889
+ : buildBatch({
890
+ ...input,
891
+ visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
892
+ changes: requeue.changes,
893
+ });
894
+ if (built?.kind === 'refused')
751
895
  return { outcome: 'refused', ...built.refusal };
752
- const batches = [...replica.queue, built.batch];
896
+ const [owner] = owners;
897
+ const join = built === null || owner === undefined
898
+ ? null
899
+ : joined(requeue.queue, owner.batch, built.batch);
900
+ if (join?.kind === 'refused')
901
+ return { outcome: 'refused', ...join.refusal };
902
+ const withdrawn = new Map(requeue.withdrawn);
903
+ if (join !== null && owner !== undefined)
904
+ withdrawn.set(owner.origin, join.batch);
905
+ const drafted = new Map();
906
+ for (const [origin, replaced] of withdrawn) {
907
+ if (replaced === null)
908
+ continue;
909
+ const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
910
+ drafted.set(replaced.id, replaced === join?.batch
911
+ ? [
912
+ ...carried.map((held) => joinedDraft(held, saved)),
913
+ ...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
914
+ ]
915
+ : carried);
916
+ }
917
+ if (built !== null && join === null)
918
+ drafted.set(built.batch.id, saved);
919
+ const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
920
+ for (const replaced of withdrawn.values()) {
921
+ if (replaced === null || !batches.includes(replaced))
922
+ continue;
923
+ const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
924
+ if (limited !== null)
925
+ return { outcome: 'refused', ...limited };
926
+ }
753
927
  const consumed = saved.map(({ path }) => path);
754
928
  try {
755
- await local.write({
756
- next: replica.confirmed.record,
757
- previous: replica.confirmed.record,
929
+ await local.keepAuthored({
930
+ locator,
758
931
  queue: batches,
759
- previousQueue: replica.queue,
760
- saved,
932
+ saved: drafted,
761
933
  consumedRoomDrafts: consumed,
762
934
  });
763
935
  }
764
936
  catch {
765
- return { outcome: 'refused', rule: 'durable-write' };
937
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
938
+ }
939
+ for (const origin of withdrawn.keys())
940
+ pendings.delete(origin);
941
+ for (const [id, drafts] of drafted) {
942
+ pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
766
943
  }
767
- pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
768
- recorded = false;
944
+ if (!localSaves)
945
+ recorded = false;
769
946
  for (const path of consumed)
770
947
  forgetRoomDraft(locator, path);
771
948
  replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
@@ -773,9 +950,12 @@ export function composeRuntime(options) {
773
950
  queue: true,
774
951
  drafts: consumed,
775
952
  }));
776
- moveTo({ ...saving(status), ...attention() });
953
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
777
954
  submitLater(locator);
778
- return { outcome: 'saved', identifier: built.batch.id };
955
+ const identifier = join?.batch.id ??
956
+ built?.batch.id ??
957
+ [...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
958
+ return { outcome: 'saved', identifier };
779
959
  }
780
960
  async function mark(local, locator, identifier, submission, lost) {
781
961
  const state = pendings.get(identifier);
@@ -801,16 +981,15 @@ export function composeRuntime(options) {
801
981
  }
802
982
  async function readHistory(locator, path, query = {}) {
803
983
  if (closed || refusal !== null)
804
- return { kind: 'transport-uncertainty' };
984
+ return UNCERTAIN;
805
985
  try {
806
986
  await ensureAuthorization();
807
987
  }
808
988
  catch {
809
- return { kind: 'transport-uncertainty' };
810
- }
811
- if (closed || authorization === undefined || refusal !== null) {
812
- return { kind: 'transport-uncertainty' };
989
+ return UNCERTAIN;
813
990
  }
991
+ if (closed || authorization === undefined || refusal !== null)
992
+ return UNCERTAIN;
814
993
  const transfer = ['transfer', 'history', locator, path];
815
994
  if (query.revision !== undefined)
816
995
  transfer.push(query.revision);
@@ -832,13 +1011,97 @@ export function composeRuntime(options) {
832
1011
  }
833
1012
  if (result.kind === 'revoked-authorization')
834
1013
  await refuse('revoked-authorization');
835
- return result;
1014
+ return historyAnswer(result);
836
1015
  }
837
1016
  catch {
838
1017
  journal.record('outcome history transport-uncertainty');
839
- return { kind: 'transport-uncertainty' };
1018
+ return UNCERTAIN;
840
1019
  }
841
1020
  }
1021
+ function queuedBlob(locator, path, localId) {
1022
+ for (const batch of replicas.get(locator)?.queue ?? []) {
1023
+ const change = batch.changes[queuedFile(batch, path, localId)];
1024
+ if (change?.kind === 'binary')
1025
+ return change.blob;
1026
+ }
1027
+ return null;
1028
+ }
1029
+ async function readBinary(request) {
1030
+ if (closed || refusal !== null)
1031
+ return UNCERTAIN;
1032
+ try {
1033
+ await ensureAuthorization();
1034
+ }
1035
+ catch {
1036
+ return UNCERTAIN;
1037
+ }
1038
+ if (closed || authorization === undefined || refusal !== null)
1039
+ return UNCERTAIN;
1040
+ journal.record(request.kind === 'path'
1041
+ ? `transfer binary ${request.locator} ${request.path}`
1042
+ : `transfer binary ${request.fileRef}`);
1043
+ try {
1044
+ const result = await options.adapter.binary(authorization, request);
1045
+ journal.record(`outcome binary ${result.kind}`);
1046
+ if (result.kind === 'scoped-refusal') {
1047
+ const room = replicas.get(result.locator)?.confirmed.record;
1048
+ if (room !== undefined)
1049
+ await scoped(result, room, await openLocal());
1050
+ }
1051
+ if (result.kind === 'revoked-authorization')
1052
+ await refuse('revoked-authorization');
1053
+ return binaryAnswer(result);
1054
+ }
1055
+ catch {
1056
+ journal.record('outcome binary transport-uncertainty');
1057
+ return UNCERTAIN;
1058
+ }
1059
+ }
1060
+ async function readBinaryPath(locator, path, revision) {
1061
+ await ready;
1062
+ if (revision !== undefined)
1063
+ return readBinary({ kind: 'path', locator, path, revision });
1064
+ const visible = replicas.get(locator)?.visible;
1065
+ const held = visible?.binaries[path];
1066
+ if (visible === undefined || held === undefined)
1067
+ return absentBinary(path);
1068
+ if (held.state === 'pending') {
1069
+ const blob = queuedBlob(locator, path, held.localId);
1070
+ return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1071
+ }
1072
+ const record = binaryRecord(path, held, visible.revisions[path]);
1073
+ if (record.state !== 'accepted')
1074
+ return absentBinary(path);
1075
+ const replica = replicas.get(locator);
1076
+ const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
1077
+ const holds = (at) => {
1078
+ const kept = replica?.confirmed.record.binaries[at];
1079
+ return (kept?.state === 'accepted' &&
1080
+ kept.fileRef === record.fileRef &&
1081
+ replica?.confirmed.record.revisions[at] === record.revision);
1082
+ };
1083
+ let confirmed = path;
1084
+ for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
1085
+ const change = queued[index];
1086
+ if (change.kind === 'move' && change.to === confirmed)
1087
+ confirmed = change.path;
1088
+ }
1089
+ return readBinary({
1090
+ kind: 'path',
1091
+ locator,
1092
+ path: confirmed,
1093
+ revision: record.revision,
1094
+ fileRef: record.fileRef,
1095
+ });
1096
+ }
1097
+ async function readBinaryReference(reference) {
1098
+ await ready;
1099
+ return readBinary({
1100
+ kind: 'reference',
1101
+ fileRef: reference.fileRef,
1102
+ referrer: reference.referrer,
1103
+ });
1104
+ }
842
1105
  function declaredAgents() {
843
1106
  for (const capability of capabilities) {
844
1107
  if (capability.conversationAgents !== undefined)
@@ -858,18 +1121,17 @@ export function composeRuntime(options) {
858
1121
  async function invokeAction(name, input) {
859
1122
  await ready;
860
1123
  if (closed || refusal !== null)
861
- return { kind: 'transport-uncertainty' };
1124
+ return UNCERTAIN;
862
1125
  if (declaredAction(name) === undefined)
863
- return { kind: 'undeclared' };
1126
+ return undeclared(name);
864
1127
  try {
865
1128
  await ensureAuthorization();
866
1129
  }
867
1130
  catch {
868
- return { kind: 'transport-uncertainty' };
869
- }
870
- if (closed || authorization === undefined || refusal !== null) {
871
- return { kind: 'transport-uncertainty' };
1131
+ return UNCERTAIN;
872
1132
  }
1133
+ if (closed || authorization === undefined || refusal !== null)
1134
+ return UNCERTAIN;
873
1135
  journal.record(`transfer action ${name}`);
874
1136
  try {
875
1137
  const result = await options.adapter.action(authorization, { name, input });
@@ -883,11 +1145,11 @@ export function composeRuntime(options) {
883
1145
  journal.record(`outcome action ${result.kind}`);
884
1146
  if (result.kind === 'rejected')
885
1147
  journal.record(`refusal rejected ${result.code}`);
886
- return result;
1148
+ return calledResult(result);
887
1149
  }
888
1150
  catch {
889
1151
  journal.record('outcome action transport-uncertainty');
890
- return { kind: 'transport-uncertainty' };
1152
+ return UNCERTAIN;
891
1153
  }
892
1154
  }
893
1155
  function continuing() {
@@ -918,14 +1180,33 @@ export function composeRuntime(options) {
918
1180
  throw error;
919
1181
  }
920
1182
  }
921
- async function pullBootstrap(authorized, room) {
1183
+ function staging(local) {
1184
+ return {
1185
+ held: (locator) => transfers.get(locator) ?? null,
1186
+ staged: (record) => local.staged(record),
1187
+ stage: (record, page) => exclusive(async () => {
1188
+ if (!continuing())
1189
+ return false;
1190
+ await local.stage(record, page);
1191
+ transfers.set(record.locator, record);
1192
+ return true;
1193
+ }),
1194
+ abandon: (locator) => exclusive(async () => {
1195
+ if (!continuing())
1196
+ return;
1197
+ await local.abandon(locator);
1198
+ transfers.delete(locator);
1199
+ }),
1200
+ };
1201
+ }
1202
+ async function pullBootstrap(authorized, room, local) {
922
1203
  if (!continuing())
923
1204
  return unconfirmed;
924
1205
  try {
925
1206
  const result = await bootstrapRoom(options.adapter, authorized, room, {
926
1207
  collection: modelOf(room.locator),
927
1208
  selection: modelOf(room.locator, 'typed-selection'),
928
- }, (models, held) => remodel(room.locator, models, held), continuing);
1209
+ }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
929
1210
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
930
1211
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
931
1212
  return result;
@@ -953,7 +1234,7 @@ export function composeRuntime(options) {
953
1234
  return null;
954
1235
  }
955
1236
  if (refreshed.kind === 'advanced') {
956
- if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
1237
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
957
1238
  const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
958
1239
  const reached = now !== undefined &&
959
1240
  options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
@@ -965,12 +1246,12 @@ export function composeRuntime(options) {
965
1246
  if (refreshed.kind !== 'beyond-horizon')
966
1247
  return refreshed;
967
1248
  }
968
- const bootstrapped = await pullBootstrap(authorized, room);
1249
+ const bootstrapped = await pullBootstrap(authorized, room, local);
969
1250
  if (!continuing())
970
1251
  return unconfirmed;
971
1252
  if (bootstrapped.kind !== 'staged')
972
1253
  return bootstrapped;
973
- await publish(local, room, bootstrapped.confirmed);
1254
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
974
1255
  return null;
975
1256
  }
976
1257
  async function withhold(withheld, room, local, progress) {
@@ -1002,6 +1283,7 @@ export function composeRuntime(options) {
1002
1283
  function refusedSource(room, { refusal }) {
1003
1284
  sourceRefusals.set(room.locator, {
1004
1285
  code: refusal.rule,
1286
+ retryable: false,
1005
1287
  fields: {
1006
1288
  file: refusal.file,
1007
1289
  ...(refusal.location === undefined ? {} : { location: refusal.location }),
@@ -1016,8 +1298,16 @@ export function composeRuntime(options) {
1016
1298
  }
1017
1299
  function leaving(batch, result) {
1018
1300
  if (result.kind === 'expired')
1019
- return { batch, returned: { kind: 'expired' } };
1020
- return { batch, returned: { kind: result.kind, code: result.code, fields: result.fields } };
1301
+ return { batch, returned: returnedBy('expired') };
1302
+ return {
1303
+ batch,
1304
+ returned: {
1305
+ kind: result.kind,
1306
+ code: result.code,
1307
+ fields: result.fields,
1308
+ retryable: result.retryable,
1309
+ },
1310
+ };
1021
1311
  }
1022
1312
  function logAccepted(locator, submitted, accepted) {
1023
1313
  journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
@@ -1035,7 +1325,7 @@ export function composeRuntime(options) {
1035
1325
  logAccepted(room.locator, submitted, accepted);
1036
1326
  const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1037
1327
  ? { 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)
1328
+ : applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
1039
1329
  ? accepted.roomVersion
1040
1330
  : held, accepted.roomVersion);
1041
1331
  if (applied.kind !== 'staged') {
@@ -1181,7 +1471,8 @@ export function composeRuntime(options) {
1181
1471
  return;
1182
1472
  }
1183
1473
  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)
1474
+ leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1475
+ (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1185
1476
  ? { ...left, returned: null }
1186
1477
  : left,
1187
1478
  });
@@ -1242,7 +1533,7 @@ export function composeRuntime(options) {
1242
1533
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1243
1534
  };
1244
1535
  const reclassified = sameMembership(declared, held.confirmed.record)
1245
- ? readConfirmed(declared, held.confirmed)
1536
+ ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
1246
1537
  : readReclassified(declared, held.confirmed);
1247
1538
  const record = reclassified.kind === 'staged'
1248
1539
  ? declared
@@ -1250,12 +1541,10 @@ export function composeRuntime(options) {
1250
1541
  ...declared,
1251
1542
  typedSelection: held.confirmed.record.typedSelection,
1252
1543
  };
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
- });
1544
+ const read = reclassified.kind === 'deferred'
1545
+ ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
1546
+ : reclassified;
1547
+ await local.write({ next: record, previous: held.confirmed.record });
1259
1548
  if (read.kind === 'staged') {
1260
1549
  replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1261
1550
  if (declarationRefusals.delete(room.locator)) {
@@ -1282,7 +1571,11 @@ export function composeRuntime(options) {
1282
1571
  });
1283
1572
  }
1284
1573
  async function scoped(result, room, local) {
1285
- scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
1574
+ scopedRefusals.set(room.locator, {
1575
+ code: result.code,
1576
+ fields: result.fields,
1577
+ retryable: result.retryable,
1578
+ });
1286
1579
  let scopedAway = room;
1287
1580
  if (result.scope === 'model')
1288
1581
  await retainModel(local, room, 'collection', {
@@ -1319,7 +1612,6 @@ export function composeRuntime(options) {
1319
1612
  roomVersion: record.roomVersion,
1320
1613
  documents: record.documents,
1321
1614
  revisions: record.revisions,
1322
- attachments: record.attachments,
1323
1615
  },
1324
1616
  ];
1325
1617
  });
@@ -1330,8 +1622,10 @@ export function composeRuntime(options) {
1330
1622
  const outcome = prepared[index];
1331
1623
  return outcome?.status === 'fulfilled' && outcome.value;
1332
1624
  });
1333
- if (attempt === 'opened')
1625
+ if (attempt === 'opened') {
1334
1626
  options.adapter.roomStream?.hold(heldRooms(ready));
1627
+ handed = true;
1628
+ }
1335
1629
  const loaded = await Promise.allSettled(ready
1336
1630
  .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1337
1631
  .map((room) => loadRoom(room, local, progress)));
@@ -1397,14 +1691,14 @@ export function composeRuntime(options) {
1397
1691
  async function reload(room, local, progress) {
1398
1692
  if (discoveryPaused || authorization === undefined)
1399
1693
  return;
1400
- const bootstrapped = await pullBootstrap(authorization, room);
1694
+ const bootstrapped = await pullBootstrap(authorization, room, local);
1401
1695
  if (!continuing())
1402
1696
  return;
1403
1697
  if (bootstrapped.kind !== 'staged') {
1404
1698
  await withhold(bootstrapped, room, local, progress);
1405
1699
  return;
1406
1700
  }
1407
- await publish(local, room, bootstrapped.confirmed);
1701
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1408
1702
  await caughtUp(room, local, progress);
1409
1703
  }
1410
1704
  async function discoverRooms(local, progress, attempt) {
@@ -1434,15 +1728,16 @@ export function composeRuntime(options) {
1434
1728
  }
1435
1729
  progress.reachable = false;
1436
1730
  progress.complete = false;
1437
- if (attempt === 'opened') {
1438
- options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1439
- }
1440
1731
  notifyReady();
1441
1732
  return;
1442
1733
  }
1443
1734
  if (result.kind === 'contract-unavailable') {
1444
1735
  journal.record(`connection contract unavailable ${result.code}`);
1445
- await refuse('missing-required-guarantee', { code: result.code, fields: result.fields });
1736
+ await refuse('missing-required-guarantee', {
1737
+ code: result.code,
1738
+ fields: result.fields,
1739
+ retryable: result.retryable,
1740
+ });
1446
1741
  notifyReady();
1447
1742
  return;
1448
1743
  }
@@ -1483,6 +1778,12 @@ export function composeRuntime(options) {
1483
1778
  limits = result.providerLimits;
1484
1779
  await local.declare(limits, capabilities);
1485
1780
  }
1781
+ const signedIn = result.member ?? null;
1782
+ if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
1783
+ await local.keepMember(signedIn);
1784
+ member = signedIn;
1785
+ emitMember();
1786
+ }
1486
1787
  await applyDiscovery(result.rooms, local);
1487
1788
  await synchronize(result.rooms, local, progress, attempt);
1488
1789
  }
@@ -1512,9 +1813,12 @@ export function composeRuntime(options) {
1512
1813
  }
1513
1814
  const progress = { complete: true, reachable: true };
1514
1815
  underway = attempt === 'opened' ? progress : null;
1816
+ if (attempt === 'opened')
1817
+ streamed = progress;
1515
1818
  complete = false;
1516
1819
  recorded = false;
1517
1820
  moveTo(transferring(status));
1821
+ handed = false;
1518
1822
  try {
1519
1823
  const local = await openLocal();
1520
1824
  if (closed)
@@ -1526,16 +1830,22 @@ export function composeRuntime(options) {
1526
1830
  }
1527
1831
  catch {
1528
1832
  progress.complete = false;
1529
- if (attempt === 'opened') {
1530
- options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1531
- }
1833
+ if (attempt === 'opened' && !handed)
1834
+ progress.reachable = false;
1532
1835
  }
1533
1836
  finally {
1534
1837
  notifyReady();
1535
1838
  }
1536
- if (attempt === 'opened')
1839
+ if (attempt !== 'opened') {
1840
+ await finishOpening(progress);
1537
1841
  return;
1538
- await finishOpening(progress);
1842
+ }
1843
+ if (handed || closed || progress.reachable)
1844
+ return;
1845
+ underway = null;
1846
+ journal.record('connection reopen');
1847
+ await rest(progress);
1848
+ options.adapter.roomStream?.reopen();
1539
1849
  });
1540
1850
  }
1541
1851
  function closedSocket(revoked) {
@@ -1545,9 +1855,14 @@ export function composeRuntime(options) {
1545
1855
  void refuse('revoked-authorization');
1546
1856
  return;
1547
1857
  }
1548
- if (opened || status.providerUnreachable)
1549
- return;
1550
- moveTo({ ...status, providerUnreachable: true });
1858
+ complete = false;
1859
+ if (streamed !== null)
1860
+ streamed.complete = false;
1861
+ moveTo({
1862
+ ...status,
1863
+ state: status.state === 'synced' ? 'syncing' : status.state,
1864
+ providerUnreachable: status.providerUnreachable || !opened,
1865
+ });
1551
1866
  }
1552
1867
  function applyOpening(expired) {
1553
1868
  return task(async () => {
@@ -1602,6 +1917,8 @@ export function composeRuntime(options) {
1602
1917
  }
1603
1918
  }
1604
1919
  function submitLater(locator) {
1920
+ if (localSaves)
1921
+ return;
1605
1922
  const running = submitting.get(locator);
1606
1923
  if (running !== undefined) {
1607
1924
  running.again = true;
@@ -1681,7 +1998,7 @@ export function composeRuntime(options) {
1681
1998
  }
1682
1999
  }
1683
2000
  moveTo({
1684
- state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
2001
+ state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
1685
2002
  lastCompleted,
1686
2003
  providerUnreachable: !progress.reachable,
1687
2004
  ...attention(),
@@ -1790,20 +2107,15 @@ export function composeRuntime(options) {
1790
2107
  show();
1791
2108
  return;
1792
2109
  }
1793
- const resting = status;
1794
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1795
- moveTo(transferring(resting));
2110
+ const progress = { complete: false, reachable: !status.providerUnreachable };
2111
+ moveTo(transferring(status));
1796
2112
  try {
1797
2113
  await conversations.catchUp(progress);
1798
2114
  }
1799
2115
  catch {
1800
2116
  progress.reachable = false;
1801
2117
  }
1802
- finally {
1803
- if (!closed) {
1804
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1805
- }
1806
- }
2118
+ await rest(progress);
1807
2119
  }
1808
2120
  function close() {
1809
2121
  if (closed)
@@ -1825,6 +2137,7 @@ export function composeRuntime(options) {
1825
2137
  storageLossListeners.clear();
1826
2138
  statusListeners.clear();
1827
2139
  serviceModeListeners.clear();
2140
+ memberListeners.clear();
1828
2141
  readSide.close();
1829
2142
  queueSubscriptions.clear();
1830
2143
  roomDraftListeners.clear();
@@ -1856,13 +2169,16 @@ export function composeRuntime(options) {
1856
2169
  return planOf(input, roomFacts(reach), planReach(reach));
1857
2170
  }
1858
2171
  function watchPlan(plan, deliver) {
2172
+ materialize(reachedRooms(plan));
1859
2173
  return readSide.watch(plan, identityOf(plan), deliver);
1860
2174
  }
1861
2175
  async function searched(input, reach) {
1862
2176
  const plan = searchPlanOf(input);
1863
2177
  const selected = plan.documents === undefined && reach === null
1864
2178
  ? null
1865
- : (await readSide.query(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
2179
+ : (await rowsQuery(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
2180
+ if (selected === null)
2181
+ materialize(null);
1866
2182
  return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
1867
2183
  }
1868
2184
  const watch = (topic, input, deliver) => {
@@ -1888,6 +2204,8 @@ export function composeRuntime(options) {
1888
2204
  }
1889
2205
  if (topic === 'conversations')
1890
2206
  return conversations.watchList(deliver);
2207
+ if (topic === 'agents')
2208
+ return conversations.watchAgents(deliver);
1891
2209
  if (topic === 'presentation') {
1892
2210
  const listener = {
1893
2211
  app: input[0],
@@ -1942,6 +2260,13 @@ export function composeRuntime(options) {
1942
2260
  serviceModeListeners.delete(deliver);
1943
2261
  };
1944
2262
  }
2263
+ if (topic === 'member') {
2264
+ memberListeners.add(deliver);
2265
+ deliver(member);
2266
+ return () => {
2267
+ memberListeners.delete(deliver);
2268
+ };
2269
+ }
1945
2270
  if (topic === 'queue') {
1946
2271
  const queue = { locator: input[0], notify: deliver };
1947
2272
  queueSubscriptions.add(queue);
@@ -1959,7 +2284,7 @@ export function composeRuntime(options) {
1959
2284
  rooms() {
1960
2285
  return listed();
1961
2286
  },
1962
- query: ((input) => readSide.query(planned(input, null))),
2287
+ query: ((input) => rowsQuery(planned(input, null))),
1963
2288
  search: (input) => searched(input, null),
1964
2289
  types(locator) {
1965
2290
  return {
@@ -1988,20 +2313,20 @@ export function composeRuntime(options) {
1988
2313
  return exclusive(async () => {
1989
2314
  const parts = readParts(input);
1990
2315
  if (path === '' || normalize(path) !== path)
1991
- throw new Error('room-draft-malformed');
2316
+ throw runtimeError('room-draft-malformed');
1992
2317
  const local = await openLocal();
1993
2318
  const current = roomDrafts.get(room)?.get(path);
1994
2319
  if ((current?.revision ?? null) !== revision) {
1995
- return {
1996
- outcome: 'stale',
1997
- current: current === undefined ? null : roomDraftOf(current),
1998
- };
2320
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
1999
2321
  }
2322
+ const file = parts.file === undefined
2323
+ ? undefined
2324
+ : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
2000
2325
  const record = {
2001
2326
  locator: room,
2002
2327
  path,
2003
2328
  revision: roomDraftRevision(),
2004
- parts: held(current?.parts, parts),
2329
+ parts: held(current?.parts, parts, file),
2005
2330
  returned: current?.returned ?? null,
2006
2331
  };
2007
2332
  await local.keepRoomDraft(record);
@@ -2015,10 +2340,7 @@ export function composeRuntime(options) {
2015
2340
  const local = await openLocal();
2016
2341
  const current = roomDrafts.get(room)?.get(path);
2017
2342
  if (current === undefined || current.revision !== revision) {
2018
- return {
2019
- outcome: 'stale',
2020
- current: current === undefined ? null : roomDraftOf(current),
2021
- };
2343
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2022
2344
  }
2023
2345
  await local.discardRoomDraft(room, path);
2024
2346
  forgetRoomDraft(room, path);
@@ -2053,6 +2375,10 @@ export function composeRuntime(options) {
2053
2375
  history(locator, path, query) {
2054
2376
  return readHistory(locator, path, query);
2055
2377
  },
2378
+ binaryFile: {
2379
+ read: readBinaryPath,
2380
+ readReference: readBinaryReference,
2381
+ },
2056
2382
  call(name, input) {
2057
2383
  return invokeAction(name, input);
2058
2384
  },
@@ -2085,8 +2411,9 @@ export function composeRuntime(options) {
2085
2411
  const reachable = {
2086
2412
  md,
2087
2413
  watch,
2414
+ referrerScope: (referrer) => options.adapter.referrerScope(referrer),
2088
2415
  plan: planned,
2089
- query: (plan) => readSide.query(plan),
2416
+ query: (plan) => rowsQuery(plan),
2090
2417
  watchPlan,
2091
2418
  search: searched,
2092
2419
  };