@agentshouse/mdruntime-adapter 0.1.0-cd028205a5103 → 0.1.0-cd23c45caa798

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 (53) hide show
  1. package/AGENTS.md +285 -225
  2. package/dist/actions.d.ts +68 -1
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +196 -193
  5. package/dist/adapter.d.ts +1 -0
  6. package/dist/adapter.d.ts.map +1 -1
  7. package/dist/adapter.js +384 -103
  8. package/dist/discovery.d.ts +7 -4
  9. package/dist/discovery.d.ts.map +1 -1
  10. package/dist/discovery.js +46 -13
  11. package/dist/gather.d.ts +2 -1
  12. package/dist/gather.d.ts.map +1 -1
  13. package/dist/gather.js +15 -20
  14. package/dist/handed.d.ts +15 -0
  15. package/dist/handed.d.ts.map +1 -0
  16. package/dist/handed.js +45 -0
  17. package/dist/index.d.ts +2 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/model.d.ts +1 -1
  21. package/dist/model.d.ts.map +1 -1
  22. package/dist/model.js +6 -2
  23. package/dist/part.d.ts +13 -0
  24. package/dist/part.d.ts.map +1 -0
  25. package/dist/part.js +80 -0
  26. package/dist/read.d.ts +53 -16
  27. package/dist/read.d.ts.map +1 -1
  28. package/dist/read.js +265 -147
  29. package/dist/referrer.d.ts +7 -0
  30. package/dist/referrer.d.ts.map +1 -0
  31. package/dist/referrer.js +33 -0
  32. package/dist/refusal.d.ts +2 -5
  33. package/dist/refusal.d.ts.map +1 -1
  34. package/dist/refusal.js +6 -9
  35. package/dist/site.d.ts +1 -1
  36. package/dist/site.d.ts.map +1 -1
  37. package/dist/site.js +175 -58
  38. package/dist/stream.d.ts +4 -4
  39. package/dist/stream.d.ts.map +1 -1
  40. package/dist/stream.js +340 -42
  41. package/dist/transport.d.ts +64 -10
  42. package/dist/transport.d.ts.map +1 -1
  43. package/dist/transport.js +3 -0
  44. package/dist/version.d.ts +1 -0
  45. package/dist/version.d.ts.map +1 -1
  46. package/dist/version.js +1 -1
  47. package/dist/write.d.ts +14 -9
  48. package/dist/write.d.ts.map +1 -1
  49. package/dist/write.js +156 -37
  50. package/package.json +3 -4
  51. package/dist/links.d.ts +0 -20
  52. package/dist/links.d.ts.map +0 -1
  53. package/dist/links.js +0 -104
package/dist/adapter.js CHANGED
@@ -1,23 +1,66 @@
1
+ import { sortedKeys, stepped } from '@agentshouse/mdruntime';
1
2
  import { DISCOVER_PREFIXES, capabilities, discoveredRoom, mapDiscovery, parseDiscovery, trackedFrom, } from './discovery.js';
2
- import { scopeModels, streamedModels, viewModels } from './model.js';
3
- import { gathered, selections } from './gather.js';
3
+ import { partModels, streamedModels, viewModels } from './model.js';
4
+ import { gathered, gatheredEach, selections } from './gather.js';
4
5
  import { createSiteAdapter } from './site.js';
5
- import { bootstrapOf, contentPrefixes, matchesScope, parseEnumeration, parseEnvelope, parseGrant, refreshFrom, viewOf, } from './read.js';
6
- import { acceptedLinks, enumeratedLinks, heldLinks, scopeLinks, streamedLinks, } from './links.js';
6
+ import { authorityRoot, bootstrapping, emptyScope, housePart, parseEnumeration, parseGrant, parseParted, refreshFrom, viewOf, wholeView, } from './read.js';
7
+ import { PART_ATTEMPTS, unverifiedPart } from './part.js';
8
+ import { sha256Hex } from './digest.js';
9
+ import { acceptedHanded, enumeratedHanded, heldHanded, scopeHanded, streamedHanded, } from './handed.js';
7
10
  import { houseAction } from './actions.js';
8
11
  import { classifyDoor, classifyRead, discoveryFailure, doorRefusal } from './refusal.js';
9
12
  import { houseNotifications } from './stream.js';
10
- import { DOOR_BATCH, DOOR_BATCH_UPLOAD, DOOR_BOOTSTRAP, DOOR_DISCOVER, DOOR_ENUMERATE, DOOR_REVISIONS, } from './transport.js';
11
- import { batchPaths, deltaBody, deltaBytes, deltaDigest, historyOutcome, houseChanges, mutationOutcome, operationHead, rejectedMapping, revisionBody, uploadable, } from './write.js';
13
+ import { DOOR_BATCH, DOOR_BATCH_UPLOAD, DOOR_BOOTSTRAP, DOOR_DISCOVER, DOOR_ENUMERATE, DOOR_FILES_GET, DOOR_REVISIONS, } from './transport.js';
14
+ import { batchPaths, deltaBody, deltaBytes, deltaDigest, historyOutcome, houseChanges, houseMove, mutationOutcome, operationHead, rejectedMapping, revisionBody, uploadable, } from './write.js';
15
+ import { houseReferrer } from './referrer.js';
12
16
  import { followsVersion, logPosition, presented, reachedVersion, roomVersion, } from './version.js';
13
17
  const UNCERTAIN = { kind: 'transport-uncertainty' };
14
18
  const UNCERTAIN_READ = { kind: 'transport-uncertainty' };
15
19
  function lapsedGrant(outcome) {
16
20
  return outcome.kind === 'rejected' && outcome.code === 'upload_unavailable';
17
21
  }
22
+ const heldPaths = new WeakMap();
23
+ async function sortedHeld(held) {
24
+ const known = heldPaths.get(held.documents);
25
+ if (known !== undefined)
26
+ return known;
27
+ const sorted = await stepped(sortedKeys(held.documents));
28
+ heldPaths.set(held.documents, sorted);
29
+ return sorted;
30
+ }
31
+ function heldFiles(authority, held, paths, bound) {
32
+ const files = [];
33
+ let through = null;
34
+ for (let at = 0; at < paths.length && through === null; at += 1) {
35
+ const revision = held.revisions[paths[at]];
36
+ if (revision === undefined)
37
+ continue;
38
+ if (files.length === bound)
39
+ through = files.at(-1).path;
40
+ else
41
+ files.push({ path: paths[at], revision });
42
+ }
43
+ return files.length === 0 ? null : { room: authority, through, files };
44
+ }
45
+ async function reused(scope, held) {
46
+ const sent = new Set(scope.contents.map((entry) => entry.path));
47
+ const contents = [...scope.contents];
48
+ for (const file of scope.files) {
49
+ if (file.binary !== undefined || sent.has(file.path))
50
+ continue;
51
+ const content = held?.documents[file.path];
52
+ if (content === undefined ||
53
+ held?.revisions[file.path] !== file.revision ||
54
+ (await sha256Hex(content)) !== file.sha256) {
55
+ return null;
56
+ }
57
+ contents.push({ path: file.path, content });
58
+ }
59
+ return { ...scope, contents };
60
+ }
18
61
  export function createAgentsHouseAdapter(options) {
19
62
  if ('site' in options)
20
- return createSiteAdapter(options.site);
63
+ return createSiteAdapter(options.site, options.snapshot === true);
21
64
  const transport = options.transport;
22
65
  const notifications = houseNotifications(transport, 'credential');
23
66
  const tracked = new Map();
@@ -28,38 +71,109 @@ export function createAgentsHouseAdapter(options) {
28
71
  throw new Error('no Discovery declared the bounds');
29
72
  return bounds;
30
73
  }
31
- async function bootstrappedRoom(envelope, entry) {
32
- const scope = envelope.scopes.find((candidate) => matchesScope(candidate, entry));
33
- if (scope === undefined)
34
- return { kind: 'room-not-readable', locator: entry.locator };
35
- handed.set(entry.locator, scopeLinks(scope));
74
+ function hand(locator, steps) {
75
+ const held = handed.get(locator);
76
+ const next = stepped(steps);
77
+ if (!(next instanceof Promise)) {
78
+ handed.set(locator, next);
79
+ return;
80
+ }
81
+ return next.then((room) => {
82
+ if (handed.get(locator) === held)
83
+ handed.set(locator, room);
84
+ else
85
+ handed.delete(locator);
86
+ });
87
+ }
88
+ async function fetchedPart(entry, continued, listed) {
89
+ for (let attempt = 0; attempt < PART_ATTEMPTS; attempt += 1) {
90
+ const answered = await transport.door(DOOR_BOOTSTRAP, {
91
+ prefixes: [authorityRoot(entry.handle)],
92
+ part: listed.part,
93
+ ...(continued.held === undefined ? {} : { held: [continued.held] }),
94
+ });
95
+ const failure = classifyRead(answered.status, answered.body, entry.locator);
96
+ if (failure !== null)
97
+ return failure;
98
+ const scope = await housePart(continued.room, answered.body, listed, 'credential');
99
+ if (scope !== null)
100
+ return scope;
101
+ }
102
+ return unverifiedPart(entry.locator);
103
+ }
104
+ async function partPage(asked, continued) {
105
+ const { entry } = asked;
106
+ const listed = continued.room.parts[continued.next];
107
+ const sent = listed === undefined
108
+ ? emptyScope(continued.room)
109
+ : await fetchedPart(entry, continued, listed);
110
+ if ('kind' in sent)
111
+ return sent;
112
+ const scope = await reused(sent, asked.held);
113
+ if (scope === null) {
114
+ return asked.held === undefined
115
+ ? UNCERTAIN_READ
116
+ : bootstrapTogether({ entry, held: undefined });
117
+ }
118
+ const next = continued.next + 1;
119
+ const more = next < continued.room.parts.length;
120
+ if (continued.next === 0 && !more)
121
+ await hand(entry.locator, scopeHanded(scope));
122
+ else
123
+ handed.delete(entry.locator);
36
124
  notifications.reach(entry.authority, scope);
37
- const models = await scopeModels(scope, entry);
38
- return { ...bootstrapOf(scope), ...(models === undefined ? {} : { models }) };
125
+ const seen = [
126
+ ...continued.seen,
127
+ ...entry.schemas.filter((schema) => scope.files.some((file) => file.path === schema)),
128
+ ];
129
+ const models = await partModels(scope, entry, new Set(seen), !more);
130
+ return {
131
+ ...(await stepped(bootstrapping(scope))),
132
+ ...(models === undefined ? {} : { models }),
133
+ ...(more ? { pageToken: JSON.stringify({ ...continued, next, seen }) } : {}),
134
+ };
135
+ }
136
+ async function enumeratedPage(ask, page) {
137
+ const answer = await transport.door(DOOR_ENUMERATE, {
138
+ prefixes: [authorityRoot(ask.entry.handle)],
139
+ positions: [
140
+ {
141
+ ...presented(ask.entry.authority, { position: page.after, logEpoch: ask.from.logEpoch }),
142
+ page: { through: page.through, read: page.read },
143
+ },
144
+ ],
145
+ });
146
+ const failure = classifyRead(answer.status, answer.body, ask.entry.locator);
147
+ if (failure !== null)
148
+ return failure;
149
+ const enumeration = parseEnumeration(answer.body, 'credential');
150
+ if (enumeration === null)
151
+ return UNCERTAIN_READ;
152
+ return (viewOf(enumeration, ask.entry) ?? { kind: 'room-not-readable', locator: ask.entry.locator });
39
153
  }
40
154
  async function enumeratedRoom(enumeration, ask) {
41
- const view = viewOf(enumeration, ask.entry);
42
- if (view === undefined)
155
+ const first = viewOf(enumeration, ask.entry);
156
+ if (first === undefined)
43
157
  return { kind: 'room-not-readable', locator: ask.entry.locator };
158
+ const view = await wholeView(first, (page) => enumeratedPage(ask, page));
159
+ if ('kind' in view)
160
+ return view;
44
161
  notifications.reach(ask.entry.authority, view);
45
- const answer = refreshFrom(view, ask.fromRoomVersion);
162
+ const answer = await refreshFrom(view, ask.fromRoomVersion);
46
163
  if (answer.kind !== 'net-advancement')
47
164
  return answer;
48
- handed.set(ask.entry.locator, enumeratedLinks(view, handed.get(ask.entry.locator)));
165
+ await hand(ask.entry.locator, enumeratedHanded(view, handed.get(ask.entry.locator)));
49
166
  const models = await viewModels(view, ask.entry);
50
167
  return models === undefined ? answer : { ...answer, models };
51
168
  }
52
169
  async function streamed(entry, request, from) {
53
- const links = handed.get(entry.locator);
54
- if (links?.roomVersion !== request.fromRoomVersion)
170
+ const held = handed.get(entry.locator);
171
+ if (held?.roomVersion !== request.fromRoomVersion)
55
172
  return null;
56
- const advanced = notifications.advance(entry, from, (path, revision) => links.revisions.get(path) === revision);
173
+ const advanced = await stepped(notifications.advance(entry, from, (path, revision) => held.revisions[path] === revision));
57
174
  if (advanced === null)
58
175
  return null;
59
- const relinked = streamedLinks(links, advanced);
60
- if (relinked === null)
61
- return null;
62
- handed.set(entry.locator, relinked);
176
+ await hand(entry.locator, streamedHanded(held, advanced));
63
177
  const models = await streamedModels(advanced.schemas, entry, 'credential');
64
178
  return models === undefined ? advanced.page : { ...advanced.page, models };
65
179
  }
@@ -69,28 +183,30 @@ export function createAgentsHouseAdapter(options) {
69
183
  const at = logPosition(room.roomVersion);
70
184
  if (entry === undefined || at === null)
71
185
  return [];
72
- handed.set(room.locator, heldLinks(room));
186
+ handed.set(room.locator, heldHanded(room));
73
187
  notifications.reach(entry.authority, at);
74
188
  return [presented(entry.authority, at)];
75
189
  });
76
190
  notifications.send(kept);
77
191
  }
78
- function adopted(entry, outcome) {
192
+ async function adoptedMove(entry, into, outcome) {
193
+ if (outcome.kind === 'accepted' && outcome.destination !== undefined) {
194
+ await adopted(into, { kind: 'accepted', ...outcome.destination });
195
+ }
196
+ return adopted(entry, outcome);
197
+ }
198
+ async function adopted(entry, outcome) {
79
199
  if (outcome.kind !== 'accepted')
80
200
  return outcome;
81
- const { originals, ...accepted } = outcome;
82
- const links = handed.get(entry.locator);
83
- if (links === undefined)
84
- return accepted;
85
- const follows = followsVersion(links.roomVersion, accepted.roomVersion);
86
- const relinked = acceptedLinks(links, accepted, follows ? accepted.roomVersion : links.roomVersion, originals);
87
- handed.set(entry.locator, relinked.links);
88
- const at = logPosition(accepted.roomVersion);
201
+ const held = handed.get(entry.locator);
202
+ if (held === undefined)
203
+ return outcome;
204
+ const follows = followsVersion(held.roomVersion, outcome.roomVersion);
205
+ await hand(entry.locator, acceptedHanded(held, outcome, follows ? outcome.roomVersion : held.roomVersion));
206
+ const at = logPosition(outcome.roomVersion);
89
207
  if (follows && at !== null)
90
208
  notifications.reach(entry.authority, at);
91
- return relinked.attachments === undefined
92
- ? accepted
93
- : { ...accepted, attachments: relinked.attachments };
209
+ return outcome;
94
210
  }
95
211
  function authorityFor(locator) {
96
212
  return tracked.get(locator);
@@ -109,88 +225,109 @@ export function createAgentsHouseAdapter(options) {
109
225
  return result;
110
226
  return { ...result, at: { locator, roomVersion: roomVersion({ position, logEpoch }) } };
111
227
  }
112
- async function bundled(body) {
113
- const grant = parseGrant(body);
114
- if (grant === null)
115
- return body;
116
- if (grant.method !== 'GET')
117
- return null;
118
- const answer = await transport.transfer({
119
- act: 'local_copy_bundle',
120
- method: 'GET',
121
- url: grant.url,
122
- answer: 'json',
123
- });
124
- return answer.status === 200 ? answer.body : null;
125
- }
126
- function weight(entry) {
127
- return contentPrefixes(entry).length;
228
+ async function holding(asks) {
229
+ const bound = Math.floor(declared().max_held_files / Math.max(1, asks.filter((ask) => ask.held !== undefined).length));
230
+ const held = [];
231
+ for (const ask of asks) {
232
+ if (ask.held === undefined || bound === 0)
233
+ continue;
234
+ const paths = await sortedHeld(ask.held);
235
+ const files = heldFiles(ask.entry.authority, ask.held, paths, bound);
236
+ if (files !== null)
237
+ held.push(files);
238
+ }
239
+ return held;
128
240
  }
129
- async function bootstrapped(entries) {
130
- const answers = [];
131
- for (const part of selections(entries, weight, declared().max_prefixes)) {
132
- const answer = await transport.door(DOOR_BOOTSTRAP, {
133
- prefixes: part.flatMap((entry) => contentPrefixes(entry)),
134
- });
135
- const classified = part.map((entry) => classifyDoor(answer.status, answer.body, entry.locator));
136
- const envelope = classified[0] === null ? parseEnvelope(await bundled(answer.body)) : null;
137
- for (const [index, entry] of part.entries()) {
138
- const failure = classified[index];
139
- if (failure === null || failure === undefined) {
140
- answers.push(envelope === null ? UNCERTAIN_READ : await bootstrappedRoom(envelope, entry));
141
- continue;
241
+ async function bootstrapped(asks, answer) {
242
+ let open = asks.map((asked, ask) => ({ entry: asked.entry, asked, ask }));
243
+ while (open.length > 0) {
244
+ const unreached = [];
245
+ for (const part of selections(open, declared().max_prefixes)) {
246
+ const held = await holding(part.map(({ asked }) => asked));
247
+ const answered = await transport.door(DOOR_BOOTSTRAP, {
248
+ prefixes: part.map(({ entry }) => authorityRoot(entry.handle)),
249
+ ...(held.length === 0 ? {} : { held }),
250
+ });
251
+ const classified = part.map(({ entry }) => classifyDoor(answered.status, answered.body, entry.locator));
252
+ const parted = classified[0] === null ? parseParted(answered.body) : null;
253
+ for (const [index, asked] of part.entries()) {
254
+ const failure = classified[index];
255
+ if (failure !== null && failure !== undefined) {
256
+ answer(asked.ask, failure);
257
+ continue;
258
+ }
259
+ if (parted === null) {
260
+ answer(asked.ask, UNCERTAIN_READ);
261
+ continue;
262
+ }
263
+ const room = parted.rooms.find((candidate) => candidate.anchor === asked.entry.authority);
264
+ if (room === undefined && parted.next !== undefined) {
265
+ unreached.push(asked);
266
+ continue;
267
+ }
268
+ if (room === undefined) {
269
+ answer(asked.ask, { kind: 'room-not-readable', locator: asked.entry.locator });
270
+ continue;
271
+ }
272
+ const kept = held.find((files) => files.room === asked.entry.authority);
273
+ answer(asked.ask, await partPage(asked.asked, {
274
+ room,
275
+ next: 0,
276
+ seen: [],
277
+ ...(kept === undefined ? {} : { held: kept }),
278
+ }));
142
279
  }
143
- answers.push(failure);
144
280
  }
281
+ open = unreached;
145
282
  }
146
- return answers;
147
283
  }
148
284
  function layered(asks) {
149
285
  const depths = new Map();
150
286
  const layers = [];
151
287
  for (const ask of asks) {
152
- const depth = depths.get(ask.entry.authority) ?? 0;
153
- depths.set(ask.entry.authority, depth + 1);
288
+ const depth = depths.get(ask.ask.entry.authority) ?? 0;
289
+ depths.set(ask.ask.entry.authority, depth + 1);
154
290
  (layers[depth] ??= []).push(ask);
155
291
  }
156
292
  return layers;
157
293
  }
158
294
  async function enumerated(asks) {
159
- const answers = new Map();
160
- const parts = layered(asks).flatMap((layer) => selections(layer, (ask) => weight(ask.entry), declared().max_prefixes));
295
+ const answers = [];
296
+ const parts = layered(asks.map((ask, index) => ({ ask, index }))).flatMap((layer) => selections(layer, declared().max_prefixes));
161
297
  for (const part of parts) {
162
298
  let open = [...part];
163
299
  while (open.length > 0) {
164
300
  const answer = await transport.door(DOOR_ENUMERATE, {
165
- prefixes: open.flatMap((ask) => contentPrefixes(ask.entry)),
166
- positions: open.map((ask) => presented(ask.entry.authority, ask.from)),
301
+ prefixes: open.map(({ ask }) => authorityRoot(ask.entry.handle)),
302
+ positions: open.map(({ ask }) => presented(ask.entry.authority, ask.from)),
167
303
  });
168
- const classified = open.map((ask) => classifyRead(answer.status, answer.body, ask.entry.locator));
304
+ const classified = open.map(({ ask }) => classifyRead(answer.status, answer.body, ask.entry.locator));
169
305
  if (classified[0]?.kind === 'beyond-horizon') {
170
306
  const named = doorRefusal(answer.body)?.fields.authority;
171
- const expired = open.filter((ask) => ask.entry.authority === named);
307
+ const expired = open.filter(({ ask }) => ask.entry.authority === named);
172
308
  const answered = expired.length === 0 ? UNCERTAIN_READ : { kind: 'beyond-horizon' };
173
309
  const gone = expired.length === 0 ? open : expired;
174
310
  for (const ask of gone)
175
- answers.set(ask, answered);
311
+ answers[ask.index] = answered;
176
312
  open = open.filter((ask) => !gone.includes(ask));
177
313
  continue;
178
314
  }
179
315
  const enumeration = classified[0] === null ? parseEnumeration(answer.body, 'credential') : null;
180
- for (const [index, ask] of open.entries()) {
181
- const failure = classified[index];
316
+ for (const [answerIndex, { ask, index }] of open.entries()) {
317
+ const failure = classified[answerIndex];
182
318
  if (failure !== null && failure !== undefined) {
183
- answers.set(ask, failure);
319
+ answers[index] = failure;
184
320
  continue;
185
321
  }
186
- answers.set(ask, enumeration === null ? UNCERTAIN_READ : await enumeratedRoom(enumeration, ask));
322
+ answers[index] =
323
+ enumeration === null ? UNCERTAIN_READ : await enumeratedRoom(enumeration, ask);
187
324
  }
188
325
  open = [];
189
326
  }
190
327
  }
191
- return asks.map((ask) => answers.get(ask) ?? UNCERTAIN_READ);
328
+ return answers;
192
329
  }
193
- const bootstrapTogether = gathered(bootstrapped);
330
+ const bootstrapTogether = gatheredEach(bootstrapped);
194
331
  const enumerateTogether = gathered(enumerated);
195
332
  async function uploaded(head, changes, delta, bytes, entry, handed, keep) {
196
333
  const paths = batchPaths(changes);
@@ -200,16 +337,16 @@ export function createAgentsHouseAdapter(options) {
200
337
  }
201
338
  const minted = await transport.door(DOOR_BATCH_UPLOAD, { ...head, paths, bytes, sha256 });
202
339
  if (minted.status !== 200) {
203
- return { outcome: mutationOutcome(minted.status, minted.body, entry.locator, entry.handle) };
340
+ return {
341
+ outcome: await stepped(mutationOutcome(minted.status, minted.body, entry.locator, entry.handle)),
342
+ };
204
343
  }
205
344
  const grant = parseGrant(minted.body);
206
345
  if (grant === null || grant.method !== 'POST')
207
346
  return { outcome: UNCERTAIN };
208
347
  const delivered = await transport.transfer({
209
348
  act: 'authored_batch_upload',
210
- method: 'POST',
211
349
  url: grant.url,
212
- answer: 'json',
213
350
  body: `${grant.operation}\n${delta}`,
214
351
  });
215
352
  if (delivered.status !== 200)
@@ -221,7 +358,30 @@ export function createAgentsHouseAdapter(options) {
221
358
  await keep(upload);
222
359
  return { body: { ...head, paths, upload, runtime: notifications.runtime } };
223
360
  }
224
- async function submit(head, changes, delta, bytes, entry, handed, keep) {
361
+ async function staged(answer, batch) {
362
+ const refusal = doorRefusal(answer.body);
363
+ if (refusal?.code !== 'files_unstaged')
364
+ return null;
365
+ const grants = refusal.fields.upload;
366
+ if (!Array.isArray(grants))
367
+ return false;
368
+ for (const grant of grants) {
369
+ const { url, operation, sha256 } = grant;
370
+ const change = batch.changes.find((held) => held.kind === 'binary' && held.sha256 === sha256);
371
+ if (typeof url !== 'string' || typeof operation !== 'string' || change === undefined)
372
+ return false;
373
+ const delivered = await transport.transfer({
374
+ act: 'binary_stage',
375
+ url,
376
+ body: change.blob,
377
+ operation,
378
+ });
379
+ if (delivered.status !== 200)
380
+ return false;
381
+ }
382
+ return true;
383
+ }
384
+ async function submit(batch, head, changes, delta, bytes, entry, handed, keep, into) {
225
385
  const inline = { ...head, changes, runtime: notifications.runtime };
226
386
  const prepared = uploadable(inline, bytes, declared())
227
387
  ? await uploaded(head, changes, delta, bytes, entry, handed, keep)
@@ -229,14 +389,66 @@ export function createAgentsHouseAdapter(options) {
229
389
  if ('outcome' in prepared)
230
390
  return prepared.outcome;
231
391
  const answer = await transport.door(DOOR_BATCH, prepared.body);
232
- return mutationOutcome(answer.status, answer.body, entry.locator, entry.handle);
392
+ const stagedFiles = await staged(answer, batch);
393
+ if (stagedFiles === false)
394
+ return UNCERTAIN;
395
+ const settled = stagedFiles === null ? answer : await transport.door(DOOR_BATCH, prepared.body);
396
+ if (stagedFiles === true && (await staged(settled, batch)) !== null)
397
+ return UNCERTAIN;
398
+ return stepped(mutationOutcome(settled.status, settled.body, entry.locator, entry.handle, into === undefined
399
+ ? undefined
400
+ : {
401
+ source: { authority: entry.authority, handle: entry.handle },
402
+ destination: { authority: into.authority, handle: into.handle },
403
+ }));
404
+ }
405
+ function granted(answer, locator) {
406
+ const failure = classifyDoor(answer.status, answer.body, locator);
407
+ if (failure !== null) {
408
+ return failure.kind === 'scoped-refusal'
409
+ ? {
410
+ kind: 'rejected',
411
+ code: failure.code,
412
+ fields: failure.fields,
413
+ retryable: failure.retryable,
414
+ }
415
+ : failure;
416
+ }
417
+ const grant = parseGrant(answer.body);
418
+ const name = answer.body.name;
419
+ if (grant === null || grant.method !== 'GET' || typeof name !== 'string') {
420
+ return UNCERTAIN_READ;
421
+ }
422
+ return { kind: 'link', url: grant.url, name };
423
+ }
424
+ async function downloaded(answer, locator) {
425
+ const link = granted(answer, locator);
426
+ if (link.kind !== 'link')
427
+ return link;
428
+ const read = await transport.download(link.url);
429
+ if (read.status !== 200 || read.blob === null)
430
+ return UNCERTAIN_READ;
431
+ return { kind: 'binary', blob: read.blob };
432
+ }
433
+ function pathGrant(request) {
434
+ const entry = authorityFor(request.locator);
435
+ if (entry === undefined)
436
+ return null;
437
+ return transport.door(DOOR_FILES_GET, {
438
+ room_ref: entry.authority,
439
+ path: request.path,
440
+ revision: request.revision,
441
+ });
233
442
  }
234
443
  return {
235
444
  async authorize() {
236
445
  return options.authorize();
237
446
  },
238
- async discover(_authorization) {
239
- const answer = await transport.door(DOOR_DISCOVER, { prefixes: [...DISCOVER_PREFIXES] });
447
+ async discover(_authorization, scope) {
448
+ const answer = await transport.door(DOOR_DISCOVER, {
449
+ prefixes: [...DISCOVER_PREFIXES],
450
+ ...(scope === undefined ? {} : { rooms: scope.authorities }),
451
+ });
240
452
  const failed = discoveryFailure(answer.status, answer.body);
241
453
  if (failed !== null)
242
454
  return failed;
@@ -245,22 +457,42 @@ export function createAgentsHouseAdapter(options) {
245
457
  return { kind: 'transport-uncertainty' };
246
458
  bounds = document.bounds;
247
459
  const entries = trackedFrom(document);
248
- tracked.clear();
460
+ const covered = scope === undefined
461
+ ? null
462
+ : [
463
+ ...new Set([
464
+ ...[...tracked.values()]
465
+ .filter((entry) => scope.authorities.includes(entry.authority))
466
+ .map((entry) => entry.locator),
467
+ ...entries.map((entry) => entry.locator),
468
+ ]),
469
+ ];
470
+ if (covered === null)
471
+ tracked.clear();
472
+ else
473
+ for (const locator of covered)
474
+ tracked.delete(locator);
249
475
  for (const entry of entries)
250
476
  tracked.set(entry.locator, entry);
251
477
  for (const locator of handed.keys())
252
478
  if (!tracked.has(locator))
253
479
  handed.delete(locator);
254
- notifications.track(entries);
255
- return mapDiscovery(document, document.authorities.map((authority) => discoveredRoom(authority)), capabilities(document));
480
+ notifications.track([...tracked.values()]);
481
+ return {
482
+ ...mapDiscovery(document, document.authorities.map((authority) => discoveredRoom(authority)), capabilities(document.actions, [...tracked.values()])),
483
+ member: document.member,
484
+ ...(covered === null ? {} : { covered }),
485
+ };
256
486
  },
257
487
  async bootstrap(_authorization, request) {
258
488
  const entry = authorityFor(request.locator);
259
489
  if (entry === undefined)
260
490
  return { kind: 'room-not-readable', locator: request.locator };
261
- if (request.pageToken !== undefined)
262
- return { kind: 'transport-uncertainty' };
263
- return bootstrapTogether(entry);
491
+ const asked = { entry, held: request.held };
492
+ if (request.pageToken !== undefined) {
493
+ return partPage(asked, JSON.parse(request.pageToken));
494
+ }
495
+ return bootstrapTogether(asked);
264
496
  },
265
497
  async refresh(_authorization, request) {
266
498
  const entry = authorityFor(request.locator);
@@ -280,17 +512,27 @@ export function createAgentsHouseAdapter(options) {
280
512
  const entry = authorityFor(request.locator);
281
513
  if (entry === undefined)
282
514
  return { kind: 'room-not-readable', locator: request.locator };
283
- const mapped = houseChanges(request.changeBatch, entry.handle);
515
+ const batch = request.changeBatch;
516
+ const destination = batch.between?.destination.room;
517
+ const into = destination === undefined ? undefined : authorityFor(destination);
518
+ if (destination !== undefined && into === undefined) {
519
+ return { kind: 'room-not-readable', locator: destination };
520
+ }
521
+ const mapped = into === undefined
522
+ ? houseChanges(batch, entry.handle)
523
+ : houseMove(batch, entry.handle, into.handle);
284
524
  if (mapped.kind === 'unmapped')
285
525
  return rejectedMapping(mapped);
286
- const head = operationHead(request.changeBatch);
526
+ const head = operationHead(batch);
287
527
  const delta = deltaBody(mapped.changes);
288
528
  const bytes = deltaBytes(delta);
289
529
  const handed = request.upload;
290
- const outcome = await submit(head, mapped.changes, delta, bytes, entry, handed, keep);
530
+ const answer = (upload) => submit(batch, head, mapped.changes, delta, bytes, entry, upload, keep, into);
531
+ const settle = (outcome) => into === undefined ? adopted(entry, outcome) : adoptedMove(entry, into, outcome);
532
+ const outcome = await answer(handed);
291
533
  if (handed === undefined || !lapsedGrant(outcome))
292
- return adopted(entry, outcome);
293
- return adopted(entry, await submit(head, mapped.changes, delta, bytes, entry, undefined, keep));
534
+ return settle(outcome);
535
+ return settle(await answer(undefined));
294
536
  },
295
537
  async history(_authorization, request) {
296
538
  const entry = authorityFor(request.locator);
@@ -307,6 +549,36 @@ export function createAgentsHouseAdapter(options) {
307
549
  ...(request.revision === undefined ? {} : { revision: request.revision }),
308
550
  });
309
551
  },
552
+ async binary(_authorization, request) {
553
+ if (request.kind === 'path') {
554
+ const asked = pathGrant(request);
555
+ if (asked === null)
556
+ return { kind: 'room-not-readable', locator: request.locator };
557
+ return downloaded(await asked, request.locator);
558
+ }
559
+ const referrer = houseReferrer(request.referrer, tracked);
560
+ if (referrer === null) {
561
+ return { kind: 'rejected', code: 'referrer_unknown', fields: {}, retryable: false };
562
+ }
563
+ const answer = await transport.door(DOOR_FILES_GET, {
564
+ file: request.fileRef,
565
+ referrer: referrer.referrer,
566
+ });
567
+ return downloaded(answer, referrer.locator);
568
+ },
569
+ async link(_authorization, request) {
570
+ const asked = pathGrant(request);
571
+ if (asked === null)
572
+ return { kind: 'room-not-readable', locator: request.locator };
573
+ return granted(await asked, request.locator);
574
+ },
575
+ referrerScope(referrer) {
576
+ const held = houseReferrer(referrer, tracked);
577
+ if (held === null)
578
+ return null;
579
+ const { path } = held.referrer;
580
+ return { locator: held.locator, ...(path === undefined ? {} : { path }) };
581
+ },
310
582
  reached(locator, version, target) {
311
583
  return reachedVersion(version, target, authorityFor(locator)?.logEpoch);
312
584
  },
@@ -316,7 +588,16 @@ export function createAgentsHouseAdapter(options) {
316
588
  async action(_authorization, request) {
317
589
  return withAt(await houseAction(transport, request));
318
590
  },
319
- roomStream: { hold: held, reopen: notifications.reopen, observe: notifications.observe },
591
+ roomStream: {
592
+ hold: held,
593
+ reopen: notifications.reopen,
594
+ observe: notifications.observe,
595
+ published(locator, roomVersion) {
596
+ const at = logPosition(roomVersion);
597
+ if (at !== null)
598
+ notifications.published(locator, at);
599
+ },
600
+ },
320
601
  conversations: notifications.conversations,
321
602
  serviceMode: notifications.serviceMode,
322
603
  };