@agentshouse/mdruntime 0.1.0-cf6d5f0a5c9c8 → 0.1.0-cf96c797c5fa8

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 (158) hide show
  1. package/AGENTS.md +96 -31
  2. package/README.md +11 -1
  3. package/dist/adapter.d.ts +41 -14
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/attempt.d.ts +16 -0
  6. package/dist/attempt.d.ts.map +1 -0
  7. package/dist/attempt.js +57 -0
  8. package/dist/binary.d.ts +23 -0
  9. package/dist/binary.d.ts.map +1 -0
  10. package/dist/binary.js +8 -0
  11. package/dist/bootstrap.d.ts.map +1 -1
  12. package/dist/bootstrap.js +5 -8
  13. package/dist/change-set.d.ts.map +1 -1
  14. package/dist/change-set.js +8 -7
  15. package/dist/change.d.ts +15 -0
  16. package/dist/change.d.ts.map +1 -1
  17. package/dist/change.js +26 -1
  18. package/dist/chat/acts.d.ts +34 -0
  19. package/dist/chat/acts.d.ts.map +1 -0
  20. package/dist/chat/acts.js +36 -0
  21. package/dist/chat/context.d.ts +3 -0
  22. package/dist/chat/context.d.ts.map +1 -0
  23. package/dist/chat/context.js +10 -0
  24. package/dist/chat/input.d.ts +9 -0
  25. package/dist/chat/input.d.ts.map +1 -0
  26. package/dist/chat/input.js +99 -0
  27. package/dist/chat/kit.d.ts +60 -0
  28. package/dist/chat/kit.d.ts.map +1 -0
  29. package/dist/chat/kit.js +123 -0
  30. package/dist/chat/lasting.d.ts +6 -0
  31. package/dist/chat/lasting.d.ts.map +1 -0
  32. package/dist/chat/lasting.js +19 -0
  33. package/dist/chat/list.d.ts +20 -0
  34. package/dist/chat/list.d.ts.map +1 -0
  35. package/dist/chat/list.js +59 -0
  36. package/dist/chat/markdown.d.ts +19 -0
  37. package/dist/chat/markdown.d.ts.map +1 -0
  38. package/dist/chat/markdown.js +187 -0
  39. package/dist/chat/message.d.ts +6 -0
  40. package/dist/chat/message.d.ts.map +1 -0
  41. package/dist/chat/message.js +192 -0
  42. package/dist/chat/messages.d.ts +21 -0
  43. package/dist/chat/messages.d.ts.map +1 -0
  44. package/dist/chat/messages.js +151 -0
  45. package/dist/chat/name.d.ts +6 -0
  46. package/dist/chat/name.d.ts.map +1 -0
  47. package/dist/chat/name.js +37 -0
  48. package/dist/chat/opened.d.ts +11 -0
  49. package/dist/chat/opened.d.ts.map +1 -0
  50. package/dist/chat/opened.js +17 -0
  51. package/dist/chat/overlay.d.ts +31 -0
  52. package/dist/chat/overlay.d.ts.map +1 -0
  53. package/dist/chat/overlay.js +97 -0
  54. package/dist/chat/parts.d.ts +57 -0
  55. package/dist/chat/parts.d.ts.map +1 -0
  56. package/dist/chat/parts.js +119 -0
  57. package/dist/chat/process.d.ts +20 -0
  58. package/dist/chat/process.d.ts.map +1 -0
  59. package/dist/chat/process.js +32 -0
  60. package/dist/chat/provider.d.ts +63 -0
  61. package/dist/chat/provider.d.ts.map +1 -0
  62. package/dist/chat/provider.js +645 -0
  63. package/dist/chat/question.d.ts +9 -0
  64. package/dist/chat/question.d.ts.map +1 -0
  65. package/dist/chat/question.js +80 -0
  66. package/dist/chat/questions.d.ts +31 -0
  67. package/dist/chat/questions.d.ts.map +1 -0
  68. package/dist/chat/questions.js +111 -0
  69. package/dist/chat/read.d.ts +5 -0
  70. package/dist/chat/read.d.ts.map +1 -0
  71. package/dist/chat/read.js +13 -0
  72. package/dist/chat/ready.d.ts +33 -0
  73. package/dist/chat/ready.d.ts.map +1 -0
  74. package/dist/chat/ready.js +91 -0
  75. package/dist/chat/style.d.ts +4 -0
  76. package/dist/chat/style.d.ts.map +1 -0
  77. package/dist/chat/style.js +302 -0
  78. package/dist/chat.d.ts +14 -0
  79. package/dist/chat.d.ts.map +1 -0
  80. package/dist/chat.js +8 -0
  81. package/dist/client.d.ts +12 -5
  82. package/dist/client.d.ts.map +1 -1
  83. package/dist/client.js +30 -15
  84. package/dist/connection.d.ts +19 -9
  85. package/dist/connection.d.ts.map +1 -1
  86. package/dist/connection.js +312 -96
  87. package/dist/conversation.d.ts +11 -3
  88. package/dist/conversation.d.ts.map +1 -1
  89. package/dist/conversation.js +47 -7
  90. package/dist/expectation.d.ts +21 -0
  91. package/dist/expectation.d.ts.map +1 -0
  92. package/dist/expectation.js +70 -0
  93. package/dist/failure.d.ts +13 -0
  94. package/dist/failure.d.ts.map +1 -0
  95. package/dist/failure.js +19 -0
  96. package/dist/host.d.ts +1 -1
  97. package/dist/host.d.ts.map +1 -1
  98. package/dist/host.js +9 -9
  99. package/dist/identity.d.ts +2 -0
  100. package/dist/identity.d.ts.map +1 -1
  101. package/dist/identity.js +6 -0
  102. package/dist/index.d.ts +9 -4
  103. package/dist/index.d.ts.map +1 -1
  104. package/dist/index.js +3 -0
  105. package/dist/memory.d.ts +3 -3
  106. package/dist/memory.d.ts.map +1 -1
  107. package/dist/memory.js +47 -16
  108. package/dist/model.d.ts +3 -2
  109. package/dist/model.d.ts.map +1 -1
  110. package/dist/open.d.ts +2 -0
  111. package/dist/open.d.ts.map +1 -0
  112. package/dist/open.js +1 -0
  113. package/dist/outcome.d.ts +28 -3
  114. package/dist/outcome.d.ts.map +1 -1
  115. package/dist/outcome.js +41 -1
  116. package/dist/overlay.d.ts +14 -3
  117. package/dist/overlay.d.ts.map +1 -1
  118. package/dist/overlay.js +75 -37
  119. package/dist/query.d.ts +4 -0
  120. package/dist/query.d.ts.map +1 -1
  121. package/dist/query.js +3 -0
  122. package/dist/reach.d.ts +2 -0
  123. package/dist/reach.d.ts.map +1 -1
  124. package/dist/reach.js +43 -19
  125. package/dist/react.d.ts +4 -2
  126. package/dist/react.d.ts.map +1 -1
  127. package/dist/react.js +3 -2
  128. package/dist/read-side.d.ts.map +1 -1
  129. package/dist/read-side.js +9 -2
  130. package/dist/refresh.d.ts.map +1 -1
  131. package/dist/refresh.js +12 -17
  132. package/dist/replica.d.ts +6 -3
  133. package/dist/replica.d.ts.map +1 -1
  134. package/dist/replica.js +46 -32
  135. package/dist/room-draft.d.ts +26 -9
  136. package/dist/room-draft.d.ts.map +1 -1
  137. package/dist/room-draft.js +131 -22
  138. package/dist/rows.d.ts +3 -1
  139. package/dist/rows.d.ts.map +1 -1
  140. package/dist/rows.js +3 -1
  141. package/dist/save.d.ts +27 -1
  142. package/dist/save.d.ts.map +1 -1
  143. package/dist/save.js +323 -22
  144. package/dist/search.d.ts +3 -2
  145. package/dist/search.d.ts.map +1 -1
  146. package/dist/status.d.ts +2 -2
  147. package/dist/status.d.ts.map +1 -1
  148. package/dist/status.js +0 -8
  149. package/dist/store.d.ts +13 -12
  150. package/dist/store.d.ts.map +1 -1
  151. package/dist/store.js +150 -157
  152. package/dist/wire.d.ts +6 -3
  153. package/dist/wire.d.ts.map +1 -1
  154. package/dist/wire.js +14 -2
  155. package/package.json +17 -9
  156. package/dist/attachment.d.ts +0 -3
  157. package/dist/attachment.d.ts.map +0 -1
  158. package/dist/attachment.js +0 -8
@@ -8,13 +8,17 @@ import { createSearchIndex, searchPlanOf, } from './search.js';
8
8
  import { bootstrapRoom } from './bootstrap.js';
9
9
  import { changeSet, mergeChangeSets } from './change-set.js';
10
10
  import { createConversationStore, } from './conversation.js';
11
+ import { queuedFile } from './change.js';
11
12
  import { validRoot } from './collection.js';
13
+ import { runtimeError } from './failure.js';
12
14
  import { createLog } from './log.js';
15
+ import { binaryRecord } from './binary.js';
16
+ import { absentBinary, binaryAnswer, calledResult, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
13
17
  import { refreshRoom } from './refresh.js';
14
- import { buildBatch } from './save.js';
18
+ import { buildBatch, joined, limitedIn, requeued } from './save.js';
15
19
  import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
16
- import { changedParts, changesOf, held, readParts, returnedInto, underneath, } from './room-draft.js';
17
- import { settled, same, saving, STARTING, transferring, } from './status.js';
20
+ import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
21
+ import { same, saving, STARTING, transferring, } from './status.js';
18
22
  import { detectStorageLoss, nextGeneration } from './durability.js';
19
23
  import { roomDraftRevision } from './identity.js';
20
24
  import { beneath, normalize } from './paths.js';
@@ -35,10 +39,12 @@ function declaredSelection(room) {
35
39
  selection.roots.every((other, at) => at === index || !within(other, root)));
36
40
  }
37
41
  export function composeRuntime(options) {
42
+ const localSaves = options.adapter.localSaves === true;
38
43
  let authorization = options.authorization;
39
44
  let opening;
40
45
  let store;
41
46
  const replicas = new Map();
47
+ const queuesWithoutReplica = new Map();
42
48
  const pendings = new Map();
43
49
  const roomDrafts = new Map();
44
50
  const presentations = new Map();
@@ -84,6 +90,7 @@ export function composeRuntime(options) {
84
90
  let withdrawStream;
85
91
  const announced = new Map();
86
92
  let underway = null;
93
+ let streamed = null;
87
94
  let handed = false;
88
95
  const submitting = new Map();
89
96
  const answering = new Map();
@@ -190,7 +197,9 @@ export function composeRuntime(options) {
190
197
  }
191
198
  }
192
199
  function pending() {
193
- 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)));
194
203
  }
195
204
  function attention() {
196
205
  const attentionRequired = [];
@@ -291,6 +300,8 @@ export function composeRuntime(options) {
291
300
  listener(shown);
292
301
  }
293
302
  function entryOf(batch) {
303
+ if (localSaves)
304
+ return Object.freeze({ batch, state: 'local' });
294
305
  const state = pendings.get(batch.id);
295
306
  if (state?.accepted !== undefined) {
296
307
  return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
@@ -298,10 +309,7 @@ export function composeRuntime(options) {
298
309
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
299
310
  }
300
311
  function entriesOf(locator) {
301
- const replica = replicas.get(locator);
302
- if (replica === undefined)
303
- return [];
304
- return replica.queue.map(entryOf);
312
+ return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
305
313
  }
306
314
  function emitQueue(locator) {
307
315
  const entries = entriesOf(locator);
@@ -398,10 +406,7 @@ export function composeRuntime(options) {
398
406
  const revoked = { status: 'unavailable', reason: 'revoked' };
399
407
  try {
400
408
  const local = await openLocal();
401
- await local.revokeModels([...replicas.values()].map((replica) => ({
402
- record: replica.confirmed.record,
403
- queue: replica.queue,
404
- })));
409
+ await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
405
410
  }
406
411
  catch {
407
412
  journal.record('model revocation persistence failed');
@@ -442,7 +447,8 @@ export function composeRuntime(options) {
442
447
  }
443
448
  finally {
444
449
  refusal = {
445
- reason,
450
+ code: reason,
451
+ retryable: false,
446
452
  reauthorizationOffered: reason === 'revoked-authorization',
447
453
  refusal: provider,
448
454
  };
@@ -467,7 +473,6 @@ export function composeRuntime(options) {
467
473
  },
468
474
  providerOwnedPaths: room.providerOwnedPaths,
469
475
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
470
- attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
471
476
  synchronizationAvailable,
472
477
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
473
478
  });
@@ -492,7 +497,14 @@ export function composeRuntime(options) {
492
497
  emitRooms();
493
498
  }
494
499
  function unlistedHeld(listed) {
495
- return [...new Set([...rooms.keys(), ...roomDrafts.keys(), ...transfers.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));
496
508
  }
497
509
  async function dropUnlisted(listed, local) {
498
510
  if (unlistedHeld(listed).length === 0)
@@ -506,9 +518,10 @@ export function composeRuntime(options) {
506
518
  drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
507
519
  }));
508
520
  for (const locator of unlisted) {
509
- for (const batch of replicas.get(locator)?.queue ?? [])
521
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
510
522
  pendings.delete(batch.id);
511
523
  replicas.delete(locator);
524
+ queuesWithoutReplica.delete(locator);
512
525
  rooms.delete(locator);
513
526
  roomDrafts.delete(locator);
514
527
  transfers.delete(locator);
@@ -582,6 +595,12 @@ export function composeRuntime(options) {
582
595
  }
583
596
  const previousReplicas = new Map(replicas);
584
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
+ }
585
604
  for (const record of records) {
586
605
  const model = restored(modelKey(record.locator, 'collection'), record.model);
587
606
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
@@ -595,7 +614,6 @@ export function composeRuntime(options) {
595
614
  rooms.set(record.locator, {
596
615
  ...existing,
597
616
  roomVersion: record.roomVersion,
598
- attachments: record.attachments,
599
617
  operations: record.operations,
600
618
  replica: record.replica,
601
619
  model: modelTypes(model),
@@ -668,11 +686,12 @@ export function composeRuntime(options) {
668
686
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
669
687
  }
670
688
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
671
- const withdrawn = withdrawals(confirmed, queue, withdrawable);
689
+ const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
690
+ const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
672
691
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
673
692
  const unqueued = withdrawn.map((batch) => ({
674
693
  batch,
675
- returned: landed(batch, confirmed) ? null : { kind: 'withdrawn' },
694
+ returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
676
695
  }));
677
696
  const returning = [
678
697
  ...[...unqueued].reverse(),
@@ -693,27 +712,44 @@ export function composeRuntime(options) {
693
712
  });
694
713
  }
695
714
  }
696
- await local.write({
697
- next: confirmed.record,
698
- previous: before?.confirmed.record ?? null,
699
- ...(settling.stored === undefined ? {} : { stored: settling.stored }),
700
- queue: kept,
701
- previousQueue: before?.queue ?? [],
702
- roomDrafts: [...written.values()],
703
- });
704
- for (const batch of before?.queue ?? []) {
715
+ await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
716
+ for (const batch of previousQueue) {
705
717
  if (!kept.includes(batch))
706
718
  pendings.delete(batch.id);
707
719
  }
708
720
  for (const record of written.values())
709
721
  rememberRoomDraft(record);
722
+ try {
723
+ await local.write({
724
+ next: confirmed.record,
725
+ previous: before?.confirmed.record ?? null,
726
+ ...(settling.stored === undefined ? {} : { stored: settling.stored }),
727
+ });
728
+ }
729
+ catch (error) {
730
+ const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
731
+ if (held !== null)
732
+ replicas.set(locator, held);
733
+ else if (kept.length > 0)
734
+ queuesWithoutReplica.set(locator, kept);
735
+ else
736
+ queuesWithoutReplica.delete(locator);
737
+ publishChangeSet(changeSet(locator, before ?? null, held, {
738
+ queue: kept.length !== previousQueue.length,
739
+ drafts: [...written.keys()],
740
+ }));
741
+ moveTo({ ...status, ...attention() });
742
+ throw error;
743
+ }
744
+ queuesWithoutReplica.delete(locator);
710
745
  for (const { batch, returned } of unqueued) {
711
- journal.record(`outcome ${locator} ${batch.id} ${returned === null ? 'landed' : 'withdrawn'}`);
746
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
747
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
712
748
  }
713
749
  replicas.set(locator, replicaOf(confirmed, kept, before));
714
750
  return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
715
751
  room: settling.room,
716
- queue: kept.length !== (before?.queue.length ?? 0),
752
+ queue: kept.length !== previousQueue.length,
717
753
  drafts: [...written.keys()],
718
754
  });
719
755
  }
@@ -760,7 +796,7 @@ export function composeRuntime(options) {
760
796
  return false;
761
797
  }
762
798
  adoptModels(room.locator, confirmed.record);
763
- const queue = prior?.queue ?? [];
799
+ const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
764
800
  const roomVersion = confirmed.record.roomVersion;
765
801
  const accepted = queue.filter((batch) => {
766
802
  const accepted = pendings.get(batch.id)?.accepted;
@@ -787,22 +823,40 @@ export function composeRuntime(options) {
787
823
  const replica = replicas.get(locator);
788
824
  const room = rooms.get(locator);
789
825
  if (replica === undefined || room === undefined) {
790
- throw new Error(`No Local Replica for ${locator}`);
826
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
791
827
  }
792
828
  const saved = [];
793
829
  for (const { path, revision } of named) {
794
830
  const record = roomDrafts.get(locator)?.get(path);
795
831
  if (record === undefined || record.revision !== revision) {
796
- return { outcome: 'refused', rule: 'stale', file: path };
832
+ return { outcome: 'refused', code: 'stale', retryable: false, file: path };
797
833
  }
798
834
  if (!saved.some((draft) => draft.path === path))
799
835
  saved.push({ path, parts: record.parts });
800
836
  }
801
837
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
802
838
  if (changed.length > 0)
803
- return { outcome: 'refused', rule: 'changed-underneath', parts: changed };
839
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
804
840
  const parts = saved.map(({ path, parts }) => changesOf(path, parts));
805
- 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,
806
860
  baseRoomVersion: replica.confirmed.record.roomVersion,
807
861
  composition: compositionFor(replica.confirmed.record),
808
862
  operations: room.operations,
@@ -810,29 +864,86 @@ export function composeRuntime(options) {
810
864
  providerLimits: limits,
811
865
  handle: room.handle,
812
866
  visible: replica.visible,
813
- changes: parts.flatMap(({ changes }) => changes),
867
+ changes,
814
868
  bases: parts.flatMap(({ bases }) => bases),
815
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 }];
816
877
  });
817
- 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')
818
896
  return { outcome: 'refused', ...built.refusal };
819
- 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
+ }
820
928
  const consumed = saved.map(({ path }) => path);
821
929
  try {
822
- await local.write({
823
- next: replica.confirmed.record,
824
- previous: replica.confirmed.record,
930
+ await local.keepAuthored({
931
+ locator,
825
932
  queue: batches,
826
- previousQueue: replica.queue,
827
- saved,
933
+ saved: drafted,
828
934
  consumedRoomDrafts: consumed,
829
935
  });
830
936
  }
831
937
  catch {
832
- return { outcome: 'refused', rule: 'durable-write' };
938
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
833
939
  }
834
- pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
835
- 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;
836
947
  for (const path of consumed)
837
948
  forgetRoomDraft(locator, path);
838
949
  replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
@@ -840,9 +951,12 @@ export function composeRuntime(options) {
840
951
  queue: true,
841
952
  drafts: consumed,
842
953
  }));
843
- moveTo({ ...saving(status), ...attention() });
954
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
844
955
  submitLater(locator);
845
- 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 };
846
960
  }
847
961
  async function mark(local, locator, identifier, submission, lost) {
848
962
  const state = pendings.get(identifier);
@@ -868,16 +982,15 @@ export function composeRuntime(options) {
868
982
  }
869
983
  async function readHistory(locator, path, query = {}) {
870
984
  if (closed || refusal !== null)
871
- return { kind: 'transport-uncertainty' };
985
+ return UNCERTAIN;
872
986
  try {
873
987
  await ensureAuthorization();
874
988
  }
875
989
  catch {
876
- return { kind: 'transport-uncertainty' };
877
- }
878
- if (closed || authorization === undefined || refusal !== null) {
879
- return { kind: 'transport-uncertainty' };
990
+ return UNCERTAIN;
880
991
  }
992
+ if (closed || authorization === undefined || refusal !== null)
993
+ return UNCERTAIN;
881
994
  const transfer = ['transfer', 'history', locator, path];
882
995
  if (query.revision !== undefined)
883
996
  transfer.push(query.revision);
@@ -899,13 +1012,97 @@ export function composeRuntime(options) {
899
1012
  }
900
1013
  if (result.kind === 'revoked-authorization')
901
1014
  await refuse('revoked-authorization');
902
- return result;
1015
+ return historyAnswer(result);
903
1016
  }
904
1017
  catch {
905
1018
  journal.record('outcome history transport-uncertainty');
906
- return { kind: 'transport-uncertainty' };
1019
+ return UNCERTAIN;
907
1020
  }
908
1021
  }
1022
+ function queuedBlob(locator, path, localId) {
1023
+ for (const batch of replicas.get(locator)?.queue ?? []) {
1024
+ const change = batch.changes[queuedFile(batch, path, localId)];
1025
+ if (change?.kind === 'binary')
1026
+ return change.blob;
1027
+ }
1028
+ return null;
1029
+ }
1030
+ async function readBinary(request) {
1031
+ if (closed || refusal !== null)
1032
+ return UNCERTAIN;
1033
+ try {
1034
+ await ensureAuthorization();
1035
+ }
1036
+ catch {
1037
+ return UNCERTAIN;
1038
+ }
1039
+ if (closed || authorization === undefined || refusal !== null)
1040
+ return UNCERTAIN;
1041
+ journal.record(request.kind === 'path'
1042
+ ? `transfer binary ${request.locator} ${request.path}`
1043
+ : `transfer binary ${request.fileRef}`);
1044
+ try {
1045
+ const result = await options.adapter.binary(authorization, request);
1046
+ journal.record(`outcome binary ${result.kind}`);
1047
+ if (result.kind === 'scoped-refusal') {
1048
+ const room = replicas.get(result.locator)?.confirmed.record;
1049
+ if (room !== undefined)
1050
+ await scoped(result, room, await openLocal());
1051
+ }
1052
+ if (result.kind === 'revoked-authorization')
1053
+ await refuse('revoked-authorization');
1054
+ return binaryAnswer(result);
1055
+ }
1056
+ catch {
1057
+ journal.record('outcome binary transport-uncertainty');
1058
+ return UNCERTAIN;
1059
+ }
1060
+ }
1061
+ async function readBinaryPath(locator, path, revision) {
1062
+ await ready;
1063
+ if (revision !== undefined)
1064
+ return readBinary({ kind: 'path', locator, path, revision });
1065
+ const visible = replicas.get(locator)?.visible;
1066
+ const held = visible?.binaries[path];
1067
+ if (visible === undefined || held === undefined)
1068
+ return absentBinary(path);
1069
+ if (held.state === 'pending') {
1070
+ const blob = queuedBlob(locator, path, held.localId);
1071
+ return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1072
+ }
1073
+ const record = binaryRecord(path, held, visible.revisions[path]);
1074
+ if (record.state !== 'accepted')
1075
+ return absentBinary(path);
1076
+ const replica = replicas.get(locator);
1077
+ const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
1078
+ const holds = (at) => {
1079
+ const kept = replica?.confirmed.record.binaries[at];
1080
+ return (kept?.state === 'accepted' &&
1081
+ kept.fileRef === record.fileRef &&
1082
+ replica?.confirmed.record.revisions[at] === record.revision);
1083
+ };
1084
+ let confirmed = path;
1085
+ for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
1086
+ const change = queued[index];
1087
+ if (change.kind === 'move' && change.to === confirmed)
1088
+ confirmed = change.path;
1089
+ }
1090
+ return readBinary({
1091
+ kind: 'path',
1092
+ locator,
1093
+ path: confirmed,
1094
+ revision: record.revision,
1095
+ fileRef: record.fileRef,
1096
+ });
1097
+ }
1098
+ async function readBinaryReference(reference) {
1099
+ await ready;
1100
+ return readBinary({
1101
+ kind: 'reference',
1102
+ fileRef: reference.fileRef,
1103
+ referrer: reference.referrer,
1104
+ });
1105
+ }
909
1106
  function declaredAgents() {
910
1107
  for (const capability of capabilities) {
911
1108
  if (capability.conversationAgents !== undefined)
@@ -925,18 +1122,17 @@ export function composeRuntime(options) {
925
1122
  async function invokeAction(name, input) {
926
1123
  await ready;
927
1124
  if (closed || refusal !== null)
928
- return { kind: 'transport-uncertainty' };
1125
+ return UNCERTAIN;
929
1126
  if (declaredAction(name) === undefined)
930
- return { kind: 'undeclared' };
1127
+ return undeclared(name);
931
1128
  try {
932
1129
  await ensureAuthorization();
933
1130
  }
934
1131
  catch {
935
- return { kind: 'transport-uncertainty' };
936
- }
937
- if (closed || authorization === undefined || refusal !== null) {
938
- return { kind: 'transport-uncertainty' };
1132
+ return UNCERTAIN;
939
1133
  }
1134
+ if (closed || authorization === undefined || refusal !== null)
1135
+ return UNCERTAIN;
940
1136
  journal.record(`transfer action ${name}`);
941
1137
  try {
942
1138
  const result = await options.adapter.action(authorization, { name, input });
@@ -950,11 +1146,11 @@ export function composeRuntime(options) {
950
1146
  journal.record(`outcome action ${result.kind}`);
951
1147
  if (result.kind === 'rejected')
952
1148
  journal.record(`refusal rejected ${result.code}`);
953
- return result;
1149
+ return calledResult(result);
954
1150
  }
955
1151
  catch {
956
1152
  journal.record('outcome action transport-uncertainty');
957
- return { kind: 'transport-uncertainty' };
1153
+ return UNCERTAIN;
958
1154
  }
959
1155
  }
960
1156
  function continuing() {
@@ -1088,6 +1284,7 @@ export function composeRuntime(options) {
1088
1284
  function refusedSource(room, { refusal }) {
1089
1285
  sourceRefusals.set(room.locator, {
1090
1286
  code: refusal.rule,
1287
+ retryable: false,
1091
1288
  fields: {
1092
1289
  file: refusal.file,
1093
1290
  ...(refusal.location === undefined ? {} : { location: refusal.location }),
@@ -1102,8 +1299,16 @@ export function composeRuntime(options) {
1102
1299
  }
1103
1300
  function leaving(batch, result) {
1104
1301
  if (result.kind === 'expired')
1105
- return { batch, returned: { kind: 'expired' } };
1106
- 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
+ };
1107
1312
  }
1108
1313
  function logAccepted(locator, submitted, accepted) {
1109
1314
  journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
@@ -1121,7 +1326,7 @@ export function composeRuntime(options) {
1121
1326
  logAccepted(room.locator, submitted, accepted);
1122
1327
  const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1123
1328
  ? { kind: 'staged', confirmed: current.confirmed }
1124
- : applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.changes, 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)
1125
1330
  ? accepted.roomVersion
1126
1331
  : held, accepted.roomVersion);
1127
1332
  if (applied.kind !== 'staged') {
@@ -1340,11 +1545,7 @@ export function composeRuntime(options) {
1340
1545
  const read = reclassified.kind === 'deferred'
1341
1546
  ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
1342
1547
  : reclassified;
1343
- await local.write({
1344
- next: record,
1345
- previous: held.confirmed.record,
1346
- queue: held.queue,
1347
- });
1548
+ await local.write({ next: record, previous: held.confirmed.record });
1348
1549
  if (read.kind === 'staged') {
1349
1550
  replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1350
1551
  if (declarationRefusals.delete(room.locator)) {
@@ -1371,7 +1572,11 @@ export function composeRuntime(options) {
1371
1572
  });
1372
1573
  }
1373
1574
  async function scoped(result, room, local) {
1374
- 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
+ });
1375
1580
  let scopedAway = room;
1376
1581
  if (result.scope === 'model')
1377
1582
  await retainModel(local, room, 'collection', {
@@ -1408,7 +1613,6 @@ export function composeRuntime(options) {
1408
1613
  roomVersion: record.roomVersion,
1409
1614
  documents: record.documents,
1410
1615
  revisions: record.revisions,
1411
- attachments: record.attachments,
1412
1616
  },
1413
1617
  ];
1414
1618
  });
@@ -1530,7 +1734,11 @@ export function composeRuntime(options) {
1530
1734
  }
1531
1735
  if (result.kind === 'contract-unavailable') {
1532
1736
  journal.record(`connection contract unavailable ${result.code}`);
1533
- 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
+ });
1534
1742
  notifyReady();
1535
1743
  return;
1536
1744
  }
@@ -1606,6 +1814,8 @@ export function composeRuntime(options) {
1606
1814
  }
1607
1815
  const progress = { complete: true, reachable: true };
1608
1816
  underway = attempt === 'opened' ? progress : null;
1817
+ if (attempt === 'opened')
1818
+ streamed = progress;
1609
1819
  complete = false;
1610
1820
  recorded = false;
1611
1821
  moveTo(transferring(status));
@@ -1646,9 +1856,14 @@ export function composeRuntime(options) {
1646
1856
  void refuse('revoked-authorization');
1647
1857
  return;
1648
1858
  }
1649
- if (opened || status.providerUnreachable)
1650
- return;
1651
- 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
+ });
1652
1867
  }
1653
1868
  function applyOpening(expired) {
1654
1869
  return task(async () => {
@@ -1703,6 +1918,8 @@ export function composeRuntime(options) {
1703
1918
  }
1704
1919
  }
1705
1920
  function submitLater(locator) {
1921
+ if (localSaves)
1922
+ return;
1706
1923
  const running = submitting.get(locator);
1707
1924
  if (running !== undefined) {
1708
1925
  running.again = true;
@@ -1782,7 +1999,7 @@ export function composeRuntime(options) {
1782
1999
  }
1783
2000
  }
1784
2001
  moveTo({
1785
- state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
2002
+ state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
1786
2003
  lastCompleted,
1787
2004
  providerUnreachable: !progress.reachable,
1788
2005
  ...attention(),
@@ -1891,20 +2108,15 @@ export function composeRuntime(options) {
1891
2108
  show();
1892
2109
  return;
1893
2110
  }
1894
- const resting = status;
1895
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1896
- moveTo(transferring(resting));
2111
+ const progress = { complete: false, reachable: !status.providerUnreachable };
2112
+ moveTo(transferring(status));
1897
2113
  try {
1898
2114
  await conversations.catchUp(progress);
1899
2115
  }
1900
2116
  catch {
1901
2117
  progress.reachable = false;
1902
2118
  }
1903
- finally {
1904
- if (!closed) {
1905
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1906
- }
1907
- }
2119
+ await rest(progress);
1908
2120
  }
1909
2121
  function close() {
1910
2122
  if (closed)
@@ -1993,6 +2205,8 @@ export function composeRuntime(options) {
1993
2205
  }
1994
2206
  if (topic === 'conversations')
1995
2207
  return conversations.watchList(deliver);
2208
+ if (topic === 'agents')
2209
+ return conversations.watchAgents(deliver);
1996
2210
  if (topic === 'presentation') {
1997
2211
  const listener = {
1998
2212
  app: input[0],
@@ -2100,20 +2314,20 @@ export function composeRuntime(options) {
2100
2314
  return exclusive(async () => {
2101
2315
  const parts = readParts(input);
2102
2316
  if (path === '' || normalize(path) !== path)
2103
- throw new Error('room-draft-malformed');
2317
+ throw runtimeError('room-draft-malformed');
2104
2318
  const local = await openLocal();
2105
2319
  const current = roomDrafts.get(room)?.get(path);
2106
2320
  if ((current?.revision ?? null) !== revision) {
2107
- return {
2108
- outcome: 'stale',
2109
- current: current === undefined ? null : roomDraftOf(current),
2110
- };
2321
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2111
2322
  }
2323
+ const file = parts.file === undefined
2324
+ ? undefined
2325
+ : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
2112
2326
  const record = {
2113
2327
  locator: room,
2114
2328
  path,
2115
2329
  revision: roomDraftRevision(),
2116
- parts: held(current?.parts, parts),
2330
+ parts: held(current?.parts, parts, file),
2117
2331
  returned: current?.returned ?? null,
2118
2332
  };
2119
2333
  await local.keepRoomDraft(record);
@@ -2127,10 +2341,7 @@ export function composeRuntime(options) {
2127
2341
  const local = await openLocal();
2128
2342
  const current = roomDrafts.get(room)?.get(path);
2129
2343
  if (current === undefined || current.revision !== revision) {
2130
- return {
2131
- outcome: 'stale',
2132
- current: current === undefined ? null : roomDraftOf(current),
2133
- };
2344
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2134
2345
  }
2135
2346
  await local.discardRoomDraft(room, path);
2136
2347
  forgetRoomDraft(room, path);
@@ -2165,6 +2376,10 @@ export function composeRuntime(options) {
2165
2376
  history(locator, path, query) {
2166
2377
  return readHistory(locator, path, query);
2167
2378
  },
2379
+ binaryFile: {
2380
+ read: readBinaryPath,
2381
+ readReference: readBinaryReference,
2382
+ },
2168
2383
  call(name, input) {
2169
2384
  return invokeAction(name, input);
2170
2385
  },
@@ -2197,6 +2412,7 @@ export function composeRuntime(options) {
2197
2412
  const reachable = {
2198
2413
  md,
2199
2414
  watch,
2415
+ referrerScope: (referrer) => options.adapter.referrerScope(referrer),
2200
2416
  plan: planned,
2201
2417
  query: (plan) => rowsQuery(plan),
2202
2418
  watchPlan,