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

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