@agentshouse/mdruntime-adapter 0.1.0-ca218e62303fd → 0.1.0-ca231c2cbedb4

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