@agentshouse/mdruntime-adapter 0.1.0-ce3fe65a95f49 → 0.1.0-ce5ef9afd7e9a

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