@agentshouse/mdruntime-adapter 0.1.0-ca0d16f904a93 → 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 (58) 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.map +1 -1
  6. package/dist/adapter.js +292 -171
  7. package/dist/app-reference.d.ts +2 -0
  8. package/dist/app-reference.d.ts.map +1 -0
  9. package/dist/app-reference.js +2 -0
  10. package/dist/crockford.d.ts +4 -0
  11. package/dist/crockford.d.ts.map +1 -0
  12. package/dist/crockford.js +3 -0
  13. package/dist/discovery.d.ts +6 -10
  14. package/dist/discovery.d.ts.map +1 -1
  15. package/dist/discovery.js +37 -32
  16. package/dist/gather.d.ts +1 -1
  17. package/dist/gather.d.ts.map +1 -1
  18. package/dist/gather.js +4 -14
  19. package/dist/house-model.d.ts +2 -0
  20. package/dist/house-model.d.ts.map +1 -0
  21. package/dist/house-model.js +484 -0
  22. package/dist/index.d.ts +7 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +6 -1
  25. package/dist/model.d.ts +5 -1
  26. package/dist/model.d.ts.map +1 -1
  27. package/dist/model.js +11 -13
  28. package/dist/part.d.ts +15 -0
  29. package/dist/part.d.ts.map +1 -0
  30. package/dist/part.js +82 -0
  31. package/dist/read.d.ts +58 -24
  32. package/dist/read.d.ts.map +1 -1
  33. package/dist/read.js +270 -144
  34. package/dist/room-types.d.ts +7 -0
  35. package/dist/room-types.d.ts.map +1 -0
  36. package/dist/room-types.js +124 -0
  37. package/dist/site.d.ts.map +1 -1
  38. package/dist/site.js +101 -107
  39. package/dist/stream.d.ts +5 -5
  40. package/dist/stream.d.ts.map +1 -1
  41. package/dist/stream.js +66 -39
  42. package/dist/transport.d.ts +33 -16
  43. package/dist/transport.d.ts.map +1 -1
  44. package/dist/transport.js +2 -1
  45. package/dist/upload.d.ts +12 -0
  46. package/dist/upload.d.ts.map +1 -0
  47. package/dist/upload.js +63 -0
  48. package/dist/write.d.ts +12 -1
  49. package/dist/write.d.ts.map +1 -1
  50. package/dist/write.js +93 -55
  51. package/package.json +8 -4
  52. package/AGENTS.md +0 -300
  53. package/dist/handed.d.ts +0 -15
  54. package/dist/handed.d.ts.map +0 -1
  55. package/dist/handed.js +0 -38
  56. package/dist/referrer.d.ts +0 -7
  57. package/dist/referrer.d.ts.map +0 -1
  58. package/dist/referrer.js +0 -35
package/dist/adapter.js CHANGED
@@ -1,46 +1,49 @@
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
  }
@@ -51,48 +54,83 @@ export function createAgentsHouseAdapter(options) {
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,52 +154,54 @@ 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
  }
137
- function held(rooms, demand) {
178
+ function held(rooms) {
138
179
  const kept = rooms.flatMap((room) => {
139
180
  const entry = tracked.get(room.locator);
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
- notifications.send(kept, demand === null
148
- ? null
149
- : {
150
- rooms: demand.rooms.flatMap((locator) => {
151
- const entry = tracked.get(locator);
152
- return entry === undefined ? [] : [entry.authority];
153
- }),
154
- conversations: demand.conversations,
155
- });
188
+ notifications.send(kept);
156
189
  }
157
- 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) {
158
197
  if (outcome.kind !== 'accepted')
159
198
  return outcome;
160
199
  const held = handed.get(entry.locator);
161
200
  if (held === undefined)
162
201
  return outcome;
163
- const follows = followsVersion(held.roomVersion, outcome.roomVersion);
164
- 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);
165
205
  const at = logPosition(outcome.roomVersion);
166
206
  if (follows && at !== null)
167
207
  notifications.reach(entry.authority, at);
@@ -184,68 +224,61 @@ export function createAgentsHouseAdapter(options) {
184
224
  return result;
185
225
  return { ...result, at: { locator, roomVersion: roomVersion({ position, logEpoch }) } };
186
226
  }
187
- function weight(entry) {
188
- return contentPrefixes(entry).length;
189
- }
190
- function holding(asks, after) {
191
- const budget = Math.floor(declared().max_held_files / Math.max(1, asks.filter((ask) => ask.held !== undefined).length));
192
- 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;
193
238
  }
194
239
  async function bootstrapped(asks, answer) {
195
240
  let open = asks.map((asked, ask) => ({ entry: asked.entry, asked, ask }));
196
241
  while (open.length > 0) {
197
242
  const unreached = [];
198
- for (const part of selections(open, ({ entry }) => weight(entry), declared().max_prefixes)) {
199
- 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));
200
245
  const answered = await transport.door(DOOR_BOOTSTRAP, {
201
- prefixes: part.flatMap(({ entry }) => contentPrefixes(entry)),
246
+ prefixes: part.map(({ entry }) => authorityRoot(entry.handle)),
202
247
  ...(held.length === 0 ? {} : { held }),
203
248
  });
204
249
  const classified = part.map(({ entry }) => classifyDoor(answered.status, answered.body, entry.locator));
205
- const envelope = classified[0] === null ? parseEnvelope(answered.body) : null;
250
+ const parted = classified[0] === null ? parseParted(answered.body) : null;
206
251
  for (const [index, asked] of part.entries()) {
207
252
  const failure = classified[index];
208
253
  if (failure !== null && failure !== undefined) {
209
254
  answer(asked.ask, failure);
210
255
  continue;
211
256
  }
212
- if (envelope === null) {
257
+ if (parted === null) {
213
258
  answer(asked.ask, UNCERTAIN_READ);
214
259
  continue;
215
260
  }
216
- const scope = envelope.scopes.find((candidate) => matchesScope(candidate, asked.entry));
217
- 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) {
218
263
  unreached.push(asked);
219
264
  continue;
220
265
  }
221
- 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
+ }));
222
277
  }
223
278
  }
224
279
  open = unreached;
225
280
  }
226
281
  }
227
- async function continuedBootstrap(asked, continued) {
228
- const { entry } = asked;
229
- const after = {
230
- room: entry.authority,
231
- path: continued.after,
232
- ...(continued.cut === undefined ? {} : { cut: continued.cut }),
233
- };
234
- const held = holding([asked], continued.after);
235
- const answered = await transport.door(DOOR_BOOTSTRAP, {
236
- prefixes: contentPrefixes(entry),
237
- after,
238
- ...(held.length === 0 ? {} : { held }),
239
- });
240
- const failure = classifyRead(answered.status, answered.body, entry.locator);
241
- if (failure !== null)
242
- return failure;
243
- const envelope = parseEnvelope(answered.body);
244
- if (envelope === null)
245
- return UNCERTAIN_READ;
246
- const scope = envelope.scopes.find((candidate) => matchesScope(candidate, entry));
247
- return bootstrappedRoom(envelope, scope, asked, continued);
248
- }
249
282
  function layered(asks) {
250
283
  const depths = new Map();
251
284
  const layers = [];
@@ -258,12 +291,12 @@ export function createAgentsHouseAdapter(options) {
258
291
  }
259
292
  async function enumerated(asks) {
260
293
  const answers = [];
261
- const parts = layered(asks.map((ask, index) => ({ ask, index }))).flatMap((layer) => selections(layer, ({ ask }) => weight(ask.entry), declared().max_prefixes));
294
+ const parts = layered(asks.map((ask, index) => ({ ask, index }))).flatMap((layer) => selections(layer, declared().max_prefixes));
262
295
  for (const part of parts) {
263
296
  let open = [...part];
264
297
  while (open.length > 0) {
265
298
  const answer = await transport.door(DOOR_ENUMERATE, {
266
- prefixes: open.flatMap(({ ask }) => contentPrefixes(ask.entry)),
299
+ prefixes: open.map(({ ask }) => authorityRoot(ask.entry.handle)),
267
300
  positions: open.map(({ ask }) => presented(ask.entry.authority, ask.from)),
268
301
  });
269
302
  const classified = open.map(({ ask }) => classifyRead(answer.status, answer.body, ask.entry.locator));
@@ -302,10 +335,12 @@ export function createAgentsHouseAdapter(options) {
302
335
  }
303
336
  const minted = await transport.door(DOOR_BATCH_UPLOAD, { ...head, paths, bytes, sha256 });
304
337
  if (minted.status !== 200) {
305
- 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
+ };
306
341
  }
307
342
  const grant = parseGrant(minted.body);
308
- if (grant === null || grant.method !== 'POST')
343
+ if (grant === null)
309
344
  return { outcome: UNCERTAIN };
310
345
  const delivered = await transport.transfer({
311
346
  act: 'authored_batch_upload',
@@ -315,36 +350,31 @@ export function createAgentsHouseAdapter(options) {
315
350
  if (delivered.status !== 200)
316
351
  return { outcome: UNCERTAIN };
317
352
  const holding = parseGrant(delivered.body);
318
- if (holding === null || holding.method !== 'POST')
353
+ if (holding === null)
319
354
  return { outcome: UNCERTAIN };
320
355
  const upload = { url: holding.url, operation: holding.operation, bytes, sha256 };
321
356
  await keep(upload);
322
357
  return { body: { ...head, paths, upload, runtime: notifications.runtime } };
323
358
  }
324
- async function staged(answer, batch) {
359
+ async function staged(answer, { changeBatch: batch, file }) {
325
360
  const refusal = doorRefusal(answer.body);
326
361
  if (refusal?.code !== 'files_unstaged')
327
362
  return null;
328
- const grants = refusal.fields.upload;
329
- if (!Array.isArray(grants))
363
+ const uploads = refusal.fields.upload;
364
+ if (!Array.isArray(uploads))
330
365
  return false;
331
- for (const grant of grants) {
332
- const { url, operation, sha256 } = grant;
333
- const change = batch.changes.find((held) => held.kind === 'binary' && held.sha256 === sha256);
334
- 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)
335
370
  return false;
336
- const delivered = await transport.transfer({
337
- act: 'binary_stage',
338
- url,
339
- body: change.blob,
340
- operation,
341
- });
342
- if (delivered.status !== 200)
371
+ const finished = await finishedUpload(transport, upload, await file(change.localId));
372
+ if (finished?.body?.staged !== true)
343
373
  return false;
344
374
  }
345
375
  return true;
346
376
  }
347
- async function submit(batch, head, changes, delta, bytes, entry, handed, keep) {
377
+ async function submit(request, head, changes, delta, bytes, entry, handed, keep, into) {
348
378
  const inline = { ...head, changes, runtime: notifications.runtime };
349
379
  const prepared = uploadable(inline, bytes, declared())
350
380
  ? await uploaded(head, changes, delta, bytes, entry, handed, keep)
@@ -352,15 +382,20 @@ export function createAgentsHouseAdapter(options) {
352
382
  if ('outcome' in prepared)
353
383
  return prepared.outcome;
354
384
  const answer = await transport.door(DOOR_BATCH, prepared.body);
355
- const stagedFiles = await staged(answer, batch);
385
+ const stagedFiles = await staged(answer, request);
356
386
  if (stagedFiles === false)
357
387
  return UNCERTAIN;
358
388
  const settled = stagedFiles === null ? answer : await transport.door(DOOR_BATCH, prepared.body);
359
- if (stagedFiles === true && (await staged(settled, batch)) !== null)
389
+ if (stagedFiles === true && (await staged(settled, request)) !== null)
360
390
  return UNCERTAIN;
361
- 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
+ }));
362
397
  }
363
- async function downloaded(answer, locator) {
398
+ function addressed(answer, locator) {
364
399
  const failure = classifyDoor(answer.status, answer.body, locator);
365
400
  if (failure !== null) {
366
401
  return failure.kind === 'scoped-refusal'
@@ -372,20 +407,69 @@ export function createAgentsHouseAdapter(options) {
372
407
  }
373
408
  : failure;
374
409
  }
375
- const grant = parseGrant(answer.body);
376
- if (grant === null || grant.method !== 'GET')
377
- return UNCERTAIN_READ;
378
- const read = await transport.download(grant.url);
379
- 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)
380
444
  return UNCERTAIN_READ;
381
- 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
+ });
382
461
  }
383
462
  return {
463
+ roomTypes: HOUSE_ROOM_TYPES,
464
+ nameableRoomTypes: HOUSE_NAMEABLE_ROOM_TYPES,
384
465
  async authorize() {
385
466
  return options.authorize();
386
467
  },
387
- async discover(_authorization) {
388
- 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
+ });
389
473
  const failed = discoveryFailure(answer.status, answer.body);
390
474
  if (failed !== null)
391
475
  return failed;
@@ -394,16 +478,31 @@ export function createAgentsHouseAdapter(options) {
394
478
  return { kind: 'transport-uncertainty' };
395
479
  bounds = document.bounds;
396
480
  const entries = trackedFrom(document);
397
- 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);
398
496
  for (const entry of entries)
399
497
  tracked.set(entry.locator, entry);
400
498
  for (const locator of handed.keys())
401
499
  if (!tracked.has(locator))
402
500
  handed.delete(locator);
403
- notifications.track(entries);
501
+ notifications.track([...tracked.values()]);
404
502
  return {
405
- ...mapDiscovery(document, document.authorities.map((authority) => discoveredRoom(authority)), capabilities(document)),
503
+ ...mapDiscovery(document, document.authorities.map((authority) => discoveredRoom(authority)), capabilities(document.actions, [...tracked.values()])),
406
504
  member: document.member,
505
+ ...(covered === null ? {} : { covered }),
407
506
  };
408
507
  },
409
508
  async bootstrap(_authorization, request) {
@@ -412,7 +511,7 @@ export function createAgentsHouseAdapter(options) {
412
511
  return { kind: 'room-not-readable', locator: request.locator };
413
512
  const asked = { entry, held: request.held };
414
513
  if (request.pageToken !== undefined) {
415
- return continuedBootstrap(asked, JSON.parse(request.pageToken));
514
+ return partPage(asked, JSON.parse(request.pageToken), request.received, request.arriving);
416
515
  }
417
516
  return bootstrapTogether(asked);
418
517
  },
@@ -434,18 +533,27 @@ export function createAgentsHouseAdapter(options) {
434
533
  const entry = authorityFor(request.locator);
435
534
  if (entry === undefined)
436
535
  return { kind: 'room-not-readable', locator: request.locator };
437
- 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);
438
545
  if (mapped.kind === 'unmapped')
439
546
  return rejectedMapping(mapped);
440
- const head = operationHead(request.changeBatch);
547
+ const head = operationHead(batch);
441
548
  const delta = deltaBody(mapped.changes);
442
549
  const bytes = deltaBytes(delta);
443
550
  const handed = request.upload;
444
- const batch = request.changeBatch;
445
- 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);
446
554
  if (handed === undefined || !lapsedGrant(outcome))
447
- return adopted(entry, outcome);
448
- return adopted(entry, await submit(batch, head, mapped.changes, delta, bytes, entry, undefined, keep));
555
+ return settle(outcome);
556
+ return settle(await answer(undefined));
449
557
  },
450
558
  async history(_authorization, request) {
451
559
  const entry = authorityFor(request.locator);
@@ -463,35 +571,45 @@ export function createAgentsHouseAdapter(options) {
463
571
  });
464
572
  },
465
573
  async binary(_authorization, request) {
466
- if (request.kind === 'path') {
467
- const entry = authorityFor(request.locator);
468
- if (entry === undefined)
469
- return { kind: 'room-not-readable', locator: request.locator };
470
- const answer = await transport.door(DOOR_FILES_GET, {
471
- room_ref: entry.authority,
472
- path: request.path,
473
- revision: request.revision,
474
- });
475
- return downloaded(answer, request.locator);
476
- }
477
- const referrer = houseReferrer(request.referrer, tracked);
478
- if (referrer === null) {
479
- return { kind: 'rejected', code: 'referrer_unknown', fields: {}, retryable: false };
480
- }
481
- const answer = await transport.door(DOOR_FILES_GET, {
482
- file: request.fileRef,
483
- referrer: referrer.referrer,
484
- });
485
- 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);
486
578
  },
487
- referrerScope(referrer) {
488
- const held = houseReferrer(referrer, tracked);
489
- if (held === null)
490
- return null;
491
- if (held.locator === '')
492
- return { kind: 'conversations' };
493
- const { path } = held.referrer;
494
- 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;
495
613
  },
496
614
  reached(locator, version, target) {
497
615
  return reachedVersion(version, target, authorityFor(locator)?.logEpoch);
@@ -505,7 +623,10 @@ export function createAgentsHouseAdapter(options) {
505
623
  roomStream: {
506
624
  hold: held,
507
625
  reopen: notifications.reopen,
508
- observe: notifications.observe,
626
+ observe(notify, receipts, revisions) {
627
+ heldRevisions = revisions;
628
+ return notifications.observe(notify, receipts);
629
+ },
509
630
  published(locator, roomVersion) {
510
631
  const at = logPosition(roomVersion);
511
632
  if (at !== null)