@agentshouse/mdruntime-adapter 0.1.0-cfd03209dcd82 → 0.1.0-cfded86738bb2

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