@agentshouse/mdruntime 0.1.0-alpha.8 → 0.1.0-alpha.9

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 (70) hide show
  1. package/dist/adapter.d.ts +76 -21
  2. package/dist/adapter.d.ts.map +1 -1
  3. package/dist/adapter.js.map +1 -1
  4. package/dist/bootstrap.d.ts +4 -1
  5. package/dist/bootstrap.d.ts.map +1 -1
  6. package/dist/bootstrap.js +4 -2
  7. package/dist/bootstrap.js.map +1 -1
  8. package/dist/client.d.ts +63 -0
  9. package/dist/client.d.ts.map +1 -0
  10. package/dist/client.js +227 -0
  11. package/dist/client.js.map +1 -0
  12. package/dist/collection.d.ts +14 -4
  13. package/dist/collection.d.ts.map +1 -1
  14. package/dist/collection.js +60 -35
  15. package/dist/collection.js.map +1 -1
  16. package/dist/connection.d.ts +28 -7
  17. package/dist/connection.d.ts.map +1 -1
  18. package/dist/connection.js +603 -368
  19. package/dist/connection.js.map +1 -1
  20. package/dist/conversation.d.ts +57 -0
  21. package/dist/conversation.d.ts.map +1 -0
  22. package/dist/conversation.js +401 -0
  23. package/dist/conversation.js.map +1 -0
  24. package/dist/durability.d.ts +8 -3
  25. package/dist/durability.d.ts.map +1 -1
  26. package/dist/durability.js +5 -59
  27. package/dist/durability.js.map +1 -1
  28. package/dist/file.d.ts +20 -0
  29. package/dist/file.d.ts.map +1 -0
  30. package/dist/file.js +149 -0
  31. package/dist/file.js.map +1 -0
  32. package/dist/host.d.ts +9 -0
  33. package/dist/host.d.ts.map +1 -0
  34. package/dist/host.js +200 -0
  35. package/dist/host.js.map +1 -0
  36. package/dist/index.d.ts +7 -4
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/memory.d.ts +14 -2
  40. package/dist/memory.d.ts.map +1 -1
  41. package/dist/memory.js +171 -11
  42. package/dist/memory.js.map +1 -1
  43. package/dist/outcome.d.ts +16 -9
  44. package/dist/outcome.d.ts.map +1 -1
  45. package/dist/outcome.js +10 -0
  46. package/dist/outcome.js.map +1 -1
  47. package/dist/query.d.ts +1 -0
  48. package/dist/query.d.ts.map +1 -1
  49. package/dist/query.js.map +1 -1
  50. package/dist/replica.d.ts +2 -1
  51. package/dist/replica.d.ts.map +1 -1
  52. package/dist/replica.js +16 -3
  53. package/dist/replica.js.map +1 -1
  54. package/dist/save.d.ts +3 -3
  55. package/dist/save.d.ts.map +1 -1
  56. package/dist/save.js +33 -14
  57. package/dist/save.js.map +1 -1
  58. package/dist/status.d.ts +11 -2
  59. package/dist/status.d.ts.map +1 -1
  60. package/dist/status.js +5 -4
  61. package/dist/status.js.map +1 -1
  62. package/dist/store.d.ts +32 -3
  63. package/dist/store.d.ts.map +1 -1
  64. package/dist/store.js +64 -7
  65. package/dist/store.js.map +1 -1
  66. package/dist/wire.d.ts +63 -0
  67. package/dist/wire.d.ts.map +1 -0
  68. package/dist/wire.js +139 -0
  69. package/dist/wire.js.map +1 -0
  70. package/package.json +10 -2
@@ -2,18 +2,35 @@ import { rawQuery } from './query.js';
2
2
  import { cached, modelTypes, NO_MODEL, sameModel } from './model.js';
3
3
  import { ROOM_CONTRACT_VERSION, VERSION_1_CAPABILITIES } from './adapter.js';
4
4
  import { bootstrapRoom } from './bootstrap.js';
5
+ import { createConversationStore } from './conversation.js';
5
6
  import { MD_MODEL, PRESENTED, validRoot } from './collection.js';
6
7
  import { createLog } from './log.js';
7
- import { queuedOutcome, refused } from './outcome.js';
8
+ import { queuedOutcome, refusalOf, refused, withheldUntilReset } from './outcome.js';
8
9
  import { refreshRoom } from './refresh.js';
9
10
  import { buildBatch } from './save.js';
10
- import { applyReturned, readConfirmed, replicaOf } from './replica.js';
11
+ import { applyReturned, compositionOf, readConfirmed, replicaOf } from './replica.js';
11
12
  import { settled, same, saving, STARTING, transferring } from './status.js';
12
13
  import { detectStorageLoss, nextGeneration } from './durability.js';
13
- import { openReplicaStore } from './store.js';
14
+ import { beneath } from './paths.js';
15
+ import { openRuntimeStore } from './store.js';
14
16
  import { unconfirmed } from './transfer.js';
15
17
  const DEFAULT_DISCOVERY_INTERVAL_MS = 60_000;
16
18
  const knownCapabilities = new Set(VERSION_1_CAPABILITIES);
19
+ function within(root, path) {
20
+ return root === '' || beneath(root, path);
21
+ }
22
+ function declaredIdentity(model) {
23
+ if (model === undefined) return false;
24
+ if (model === null) return true;
25
+ return typeof model.identity === 'string' && model.identity.length > 0 && typeof model.version === 'string' && model.version.length > 0;
26
+ }
27
+ function declaredSelection(room) {
28
+ const selection = room.typedSelection;
29
+ if (selection === undefined) return true;
30
+ if (!Array.isArray(selection.roots) || selection.roots.length === 0) return false;
31
+ if (!declaredIdentity(selection.attachedModel)) return false;
32
+ return selection.roots.every((root, index)=>validRoot(root) && root !== '' && !within(room.collectionRoot, root) && selection.roots.every((other, at)=>at === index || !within(other, root)));
33
+ }
17
34
  export function createMdRuntime(options) {
18
35
  const period = options.discoveryIntervalMs ?? DEFAULT_DISCOVERY_INTERVAL_MS;
19
36
  let authorization = options.authorization;
@@ -21,6 +38,7 @@ export function createMdRuntime(options) {
21
38
  let store;
22
39
  const replicas = new Map();
23
40
  const drafts = new Map();
41
+ const presentations = new Map();
24
42
  const outcomes = new Map();
25
43
  let limits = [];
26
44
  let capabilities = [];
@@ -30,8 +48,26 @@ export function createMdRuntime(options) {
30
48
  const liveModels = new Map();
31
49
  const invalidModels = new Set();
32
50
  const modelEpoch = new Map();
33
- function modelOf(locator) {
34
- return models.get(locator) ?? NO_MODEL;
51
+ const discovered = new Map();
52
+ const scopedRefusals = new Map();
53
+ function modelKey(locator, attached) {
54
+ return `${attached} ${locator}`;
55
+ }
56
+ function modelOf(locator, attached = 'collection') {
57
+ return models.get(modelKey(locator, attached)) ?? NO_MODEL;
58
+ }
59
+ function savedModel(locator, attached) {
60
+ return invalidModels.has(modelKey(locator, attached)) ? {
61
+ status: 'unavailable',
62
+ reason: 'revoked'
63
+ } : modelOf(locator, attached);
64
+ }
65
+ function compositionFor(record) {
66
+ return compositionOf({
67
+ ...record,
68
+ model: savedModel(record.locator, 'collection'),
69
+ selectionModel: savedModel(record.locator, 'typed-selection')
70
+ });
35
71
  }
36
72
  const mismatchedRoots = new Set();
37
73
  let refusal = null;
@@ -40,6 +76,9 @@ export function createMdRuntime(options) {
40
76
  let discoveryPaused = false;
41
77
  let timer;
42
78
  let closed = false;
79
+ let withdrawNotifications;
80
+ let handed;
81
+ const notices = new Map();
43
82
  let chain = Promise.resolve();
44
83
  let mutations = Promise.resolve();
45
84
  let inflight = false;
@@ -51,15 +90,21 @@ export function createMdRuntime(options) {
51
90
  const querySubscriptions = new Set();
52
91
  const queueSubscriptions = new Set();
53
92
  const draftListeners = new Set();
93
+ const presentationListeners = new Set();
94
+ let withdrawConversations;
95
+ const conversations = createConversationStore({
96
+ cards: declaredCards,
97
+ documents: ()=>visible({}),
98
+ call: (name, input)=>invokeAction(name, input),
99
+ durably,
100
+ path: ()=>options.adapter.conversations,
101
+ closed: ()=>closed
102
+ });
54
103
  const journal = createLog();
55
104
  let notifyReady = ()=>{};
56
105
  const ready = new Promise((resolve)=>{
57
106
  notifyReady = resolve;
58
107
  });
59
- let leader = typeof window === 'undefined';
60
- let applyingRemote = false;
61
- let releaseLeadership = ()=>{};
62
- const channel = typeof window !== 'undefined' ? new BroadcastChannel('mdruntime') : undefined;
63
108
  function draftContent(locator, path) {
64
109
  return drafts.get(locator)?.get(path);
65
110
  }
@@ -93,9 +138,7 @@ export function createMdRuntime(options) {
93
138
  return entries;
94
139
  }
95
140
  async function readDrafts(opened) {
96
- await acrossTabs('mdruntime-replica', async ()=>{
97
- await hydrate(opened);
98
- });
141
+ await hydrate(opened);
99
142
  return snapshotDrafts();
100
143
  }
101
144
  function publishDrafts(waitingOnly) {
@@ -113,16 +156,23 @@ export function createMdRuntime(options) {
113
156
  function releaseWaitingDrafts() {
114
157
  publishDrafts(true);
115
158
  }
159
+ function presentationOf(app) {
160
+ const value = presentations.get(app);
161
+ return value === undefined ? null : structuredClone(value);
162
+ }
163
+ function emitPresentation(app) {
164
+ if (closed) return;
165
+ for (const listener of presentationListeners){
166
+ if (listener.app === app && listener.ready) listener.notify(presentationOf(app));
167
+ }
168
+ }
116
169
  function commitDraft(work) {
117
170
  return exclusive(async ()=>{
118
171
  const local = await openLocal();
119
- await acrossTabs('mdruntime-replica', async ()=>{
120
- await hydrate(local);
121
- await work(local);
122
- });
172
+ await hydrate(local);
173
+ await work(local);
123
174
  }).then(()=>{
124
175
  emitDrafts();
125
- announce();
126
176
  });
127
177
  }
128
178
  function pending() {
@@ -131,12 +181,33 @@ export function createMdRuntime(options) {
131
181
  ].some((replica)=>replica.queue.length > 0);
132
182
  }
133
183
  function attention() {
134
- return [
135
- ...replicas.values()
136
- ].some((replica)=>{
184
+ const attentionRequired = [];
185
+ let reset = null;
186
+ for (const [locator, replica] of replicas){
137
187
  const head = replica.queue[0];
138
- return head !== undefined && refused(outcomes.get(head.id));
139
- });
188
+ if (head === undefined) continue;
189
+ const outcome = outcomes.get(head.id);
190
+ if (outcome?.kind === 'retryable-refusal') {
191
+ if (outcome.reset !== undefined && (reset === null || outcome.reset < reset)) {
192
+ reset = outcome.reset;
193
+ }
194
+ continue;
195
+ }
196
+ if (!refused(outcome)) continue;
197
+ attentionRequired.push({
198
+ locator,
199
+ batch: head.id,
200
+ outcome: outcome.kind,
201
+ refusal: refusalOf(outcome),
202
+ ...outcome.kind === 'conflicted' && outcome.roomVersion !== undefined ? {
203
+ roomVersion: outcome.roomVersion
204
+ } : {}
205
+ });
206
+ }
207
+ return {
208
+ attentionRequired,
209
+ reset
210
+ };
140
211
  }
141
212
  function storedOutcomes(batches) {
142
213
  const stored = {};
@@ -152,7 +223,6 @@ export function createMdRuntime(options) {
152
223
  batches,
153
224
  outcomes: storedOutcomes(batches)
154
225
  });
155
- announce();
156
226
  }
157
227
  function listed() {
158
228
  return structuredClone([
@@ -166,7 +236,8 @@ export function createMdRuntime(options) {
166
236
  room: replica.confirmed.record.locator,
167
237
  roomVersion: replica.confirmed.record.roomVersion,
168
238
  documents: replica.visible.collection.documents,
169
- model: modelTypes(modelOf(replica.confirmed.record.locator))
239
+ model: modelTypes(modelOf(replica.confirmed.record.locator)),
240
+ typedSelection: replica.confirmed.record.typedSelection === null ? null : modelTypes(modelOf(replica.confirmed.record.locator, 'typed-selection'))
170
241
  }));
171
242
  }
172
243
  function emitRooms() {
@@ -179,29 +250,11 @@ export function createMdRuntime(options) {
179
250
  function emitStorageLoss() {
180
251
  for (const listener of storageLossListeners)listener(storageLoss);
181
252
  }
182
- function announce(type = 'changed') {
183
- if (applyingRemote || channel === undefined) return;
184
- if (type === 'hello') {
185
- channel.postMessage({
186
- type: 'hello'
187
- });
188
- return;
189
- }
190
- channel.postMessage({
191
- type,
192
- status
193
- });
194
- }
195
- function adopt(next) {
253
+ function moveTo(next) {
196
254
  if (closed || same(status, next)) return;
197
255
  status = next;
198
256
  for (const listener of statusListeners)listener(status);
199
257
  }
200
- function moveTo(next) {
201
- if (closed || same(status, next)) return;
202
- adopt(next);
203
- announce();
204
- }
205
258
  function visible(request) {
206
259
  if (options.queryBinding === null) {
207
260
  if (Object.keys(request).length > 0) throw new Error('query-binding-unavailable');
@@ -247,35 +300,42 @@ export function createMdRuntime(options) {
247
300
  void enqueue();
248
301
  }, period);
249
302
  }
250
- function refuse(reason) {
303
+ function refuse(reason, provider = null) {
251
304
  if (reason === 'revoked-authorization') {
252
305
  const revoked = {
253
306
  status: 'unavailable',
254
307
  reason: 'revoked'
255
308
  };
256
- for (const locator of new Set([
257
- ...models.keys(),
258
- ...rooms.keys()
259
- ])){
260
- invalidModels.add(locator);
261
- modelEpoch.set(locator, (modelEpoch.get(locator) ?? 0) + 1);
262
- models.set(locator, revoked);
263
- liveModels.delete(locator);
309
+ const attachments = [
310
+ 'collection',
311
+ 'typed-selection'
312
+ ];
313
+ const keys = new Set(models.keys());
314
+ for (const locator of rooms.keys()){
315
+ for (const attached of attachments)keys.add(modelKey(locator, attached));
316
+ }
317
+ for (const key of keys){
318
+ invalidModels.add(key);
319
+ modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
320
+ models.set(key, revoked);
321
+ liveModels.delete(key);
264
322
  }
265
323
  for (const [locator, room] of rooms)rooms.set(locator, {
266
324
  ...room,
267
- model: modelTypes(revoked)
325
+ model: modelTypes(revoked),
326
+ typedSelection: room.typedSelection === null ? null : {
327
+ ...room.typedSelection,
328
+ model: modelTypes(revoked)
329
+ }
268
330
  });
269
331
  emitRooms();
270
332
  void exclusive(async ()=>{
271
333
  const local = await openLocal();
272
- await acrossTabs('mdruntime-replica', async ()=>{
273
- for (const record of (await local.all()))await local.publish({
274
- ...record,
275
- model: revoked
276
- });
334
+ for (const record of (await local.all()))await local.publish({
335
+ ...record,
336
+ model: revoked,
337
+ selectionModel: revoked
277
338
  });
278
- announce();
279
339
  }).catch(()=>{
280
340
  journal.record('model invalidation durable-write failure');
281
341
  });
@@ -284,12 +344,14 @@ export function createMdRuntime(options) {
284
344
  discoveryPaused = true;
285
345
  refusal = {
286
346
  reason,
287
- reauthorizationOffered: reason === 'revoked-authorization'
347
+ reauthorizationOffered: reason === 'revoked-authorization',
348
+ refusal: provider
288
349
  };
289
350
  journal.record(`connection refused ${reason}`);
290
351
  emitRefusal();
291
352
  }
292
353
  function list(room, synchronizationAvailable) {
354
+ const selection = room.typedSelection ?? null;
293
355
  rooms.set(room.locator, {
294
356
  name: room.name,
295
357
  collectionRoot: room.collectionRoot,
@@ -298,14 +360,23 @@ export function createMdRuntime(options) {
298
360
  replica: room.replica,
299
361
  attachedModel: room.attachedModel,
300
362
  model: modelTypes(modelOf(room.locator)),
363
+ typedSelection: selection === null ? null : {
364
+ roots: selection.roots,
365
+ attachedModel: selection.attachedModel,
366
+ model: modelTypes(modelOf(room.locator, 'typed-selection'))
367
+ },
301
368
  providerOwnedPaths: room.providerOwnedPaths,
302
369
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
303
- synchronizationAvailable
370
+ synchronizationAvailable,
371
+ refusal: scopedRefusals.get(room.locator) ?? null
304
372
  });
305
373
  }
306
- function applyDiscovery(discovered) {
307
- const listedLocators = new Set(discovered.map((room)=>room.locator));
308
- for (const room of discovered){
374
+ function applyDiscovery(listed) {
375
+ const listedLocators = new Set(listed.map((room)=>room.locator));
376
+ for (const locator of listedLocators)scopedRefusals.delete(locator);
377
+ discovered.clear();
378
+ for (const room of listed)discovered.set(room.locator, room);
379
+ for (const room of listed){
309
380
  const held = replicas.get(room.locator)?.confirmed.record;
310
381
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
311
382
  list(held, false);
@@ -323,22 +394,41 @@ export function createMdRuntime(options) {
323
394
  emitRooms();
324
395
  return listedLocators;
325
396
  }
397
+ function restored(key, persisted) {
398
+ const prior = models.get(key);
399
+ if (prior !== undefined && prior.status !== 'unavailable' && (persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
400
+ modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
401
+ invalidModels.add(key);
402
+ }
403
+ const model = persisted.status === 'unavailable' || liveModels.get(key) === JSON.stringify(persisted.snapshot) ? persisted : cached(persisted);
404
+ models.set(key, model);
405
+ if (model.status === 'unavailable') {
406
+ liveModels.delete(key);
407
+ invalidModels.delete(key);
408
+ }
409
+ return model;
410
+ }
326
411
  async function hydrate(opened) {
327
- const [records, queues, kept, lastCompleted, declared] = await Promise.all([
412
+ const [records, queues, kept, presented, cached, unsent, lastCompleted, declared] = await Promise.all([
328
413
  opened.all(),
329
414
  opened.queues(),
330
415
  opened.drafts(),
416
+ opened.presentation(),
417
+ opened.conversations(),
418
+ opened.instructions(),
331
419
  opened.lastCompleted(),
332
420
  opened.providerLimits()
333
421
  ]);
334
422
  limits = declared;
335
423
  drafts.clear();
336
424
  for (const draft of kept)rememberDraft(draft.locator, draft.path, draft.content);
425
+ presentations.clear();
426
+ for (const record of presented)presentations.set(record.app, record.value);
427
+ conversations.hydrate(cached, unsent);
337
428
  const queuedBatches = new Map(queues.map((queue)=>[
338
429
  queue.locator,
339
430
  queue.batches
340
431
  ]));
341
- outcomes.clear();
342
432
  for (const queue of queues){
343
433
  for (const [id, outcome] of Object.entries(queue.outcomes)){
344
434
  outcomes.set(id, outcome);
@@ -346,21 +436,12 @@ export function createMdRuntime(options) {
346
436
  }
347
437
  replicas.clear();
348
438
  for (const record of records){
349
- const persisted = record.model;
350
- const priorModel = models.get(record.locator);
351
- if (priorModel !== undefined && priorModel.status !== 'unavailable' && (persisted.status === 'unavailable' || !sameModel(priorModel.snapshot, persisted.snapshot))) {
352
- modelEpoch.set(record.locator, (modelEpoch.get(record.locator) ?? 0) + 1);
353
- invalidModels.add(record.locator);
354
- }
355
- const model = persisted.status === 'unavailable' || liveModels.get(record.locator) === JSON.stringify(persisted.snapshot) ? persisted : cached(persisted);
356
- models.set(record.locator, model);
357
- if (model.status === 'unavailable') {
358
- liveModels.delete(record.locator);
359
- invalidModels.delete(record.locator);
360
- }
439
+ const model = restored(modelKey(record.locator, 'collection'), record.model);
440
+ const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
361
441
  const confirmed = readConfirmed({
362
442
  ...record,
363
- model
443
+ model,
444
+ selectionModel
364
445
  });
365
446
  if (confirmed === null) continue;
366
447
  replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? []));
@@ -373,7 +454,12 @@ export function createMdRuntime(options) {
373
454
  operations: record.operations,
374
455
  replica: record.replica,
375
456
  attachedModel: record.attachedModel,
376
- model: modelTypes(model)
457
+ model: modelTypes(model),
458
+ typedSelection: record.typedSelection === null ? null : {
459
+ roots: record.typedSelection.roots,
460
+ attachedModel: record.typedSelection.attachedModel,
461
+ model: modelTypes(selectionModel)
462
+ }
377
463
  });
378
464
  }
379
465
  }
@@ -395,52 +481,34 @@ export function createMdRuntime(options) {
395
481
  emitDrafts();
396
482
  return lastCompleted;
397
483
  }
398
- async function applyRemote(message) {
399
- if (closed) return;
400
- if (message.type === 'hello') {
401
- if (leader) announce();
402
- return;
403
- }
404
- applyingRemote = true;
405
- try {
406
- const local = await openLocal();
407
- const lastCompleted = await readState(local);
408
- if (message.status !== undefined) adopt(message.status);
409
- else {
410
- adopt({
411
- ...status,
412
- lastCompleted,
413
- attentionRequired: attention(),
414
- state: pending() ? 'pending' : status.state
415
- });
416
- }
417
- } finally{
418
- applyingRemote = false;
419
- }
420
- if (message.type === 'enqueue' && leader) void enqueue();
421
- }
422
484
  async function open() {
423
- const opened = await openReplicaStore();
485
+ const opened = await openRuntimeStore();
424
486
  store = opened;
425
- await acrossTabs('mdruntime-generation', async ()=>{
426
- const storedGeneration = await opened.generation();
427
- const loss = detectStorageLoss(storedGeneration);
428
- const generation = nextGeneration(storedGeneration);
429
- if (generation !== storedGeneration) await opened.rememberGeneration(generation);
430
- if (loss !== null) {
431
- storageLoss = loss;
432
- emitStorageLoss();
433
- }
434
- });
487
+ const kept = options.shell.generation;
488
+ const storedGeneration = await opened.generation();
489
+ const loss = detectStorageLoss(storedGeneration, kept);
490
+ const generation = nextGeneration(storedGeneration, kept);
491
+ if (generation !== storedGeneration) await opened.rememberGeneration(generation);
492
+ if (generation !== kept) await options.shell.keepGeneration(generation);
493
+ if (loss !== null) {
494
+ storageLoss = loss;
495
+ emitStorageLoss();
496
+ }
435
497
  const lastCompleted = await readState(opened);
436
- adopt({
498
+ moveTo({
437
499
  ...status,
438
500
  lastCompleted,
439
- attentionRequired: attention()
501
+ ...attention()
440
502
  });
441
- if (!leader) announce('hello');
442
503
  return opened;
443
504
  }
505
+ function durably(work) {
506
+ return exclusive(async ()=>{
507
+ const local = await openLocal();
508
+ await hydrate(local);
509
+ return work(local);
510
+ });
511
+ }
444
512
  function openLocal() {
445
513
  opening ??= open().catch((error)=>{
446
514
  opening = undefined;
@@ -453,132 +521,117 @@ export function createMdRuntime(options) {
453
521
  mutations = next.then(()=>undefined, ()=>undefined);
454
522
  return next;
455
523
  }
456
- function acrossTabs(name, work) {
457
- if (typeof window === 'undefined') return work();
458
- return window.navigator.locks.request(name, work);
459
- }
460
524
  function publish(local, room, confirmed) {
461
525
  return exclusive(async ()=>{
462
- await acrossTabs('mdruntime-replica', async ()=>{
463
- await hydrate(local);
464
- const retained = readConfirmed({
465
- ...confirmed.record,
466
- model: modelOf(room.locator)
467
- });
468
- if (retained === null) return;
526
+ await hydrate(local);
527
+ const retained = readConfirmed({
528
+ ...confirmed.record,
529
+ model: modelOf(room.locator),
530
+ selectionModel: modelOf(room.locator, 'typed-selection')
531
+ });
532
+ if (retained !== null) {
469
533
  await local.publish(retained.record);
470
534
  unreadable.delete(room.locator);
471
535
  replicas.set(room.locator, replicaOf(retained, replicas.get(room.locator)?.queue ?? []));
472
- });
536
+ }
473
537
  list(room, true);
474
538
  emitRooms();
475
539
  emitVisible();
476
- announce();
477
540
  });
478
541
  }
479
542
  async function write(locator, changes, replacing) {
480
543
  const local = await openLocal();
481
- const result = await acrossTabs('mdruntime-replica', async ()=>{
482
- await hydrate(local);
483
- const replica = replicas.get(locator);
484
- const room = rooms.get(locator);
485
- if (replica === undefined || room === undefined) {
486
- throw new Error(`No Local Replica for ${locator}`);
487
- }
488
- if (mismatchedRoots.has(locator)) return {
489
- outcome: 'refused',
490
- rule: 'collection-root-mismatch'
491
- };
492
- const built = buildBatch({
493
- baseRoomVersion: replica.confirmed.record.roomVersion,
494
- collectionRoot: replica.confirmed.record.collectionRoot,
495
- operations: room.operations,
496
- model: invalidModels.has(locator) ? {
497
- status: 'unavailable',
498
- reason: 'revoked'
499
- } : modelOf(locator),
500
- binding: options.modelBinding,
501
- providerOwnedPaths: room.providerOwnedPaths,
502
- providerLimits: limits,
503
- visible: replica.visible,
504
- changes
505
- });
506
- if (built.kind === 'refused') return {
507
- outcome: 'refused',
508
- ...built.refusal
509
- };
510
- const head = replica.queue[0];
511
- if (replacing !== undefined) {
512
- if (head === undefined || head.id !== replacing || !refused(outcomes.get(head.id))) {
513
- return {
514
- outcome: 'refused',
515
- rule: 'no-refused-head'
516
- };
517
- }
518
- }
519
- const batches = replacing === undefined ? [
520
- ...replica.queue,
521
- built.batch
522
- ] : [
523
- built.batch,
524
- ...replica.queue.slice(1)
525
- ];
526
- const consumed = [
527
- ...new Set(changes.map((change)=>change.path))
528
- ];
529
- try {
530
- await local.append({
531
- locator,
532
- batches,
533
- outcomes: storedOutcomes(batches)
534
- }, consumed);
535
- } catch {
544
+ await hydrate(local);
545
+ const replica = replicas.get(locator);
546
+ const room = rooms.get(locator);
547
+ if (replica === undefined || room === undefined) {
548
+ throw new Error(`No Local Replica for ${locator}`);
549
+ }
550
+ if (mismatchedRoots.has(locator)) return {
551
+ outcome: 'refused',
552
+ rule: 'collection-root-mismatch'
553
+ };
554
+ const built = buildBatch({
555
+ baseRoomVersion: replica.confirmed.record.roomVersion,
556
+ composition: compositionFor(replica.confirmed.record),
557
+ operations: room.operations,
558
+ binding: options.modelBinding,
559
+ providerOwnedPaths: room.providerOwnedPaths,
560
+ providerLimits: limits,
561
+ visible: replica.visible,
562
+ changes
563
+ });
564
+ if (built.kind === 'refused') return {
565
+ outcome: 'refused',
566
+ ...built.refusal
567
+ };
568
+ const head = replica.queue[0];
569
+ if (replacing !== undefined) {
570
+ if (head === undefined || head.id !== replacing || !refused(outcomes.get(head.id))) {
536
571
  return {
537
572
  outcome: 'refused',
538
- rule: 'durable-write'
573
+ rule: 'no-refused-head'
539
574
  };
540
575
  }
541
- if (replacing !== undefined) outcomes.delete(replacing);
542
- for (const path of consumed)forgetDraft(locator, path);
543
- replicas.set(locator, replicaOf(replica.confirmed, batches));
544
- emitQueue(locator);
545
- emitVisible();
546
- emitDrafts();
547
- moveTo({
548
- ...saving(status),
549
- attentionRequired: attention()
550
- });
576
+ }
577
+ const batches = replacing === undefined ? [
578
+ ...replica.queue,
579
+ built.batch
580
+ ] : [
581
+ built.batch,
582
+ ...replica.queue.slice(1)
583
+ ];
584
+ const consumed = [
585
+ ...new Set(changes.map((change)=>change.path))
586
+ ];
587
+ try {
588
+ await local.append({
589
+ locator,
590
+ batches,
591
+ outcomes: storedOutcomes(batches)
592
+ }, consumed);
593
+ } catch {
551
594
  return {
552
- outcome: 'saved',
553
- id: built.batch.id
595
+ outcome: 'refused',
596
+ rule: 'durable-write'
554
597
  };
598
+ }
599
+ if (replacing !== undefined) outcomes.delete(replacing);
600
+ for (const path of consumed)forgetDraft(locator, path);
601
+ replicas.set(locator, replicaOf(replica.confirmed, batches));
602
+ emitQueue(locator);
603
+ emitVisible();
604
+ emitDrafts();
605
+ moveTo({
606
+ ...saving(status),
607
+ ...attention()
555
608
  });
556
- if (result.outcome === 'saved') void enqueue();
557
- return result;
609
+ void enqueue();
610
+ return {
611
+ outcome: 'saved',
612
+ id: built.batch.id
613
+ };
558
614
  }
559
615
  async function discardHead(locator, identifier) {
560
616
  const local = await openLocal();
561
- const changed = await acrossTabs('mdruntime-replica', async ()=>{
562
- await hydrate(local);
563
- const replica = replicas.get(locator);
564
- const head = replica?.queue[0];
565
- if (replica === undefined || head === undefined || head.id !== identifier || !refused(outcomes.get(head.id))) {
566
- return false;
567
- }
568
- const batches = replica.queue.slice(1);
569
- outcomes.delete(identifier);
570
- await persistQueue(local, locator, batches);
571
- replicas.set(locator, replicaOf(replica.confirmed, batches));
572
- emitQueue(locator);
573
- emitVisible();
574
- moveTo({
575
- ...status,
576
- state: pending() ? 'pending' : status.state,
577
- attentionRequired: attention()
578
- });
579
- return true;
617
+ await hydrate(local);
618
+ const replica = replicas.get(locator);
619
+ const head = replica?.queue[0];
620
+ if (replica === undefined || head === undefined || head.id !== identifier || !refused(outcomes.get(head.id))) {
621
+ return;
622
+ }
623
+ const batches = replica.queue.slice(1);
624
+ outcomes.delete(identifier);
625
+ await persistQueue(local, locator, batches);
626
+ replicas.set(locator, replicaOf(replica.confirmed, batches));
627
+ emitQueue(locator);
628
+ emitVisible();
629
+ moveTo({
630
+ ...status,
631
+ state: pending() ? 'pending' : status.state,
632
+ ...attention()
580
633
  });
581
- if (changed) void enqueue();
634
+ void enqueue();
582
635
  }
583
636
  async function readHistory(locator, path, query = {}) {
584
637
  if (closed || refusal !== null) return {
@@ -630,6 +683,12 @@ export function createMdRuntime(options) {
630
683
  };
631
684
  }
632
685
  }
686
+ function declaredCards() {
687
+ for (const capability of capabilities){
688
+ if (capability.conversationCards !== undefined) return capability.conversationCards;
689
+ }
690
+ return undefined;
691
+ }
633
692
  function declaredAction(name) {
634
693
  for (const capability of capabilities){
635
694
  for (const action of capability.actions ?? []){
@@ -670,7 +729,7 @@ export function createMdRuntime(options) {
670
729
  }
671
730
  if (result.kind === 'revoked-authorization') refuse('revoked-authorization');
672
731
  journal.record(`outcome action ${result.kind}`);
673
- if (result.kind === 'rejected') journal.record(`refusal rejected ${result.text}`);
732
+ if (result.kind === 'rejected') journal.record(`refusal rejected ${result.code}`);
674
733
  return result;
675
734
  } catch {
676
735
  journal.record('outcome action transport-uncertainty');
@@ -692,7 +751,10 @@ export function createMdRuntime(options) {
692
751
  }
693
752
  async function pullBootstrap(authorized, room) {
694
753
  try {
695
- const result = await bootstrapRoom(options.adapter, authorized, room, modelOf(room.locator), options.modelBinding);
754
+ const result = await bootstrapRoom(options.adapter, authorized, room, {
755
+ collection: modelOf(room.locator),
756
+ selection: modelOf(room.locator, 'typed-selection')
757
+ }, options.modelBinding);
696
758
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
697
759
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
698
760
  return result;
@@ -729,59 +791,94 @@ export function createMdRuntime(options) {
729
791
  if (result.kind === 'expired') return {
730
792
  kind: 'expired'
731
793
  };
794
+ if (result.kind === 'retryable-refusal') {
795
+ return {
796
+ kind: 'retryable-refusal',
797
+ code: result.code,
798
+ fields: result.fields,
799
+ ...result.reset === undefined ? {} : {
800
+ reset: result.reset
801
+ }
802
+ };
803
+ }
732
804
  if (result.kind === 'conflicted') {
733
805
  return {
734
806
  kind: 'conflicted',
735
- roomVersion: result.roomVersion,
807
+ ...result.roomVersion === undefined ? {} : {
808
+ roomVersion: result.roomVersion
809
+ },
736
810
  affected: result.affected,
737
- text: result.text
811
+ code: result.code,
812
+ fields: result.fields
738
813
  };
739
814
  }
740
815
  return {
741
816
  kind: 'rejected',
742
817
  affected: result.affected,
743
- text: result.text
818
+ code: result.code,
819
+ fields: result.fields
744
820
  };
745
821
  }
746
822
  async function commitAccept(room, local, submitted, accepted, base, overlay) {
747
- return acrossTabs('mdruntime-replica', async ()=>{
748
- await hydrate(local);
749
- const current = replicas.get(room.locator);
750
- if (current === undefined || !current.queue.some((batch)=>batch.id === submitted.id)) {
751
- return true;
752
- }
753
- const remaining = current.queue.filter((batch)=>batch.id !== submitted.id);
754
- const retainedBase = readConfirmed({
755
- ...base.record,
756
- model: modelOf(room.locator)
757
- });
758
- if (retainedBase === null) return false;
759
- const applied = overlay ? applyReturned(retainedBase, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion, options.modelBinding) : retainedBase;
760
- if (applied === null) return false;
761
- await local.publish(applied.record);
762
- await persistQueue(local, room.locator, remaining);
763
- outcomes.set(submitted.id, {
764
- kind: 'accepted',
765
- roomVersion: accepted.roomVersion,
766
- author: accepted.author,
767
- time: accepted.time
768
- });
769
- unreadable.delete(room.locator);
770
- replicas.set(room.locator, replicaOf(applied, remaining));
771
- list(room, true);
772
- emitRooms();
773
- emitQueue(room.locator);
774
- emitVisible();
775
- journal.record(`outcome ${room.locator} ${submitted.id} accepted`);
823
+ await hydrate(local);
824
+ const current = replicas.get(room.locator);
825
+ if (current === undefined || !current.queue.some((batch)=>batch.id === submitted.id)) {
776
826
  return true;
827
+ }
828
+ const remaining = current.queue.filter((batch)=>batch.id !== submitted.id);
829
+ const retainedBase = readConfirmed({
830
+ ...base.record,
831
+ model: modelOf(room.locator),
832
+ selectionModel: modelOf(room.locator, 'typed-selection')
777
833
  });
834
+ if (retainedBase === null) return false;
835
+ const applied = overlay ? applyReturned(retainedBase, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion, options.modelBinding) : retainedBase;
836
+ if (applied === null) return false;
837
+ await local.publish(applied.record);
838
+ await persistQueue(local, room.locator, remaining);
839
+ outcomes.set(submitted.id, {
840
+ kind: 'accepted',
841
+ roomVersion: accepted.roomVersion,
842
+ ...accepted.author === undefined ? {} : {
843
+ author: accepted.author
844
+ },
845
+ ...accepted.time === undefined ? {} : {
846
+ time: accepted.time
847
+ }
848
+ });
849
+ unreadable.delete(room.locator);
850
+ replicas.set(room.locator, replicaOf(applied, remaining));
851
+ list(room, true);
852
+ emitRooms();
853
+ emitQueue(room.locator);
854
+ emitVisible();
855
+ journal.record(`outcome ${room.locator} ${submitted.id} accepted ${accepted.roomVersion}` + (accepted.author === undefined ? '' : ` author ${accepted.author}`) + (accepted.time === undefined ? '' : ` time ${accepted.time}`));
856
+ return true;
778
857
  }
779
858
  async function submitRoom(room, local, authorized, progress) {
780
859
  while(!closed && authorization !== undefined){
781
860
  const replica = replicas.get(room.locator);
782
861
  const head = replica?.queue[0];
783
- if (replica === undefined || head === undefined || refused(outcomes.get(head.id))) {
784
- return null;
862
+ if (replica === undefined || head === undefined) return null;
863
+ const held = outcomes.get(head.id);
864
+ if (refused(held)) return null;
865
+ if (held?.kind === 'retryable-refusal') {
866
+ if (withheldUntilReset(held, Date.now())) {
867
+ progress.complete = false;
868
+ return null;
869
+ }
870
+ await exclusive(async ()=>{
871
+ await hydrate(local);
872
+ const current = replicas.get(room.locator);
873
+ if (current === undefined || current.queue[0]?.id !== head.id) return;
874
+ outcomes.delete(head.id);
875
+ await persistQueue(local, room.locator, current.queue);
876
+ emitQueue(room.locator);
877
+ moveTo({
878
+ ...status,
879
+ ...attention()
880
+ });
881
+ });
785
882
  }
786
883
  let result;
787
884
  journal.record(`submission ${room.locator} ${head.id}`);
@@ -817,23 +914,22 @@ export function createMdRuntime(options) {
817
914
  progress.complete = false;
818
915
  return null;
819
916
  }
820
- if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
821
- journal.record(`outcome ${room.locator} ${head.id} ${result.kind}`);
822
- if (result.kind !== 'expired') journal.record(`refusal ${result.kind} ${result.text}`);
917
+ if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired' || result.kind === 'retryable-refusal') {
918
+ journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` + (result.kind === 'conflicted' && result.roomVersion !== undefined ? ` ${result.roomVersion}` : ''));
919
+ if (result.kind !== 'expired') journal.record(`refusal ${result.kind} ${result.code}`);
920
+ if (result.kind === 'retryable-refusal') progress.complete = false;
823
921
  await exclusive(async ()=>{
824
- await acrossTabs('mdruntime-replica', async ()=>{
825
- await hydrate(local);
826
- const current = replicas.get(room.locator);
827
- if (current === undefined || !current.queue.some((batch)=>batch.id === head.id)) {
828
- return;
829
- }
830
- outcomes.set(head.id, recordRefusal(result));
831
- await persistQueue(local, room.locator, current.queue);
832
- emitQueue(room.locator);
833
- moveTo({
834
- ...status,
835
- attentionRequired: true
836
- });
922
+ await hydrate(local);
923
+ const current = replicas.get(room.locator);
924
+ if (current === undefined || !current.queue.some((batch)=>batch.id === head.id)) {
925
+ return;
926
+ }
927
+ outcomes.set(head.id, recordRefusal(result));
928
+ await persistQueue(local, room.locator, current.queue);
929
+ emitQueue(room.locator);
930
+ moveTo({
931
+ ...status,
932
+ ...attention()
837
933
  });
838
934
  });
839
935
  return null;
@@ -909,69 +1005,83 @@ export function createMdRuntime(options) {
909
1005
  }
910
1006
  return null;
911
1007
  }
912
- async function retainModel(local, room, next) {
913
- if (next.status === 'unavailable') modelEpoch.set(room.locator, (modelEpoch.get(room.locator) ?? 0) + 1);
914
- const epoch = modelEpoch.get(room.locator) ?? 0;
915
- const previous = modelOf(room.locator);
1008
+ async function retainModel(local, room, attached, next) {
1009
+ const key = modelKey(room.locator, attached);
1010
+ if (next.status === 'unavailable') modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
1011
+ const epoch = modelEpoch.get(key) ?? 0;
1012
+ const previous = modelOf(room.locator, attached);
916
1013
  const heldBefore = replicas.get(room.locator)?.confirmed.record;
917
- if (JSON.stringify(previous) === JSON.stringify(next) && (heldBefore === undefined || JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) && JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) && JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel))) return;
1014
+ if (JSON.stringify(previous) === JSON.stringify(next) && (heldBefore === undefined || JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) && JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) && JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) && JSON.stringify(heldBefore.typedSelection) === JSON.stringify(room.typedSelection ?? null))) return;
918
1015
  const queryChanged = previous.status === 'unavailable' || next.status === 'unavailable' ? previous.status !== next.status : JSON.stringify(previous.types) !== JSON.stringify(next.types);
919
1016
  if (next.status === 'unavailable') {
920
- invalidModels.add(room.locator);
921
- liveModels.delete(room.locator);
922
- }
923
- await exclusive(async ()=>acrossTabs('mdruntime-replica', async ()=>{
924
- await hydrate(local);
925
- if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(room.locator) ?? 0)) return;
926
- const held = replicas.get(room.locator);
927
- if (held !== undefined) {
928
- const record = {
929
- ...held.confirmed.record,
930
- operations: room.operations,
931
- replica: room.replica,
932
- attachedModel: room.attachedModel,
1017
+ invalidModels.add(key);
1018
+ liveModels.delete(key);
1019
+ }
1020
+ await exclusive(async ()=>{
1021
+ await hydrate(local);
1022
+ if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0)) return;
1023
+ const held = replicas.get(room.locator);
1024
+ if (held !== undefined) {
1025
+ const record = {
1026
+ ...held.confirmed.record,
1027
+ operations: room.operations,
1028
+ replica: room.replica,
1029
+ attachedModel: room.attachedModel,
1030
+ typedSelection: room.typedSelection ?? null,
1031
+ ...attached === 'collection' ? {
933
1032
  model: next
934
- };
935
- await local.publish(record);
936
- const confirmed = readConfirmed(record);
937
- if (confirmed !== null) replicas.set(room.locator, replicaOf(confirmed, held.queue));
938
- }
939
- models.set(room.locator, next);
940
- if (next.status === 'current') {
941
- liveModels.set(room.locator, JSON.stringify(next.snapshot));
942
- invalidModels.delete(room.locator);
943
- }
944
- list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
945
- emitRooms();
946
- if (queryChanged && held !== undefined) emitVisible();
947
- announce();
948
- }));
1033
+ } : {
1034
+ selectionModel: next
1035
+ }
1036
+ };
1037
+ await local.publish(record);
1038
+ const confirmed = readConfirmed(record);
1039
+ if (confirmed !== null) replicas.set(room.locator, replicaOf(confirmed, held.queue));
1040
+ }
1041
+ models.set(key, next);
1042
+ if (next.status === 'current') {
1043
+ liveModels.set(key, JSON.stringify(next.snapshot));
1044
+ invalidModels.delete(key);
1045
+ }
1046
+ list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
1047
+ emitRooms();
1048
+ if (queryChanged && held !== undefined) emitVisible();
1049
+ });
949
1050
  }
950
- async function acquireModel(local, room, authorized) {
951
- let previous = modelOf(room.locator);
952
- if (previous.status !== 'unavailable' && (room.attachedModel === null || !sameModel(previous.snapshot, room.attachedModel))) {
1051
+ async function acquireModel(local, room, authorized, selection) {
1052
+ const attached = selection === null ? 'collection' : 'typed-selection';
1053
+ const key = modelKey(room.locator, attached);
1054
+ const declared = selection === null ? room.attachedModel : selection.attachedModel;
1055
+ let previous = modelOf(room.locator, attached);
1056
+ if (previous.status !== 'unavailable' && (declared === null || !sameModel(previous.snapshot, declared))) {
953
1057
  previous = {
954
1058
  status: 'unavailable',
955
1059
  reason: 'version-changed'
956
1060
  };
957
- await retainModel(local, room, previous);
1061
+ await retainModel(local, room, attached, previous);
958
1062
  }
959
- const epoch = modelEpoch.get(room.locator) ?? 0;
1063
+ const epoch = modelEpoch.get(key) ?? 0;
960
1064
  let answer;
961
1065
  try {
962
- answer = await options.adapter.model(authorized, {
1066
+ answer = await options.adapter.model(authorized, selection === null ? {
963
1067
  locator: room.locator,
1068
+ attached: 'collection',
964
1069
  collectionRoot: room.collectionRoot,
965
- model: room.attachedModel
1070
+ model: declared
1071
+ } : {
1072
+ locator: room.locator,
1073
+ attached: 'typed-selection',
1074
+ roots: selection.roots,
1075
+ model: declared
966
1076
  });
967
1077
  } catch {
968
1078
  answer = {
969
1079
  kind: 'transport-uncertainty'
970
1080
  };
971
1081
  }
972
- if (epoch !== (modelEpoch.get(room.locator) ?? 0)) return;
1082
+ if (epoch !== (modelEpoch.get(key) ?? 0)) return;
973
1083
  if (answer.kind === 'revoked-authorization') {
974
- await retainModel(local, room, {
1084
+ await retainModel(local, room, attached, {
975
1085
  status: 'unavailable',
976
1086
  reason: 'revoked'
977
1087
  });
@@ -979,32 +1089,32 @@ export function createMdRuntime(options) {
979
1089
  return;
980
1090
  }
981
1091
  if (answer.kind === 'transport-uncertainty') {
982
- await retainModel(local, room, cached(previous));
1092
+ await retainModel(local, room, attached, cached(previous));
983
1093
  return;
984
1094
  }
985
1095
  if (answer.kind === 'model-unavailable') {
986
- await retainModel(local, room, {
1096
+ await retainModel(local, room, attached, {
987
1097
  status: 'unavailable',
988
1098
  reason: answer.reason
989
1099
  });
990
1100
  return;
991
1101
  }
992
- if (room.attachedModel === null || !sameModel(answer.snapshot, room.attachedModel)) {
993
- await retainModel(local, room, {
1102
+ if (declared === null || !sameModel(answer.snapshot, declared)) {
1103
+ await retainModel(local, room, attached, {
994
1104
  status: 'unavailable',
995
1105
  reason: 'version-changed'
996
1106
  });
997
1107
  return;
998
1108
  }
999
1109
  if (previous.status !== 'unavailable' && sameModel(previous.snapshot, answer.snapshot) && JSON.stringify(Object.entries(previous.snapshot.definitions).sort()) !== JSON.stringify(Object.entries(answer.snapshot.definitions).sort())) {
1000
- await retainModel(local, room, {
1110
+ await retainModel(local, room, attached, {
1001
1111
  status: 'unavailable',
1002
1112
  reason: 'invalid'
1003
1113
  });
1004
1114
  return;
1005
1115
  }
1006
1116
  const verified = options.modelBinding?.verify(answer.snapshot);
1007
- await retainModel(local, room, verified?.kind === 'verified' ? {
1117
+ await retainModel(local, room, attached, verified?.kind === 'verified' ? {
1008
1118
  status: 'current',
1009
1119
  snapshot: structuredClone(answer.snapshot),
1010
1120
  verifiedWith: MD_MODEL,
@@ -1015,13 +1125,18 @@ export function createMdRuntime(options) {
1015
1125
  });
1016
1126
  }
1017
1127
  async function scoped(result, room, local) {
1018
- if (result.scope === 'model') await retainModel(local, room, {
1128
+ scopedRefusals.set(room.locator, {
1129
+ code: result.code,
1130
+ fields: result.fields
1131
+ });
1132
+ let scopedAway = room;
1133
+ if (result.scope === 'model') await retainModel(local, room, 'collection', {
1019
1134
  status: 'unavailable',
1020
1135
  reason: 'not-readable'
1021
1136
  });
1022
1137
  else if (result.scope !== 'operation') {
1023
1138
  unreadable.add(room.locator);
1024
- const unavailable = {
1139
+ scopedAway = {
1025
1140
  ...room,
1026
1141
  replica: {
1027
1142
  ...room.replica,
@@ -1037,11 +1152,11 @@ export function createMdRuntime(options) {
1037
1152
  } : {}
1038
1153
  }
1039
1154
  };
1040
- await retainModel(local, unavailable, modelOf(room.locator));
1041
- list(unavailable, false);
1042
- emitRooms();
1155
+ await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
1043
1156
  }
1044
- journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.text}`);
1157
+ list(scopedAway, scopedAway.replica.admission === 'admitted' && !unreadable.has(room.locator));
1158
+ emitRooms();
1159
+ journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
1045
1160
  }
1046
1161
  async function synchronize(discovered, local, progress) {
1047
1162
  for (const room of discovered){
@@ -1057,8 +1172,12 @@ export function createMdRuntime(options) {
1057
1172
  progress.complete = false;
1058
1173
  continue;
1059
1174
  }
1060
- await acquireModel(local, room, authorization);
1175
+ await acquireModel(local, room, authorization, null);
1061
1176
  if (discoveryPaused) return;
1177
+ if (room.typedSelection !== undefined) {
1178
+ await acquireModel(local, room, authorization, room.typedSelection);
1179
+ if (discoveryPaused) return;
1180
+ }
1062
1181
  if (room.replica.admission !== 'admitted') {
1063
1182
  progress.complete = false;
1064
1183
  continue;
@@ -1114,14 +1233,22 @@ export function createMdRuntime(options) {
1114
1233
  }
1115
1234
  journal.record(`connection discover ${result.kind}`);
1116
1235
  if (result.kind === 'transport-uncertainty') {
1117
- for (const room of rooms.values())await retainModel(local, room, cached(modelOf(room.locator)));
1236
+ for (const room of rooms.values()){
1237
+ await retainModel(local, room, 'collection', cached(modelOf(room.locator)));
1238
+ if (room.typedSelection !== null) {
1239
+ await retainModel(local, room, 'typed-selection', cached(modelOf(room.locator, 'typed-selection')));
1240
+ }
1241
+ }
1118
1242
  progress.reachable = false;
1119
1243
  notifyReady();
1120
1244
  return;
1121
1245
  }
1122
1246
  if (result.kind === 'contract-unavailable') {
1123
- journal.record(`connection contract unavailable ${result.text}`);
1124
- refuse('missing-required-guarantee');
1247
+ journal.record(`connection contract unavailable ${result.code}`);
1248
+ refuse('missing-required-guarantee', {
1249
+ code: result.code,
1250
+ fields: result.fields
1251
+ });
1125
1252
  notifyReady();
1126
1253
  return;
1127
1254
  }
@@ -1152,12 +1279,18 @@ export function createMdRuntime(options) {
1152
1279
  ].every((operation)=>Array.isArray(room.operations[operation]) && room.operations[operation].every((scope)=>typeof scope === 'string')) || room.replica === undefined || typeof room.replica.selection !== 'string' || room.replica.selection.length === 0 || ![
1153
1280
  'admitted',
1154
1281
  'unavailable'
1155
- ].includes(room.replica.admission) || room.attachedModel === undefined || room.attachedModel !== null && (typeof room.attachedModel.identity !== 'string' || !room.attachedModel.identity || typeof room.attachedModel.version !== 'string' || !room.attachedModel.version)) {
1282
+ ].includes(room.replica.admission) || !declaredIdentity(room.attachedModel)) {
1156
1283
  journal.record(`invalid model or replica declaration ${room.locator}`);
1157
1284
  refuse('missing-required-guarantee');
1158
1285
  notifyReady();
1159
1286
  return;
1160
1287
  }
1288
+ if (!declaredSelection(room)) {
1289
+ journal.record(`invalid typed selection declaration ${room.locator}`);
1290
+ refuse('missing-required-guarantee');
1291
+ notifyReady();
1292
+ return;
1293
+ }
1161
1294
  if (malformed || held !== undefined && held.collectionRoot !== room.collectionRoot) {
1162
1295
  mismatchedRoots.add(room.locator);
1163
1296
  journal.record(`collection root ${malformed ? 'invalid' : 'changed'} ${room.locator}`);
@@ -1182,11 +1315,14 @@ export function createMdRuntime(options) {
1182
1315
  }
1183
1316
  capabilities = result.capabilities;
1184
1317
  notifyReady();
1318
+ if (declared.has('room-version-notifications')) observeRoomVersions();
1319
+ if (declared.has('provider-actions')) observeConversations();
1185
1320
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits)) {
1186
1321
  limits = result.providerLimits;
1187
1322
  await local.declare(limits);
1188
1323
  }
1189
1324
  const listedLocators = applyDiscovery(result.rooms);
1325
+ handSelected(result.rooms.filter((room)=>room.replica.admission === 'admitted').map((room)=>room.locator).sort());
1190
1326
  progress.complete = [
1191
1327
  ...replicas.keys()
1192
1328
  ].every((locator)=>listedLocators.has(locator));
@@ -1244,11 +1380,80 @@ export function createMdRuntime(options) {
1244
1380
  }
1245
1381
  }
1246
1382
  }
1247
- function enqueue() {
1248
- if (!leader) {
1249
- announce('enqueue');
1250
- return chain;
1383
+ function observeRoomVersions() {
1384
+ const path = options.adapter.roomVersions;
1385
+ if (path === undefined || withdrawNotifications !== undefined) return;
1386
+ withdrawNotifications = path.observe((notification)=>{
1387
+ if (closed || discoveryPaused) return;
1388
+ notices.set(notification.locator, notification.roomVersion);
1389
+ chain = chain.then(runNotified, runNotified);
1390
+ });
1391
+ }
1392
+ function observeConversations() {
1393
+ const path = options.adapter.conversations;
1394
+ if (path === undefined || withdrawConversations !== undefined) return;
1395
+ withdrawConversations = path.observe((event)=>{
1396
+ conversations.receive(event);
1397
+ });
1398
+ }
1399
+ function handSelected(locators) {
1400
+ const path = options.adapter.roomVersions;
1401
+ if (path === undefined || withdrawNotifications === undefined) return;
1402
+ if (handed !== undefined && JSON.stringify(locators) === JSON.stringify(handed)) return;
1403
+ handed = locators;
1404
+ path.select(locators);
1405
+ }
1406
+ async function runNotified() {
1407
+ const announced = [
1408
+ ...notices
1409
+ ];
1410
+ notices.clear();
1411
+ if (announced.length === 0 || closed || discoveryPaused) return;
1412
+ const resting = status;
1413
+ const progress = {
1414
+ complete: false,
1415
+ reachable: !resting.providerUnreachable
1416
+ };
1417
+ moveTo(transferring(resting));
1418
+ try {
1419
+ const local = await openLocal();
1420
+ for (const [locator, roomVersion] of announced){
1421
+ if (closed || discoveryPaused || authorization === undefined) return;
1422
+ if (handed?.includes(locator) !== true) continue;
1423
+ const room = discovered.get(locator);
1424
+ const held = replicas.get(locator);
1425
+ if (room === undefined || held === undefined) continue;
1426
+ if (held.confirmed.record.roomVersion === roomVersion) continue;
1427
+ const withheld = await advance(room, local, authorization);
1428
+ if (withheld === null) continue;
1429
+ if (withheld.kind === 'scoped-refusal') {
1430
+ await scoped(withheld, room, local);
1431
+ continue;
1432
+ }
1433
+ if (withheld.kind === 'transport-uncertainty') {
1434
+ progress.reachable = false;
1435
+ continue;
1436
+ }
1437
+ if (withheld.kind === 'room-not-readable') {
1438
+ unreadable.add(locator);
1439
+ list(room, false);
1440
+ emitRooms();
1441
+ continue;
1442
+ }
1443
+ if (withheld.kind === 'revoked-authorization') {
1444
+ refuse('revoked-authorization');
1445
+ return;
1446
+ }
1447
+ }
1448
+ } catch {
1449
+ progress.reachable = false;
1450
+ } finally{
1451
+ if (!closed) {
1452
+ moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1453
+ }
1251
1454
  }
1455
+ }
1456
+ function enqueue() {
1252
1457
  if (inflight) {
1253
1458
  queued = true;
1254
1459
  return chain;
@@ -1257,32 +1462,18 @@ export function createMdRuntime(options) {
1257
1462
  chain = chain.then(run, run);
1258
1463
  return chain;
1259
1464
  }
1260
- function startProcessing() {
1261
- if (leader) {
1262
- void enqueue();
1263
- return;
1264
- }
1265
- void window.navigator.locks.request('mdruntime-sync', {
1266
- signal: options.signal
1267
- }, async ()=>{
1268
- if (closed) return;
1269
- leader = true;
1270
- announce();
1271
- void enqueue();
1272
- await new Promise((resolve)=>{
1273
- releaseLeadership = ()=>{
1274
- leader = false;
1275
- resolve();
1276
- };
1277
- });
1278
- }).catch(()=>{});
1279
- }
1280
1465
  function close() {
1281
1466
  closed = true;
1467
+ withdrawNotifications?.();
1468
+ withdrawNotifications = undefined;
1469
+ withdrawConversations?.();
1470
+ withdrawConversations = undefined;
1471
+ conversations.close();
1282
1472
  stopTimer();
1283
- releaseLeadership();
1284
- channel?.close();
1285
1473
  store?.close();
1474
+ void opening?.then((opened)=>{
1475
+ opened.close();
1476
+ }, ()=>{});
1286
1477
  roomListeners.clear();
1287
1478
  refusalListeners.clear();
1288
1479
  storageLossListeners.clear();
@@ -1290,22 +1481,16 @@ export function createMdRuntime(options) {
1290
1481
  querySubscriptions.clear();
1291
1482
  queueSubscriptions.clear();
1292
1483
  draftListeners.clear();
1484
+ presentationListeners.clear();
1293
1485
  }
1294
- channel?.addEventListener('message', (event)=>{
1295
- const data = event.data;
1296
- if (data === null || typeof data !== 'object') return;
1297
- void exclusive(()=>applyRemote(data));
1298
- });
1299
1486
  options.signal?.addEventListener('abort', close, {
1300
1487
  once: true
1301
1488
  });
1302
1489
  if (options.signal?.aborted) close();
1303
- void openLocal().then(()=>{
1304
- if (!leader) notifyReady();
1305
- }, ()=>{
1490
+ void openLocal().catch(()=>{
1306
1491
  notifyReady();
1307
1492
  });
1308
- startProcessing();
1493
+ void enqueue();
1309
1494
  return {
1310
1495
  rooms () {
1311
1496
  return listed();
@@ -1314,7 +1499,10 @@ export function createMdRuntime(options) {
1314
1499
  return visible(request);
1315
1500
  },
1316
1501
  types (locator) {
1317
- return modelTypes(modelOf(locator));
1502
+ return {
1503
+ collection: modelTypes(modelOf(locator)),
1504
+ typedSelection: rooms.get(locator)?.typedSelection === undefined || rooms.get(locator)?.typedSelection === null ? null : modelTypes(modelOf(locator, 'typed-selection'))
1505
+ };
1318
1506
  },
1319
1507
  syncStatus () {
1320
1508
  return status;
@@ -1351,6 +1539,29 @@ export function createMdRuntime(options) {
1351
1539
  });
1352
1540
  }
1353
1541
  },
1542
+ conversation: conversations.surface,
1543
+ presentation: {
1544
+ read (app) {
1545
+ return exclusive(async ()=>{
1546
+ await hydrate(await openLocal());
1547
+ return presentationOf(app);
1548
+ });
1549
+ },
1550
+ write (app, value) {
1551
+ return exclusive(async ()=>{
1552
+ const local = await openLocal();
1553
+ await hydrate(local);
1554
+ const stored = structuredClone(value);
1555
+ await local.present({
1556
+ app,
1557
+ value: stored
1558
+ });
1559
+ presentations.set(app, stored);
1560
+ }).then(()=>{
1561
+ emitPresentation(app);
1562
+ });
1563
+ }
1564
+ },
1354
1565
  save (locator, changes, replacing) {
1355
1566
  return exclusive(()=>write(locator, changes, replacing));
1356
1567
  },
@@ -1381,6 +1592,9 @@ export function createMdRuntime(options) {
1381
1592
  storageLoss () {
1382
1593
  return storageLoss;
1383
1594
  },
1595
+ persistence () {
1596
+ return options.shell.persistentStorage;
1597
+ },
1384
1598
  observe (topic, second, third) {
1385
1599
  if (topic === 'drafts') {
1386
1600
  const notify = second;
@@ -1402,6 +1616,27 @@ export function createMdRuntime(options) {
1402
1616
  draftListeners.delete(listener);
1403
1617
  };
1404
1618
  }
1619
+ if (topic === 'conversation') {
1620
+ return conversations.observe(second, third);
1621
+ }
1622
+ if (topic === 'presentation') {
1623
+ const listener = {
1624
+ app: second,
1625
+ notify: third,
1626
+ ready: false
1627
+ };
1628
+ presentationListeners.add(listener);
1629
+ void exclusive(async ()=>{
1630
+ await hydrate(await openLocal());
1631
+ }).then(()=>{
1632
+ if (closed || !presentationListeners.has(listener)) return;
1633
+ listener.ready = true;
1634
+ listener.notify(presentationOf(listener.app));
1635
+ }, ()=>{});
1636
+ return ()=>{
1637
+ presentationListeners.delete(listener);
1638
+ };
1639
+ }
1405
1640
  if (topic === 'rooms') {
1406
1641
  const notify = second;
1407
1642
  roomListeners.add(notify);