@agentshouse/mdruntime-adapter 0.1.0-cca5d676bc88e → 0.1.0-ccda1a2da11c3

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