@agentshouse/mdruntime 0.1.0-cebcd51f70ed1 → 0.1.0-cec6690d5712e

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 (156) hide show
  1. package/AGENTS.md +82 -27
  2. package/README.md +13 -3
  3. package/dist/adapter.d.ts +13 -3
  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/bootstrap.d.ts +17 -5
  11. package/dist/bootstrap.d.ts.map +1 -1
  12. package/dist/bootstrap.js +88 -35
  13. package/dist/change-set.d.ts.map +1 -1
  14. package/dist/change-set.js +13 -7
  15. package/dist/change.d.ts +4 -0
  16. package/dist/change.d.ts.map +1 -1
  17. package/dist/chat/acts.d.ts +34 -0
  18. package/dist/chat/acts.d.ts.map +1 -0
  19. package/dist/chat/acts.js +36 -0
  20. package/dist/chat/context.d.ts +3 -0
  21. package/dist/chat/context.d.ts.map +1 -0
  22. package/dist/chat/context.js +10 -0
  23. package/dist/chat/input.d.ts +9 -0
  24. package/dist/chat/input.d.ts.map +1 -0
  25. package/dist/chat/input.js +99 -0
  26. package/dist/chat/kit.d.ts +60 -0
  27. package/dist/chat/kit.d.ts.map +1 -0
  28. package/dist/chat/kit.js +123 -0
  29. package/dist/chat/lasting.d.ts +6 -0
  30. package/dist/chat/lasting.d.ts.map +1 -0
  31. package/dist/chat/lasting.js +19 -0
  32. package/dist/chat/list.d.ts +20 -0
  33. package/dist/chat/list.d.ts.map +1 -0
  34. package/dist/chat/list.js +59 -0
  35. package/dist/chat/markdown.d.ts +19 -0
  36. package/dist/chat/markdown.d.ts.map +1 -0
  37. package/dist/chat/markdown.js +187 -0
  38. package/dist/chat/message.d.ts +6 -0
  39. package/dist/chat/message.d.ts.map +1 -0
  40. package/dist/chat/message.js +192 -0
  41. package/dist/chat/messages.d.ts +21 -0
  42. package/dist/chat/messages.d.ts.map +1 -0
  43. package/dist/chat/messages.js +151 -0
  44. package/dist/chat/name.d.ts +6 -0
  45. package/dist/chat/name.d.ts.map +1 -0
  46. package/dist/chat/name.js +37 -0
  47. package/dist/chat/opened.d.ts +11 -0
  48. package/dist/chat/opened.d.ts.map +1 -0
  49. package/dist/chat/opened.js +17 -0
  50. package/dist/chat/overlay.d.ts +31 -0
  51. package/dist/chat/overlay.d.ts.map +1 -0
  52. package/dist/chat/overlay.js +97 -0
  53. package/dist/chat/parts.d.ts +57 -0
  54. package/dist/chat/parts.d.ts.map +1 -0
  55. package/dist/chat/parts.js +119 -0
  56. package/dist/chat/process.d.ts +20 -0
  57. package/dist/chat/process.d.ts.map +1 -0
  58. package/dist/chat/process.js +32 -0
  59. package/dist/chat/provider.d.ts +63 -0
  60. package/dist/chat/provider.d.ts.map +1 -0
  61. package/dist/chat/provider.js +645 -0
  62. package/dist/chat/question.d.ts +9 -0
  63. package/dist/chat/question.d.ts.map +1 -0
  64. package/dist/chat/question.js +80 -0
  65. package/dist/chat/questions.d.ts +31 -0
  66. package/dist/chat/questions.d.ts.map +1 -0
  67. package/dist/chat/questions.js +111 -0
  68. package/dist/chat/read.d.ts +5 -0
  69. package/dist/chat/read.d.ts.map +1 -0
  70. package/dist/chat/read.js +13 -0
  71. package/dist/chat/ready.d.ts +33 -0
  72. package/dist/chat/ready.d.ts.map +1 -0
  73. package/dist/chat/ready.js +91 -0
  74. package/dist/chat/style.d.ts +4 -0
  75. package/dist/chat/style.d.ts.map +1 -0
  76. package/dist/chat/style.js +302 -0
  77. package/dist/chat.d.ts +14 -0
  78. package/dist/chat.d.ts.map +1 -0
  79. package/dist/chat.js +8 -0
  80. package/dist/client.d.ts +9 -5
  81. package/dist/client.d.ts.map +1 -1
  82. package/dist/client.js +61 -15
  83. package/dist/collection.d.ts +6 -11
  84. package/dist/collection.d.ts.map +1 -1
  85. package/dist/collection.js +56 -19
  86. package/dist/connection.d.ts +14 -7
  87. package/dist/connection.d.ts.map +1 -1
  88. package/dist/connection.js +239 -102
  89. package/dist/conversation.d.ts +11 -3
  90. package/dist/conversation.d.ts.map +1 -1
  91. package/dist/conversation.js +47 -7
  92. package/dist/expectation.d.ts +21 -0
  93. package/dist/expectation.d.ts.map +1 -0
  94. package/dist/expectation.js +70 -0
  95. package/dist/failure.d.ts +13 -0
  96. package/dist/failure.d.ts.map +1 -0
  97. package/dist/failure.js +19 -0
  98. package/dist/host.d.ts +1 -0
  99. package/dist/host.d.ts.map +1 -1
  100. package/dist/host.js +5 -8
  101. package/dist/identity.d.ts +2 -0
  102. package/dist/identity.d.ts.map +1 -1
  103. package/dist/identity.js +10 -1
  104. package/dist/index.d.ts +7 -4
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js +2 -0
  107. package/dist/memory.d.ts +2 -1
  108. package/dist/memory.d.ts.map +1 -1
  109. package/dist/memory.js +31 -11
  110. package/dist/model.d.ts +10 -2
  111. package/dist/model.d.ts.map +1 -1
  112. package/dist/model.js +2 -0
  113. package/dist/open.d.ts +2 -0
  114. package/dist/open.d.ts.map +1 -0
  115. package/dist/open.js +1 -0
  116. package/dist/outcome.d.ts +22 -3
  117. package/dist/outcome.d.ts.map +1 -1
  118. package/dist/outcome.js +35 -1
  119. package/dist/overlay.d.ts +1 -1
  120. package/dist/overlay.d.ts.map +1 -1
  121. package/dist/overlay.js +19 -15
  122. package/dist/query.d.ts +2 -0
  123. package/dist/query.d.ts.map +1 -1
  124. package/dist/query.js +5 -31
  125. package/dist/reach.d.ts.map +1 -1
  126. package/dist/reach.js +21 -19
  127. package/dist/react.d.ts +25 -16
  128. package/dist/react.d.ts.map +1 -1
  129. package/dist/react.js +8 -6
  130. package/dist/refresh.d.ts +10 -0
  131. package/dist/refresh.d.ts.map +1 -1
  132. package/dist/refresh.js +33 -24
  133. package/dist/replica.d.ts +5 -5
  134. package/dist/replica.d.ts.map +1 -1
  135. package/dist/replica.js +44 -29
  136. package/dist/room-draft.d.ts +8 -8
  137. package/dist/room-draft.d.ts.map +1 -1
  138. package/dist/room-draft.js +13 -22
  139. package/dist/rows.d.ts +2 -0
  140. package/dist/rows.d.ts.map +1 -1
  141. package/dist/rows.js +7 -1
  142. package/dist/save.d.ts +3 -1
  143. package/dist/save.d.ts.map +1 -1
  144. package/dist/save.js +38 -21
  145. package/dist/search.d.ts +3 -2
  146. package/dist/search.d.ts.map +1 -1
  147. package/dist/status.d.ts +2 -2
  148. package/dist/status.d.ts.map +1 -1
  149. package/dist/status.js +0 -8
  150. package/dist/store.d.ts +25 -1
  151. package/dist/store.d.ts.map +1 -1
  152. package/dist/store.js +206 -90
  153. package/dist/wire.d.ts +5 -3
  154. package/dist/wire.d.ts.map +1 -1
  155. package/dist/wire.js +13 -2
  156. package/package.json +17 -9
@@ -3,17 +3,20 @@ 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 { validRoot } from './collection.js';
12
+ import { runtimeError } from './failure.js';
11
13
  import { createLog } from './log.js';
14
+ import { calledResult, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
12
15
  import { refreshRoom } from './refresh.js';
13
16
  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';
17
+ import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
18
+ import { changedParts, changesOf, held, readParts, returnedBy, returnedInto, staleWrite, underneath, } from './room-draft.js';
19
+ import { same, saving, STARTING, transferring, } from './status.js';
17
20
  import { detectStorageLoss, nextGeneration } from './durability.js';
18
21
  import { roomDraftRevision } from './identity.js';
19
22
  import { beneath, normalize } from './paths.js';
@@ -34,6 +37,7 @@ function declaredSelection(room) {
34
37
  selection.roots.every((other, at) => at === index || !within(other, root)));
35
38
  }
36
39
  export function composeRuntime(options) {
40
+ const localSaves = options.adapter.localSaves === true;
37
41
  let authorization = options.authorization;
38
42
  let opening;
39
43
  let store;
@@ -44,6 +48,7 @@ export function composeRuntime(options) {
44
48
  let limits = [];
45
49
  let capabilities = [];
46
50
  const rooms = new Map();
51
+ const counted = new Map();
47
52
  const unreadable = new Set();
48
53
  const models = new Map();
49
54
  const liveModels = new Map();
@@ -82,6 +87,8 @@ export function composeRuntime(options) {
82
87
  let withdrawStream;
83
88
  const announced = new Map();
84
89
  let underway = null;
90
+ let streamed = null;
91
+ let handed = false;
85
92
  const submitting = new Map();
86
93
  const answering = new Map();
87
94
  let complete = false;
@@ -104,9 +111,14 @@ export function composeRuntime(options) {
104
111
  let haltedRefusal = false;
105
112
  let openSignals = 0;
106
113
  const serviceModeListeners = new Set();
114
+ let member = null;
115
+ const memberListeners = new Set();
116
+ const materialized = new Set();
117
+ let everyRoom = false;
118
+ const transfers = new Map();
107
119
  const conversations = createConversationStore({
108
120
  agents: declaredAgents,
109
- documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
121
+ documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
110
122
  call: (name, input) => invokeAction(name, input),
111
123
  durably,
112
124
  stream: () => options.adapter.conversations,
@@ -182,7 +194,7 @@ export function composeRuntime(options) {
182
194
  }
183
195
  }
184
196
  function pending() {
185
- return [...replicas.values()].some((replica) => replica.queue.length > 0);
197
+ return !localSaves && [...replicas.values()].some((replica) => replica.queue.length > 0);
186
198
  }
187
199
  function attention() {
188
200
  const attentionRequired = [];
@@ -211,8 +223,24 @@ export function composeRuntime(options) {
211
223
  }
212
224
  return { attentionRequired, reset };
213
225
  }
226
+ function documentsIn(locator) {
227
+ const replica = replicas.get(locator);
228
+ if (replica === undefined)
229
+ return 0;
230
+ const roots = replica.confirmed.record.typedSelection?.roots ?? [];
231
+ let count = 0;
232
+ for (const path of Object.keys(replica.visible.documents)) {
233
+ if (markdownPath(path) && !roots.some((root) => beneath(root, path)))
234
+ count += 1;
235
+ }
236
+ return count;
237
+ }
214
238
  function listed() {
215
- return structuredClone([...rooms.values()]);
239
+ return structuredClone([...rooms.values()].map((room) => {
240
+ const documents = documentsIn(room.locator);
241
+ counted.set(room.locator, documents);
242
+ return { ...room, documents };
243
+ }));
216
244
  }
217
245
  function roomFacts(reach = null) {
218
246
  const held = [...rooms.values()]
@@ -244,6 +272,10 @@ export function composeRuntime(options) {
244
272
  for (const listener of storageLossListeners)
245
273
  listener(storageLoss);
246
274
  }
275
+ function emitMember() {
276
+ for (const listener of memberListeners)
277
+ listener(member);
278
+ }
247
279
  function moveTo(next) {
248
280
  if (closed)
249
281
  return;
@@ -263,6 +295,8 @@ export function composeRuntime(options) {
263
295
  listener(shown);
264
296
  }
265
297
  function entryOf(batch) {
298
+ if (localSaves)
299
+ return Object.freeze({ batch, state: 'local' });
266
300
  const state = pendings.get(batch.id);
267
301
  if (state?.accepted !== undefined) {
268
302
  return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
@@ -283,12 +317,47 @@ export function composeRuntime(options) {
283
317
  }
284
318
  }
285
319
  function applyChangeSet(set) {
320
+ const held = everyRoom
321
+ ? set
322
+ : { ...set, documents: set.documents.filter(({ locator }) => materialized.has(locator)) };
323
+ readSide.apply(held);
324
+ searchIndex.changed(held);
325
+ }
326
+ function materialize(locators) {
327
+ if (everyRoom)
328
+ return;
329
+ if (locators === null)
330
+ everyRoom = true;
331
+ const sets = [];
332
+ for (const locator of locators ?? [...replicas.keys()]) {
333
+ if (materialized.has(locator))
334
+ continue;
335
+ materialized.add(locator);
336
+ const replica = replicas.get(locator);
337
+ if (replica !== undefined)
338
+ sets.push(changeSet(locator, null, replica));
339
+ }
340
+ if (sets.length === 0)
341
+ return;
342
+ const set = mergeChangeSets(sets);
286
343
  readSide.apply(set);
287
344
  searchIndex.changed(set);
288
345
  }
346
+ function reachedRooms(plan) {
347
+ if (plan.rooms !== null)
348
+ return plan.rooms;
349
+ if (plan.reach === null)
350
+ return null;
351
+ return [...plan.reach.rooms, ...plan.reach.documents.map(roomOfKey)];
352
+ }
353
+ function rowsQuery(plan) {
354
+ materialize(reachedRooms(plan));
355
+ return readSide.query(plan);
356
+ }
289
357
  function publishChangeSet(set) {
290
358
  applyChangeSet(set);
291
- if (set.rooms.length > 0)
359
+ if (set.rooms.length > 0 ||
360
+ [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
292
361
  emitRooms();
293
362
  for (const locator of set.queues)
294
363
  emitQueue(locator);
@@ -362,10 +431,7 @@ export function composeRuntime(options) {
362
431
  model: revoked,
363
432
  selectionModel: revoked,
364
433
  };
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 } });
434
+ replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
369
435
  }
370
436
  rooms.set(locator, {
371
437
  ...room,
@@ -382,7 +448,8 @@ export function composeRuntime(options) {
382
448
  }
383
449
  finally {
384
450
  refusal = {
385
- reason,
451
+ code: reason,
452
+ retryable: false,
386
453
  reauthorizationOffered: reason === 'revoked-authorization',
387
454
  refusal: provider,
388
455
  };
@@ -432,7 +499,7 @@ export function composeRuntime(options) {
432
499
  emitRooms();
433
500
  }
434
501
  function unlistedHeld(listed) {
435
- return [...new Set([...rooms.keys(), ...roomDrafts.keys()])].filter((locator) => !listed.has(locator));
502
+ return [...new Set([...rooms.keys(), ...roomDrafts.keys(), ...transfers.keys()])].filter((locator) => !listed.has(locator));
436
503
  }
437
504
  async function dropUnlisted(listed, local) {
438
505
  if (unlistedHeld(listed).length === 0)
@@ -451,6 +518,7 @@ export function composeRuntime(options) {
451
518
  replicas.delete(locator);
452
519
  rooms.delete(locator);
453
520
  roomDrafts.delete(locator);
521
+ transfers.delete(locator);
454
522
  unreadable.delete(locator);
455
523
  stale.delete(locator);
456
524
  scopedRefusals.delete(locator);
@@ -487,7 +555,7 @@ export function composeRuntime(options) {
487
555
  return model;
488
556
  }
489
557
  async function hydrate(opened) {
490
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
558
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
491
559
  opened.all(),
492
560
  opened.queues(),
493
561
  opened.roomDrafts(),
@@ -497,9 +565,15 @@ export function composeRuntime(options) {
497
565
  opened.lastCompleted(),
498
566
  opened.providerLimits(),
499
567
  opened.capabilities(),
568
+ opened.member(),
569
+ opened.transfers(),
500
570
  ]);
571
+ transfers.clear();
572
+ for (const record of staged)
573
+ transfers.set(record.locator, record);
501
574
  limits = declared;
502
575
  capabilities = named;
576
+ member = known;
503
577
  roomDrafts.clear();
504
578
  for (const record of kept)
505
579
  rememberRoomDraft(record);
@@ -519,14 +593,7 @@ export function composeRuntime(options) {
519
593
  const model = restored(modelKey(record.locator, 'collection'), record.model);
520
594
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
521
595
  const prior = previousReplicas.get(record.locator);
522
- const retyped = { ...record, model, selectionModel };
523
- const read = readConfirmed(retyped, prior?.confirmed);
524
- const confirmed = read.kind === 'staged'
525
- ? read.confirmed
526
- : {
527
- record: retyped,
528
- collection: parseCollection(retyped.documents, compositionOf(retyped)),
529
- };
596
+ const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
530
597
  replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
531
598
  const existing = rooms.get(record.locator);
532
599
  if (existing === undefined)
@@ -547,10 +614,6 @@ export function composeRuntime(options) {
547
614
  },
548
615
  });
549
616
  }
550
- if (read.kind === 'refused') {
551
- refusedSource(record, read);
552
- declarationRefusals.add(record.locator);
553
- }
554
617
  }
555
618
  return {
556
619
  lastCompleted,
@@ -563,6 +626,8 @@ export function composeRuntime(options) {
563
626
  applyChangeSet(read);
564
627
  if (replicas.size > 0)
565
628
  emitRooms();
629
+ if (member !== null)
630
+ emitMember();
566
631
  for (const locator of locators)
567
632
  emitQueue(locator);
568
633
  publishRoomDrafts(false);
@@ -610,10 +675,14 @@ export function composeRuntime(options) {
610
675
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
611
676
  }
612
677
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
613
- const withdrawn = withdrawals(confirmed, queue, withdrawable);
678
+ const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
614
679
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
680
+ const unqueued = withdrawn.map((batch) => ({
681
+ batch,
682
+ returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
683
+ }));
615
684
  const returning = [
616
- ...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
685
+ ...[...unqueued].reverse(),
617
686
  ...(settling.leaving === undefined ? [] : [settling.leaving]),
618
687
  ];
619
688
  const written = new Map();
@@ -634,6 +703,7 @@ export function composeRuntime(options) {
634
703
  await local.write({
635
704
  next: confirmed.record,
636
705
  previous: before?.confirmed.record ?? null,
706
+ ...(settling.stored === undefined ? {} : { stored: settling.stored }),
637
707
  queue: kept,
638
708
  previousQueue: before?.queue ?? [],
639
709
  roomDrafts: [...written.values()],
@@ -644,8 +714,10 @@ export function composeRuntime(options) {
644
714
  }
645
715
  for (const record of written.values())
646
716
  rememberRoomDraft(record);
647
- for (const batch of withdrawn)
648
- journal.record(`outcome ${locator} ${batch.id} withdrawn`);
717
+ for (const { batch, returned } of unqueued) {
718
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
719
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
720
+ }
649
721
  replicas.set(locator, replicaOf(confirmed, kept, before));
650
722
  return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
651
723
  room: settling.room,
@@ -684,7 +756,8 @@ export function composeRuntime(options) {
684
756
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
685
757
  };
686
758
  }
687
- function publish(local, room, confirmed, base) {
759
+ function publish(local, room, confirmed, landing = {}) {
760
+ const { base, stored } = landing;
688
761
  return exclusive(async () => {
689
762
  if (!continuing())
690
763
  return true;
@@ -701,7 +774,9 @@ export function composeRuntime(options) {
701
774
  const accepted = pendings.get(batch.id)?.accepted;
702
775
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
703
776
  });
704
- const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
777
+ const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
778
+ if (stored !== undefined)
779
+ transfers.delete(room.locator);
705
780
  for (const batch of accepted)
706
781
  pendings.delete(batch.id);
707
782
  stale.delete(room.locator);
@@ -720,22 +795,23 @@ export function composeRuntime(options) {
720
795
  const replica = replicas.get(locator);
721
796
  const room = rooms.get(locator);
722
797
  if (replica === undefined || room === undefined) {
723
- throw new Error(`No Local Replica for ${locator}`);
798
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
724
799
  }
725
800
  const saved = [];
726
801
  for (const { path, revision } of named) {
727
802
  const record = roomDrafts.get(locator)?.get(path);
728
803
  if (record === undefined || record.revision !== revision) {
729
- return { outcome: 'refused', rule: 'stale', file: path };
804
+ return { outcome: 'refused', code: 'stale', retryable: false, file: path };
730
805
  }
731
806
  if (!saved.some((draft) => draft.path === path))
732
807
  saved.push({ path, parts: record.parts });
733
808
  }
734
809
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
735
810
  if (changed.length > 0)
736
- return { outcome: 'refused', rule: 'changed-underneath', parts: changed };
811
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
737
812
  const parts = saved.map(({ path, parts }) => changesOf(path, parts));
738
813
  const built = buildBatch({
814
+ local: localSaves,
739
815
  baseRoomVersion: replica.confirmed.record.roomVersion,
740
816
  composition: compositionFor(replica.confirmed.record),
741
817
  operations: room.operations,
@@ -762,10 +838,11 @@ export function composeRuntime(options) {
762
838
  });
763
839
  }
764
840
  catch {
765
- return { outcome: 'refused', rule: 'durable-write' };
841
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
766
842
  }
767
843
  pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
768
- recorded = false;
844
+ if (!localSaves)
845
+ recorded = false;
769
846
  for (const path of consumed)
770
847
  forgetRoomDraft(locator, path);
771
848
  replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
@@ -773,7 +850,7 @@ export function composeRuntime(options) {
773
850
  queue: true,
774
851
  drafts: consumed,
775
852
  }));
776
- moveTo({ ...saving(status), ...attention() });
853
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
777
854
  submitLater(locator);
778
855
  return { outcome: 'saved', identifier: built.batch.id };
779
856
  }
@@ -801,16 +878,15 @@ export function composeRuntime(options) {
801
878
  }
802
879
  async function readHistory(locator, path, query = {}) {
803
880
  if (closed || refusal !== null)
804
- return { kind: 'transport-uncertainty' };
881
+ return UNCERTAIN;
805
882
  try {
806
883
  await ensureAuthorization();
807
884
  }
808
885
  catch {
809
- return { kind: 'transport-uncertainty' };
810
- }
811
- if (closed || authorization === undefined || refusal !== null) {
812
- return { kind: 'transport-uncertainty' };
886
+ return UNCERTAIN;
813
887
  }
888
+ if (closed || authorization === undefined || refusal !== null)
889
+ return UNCERTAIN;
814
890
  const transfer = ['transfer', 'history', locator, path];
815
891
  if (query.revision !== undefined)
816
892
  transfer.push(query.revision);
@@ -832,11 +908,11 @@ export function composeRuntime(options) {
832
908
  }
833
909
  if (result.kind === 'revoked-authorization')
834
910
  await refuse('revoked-authorization');
835
- return result;
911
+ return historyAnswer(result);
836
912
  }
837
913
  catch {
838
914
  journal.record('outcome history transport-uncertainty');
839
- return { kind: 'transport-uncertainty' };
915
+ return UNCERTAIN;
840
916
  }
841
917
  }
842
918
  function declaredAgents() {
@@ -858,18 +934,17 @@ export function composeRuntime(options) {
858
934
  async function invokeAction(name, input) {
859
935
  await ready;
860
936
  if (closed || refusal !== null)
861
- return { kind: 'transport-uncertainty' };
937
+ return UNCERTAIN;
862
938
  if (declaredAction(name) === undefined)
863
- return { kind: 'undeclared' };
939
+ return undeclared(name);
864
940
  try {
865
941
  await ensureAuthorization();
866
942
  }
867
943
  catch {
868
- return { kind: 'transport-uncertainty' };
869
- }
870
- if (closed || authorization === undefined || refusal !== null) {
871
- return { kind: 'transport-uncertainty' };
944
+ return UNCERTAIN;
872
945
  }
946
+ if (closed || authorization === undefined || refusal !== null)
947
+ return UNCERTAIN;
873
948
  journal.record(`transfer action ${name}`);
874
949
  try {
875
950
  const result = await options.adapter.action(authorization, { name, input });
@@ -883,11 +958,11 @@ export function composeRuntime(options) {
883
958
  journal.record(`outcome action ${result.kind}`);
884
959
  if (result.kind === 'rejected')
885
960
  journal.record(`refusal rejected ${result.code}`);
886
- return result;
961
+ return calledResult(result);
887
962
  }
888
963
  catch {
889
964
  journal.record('outcome action transport-uncertainty');
890
- return { kind: 'transport-uncertainty' };
965
+ return UNCERTAIN;
891
966
  }
892
967
  }
893
968
  function continuing() {
@@ -918,14 +993,33 @@ export function composeRuntime(options) {
918
993
  throw error;
919
994
  }
920
995
  }
921
- async function pullBootstrap(authorized, room) {
996
+ function staging(local) {
997
+ return {
998
+ held: (locator) => transfers.get(locator) ?? null,
999
+ staged: (record) => local.staged(record),
1000
+ stage: (record, page) => exclusive(async () => {
1001
+ if (!continuing())
1002
+ return false;
1003
+ await local.stage(record, page);
1004
+ transfers.set(record.locator, record);
1005
+ return true;
1006
+ }),
1007
+ abandon: (locator) => exclusive(async () => {
1008
+ if (!continuing())
1009
+ return;
1010
+ await local.abandon(locator);
1011
+ transfers.delete(locator);
1012
+ }),
1013
+ };
1014
+ }
1015
+ async function pullBootstrap(authorized, room, local) {
922
1016
  if (!continuing())
923
1017
  return unconfirmed;
924
1018
  try {
925
1019
  const result = await bootstrapRoom(options.adapter, authorized, room, {
926
1020
  collection: modelOf(room.locator),
927
1021
  selection: modelOf(room.locator, 'typed-selection'),
928
- }, (models, held) => remodel(room.locator, models, held), continuing);
1022
+ }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
929
1023
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
930
1024
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
931
1025
  return result;
@@ -953,7 +1047,7 @@ export function composeRuntime(options) {
953
1047
  return null;
954
1048
  }
955
1049
  if (refreshed.kind === 'advanced') {
956
- if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
1050
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
957
1051
  const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
958
1052
  const reached = now !== undefined &&
959
1053
  options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
@@ -965,12 +1059,12 @@ export function composeRuntime(options) {
965
1059
  if (refreshed.kind !== 'beyond-horizon')
966
1060
  return refreshed;
967
1061
  }
968
- const bootstrapped = await pullBootstrap(authorized, room);
1062
+ const bootstrapped = await pullBootstrap(authorized, room, local);
969
1063
  if (!continuing())
970
1064
  return unconfirmed;
971
1065
  if (bootstrapped.kind !== 'staged')
972
1066
  return bootstrapped;
973
- await publish(local, room, bootstrapped.confirmed);
1067
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
974
1068
  return null;
975
1069
  }
976
1070
  async function withhold(withheld, room, local, progress) {
@@ -1002,6 +1096,7 @@ export function composeRuntime(options) {
1002
1096
  function refusedSource(room, { refusal }) {
1003
1097
  sourceRefusals.set(room.locator, {
1004
1098
  code: refusal.rule,
1099
+ retryable: false,
1005
1100
  fields: {
1006
1101
  file: refusal.file,
1007
1102
  ...(refusal.location === undefined ? {} : { location: refusal.location }),
@@ -1016,8 +1111,16 @@ export function composeRuntime(options) {
1016
1111
  }
1017
1112
  function leaving(batch, result) {
1018
1113
  if (result.kind === 'expired')
1019
- return { batch, returned: { kind: 'expired' } };
1020
- return { batch, returned: { kind: result.kind, code: result.code, fields: result.fields } };
1114
+ return { batch, returned: returnedBy('expired') };
1115
+ return {
1116
+ batch,
1117
+ returned: {
1118
+ kind: result.kind,
1119
+ code: result.code,
1120
+ fields: result.fields,
1121
+ retryable: result.retryable,
1122
+ },
1123
+ };
1021
1124
  }
1022
1125
  function logAccepted(locator, submitted, accepted) {
1023
1126
  journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
@@ -1035,7 +1138,7 @@ export function composeRuntime(options) {
1035
1138
  logAccepted(room.locator, submitted, accepted);
1036
1139
  const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1037
1140
  ? { kind: 'staged', confirmed: current.confirmed }
1038
- : applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
1141
+ : applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.changes, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
1039
1142
  ? accepted.roomVersion
1040
1143
  : held, accepted.roomVersion);
1041
1144
  if (applied.kind !== 'staged') {
@@ -1181,7 +1284,8 @@ export function composeRuntime(options) {
1181
1284
  return;
1182
1285
  }
1183
1286
  const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1184
- leaving: result.kind === 'expired' && landed(head, current.confirmed)
1287
+ leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1288
+ (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1185
1289
  ? { ...left, returned: null }
1186
1290
  : left,
1187
1291
  });
@@ -1242,7 +1346,7 @@ export function composeRuntime(options) {
1242
1346
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1243
1347
  };
1244
1348
  const reclassified = sameMembership(declared, held.confirmed.record)
1245
- ? readConfirmed(declared, held.confirmed)
1349
+ ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
1246
1350
  : readReclassified(declared, held.confirmed);
1247
1351
  const record = reclassified.kind === 'staged'
1248
1352
  ? declared
@@ -1250,7 +1354,9 @@ export function composeRuntime(options) {
1250
1354
  ...declared,
1251
1355
  typedSelection: held.confirmed.record.typedSelection,
1252
1356
  };
1253
- const read = reclassified.kind === 'deferred' ? readConfirmed(record, held.confirmed) : reclassified;
1357
+ const read = reclassified.kind === 'deferred'
1358
+ ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
1359
+ : reclassified;
1254
1360
  await local.write({
1255
1361
  next: record,
1256
1362
  previous: held.confirmed.record,
@@ -1282,7 +1388,11 @@ export function composeRuntime(options) {
1282
1388
  });
1283
1389
  }
1284
1390
  async function scoped(result, room, local) {
1285
- scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
1391
+ scopedRefusals.set(room.locator, {
1392
+ code: result.code,
1393
+ fields: result.fields,
1394
+ retryable: result.retryable,
1395
+ });
1286
1396
  let scopedAway = room;
1287
1397
  if (result.scope === 'model')
1288
1398
  await retainModel(local, room, 'collection', {
@@ -1330,8 +1440,10 @@ export function composeRuntime(options) {
1330
1440
  const outcome = prepared[index];
1331
1441
  return outcome?.status === 'fulfilled' && outcome.value;
1332
1442
  });
1333
- if (attempt === 'opened')
1443
+ if (attempt === 'opened') {
1334
1444
  options.adapter.roomStream?.hold(heldRooms(ready));
1445
+ handed = true;
1446
+ }
1335
1447
  const loaded = await Promise.allSettled(ready
1336
1448
  .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1337
1449
  .map((room) => loadRoom(room, local, progress)));
@@ -1397,14 +1509,14 @@ export function composeRuntime(options) {
1397
1509
  async function reload(room, local, progress) {
1398
1510
  if (discoveryPaused || authorization === undefined)
1399
1511
  return;
1400
- const bootstrapped = await pullBootstrap(authorization, room);
1512
+ const bootstrapped = await pullBootstrap(authorization, room, local);
1401
1513
  if (!continuing())
1402
1514
  return;
1403
1515
  if (bootstrapped.kind !== 'staged') {
1404
1516
  await withhold(bootstrapped, room, local, progress);
1405
1517
  return;
1406
1518
  }
1407
- await publish(local, room, bootstrapped.confirmed);
1519
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1408
1520
  await caughtUp(room, local, progress);
1409
1521
  }
1410
1522
  async function discoverRooms(local, progress, attempt) {
@@ -1434,15 +1546,16 @@ export function composeRuntime(options) {
1434
1546
  }
1435
1547
  progress.reachable = false;
1436
1548
  progress.complete = false;
1437
- if (attempt === 'opened') {
1438
- options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1439
- }
1440
1549
  notifyReady();
1441
1550
  return;
1442
1551
  }
1443
1552
  if (result.kind === 'contract-unavailable') {
1444
1553
  journal.record(`connection contract unavailable ${result.code}`);
1445
- await refuse('missing-required-guarantee', { code: result.code, fields: result.fields });
1554
+ await refuse('missing-required-guarantee', {
1555
+ code: result.code,
1556
+ fields: result.fields,
1557
+ retryable: result.retryable,
1558
+ });
1446
1559
  notifyReady();
1447
1560
  return;
1448
1561
  }
@@ -1483,6 +1596,12 @@ export function composeRuntime(options) {
1483
1596
  limits = result.providerLimits;
1484
1597
  await local.declare(limits, capabilities);
1485
1598
  }
1599
+ const signedIn = result.member ?? null;
1600
+ if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
1601
+ await local.keepMember(signedIn);
1602
+ member = signedIn;
1603
+ emitMember();
1604
+ }
1486
1605
  await applyDiscovery(result.rooms, local);
1487
1606
  await synchronize(result.rooms, local, progress, attempt);
1488
1607
  }
@@ -1512,9 +1631,12 @@ export function composeRuntime(options) {
1512
1631
  }
1513
1632
  const progress = { complete: true, reachable: true };
1514
1633
  underway = attempt === 'opened' ? progress : null;
1634
+ if (attempt === 'opened')
1635
+ streamed = progress;
1515
1636
  complete = false;
1516
1637
  recorded = false;
1517
1638
  moveTo(transferring(status));
1639
+ handed = false;
1518
1640
  try {
1519
1641
  const local = await openLocal();
1520
1642
  if (closed)
@@ -1526,16 +1648,22 @@ export function composeRuntime(options) {
1526
1648
  }
1527
1649
  catch {
1528
1650
  progress.complete = false;
1529
- if (attempt === 'opened') {
1530
- options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1531
- }
1651
+ if (attempt === 'opened' && !handed)
1652
+ progress.reachable = false;
1532
1653
  }
1533
1654
  finally {
1534
1655
  notifyReady();
1535
1656
  }
1536
- if (attempt === 'opened')
1657
+ if (attempt !== 'opened') {
1658
+ await finishOpening(progress);
1537
1659
  return;
1538
- await finishOpening(progress);
1660
+ }
1661
+ if (handed || closed || progress.reachable)
1662
+ return;
1663
+ underway = null;
1664
+ journal.record('connection reopen');
1665
+ await rest(progress);
1666
+ options.adapter.roomStream?.reopen();
1539
1667
  });
1540
1668
  }
1541
1669
  function closedSocket(revoked) {
@@ -1545,9 +1673,14 @@ export function composeRuntime(options) {
1545
1673
  void refuse('revoked-authorization');
1546
1674
  return;
1547
1675
  }
1548
- if (opened || status.providerUnreachable)
1549
- return;
1550
- moveTo({ ...status, providerUnreachable: true });
1676
+ complete = false;
1677
+ if (streamed !== null)
1678
+ streamed.complete = false;
1679
+ moveTo({
1680
+ ...status,
1681
+ state: status.state === 'synced' ? 'syncing' : status.state,
1682
+ providerUnreachable: status.providerUnreachable || !opened,
1683
+ });
1551
1684
  }
1552
1685
  function applyOpening(expired) {
1553
1686
  return task(async () => {
@@ -1602,6 +1735,8 @@ export function composeRuntime(options) {
1602
1735
  }
1603
1736
  }
1604
1737
  function submitLater(locator) {
1738
+ if (localSaves)
1739
+ return;
1605
1740
  const running = submitting.get(locator);
1606
1741
  if (running !== undefined) {
1607
1742
  running.again = true;
@@ -1681,7 +1816,7 @@ export function composeRuntime(options) {
1681
1816
  }
1682
1817
  }
1683
1818
  moveTo({
1684
- state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
1819
+ state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
1685
1820
  lastCompleted,
1686
1821
  providerUnreachable: !progress.reachable,
1687
1822
  ...attention(),
@@ -1790,20 +1925,15 @@ export function composeRuntime(options) {
1790
1925
  show();
1791
1926
  return;
1792
1927
  }
1793
- const resting = status;
1794
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1795
- moveTo(transferring(resting));
1928
+ const progress = { complete: false, reachable: !status.providerUnreachable };
1929
+ moveTo(transferring(status));
1796
1930
  try {
1797
1931
  await conversations.catchUp(progress);
1798
1932
  }
1799
1933
  catch {
1800
1934
  progress.reachable = false;
1801
1935
  }
1802
- finally {
1803
- if (!closed) {
1804
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1805
- }
1806
- }
1936
+ await rest(progress);
1807
1937
  }
1808
1938
  function close() {
1809
1939
  if (closed)
@@ -1825,6 +1955,7 @@ export function composeRuntime(options) {
1825
1955
  storageLossListeners.clear();
1826
1956
  statusListeners.clear();
1827
1957
  serviceModeListeners.clear();
1958
+ memberListeners.clear();
1828
1959
  readSide.close();
1829
1960
  queueSubscriptions.clear();
1830
1961
  roomDraftListeners.clear();
@@ -1856,13 +1987,16 @@ export function composeRuntime(options) {
1856
1987
  return planOf(input, roomFacts(reach), planReach(reach));
1857
1988
  }
1858
1989
  function watchPlan(plan, deliver) {
1990
+ materialize(reachedRooms(plan));
1859
1991
  return readSide.watch(plan, identityOf(plan), deliver);
1860
1992
  }
1861
1993
  async function searched(input, reach) {
1862
1994
  const plan = searchPlanOf(input);
1863
1995
  const selected = plan.documents === undefined && reach === null
1864
1996
  ? null
1865
- : (await readSide.query(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
1997
+ : (await rowsQuery(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
1998
+ if (selected === null)
1999
+ materialize(null);
1866
2000
  return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
1867
2001
  }
1868
2002
  const watch = (topic, input, deliver) => {
@@ -1888,6 +2022,8 @@ export function composeRuntime(options) {
1888
2022
  }
1889
2023
  if (topic === 'conversations')
1890
2024
  return conversations.watchList(deliver);
2025
+ if (topic === 'agents')
2026
+ return conversations.watchAgents(deliver);
1891
2027
  if (topic === 'presentation') {
1892
2028
  const listener = {
1893
2029
  app: input[0],
@@ -1942,6 +2078,13 @@ export function composeRuntime(options) {
1942
2078
  serviceModeListeners.delete(deliver);
1943
2079
  };
1944
2080
  }
2081
+ if (topic === 'member') {
2082
+ memberListeners.add(deliver);
2083
+ deliver(member);
2084
+ return () => {
2085
+ memberListeners.delete(deliver);
2086
+ };
2087
+ }
1945
2088
  if (topic === 'queue') {
1946
2089
  const queue = { locator: input[0], notify: deliver };
1947
2090
  queueSubscriptions.add(queue);
@@ -1959,7 +2102,7 @@ export function composeRuntime(options) {
1959
2102
  rooms() {
1960
2103
  return listed();
1961
2104
  },
1962
- query: ((input) => readSide.query(planned(input, null))),
2105
+ query: ((input) => rowsQuery(planned(input, null))),
1963
2106
  search: (input) => searched(input, null),
1964
2107
  types(locator) {
1965
2108
  return {
@@ -1988,14 +2131,11 @@ export function composeRuntime(options) {
1988
2131
  return exclusive(async () => {
1989
2132
  const parts = readParts(input);
1990
2133
  if (path === '' || normalize(path) !== path)
1991
- throw new Error('room-draft-malformed');
2134
+ throw runtimeError('room-draft-malformed');
1992
2135
  const local = await openLocal();
1993
2136
  const current = roomDrafts.get(room)?.get(path);
1994
2137
  if ((current?.revision ?? null) !== revision) {
1995
- return {
1996
- outcome: 'stale',
1997
- current: current === undefined ? null : roomDraftOf(current),
1998
- };
2138
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
1999
2139
  }
2000
2140
  const record = {
2001
2141
  locator: room,
@@ -2015,10 +2155,7 @@ export function composeRuntime(options) {
2015
2155
  const local = await openLocal();
2016
2156
  const current = roomDrafts.get(room)?.get(path);
2017
2157
  if (current === undefined || current.revision !== revision) {
2018
- return {
2019
- outcome: 'stale',
2020
- current: current === undefined ? null : roomDraftOf(current),
2021
- };
2158
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2022
2159
  }
2023
2160
  await local.discardRoomDraft(room, path);
2024
2161
  forgetRoomDraft(room, path);
@@ -2086,7 +2223,7 @@ export function composeRuntime(options) {
2086
2223
  md,
2087
2224
  watch,
2088
2225
  plan: planned,
2089
- query: (plan) => readSide.query(plan),
2226
+ query: (plan) => rowsQuery(plan),
2090
2227
  watchPlan,
2091
2228
  search: searched,
2092
2229
  };