@agentshouse/mdruntime 0.1.0-ca9a5a0b81b98 → 0.1.0-ca9afacd5aebd

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/AGENTS.md +135 -48
  2. package/README.md +20 -10
  3. package/dist/adapter.d.ts +49 -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 +89 -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 +16 -7
  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 +24 -11
  91. package/dist/connection.d.ts.map +1 -1
  92. package/dist/connection.js +478 -145
  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 +61 -20
  114. package/dist/model.d.ts +13 -8
  115. package/dist/model.d.ts.map +1 -1
  116. package/dist/model.js +6 -3
  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/paths.d.ts +0 -1
  127. package/dist/paths.d.ts.map +1 -1
  128. package/dist/paths.js +0 -16
  129. package/dist/query.d.ts +5 -1
  130. package/dist/query.d.ts.map +1 -1
  131. package/dist/query.js +74 -33
  132. package/dist/reach.d.ts +2 -0
  133. package/dist/reach.d.ts.map +1 -1
  134. package/dist/reach.js +44 -35
  135. package/dist/react.d.ts +26 -16
  136. package/dist/react.d.ts.map +1 -1
  137. package/dist/react.js +10 -8
  138. package/dist/read-side.d.ts.map +1 -1
  139. package/dist/read-side.js +111 -48
  140. package/dist/refresh.d.ts +10 -0
  141. package/dist/refresh.d.ts.map +1 -1
  142. package/dist/refresh.js +42 -38
  143. package/dist/replica.d.ts +8 -5
  144. package/dist/replica.d.ts.map +1 -1
  145. package/dist/replica.js +86 -55
  146. package/dist/room-draft.d.ts +26 -9
  147. package/dist/room-draft.d.ts.map +1 -1
  148. package/dist/room-draft.js +132 -23
  149. package/dist/rows.d.ts +7 -2
  150. package/dist/rows.d.ts.map +1 -1
  151. package/dist/rows.js +85 -40
  152. package/dist/save.d.ts +28 -1
  153. package/dist/save.d.ts.map +1 -1
  154. package/dist/save.js +428 -77
  155. package/dist/search.d.ts +3 -2
  156. package/dist/search.d.ts.map +1 -1
  157. package/dist/status.d.ts +2 -2
  158. package/dist/status.d.ts.map +1 -1
  159. package/dist/status.js +0 -8
  160. package/dist/store.d.ts +34 -9
  161. package/dist/store.d.ts.map +1 -1
  162. package/dist/store.js +273 -164
  163. package/dist/wire.d.ts +7 -3
  164. package/dist/wire.d.ts.map +1 -1
  165. package/dist/wire.js +15 -2
  166. package/package.json +17 -9
  167. package/dist/attachment.d.ts +0 -3
  168. package/dist/attachment.d.ts.map +0 -1
  169. 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
  };
@@ -395,6 +461,7 @@ export function composeRuntime(options) {
395
461
  rooms.set(room.locator, {
396
462
  name: room.name,
397
463
  locator: room.locator,
464
+ handle: room.handle,
398
465
  operations: room.operations,
399
466
  replica: room.replica,
400
467
  model: modelTypes(modelOf(room.locator)),
@@ -406,7 +473,6 @@ export function composeRuntime(options) {
406
473
  },
407
474
  providerOwnedPaths: room.providerOwnedPaths,
408
475
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
409
- attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
410
476
  synchronizationAvailable,
411
477
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
412
478
  });
@@ -431,7 +497,14 @@ export function composeRuntime(options) {
431
497
  emitRooms();
432
498
  }
433
499
  function unlistedHeld(listed) {
434
- 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));
435
508
  }
436
509
  async function dropUnlisted(listed, local) {
437
510
  if (unlistedHeld(listed).length === 0)
@@ -445,11 +518,13 @@ export function composeRuntime(options) {
445
518
  drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
446
519
  }));
447
520
  for (const locator of unlisted) {
448
- for (const batch of replicas.get(locator)?.queue ?? [])
521
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
449
522
  pendings.delete(batch.id);
450
523
  replicas.delete(locator);
524
+ queuesWithoutReplica.delete(locator);
451
525
  rooms.delete(locator);
452
526
  roomDrafts.delete(locator);
527
+ transfers.delete(locator);
453
528
  unreadable.delete(locator);
454
529
  stale.delete(locator);
455
530
  scopedRefusals.delete(locator);
@@ -486,7 +561,7 @@ export function composeRuntime(options) {
486
561
  return model;
487
562
  }
488
563
  async function hydrate(opened) {
489
- 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([
490
565
  opened.all(),
491
566
  opened.queues(),
492
567
  opened.roomDrafts(),
@@ -496,9 +571,15 @@ export function composeRuntime(options) {
496
571
  opened.lastCompleted(),
497
572
  opened.providerLimits(),
498
573
  opened.capabilities(),
574
+ opened.member(),
575
+ opened.transfers(),
499
576
  ]);
577
+ transfers.clear();
578
+ for (const record of staged)
579
+ transfers.set(record.locator, record);
500
580
  limits = declared;
501
581
  capabilities = named;
582
+ member = known;
502
583
  roomDrafts.clear();
503
584
  for (const record of kept)
504
585
  rememberRoomDraft(record);
@@ -514,18 +595,17 @@ export function composeRuntime(options) {
514
595
  }
515
596
  const previousReplicas = new Map(replicas);
516
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
+ }
517
604
  for (const record of records) {
518
605
  const model = restored(modelKey(record.locator, 'collection'), record.model);
519
606
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
520
607
  const prior = previousReplicas.get(record.locator);
521
- const retyped = { ...record, model, selectionModel };
522
- const read = readConfirmed(retyped, prior?.confirmed);
523
- const confirmed = read.kind === 'staged'
524
- ? read.confirmed
525
- : {
526
- record: retyped,
527
- collection: parseCollection(retyped.documents, compositionOf(retyped)),
528
- };
608
+ const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
529
609
  replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
530
610
  const existing = rooms.get(record.locator);
531
611
  if (existing === undefined)
@@ -534,7 +614,6 @@ export function composeRuntime(options) {
534
614
  rooms.set(record.locator, {
535
615
  ...existing,
536
616
  roomVersion: record.roomVersion,
537
- attachments: record.attachments,
538
617
  operations: record.operations,
539
618
  replica: record.replica,
540
619
  model: modelTypes(model),
@@ -546,10 +625,6 @@ export function composeRuntime(options) {
546
625
  },
547
626
  });
548
627
  }
549
- if (read.kind === 'refused') {
550
- refusedSource(record, read);
551
- declarationRefusals.add(record.locator);
552
- }
553
628
  }
554
629
  return {
555
630
  lastCompleted,
@@ -562,6 +637,8 @@ export function composeRuntime(options) {
562
637
  applyChangeSet(read);
563
638
  if (replicas.size > 0)
564
639
  emitRooms();
640
+ if (member !== null)
641
+ emitMember();
565
642
  for (const locator of locators)
566
643
  emitQueue(locator);
567
644
  publishRoomDrafts(false);
@@ -609,10 +686,15 @@ export function composeRuntime(options) {
609
686
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
610
687
  }
611
688
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
612
- const withdrawn = withdrawals(confirmed, queue, withdrawable);
689
+ const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
690
+ const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
613
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
+ }));
614
696
  const returning = [
615
- ...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
697
+ ...[...unqueued].reverse(),
616
698
  ...(settling.leaving === undefined ? [] : [settling.leaving]),
617
699
  ];
618
700
  const written = new Map();
@@ -630,25 +712,44 @@ export function composeRuntime(options) {
630
712
  });
631
713
  }
632
714
  }
633
- await local.write({
634
- next: confirmed.record,
635
- previous: before?.confirmed.record ?? null,
636
- queue: kept,
637
- previousQueue: before?.queue ?? [],
638
- roomDrafts: [...written.values()],
639
- });
640
- for (const batch of before?.queue ?? []) {
715
+ await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
716
+ for (const batch of previousQueue) {
641
717
  if (!kept.includes(batch))
642
718
  pendings.delete(batch.id);
643
719
  }
644
720
  for (const record of written.values())
645
721
  rememberRoomDraft(record);
646
- for (const batch of withdrawn)
647
- 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
+ }
648
749
  replicas.set(locator, replicaOf(confirmed, kept, before));
649
750
  return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
650
751
  room: settling.room,
651
- queue: kept.length !== (before?.queue.length ?? 0),
752
+ queue: kept.length !== previousQueue.length,
652
753
  drafts: [...written.keys()],
653
754
  });
654
755
  }
@@ -683,7 +784,8 @@ export function composeRuntime(options) {
683
784
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
684
785
  };
685
786
  }
686
- function publish(local, room, confirmed, base) {
787
+ function publish(local, room, confirmed, landing = {}) {
788
+ const { base, stored } = landing;
687
789
  return exclusive(async () => {
688
790
  if (!continuing())
689
791
  return true;
@@ -694,13 +796,15 @@ export function composeRuntime(options) {
694
796
  return false;
695
797
  }
696
798
  adoptModels(room.locator, confirmed.record);
697
- const queue = prior?.queue ?? [];
799
+ const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
698
800
  const roomVersion = confirmed.record.roomVersion;
699
801
  const accepted = queue.filter((batch) => {
700
802
  const accepted = pendings.get(batch.id)?.accepted;
701
803
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
702
804
  });
703
- 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);
704
808
  for (const batch of accepted)
705
809
  pendings.delete(batch.id);
706
810
  stale.delete(room.locator);
@@ -719,51 +823,127 @@ export function composeRuntime(options) {
719
823
  const replica = replicas.get(locator);
720
824
  const room = rooms.get(locator);
721
825
  if (replica === undefined || room === undefined) {
722
- throw new Error(`No Local Replica for ${locator}`);
826
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
723
827
  }
724
828
  const saved = [];
725
829
  for (const { path, revision } of named) {
726
830
  const record = roomDrafts.get(locator)?.get(path);
727
831
  if (record === undefined || record.revision !== revision) {
728
- return { outcome: 'refused', rule: 'stale', file: path };
832
+ return { outcome: 'refused', code: 'stale', retryable: false, file: path };
729
833
  }
730
834
  if (!saved.some((draft) => draft.path === path))
731
835
  saved.push({ path, parts: record.parts });
732
836
  }
733
837
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
734
838
  if (changed.length > 0)
735
- return { outcome: 'refused', rule: 'changed-underneath', parts: changed };
839
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
736
840
  const parts = saved.map(({ path, parts }) => changesOf(path, parts));
737
- 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,
738
860
  baseRoomVersion: replica.confirmed.record.roomVersion,
739
861
  composition: compositionFor(replica.confirmed.record),
740
862
  operations: room.operations,
741
863
  providerOwnedPaths: room.providerOwnedPaths,
742
864
  providerLimits: limits,
865
+ handle: room.handle,
743
866
  visible: replica.visible,
744
- changes: parts.flatMap(({ changes }) => changes),
867
+ changes,
745
868
  bases: parts.flatMap(({ bases }) => bases),
746
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 }];
747
877
  });
748
- 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')
749
896
  return { outcome: 'refused', ...built.refusal };
750
- 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
+ }
751
928
  const consumed = saved.map(({ path }) => path);
752
929
  try {
753
- await local.write({
754
- next: replica.confirmed.record,
755
- previous: replica.confirmed.record,
930
+ await local.keepAuthored({
931
+ locator,
756
932
  queue: batches,
757
- previousQueue: replica.queue,
758
- saved,
933
+ saved: drafted,
759
934
  consumedRoomDrafts: consumed,
760
935
  });
761
936
  }
762
937
  catch {
763
- return { outcome: 'refused', rule: 'durable-write' };
938
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
764
939
  }
765
- pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
766
- recorded = false;
940
+ for (const origin of withdrawn.keys())
941
+ pendings.delete(origin);
942
+ for (const [id, drafts] of drafted) {
943
+ pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
944
+ }
945
+ if (!localSaves)
946
+ recorded = false;
767
947
  for (const path of consumed)
768
948
  forgetRoomDraft(locator, path);
769
949
  replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
@@ -771,9 +951,12 @@ export function composeRuntime(options) {
771
951
  queue: true,
772
952
  drafts: consumed,
773
953
  }));
774
- moveTo({ ...saving(status), ...attention() });
954
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
775
955
  submitLater(locator);
776
- 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 };
777
960
  }
778
961
  async function mark(local, locator, identifier, submission, lost) {
779
962
  const state = pendings.get(identifier);
@@ -799,16 +982,15 @@ export function composeRuntime(options) {
799
982
  }
800
983
  async function readHistory(locator, path, query = {}) {
801
984
  if (closed || refusal !== null)
802
- return { kind: 'transport-uncertainty' };
985
+ return UNCERTAIN;
803
986
  try {
804
987
  await ensureAuthorization();
805
988
  }
806
989
  catch {
807
- return { kind: 'transport-uncertainty' };
808
- }
809
- if (closed || authorization === undefined || refusal !== null) {
810
- return { kind: 'transport-uncertainty' };
990
+ return UNCERTAIN;
811
991
  }
992
+ if (closed || authorization === undefined || refusal !== null)
993
+ return UNCERTAIN;
812
994
  const transfer = ['transfer', 'history', locator, path];
813
995
  if (query.revision !== undefined)
814
996
  transfer.push(query.revision);
@@ -830,13 +1012,97 @@ export function composeRuntime(options) {
830
1012
  }
831
1013
  if (result.kind === 'revoked-authorization')
832
1014
  await refuse('revoked-authorization');
833
- return result;
1015
+ return historyAnswer(result);
834
1016
  }
835
1017
  catch {
836
1018
  journal.record('outcome history transport-uncertainty');
837
- return { kind: 'transport-uncertainty' };
1019
+ return UNCERTAIN;
1020
+ }
1021
+ }
1022
+ function queuedBlob(locator, path, localId) {
1023
+ for (const batch of replicas.get(locator)?.queue ?? []) {
1024
+ const change = batch.changes[queuedFile(batch, path, localId)];
1025
+ if (change?.kind === 'binary')
1026
+ return change.blob;
1027
+ }
1028
+ return null;
1029
+ }
1030
+ async function readBinary(request) {
1031
+ if (closed || refusal !== null)
1032
+ return UNCERTAIN;
1033
+ try {
1034
+ await ensureAuthorization();
1035
+ }
1036
+ catch {
1037
+ return UNCERTAIN;
1038
+ }
1039
+ if (closed || authorization === undefined || refusal !== null)
1040
+ return UNCERTAIN;
1041
+ journal.record(request.kind === 'path'
1042
+ ? `transfer binary ${request.locator} ${request.path}`
1043
+ : `transfer binary ${request.fileRef}`);
1044
+ try {
1045
+ const result = await options.adapter.binary(authorization, request);
1046
+ journal.record(`outcome binary ${result.kind}`);
1047
+ if (result.kind === 'scoped-refusal') {
1048
+ const room = replicas.get(result.locator)?.confirmed.record;
1049
+ if (room !== undefined)
1050
+ await scoped(result, room, await openLocal());
1051
+ }
1052
+ if (result.kind === 'revoked-authorization')
1053
+ await refuse('revoked-authorization');
1054
+ return binaryAnswer(result);
1055
+ }
1056
+ catch {
1057
+ journal.record('outcome binary transport-uncertainty');
1058
+ return UNCERTAIN;
838
1059
  }
839
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
+ }
840
1106
  function declaredAgents() {
841
1107
  for (const capability of capabilities) {
842
1108
  if (capability.conversationAgents !== undefined)
@@ -856,18 +1122,17 @@ export function composeRuntime(options) {
856
1122
  async function invokeAction(name, input) {
857
1123
  await ready;
858
1124
  if (closed || refusal !== null)
859
- return { kind: 'transport-uncertainty' };
1125
+ return UNCERTAIN;
860
1126
  if (declaredAction(name) === undefined)
861
- return { kind: 'undeclared' };
1127
+ return undeclared(name);
862
1128
  try {
863
1129
  await ensureAuthorization();
864
1130
  }
865
1131
  catch {
866
- return { kind: 'transport-uncertainty' };
867
- }
868
- if (closed || authorization === undefined || refusal !== null) {
869
- return { kind: 'transport-uncertainty' };
1132
+ return UNCERTAIN;
870
1133
  }
1134
+ if (closed || authorization === undefined || refusal !== null)
1135
+ return UNCERTAIN;
871
1136
  journal.record(`transfer action ${name}`);
872
1137
  try {
873
1138
  const result = await options.adapter.action(authorization, { name, input });
@@ -881,11 +1146,11 @@ export function composeRuntime(options) {
881
1146
  journal.record(`outcome action ${result.kind}`);
882
1147
  if (result.kind === 'rejected')
883
1148
  journal.record(`refusal rejected ${result.code}`);
884
- return result;
1149
+ return calledResult(result);
885
1150
  }
886
1151
  catch {
887
1152
  journal.record('outcome action transport-uncertainty');
888
- return { kind: 'transport-uncertainty' };
1153
+ return UNCERTAIN;
889
1154
  }
890
1155
  }
891
1156
  function continuing() {
@@ -916,14 +1181,33 @@ export function composeRuntime(options) {
916
1181
  throw error;
917
1182
  }
918
1183
  }
919
- 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) {
920
1204
  if (!continuing())
921
1205
  return unconfirmed;
922
1206
  try {
923
1207
  const result = await bootstrapRoom(options.adapter, authorized, room, {
924
1208
  collection: modelOf(room.locator),
925
1209
  selection: modelOf(room.locator, 'typed-selection'),
926
- }, (models, held) => remodel(room.locator, models, held), continuing);
1210
+ }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
927
1211
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
928
1212
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
929
1213
  return result;
@@ -940,9 +1224,9 @@ export function composeRuntime(options) {
940
1224
  if (!continuing())
941
1225
  return unconfirmed;
942
1226
  if (refreshed.kind === 'still-current') {
943
- if (sourceRefusals.has(room.locator))
944
- return unconfirmed;
945
1227
  stale.delete(room.locator);
1228
+ if (sourceRefusals.has(room.locator))
1229
+ return null;
946
1230
  if (unreadable.delete(room.locator)) {
947
1231
  scopedRefusals.delete(room.locator);
948
1232
  list(room, true);
@@ -951,7 +1235,7 @@ export function composeRuntime(options) {
951
1235
  return null;
952
1236
  }
953
1237
  if (refreshed.kind === 'advanced') {
954
- if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
1238
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
955
1239
  const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
956
1240
  const reached = now !== undefined &&
957
1241
  options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
@@ -963,16 +1247,19 @@ export function composeRuntime(options) {
963
1247
  if (refreshed.kind !== 'beyond-horizon')
964
1248
  return refreshed;
965
1249
  }
966
- const bootstrapped = await pullBootstrap(authorized, room);
1250
+ const bootstrapped = await pullBootstrap(authorized, room, local);
967
1251
  if (!continuing())
968
1252
  return unconfirmed;
969
1253
  if (bootstrapped.kind !== 'staged')
970
1254
  return bootstrapped;
971
- await publish(local, room, bootstrapped.confirmed);
1255
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
972
1256
  return null;
973
1257
  }
974
1258
  async function withhold(withheld, room, local, progress) {
975
- stale.add(room.locator);
1259
+ if (withheld.kind === 'refused')
1260
+ stale.delete(room.locator);
1261
+ else
1262
+ stale.add(room.locator);
976
1263
  if (withheld.kind === 'scoped-refusal') {
977
1264
  await scoped(withheld, room, local);
978
1265
  }
@@ -997,6 +1284,7 @@ export function composeRuntime(options) {
997
1284
  function refusedSource(room, { refusal }) {
998
1285
  sourceRefusals.set(room.locator, {
999
1286
  code: refusal.rule,
1287
+ retryable: false,
1000
1288
  fields: {
1001
1289
  file: refusal.file,
1002
1290
  ...(refusal.location === undefined ? {} : { location: refusal.location }),
@@ -1011,8 +1299,16 @@ export function composeRuntime(options) {
1011
1299
  }
1012
1300
  function leaving(batch, result) {
1013
1301
  if (result.kind === 'expired')
1014
- return { batch, returned: { kind: 'expired' } };
1015
- 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
+ };
1016
1312
  }
1017
1313
  function logAccepted(locator, submitted, accepted) {
1018
1314
  journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
@@ -1030,7 +1326,7 @@ export function composeRuntime(options) {
1030
1326
  logAccepted(room.locator, submitted, accepted);
1031
1327
  const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1032
1328
  ? { kind: 'staged', confirmed: current.confirmed }
1033
- : 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)
1034
1330
  ? accepted.roomVersion
1035
1331
  : held, accepted.roomVersion);
1036
1332
  if (applied.kind !== 'staged') {
@@ -1176,7 +1472,8 @@ export function composeRuntime(options) {
1176
1472
  return;
1177
1473
  }
1178
1474
  const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1179
- 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))
1180
1477
  ? { ...left, returned: null }
1181
1478
  : left,
1182
1479
  });
@@ -1237,7 +1534,7 @@ export function composeRuntime(options) {
1237
1534
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1238
1535
  };
1239
1536
  const reclassified = sameMembership(declared, held.confirmed.record)
1240
- ? readConfirmed(declared, held.confirmed)
1537
+ ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
1241
1538
  : readReclassified(declared, held.confirmed);
1242
1539
  const record = reclassified.kind === 'staged'
1243
1540
  ? declared
@@ -1245,12 +1542,10 @@ export function composeRuntime(options) {
1245
1542
  ...declared,
1246
1543
  typedSelection: held.confirmed.record.typedSelection,
1247
1544
  };
1248
- const read = reclassified.kind === 'deferred' ? readConfirmed(record, held.confirmed) : reclassified;
1249
- await local.write({
1250
- next: record,
1251
- previous: held.confirmed.record,
1252
- queue: held.queue,
1253
- });
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 });
1254
1549
  if (read.kind === 'staged') {
1255
1550
  replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1256
1551
  if (declarationRefusals.delete(room.locator)) {
@@ -1277,7 +1572,11 @@ export function composeRuntime(options) {
1277
1572
  });
1278
1573
  }
1279
1574
  async function scoped(result, room, local) {
1280
- 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
+ });
1281
1580
  let scopedAway = room;
1282
1581
  if (result.scope === 'model')
1283
1582
  await retainModel(local, room, 'collection', {
@@ -1314,7 +1613,6 @@ export function composeRuntime(options) {
1314
1613
  roomVersion: record.roomVersion,
1315
1614
  documents: record.documents,
1316
1615
  revisions: record.revisions,
1317
- attachments: record.attachments,
1318
1616
  },
1319
1617
  ];
1320
1618
  });
@@ -1325,8 +1623,10 @@ export function composeRuntime(options) {
1325
1623
  const outcome = prepared[index];
1326
1624
  return outcome?.status === 'fulfilled' && outcome.value;
1327
1625
  });
1328
- if (attempt === 'opened')
1626
+ if (attempt === 'opened') {
1329
1627
  options.adapter.roomStream?.hold(heldRooms(ready));
1628
+ handed = true;
1629
+ }
1330
1630
  const loaded = await Promise.allSettled(ready
1331
1631
  .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1332
1632
  .map((room) => loadRoom(room, local, progress)));
@@ -1392,14 +1692,14 @@ export function composeRuntime(options) {
1392
1692
  async function reload(room, local, progress) {
1393
1693
  if (discoveryPaused || authorization === undefined)
1394
1694
  return;
1395
- const bootstrapped = await pullBootstrap(authorization, room);
1695
+ const bootstrapped = await pullBootstrap(authorization, room, local);
1396
1696
  if (!continuing())
1397
1697
  return;
1398
1698
  if (bootstrapped.kind !== 'staged') {
1399
1699
  await withhold(bootstrapped, room, local, progress);
1400
1700
  return;
1401
1701
  }
1402
- await publish(local, room, bootstrapped.confirmed);
1702
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1403
1703
  await caughtUp(room, local, progress);
1404
1704
  }
1405
1705
  async function discoverRooms(local, progress, attempt) {
@@ -1429,15 +1729,16 @@ export function composeRuntime(options) {
1429
1729
  }
1430
1730
  progress.reachable = false;
1431
1731
  progress.complete = false;
1432
- if (attempt === 'opened') {
1433
- options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1434
- }
1435
1732
  notifyReady();
1436
1733
  return;
1437
1734
  }
1438
1735
  if (result.kind === 'contract-unavailable') {
1439
1736
  journal.record(`connection contract unavailable ${result.code}`);
1440
- 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
+ });
1441
1742
  notifyReady();
1442
1743
  return;
1443
1744
  }
@@ -1478,6 +1779,12 @@ export function composeRuntime(options) {
1478
1779
  limits = result.providerLimits;
1479
1780
  await local.declare(limits, capabilities);
1480
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
+ }
1481
1788
  await applyDiscovery(result.rooms, local);
1482
1789
  await synchronize(result.rooms, local, progress, attempt);
1483
1790
  }
@@ -1507,9 +1814,12 @@ export function composeRuntime(options) {
1507
1814
  }
1508
1815
  const progress = { complete: true, reachable: true };
1509
1816
  underway = attempt === 'opened' ? progress : null;
1817
+ if (attempt === 'opened')
1818
+ streamed = progress;
1510
1819
  complete = false;
1511
1820
  recorded = false;
1512
1821
  moveTo(transferring(status));
1822
+ handed = false;
1513
1823
  try {
1514
1824
  const local = await openLocal();
1515
1825
  if (closed)
@@ -1521,16 +1831,22 @@ export function composeRuntime(options) {
1521
1831
  }
1522
1832
  catch {
1523
1833
  progress.complete = false;
1524
- if (attempt === 'opened') {
1525
- options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1526
- }
1834
+ if (attempt === 'opened' && !handed)
1835
+ progress.reachable = false;
1527
1836
  }
1528
1837
  finally {
1529
1838
  notifyReady();
1530
1839
  }
1531
- if (attempt === 'opened')
1840
+ if (attempt !== 'opened') {
1841
+ await finishOpening(progress);
1532
1842
  return;
1533
- 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();
1534
1850
  });
1535
1851
  }
1536
1852
  function closedSocket(revoked) {
@@ -1540,9 +1856,14 @@ export function composeRuntime(options) {
1540
1856
  void refuse('revoked-authorization');
1541
1857
  return;
1542
1858
  }
1543
- if (opened || status.providerUnreachable)
1544
- return;
1545
- 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
+ });
1546
1867
  }
1547
1868
  function applyOpening(expired) {
1548
1869
  return task(async () => {
@@ -1597,6 +1918,8 @@ export function composeRuntime(options) {
1597
1918
  }
1598
1919
  }
1599
1920
  function submitLater(locator) {
1921
+ if (localSaves)
1922
+ return;
1600
1923
  const running = submitting.get(locator);
1601
1924
  if (running !== undefined) {
1602
1925
  running.again = true;
@@ -1676,7 +1999,7 @@ export function composeRuntime(options) {
1676
1999
  }
1677
2000
  }
1678
2001
  moveTo({
1679
- state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
2002
+ state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
1680
2003
  lastCompleted,
1681
2004
  providerUnreachable: !progress.reachable,
1682
2005
  ...attention(),
@@ -1785,20 +2108,15 @@ export function composeRuntime(options) {
1785
2108
  show();
1786
2109
  return;
1787
2110
  }
1788
- const resting = status;
1789
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1790
- moveTo(transferring(resting));
2111
+ const progress = { complete: false, reachable: !status.providerUnreachable };
2112
+ moveTo(transferring(status));
1791
2113
  try {
1792
2114
  await conversations.catchUp(progress);
1793
2115
  }
1794
2116
  catch {
1795
2117
  progress.reachable = false;
1796
2118
  }
1797
- finally {
1798
- if (!closed) {
1799
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1800
- }
1801
- }
2119
+ await rest(progress);
1802
2120
  }
1803
2121
  function close() {
1804
2122
  if (closed)
@@ -1820,6 +2138,7 @@ export function composeRuntime(options) {
1820
2138
  storageLossListeners.clear();
1821
2139
  statusListeners.clear();
1822
2140
  serviceModeListeners.clear();
2141
+ memberListeners.clear();
1823
2142
  readSide.close();
1824
2143
  queueSubscriptions.clear();
1825
2144
  roomDraftListeners.clear();
@@ -1851,13 +2170,16 @@ export function composeRuntime(options) {
1851
2170
  return planOf(input, roomFacts(reach), planReach(reach));
1852
2171
  }
1853
2172
  function watchPlan(plan, deliver) {
2173
+ materialize(reachedRooms(plan));
1854
2174
  return readSide.watch(plan, identityOf(plan), deliver);
1855
2175
  }
1856
2176
  async function searched(input, reach) {
1857
2177
  const plan = searchPlanOf(input);
1858
2178
  const selected = plan.documents === undefined && reach === null
1859
2179
  ? null
1860
- : (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);
1861
2183
  return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
1862
2184
  }
1863
2185
  const watch = (topic, input, deliver) => {
@@ -1883,6 +2205,8 @@ export function composeRuntime(options) {
1883
2205
  }
1884
2206
  if (topic === 'conversations')
1885
2207
  return conversations.watchList(deliver);
2208
+ if (topic === 'agents')
2209
+ return conversations.watchAgents(deliver);
1886
2210
  if (topic === 'presentation') {
1887
2211
  const listener = {
1888
2212
  app: input[0],
@@ -1937,6 +2261,13 @@ export function composeRuntime(options) {
1937
2261
  serviceModeListeners.delete(deliver);
1938
2262
  };
1939
2263
  }
2264
+ if (topic === 'member') {
2265
+ memberListeners.add(deliver);
2266
+ deliver(member);
2267
+ return () => {
2268
+ memberListeners.delete(deliver);
2269
+ };
2270
+ }
1940
2271
  if (topic === 'queue') {
1941
2272
  const queue = { locator: input[0], notify: deliver };
1942
2273
  queueSubscriptions.add(queue);
@@ -1954,7 +2285,7 @@ export function composeRuntime(options) {
1954
2285
  rooms() {
1955
2286
  return listed();
1956
2287
  },
1957
- query: ((input) => readSide.query(planned(input, null))),
2288
+ query: ((input) => rowsQuery(planned(input, null))),
1958
2289
  search: (input) => searched(input, null),
1959
2290
  types(locator) {
1960
2291
  return {
@@ -1983,20 +2314,20 @@ export function composeRuntime(options) {
1983
2314
  return exclusive(async () => {
1984
2315
  const parts = readParts(input);
1985
2316
  if (path === '' || normalize(path) !== path)
1986
- throw new Error('room-draft-malformed');
2317
+ throw runtimeError('room-draft-malformed');
1987
2318
  const local = await openLocal();
1988
2319
  const current = roomDrafts.get(room)?.get(path);
1989
2320
  if ((current?.revision ?? null) !== revision) {
1990
- return {
1991
- outcome: 'stale',
1992
- current: current === undefined ? null : roomDraftOf(current),
1993
- };
2321
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
1994
2322
  }
2323
+ const file = parts.file === undefined
2324
+ ? undefined
2325
+ : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
1995
2326
  const record = {
1996
2327
  locator: room,
1997
2328
  path,
1998
2329
  revision: roomDraftRevision(),
1999
- parts: held(current?.parts, parts),
2330
+ parts: held(current?.parts, parts, file),
2000
2331
  returned: current?.returned ?? null,
2001
2332
  };
2002
2333
  await local.keepRoomDraft(record);
@@ -2010,10 +2341,7 @@ export function composeRuntime(options) {
2010
2341
  const local = await openLocal();
2011
2342
  const current = roomDrafts.get(room)?.get(path);
2012
2343
  if (current === undefined || current.revision !== revision) {
2013
- return {
2014
- outcome: 'stale',
2015
- current: current === undefined ? null : roomDraftOf(current),
2016
- };
2344
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2017
2345
  }
2018
2346
  await local.discardRoomDraft(room, path);
2019
2347
  forgetRoomDraft(room, path);
@@ -2048,6 +2376,10 @@ export function composeRuntime(options) {
2048
2376
  history(locator, path, query) {
2049
2377
  return readHistory(locator, path, query);
2050
2378
  },
2379
+ binaryFile: {
2380
+ read: readBinaryPath,
2381
+ readReference: readBinaryReference,
2382
+ },
2051
2383
  call(name, input) {
2052
2384
  return invokeAction(name, input);
2053
2385
  },
@@ -2080,8 +2412,9 @@ export function composeRuntime(options) {
2080
2412
  const reachable = {
2081
2413
  md,
2082
2414
  watch,
2415
+ referrerScope: (referrer) => options.adapter.referrerScope(referrer),
2083
2416
  plan: planned,
2084
- query: (plan) => readSide.query(plan),
2417
+ query: (plan) => rowsQuery(plan),
2085
2418
  watchPlan,
2086
2419
  search: searched,
2087
2420
  };