@agentshouse/mdruntime-adapter 0.1.0-ce4626067e686 → 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 (55) hide show
  1. package/AGENTS.md +289 -246
  2. package/dist/actions.d.ts +68 -1
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +134 -110
  5. package/dist/adapter.d.ts +1 -0
  6. package/dist/adapter.d.ts.map +1 -1
  7. package/dist/adapter.js +335 -98
  8. package/dist/discovery.d.ts +5 -3
  9. package/dist/discovery.d.ts.map +1 -1
  10. package/dist/discovery.js +34 -12
  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 +4 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +3 -1
  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 +13 -0
  21. package/dist/part.d.ts.map +1 -0
  22. package/dist/part.js +80 -0
  23. package/dist/read.d.ts +70 -23
  24. package/dist/read.d.ts.map +1 -1
  25. package/dist/read.js +351 -136
  26. package/dist/referrer.d.ts +9 -0
  27. package/dist/referrer.d.ts.map +1 -0
  28. package/dist/referrer.js +41 -0
  29. package/dist/refusal.d.ts.map +1 -1
  30. package/dist/refusal.js +2 -1
  31. package/dist/room-types.d.ts +6 -0
  32. package/dist/room-types.d.ts.map +1 -0
  33. package/dist/room-types.js +66 -0
  34. package/dist/site.d.ts +1 -1
  35. package/dist/site.d.ts.map +1 -1
  36. package/dist/site.js +170 -56
  37. package/dist/stream.d.ts +5 -4
  38. package/dist/stream.d.ts.map +1 -1
  39. package/dist/stream.js +355 -42
  40. package/dist/transport.d.ts +67 -8
  41. package/dist/transport.d.ts.map +1 -1
  42. package/dist/transport.js +4 -0
  43. package/dist/upload.d.ts +12 -0
  44. package/dist/upload.d.ts.map +1 -0
  45. package/dist/upload.js +52 -0
  46. package/dist/version.d.ts +1 -0
  47. package/dist/version.d.ts.map +1 -1
  48. package/dist/version.js +1 -1
  49. package/dist/write.d.ts +13 -9
  50. package/dist/write.d.ts.map +1 -1
  51. package/dist/write.js +141 -40
  52. package/package.json +2 -3
  53. package/dist/links.d.ts +0 -20
  54. package/dist/links.d.ts.map +0 -1
  55. package/dist/links.js +0 -104
package/dist/adapter.js CHANGED
@@ -1,77 +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 { acceptedLinks, enumeratedLinks, heldLinks, scopeLinks, streamedLinks, } from './links.js';
6
+ import { authorityRoot, bootstrapping, emptyScope, housePart, parseEnumeration, parseFileAddress, parseGrant, parseParted, refreshFrom, viewOf, wholeView, } from './read.js';
7
+ import { PART_ATTEMPTS, unverifiedPart } from './part.js';
8
+ import { sha256Hex } from './digest.js';
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
- import { DOOR_BATCH, DOOR_BATCH_UPLOAD, DOOR_BOOTSTRAP, DOOR_DISCOVER, DOOR_ENUMERATE, DOOR_REVISIONS, } from './transport.js';
11
- import { batchPaths, deltaBody, deltaBytes, deltaDigest, historyOutcome, houseChanges, mutationOutcome, operationHead, rejectedMapping, revisionBody, uploadable, } from './write.js';
13
+ import { DOOR_BATCH, DOOR_BATCH_UPLOAD, DOOR_BOOTSTRAP, DOOR_DISCOVER, DOOR_ENUMERATE, 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 { houseReferrer, houseReferrerScope } from './referrer.js';
16
+ import { HOUSE_ROOM_TYPES } from './room-types.js';
12
17
  import { followsVersion, logPosition, presented, reachedVersion, roomVersion, } from './version.js';
13
18
  const UNCERTAIN = { kind: 'transport-uncertainty' };
14
19
  const UNCERTAIN_READ = { kind: 'transport-uncertainty' };
20
+ const EXPIRED_ADDRESS = 403;
15
21
  function lapsedGrant(outcome) {
16
22
  return outcome.kind === 'rejected' && outcome.code === 'upload_unavailable';
17
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
+ }
18
50
  export function createAgentsHouseAdapter(options) {
19
51
  if ('site' in options)
20
- return createSiteAdapter(options.site);
52
+ return createSiteAdapter(options.site, options.snapshot === true);
21
53
  const transport = options.transport;
22
54
  const notifications = houseNotifications(transport, 'credential');
23
55
  const tracked = new Map();
24
56
  const handed = new Map();
57
+ let heldRevisions;
25
58
  let bounds;
26
59
  function declared() {
27
60
  if (bounds === undefined)
28
61
  throw new Error('no Discovery declared the bounds');
29
62
  return bounds;
30
63
  }
31
- async function bootstrappedRoom(envelope, scope, entry, continued) {
32
- if (scope === undefined)
33
- return { kind: 'room-not-readable', locator: entry.locator };
34
- const next = envelope.next?.room === entry.authority ? envelope.next.path : null;
35
- if (continued === null && next === null)
36
- handed.set(entry.locator, scopeLinks(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));
37
98
  else
38
99
  handed.delete(entry.locator);
39
100
  notifications.reach(entry.authority, scope);
40
101
  const seen = [
41
- ...(continued?.seen ?? []),
102
+ ...continued.seen,
42
103
  ...entry.schemas.filter((schema) => scope.files.some((file) => file.path === schema)),
43
104
  ];
44
- const models = await partModels(scope, entry, new Set(seen), next === null);
45
- const continuation = next === null ? null : { after: next, seen };
105
+ const models = await partModels(scope, entry, new Set(seen), !more);
46
106
  return {
47
- ...bootstrapOf(scope),
107
+ ...(await stepped(bootstrapping(scope))),
48
108
  ...(models === undefined ? {} : { models }),
49
- ...(continuation === null ? {} : { pageToken: JSON.stringify(continuation) }),
109
+ ...(more ? { pageToken: JSON.stringify({ ...continued, next, seen }) } : {}),
50
110
  };
51
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
+ }
52
130
  async function enumeratedRoom(enumeration, ask) {
53
- const view = viewOf(enumeration, ask.entry);
54
- if (view === undefined)
131
+ const first = viewOf(enumeration, ask.entry);
132
+ if (first === undefined)
55
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;
56
137
  notifications.reach(ask.entry.authority, view);
57
- const answer = refreshFrom(view, ask.fromRoomVersion);
138
+ const answer = await refreshFrom(view, ask.fromRoomVersion);
58
139
  if (answer.kind !== 'net-advancement')
59
140
  return answer;
60
- handed.set(ask.entry.locator, enumeratedLinks(view, handed.get(ask.entry.locator)));
141
+ handed.set(ask.entry.locator, roomVersion(view));
61
142
  const models = await viewModels(view, ask.entry);
62
143
  return models === undefined ? answer : { ...answer, models };
63
144
  }
64
145
  async function streamed(entry, request, from) {
65
- const links = handed.get(entry.locator);
66
- if (links?.roomVersion !== request.fromRoomVersion)
146
+ if (handed.get(entry.locator) !== request.fromRoomVersion)
67
147
  return null;
68
- const advanced = notifications.advance(entry, from, (path, revision) => links.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));
69
153
  if (advanced === null)
70
154
  return null;
71
- const relinked = streamedLinks(links, advanced);
72
- if (relinked === null)
73
- return null;
74
- handed.set(entry.locator, relinked);
155
+ handed.set(entry.locator, advanced.page.roomVersion);
75
156
  const models = await streamedModels(advanced.schemas, entry, 'credential');
76
157
  return models === undefined ? advanced.page : { ...advanced.page, models };
77
158
  }
@@ -81,28 +162,31 @@ export function createAgentsHouseAdapter(options) {
81
162
  const at = logPosition(room.roomVersion);
82
163
  if (entry === undefined || at === null)
83
164
  return [];
84
- handed.set(room.locator, heldLinks(room));
165
+ handed.set(room.locator, room.roomVersion);
85
166
  notifications.reach(entry.authority, at);
86
167
  return [presented(entry.authority, at)];
87
168
  });
88
169
  notifications.send(kept);
89
170
  }
90
- 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) {
91
178
  if (outcome.kind !== 'accepted')
92
179
  return outcome;
93
- const { originals, ...accepted } = outcome;
94
- const links = handed.get(entry.locator);
95
- if (links === undefined)
96
- return accepted;
97
- const follows = followsVersion(links.roomVersion, accepted.roomVersion);
98
- const relinked = acceptedLinks(links, accepted, follows ? accepted.roomVersion : links.roomVersion, originals);
99
- handed.set(entry.locator, relinked.links);
100
- const at = logPosition(accepted.roomVersion);
180
+ const held = handed.get(entry.locator);
181
+ if (held === undefined)
182
+ return outcome;
183
+ const follows = followsVersion(held, outcome.roomVersion);
184
+ if (follows)
185
+ handed.set(entry.locator, outcome.roomVersion);
186
+ const at = logPosition(outcome.roomVersion);
101
187
  if (follows && at !== null)
102
188
  notifications.reach(entry.authority, at);
103
- return relinked.attachments === undefined
104
- ? accepted
105
- : { ...accepted, attachments: relinked.attachments };
189
+ return outcome;
106
190
  }
107
191
  function authorityFor(locator) {
108
192
  return tracked.get(locator);
@@ -121,99 +205,106 @@ export function createAgentsHouseAdapter(options) {
121
205
  return result;
122
206
  return { ...result, at: { locator, roomVersion: roomVersion({ position, logEpoch }) } };
123
207
  }
124
- function weight(entry) {
125
- 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;
126
219
  }
127
- async function bootstrapped(entries, answer) {
128
- 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 }));
129
222
  while (open.length > 0) {
130
223
  const unreached = [];
131
- 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));
132
226
  const answered = await transport.door(DOOR_BOOTSTRAP, {
133
- prefixes: part.flatMap(({ entry }) => contentPrefixes(entry)),
227
+ prefixes: part.map(({ entry }) => authorityRoot(entry.handle)),
228
+ ...(held.length === 0 ? {} : { held }),
134
229
  });
135
230
  const classified = part.map(({ entry }) => classifyDoor(answered.status, answered.body, entry.locator));
136
- const envelope = classified[0] === null ? parseEnvelope(answered.body) : null;
231
+ const parted = classified[0] === null ? parseParted(answered.body) : null;
137
232
  for (const [index, asked] of part.entries()) {
138
233
  const failure = classified[index];
139
234
  if (failure !== null && failure !== undefined) {
140
235
  answer(asked.ask, failure);
141
236
  continue;
142
237
  }
143
- if (envelope === null) {
238
+ if (parted === null) {
144
239
  answer(asked.ask, UNCERTAIN_READ);
145
240
  continue;
146
241
  }
147
- const scope = envelope.scopes.find((candidate) => matchesScope(candidate, asked.entry));
148
- 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) {
149
244
  unreached.push(asked);
150
245
  continue;
151
246
  }
152
- 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
+ }));
153
258
  }
154
259
  }
155
260
  open = unreached;
156
261
  }
157
262
  }
158
- async function continuedBootstrap(entry, continued) {
159
- const after = { room: entry.authority, path: continued.after };
160
- const answered = await transport.door(DOOR_BOOTSTRAP, {
161
- prefixes: contentPrefixes(entry),
162
- after,
163
- });
164
- const failure = classifyDoor(answered.status, answered.body, entry.locator);
165
- if (failure !== null)
166
- return failure;
167
- const envelope = parseEnvelope(answered.body);
168
- if (envelope === null)
169
- return UNCERTAIN_READ;
170
- const scope = envelope.scopes.find((candidate) => matchesScope(candidate, entry));
171
- return bootstrappedRoom(envelope, scope, entry, continued);
172
- }
173
263
  function layered(asks) {
174
264
  const depths = new Map();
175
265
  const layers = [];
176
266
  for (const ask of asks) {
177
- const depth = depths.get(ask.entry.authority) ?? 0;
178
- 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);
179
269
  (layers[depth] ??= []).push(ask);
180
270
  }
181
271
  return layers;
182
272
  }
183
273
  async function enumerated(asks) {
184
- const answers = new Map();
185
- 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));
186
276
  for (const part of parts) {
187
277
  let open = [...part];
188
278
  while (open.length > 0) {
189
279
  const answer = await transport.door(DOOR_ENUMERATE, {
190
- prefixes: open.flatMap((ask) => contentPrefixes(ask.entry)),
191
- 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)),
192
282
  });
193
- 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));
194
284
  if (classified[0]?.kind === 'beyond-horizon') {
195
285
  const named = doorRefusal(answer.body)?.fields.authority;
196
- const expired = open.filter((ask) => ask.entry.authority === named);
286
+ const expired = open.filter(({ ask }) => ask.entry.authority === named);
197
287
  const answered = expired.length === 0 ? UNCERTAIN_READ : { kind: 'beyond-horizon' };
198
288
  const gone = expired.length === 0 ? open : expired;
199
289
  for (const ask of gone)
200
- answers.set(ask, answered);
290
+ answers[ask.index] = answered;
201
291
  open = open.filter((ask) => !gone.includes(ask));
202
292
  continue;
203
293
  }
204
294
  const enumeration = classified[0] === null ? parseEnumeration(answer.body, 'credential') : null;
205
- for (const [index, ask] of open.entries()) {
206
- const failure = classified[index];
295
+ for (const [answerIndex, { ask, index }] of open.entries()) {
296
+ const failure = classified[answerIndex];
207
297
  if (failure !== null && failure !== undefined) {
208
- answers.set(ask, failure);
298
+ answers[index] = failure;
209
299
  continue;
210
300
  }
211
- answers.set(ask, enumeration === null ? UNCERTAIN_READ : await enumeratedRoom(enumeration, ask));
301
+ answers[index] =
302
+ enumeration === null ? UNCERTAIN_READ : await enumeratedRoom(enumeration, ask);
212
303
  }
213
304
  open = [];
214
305
  }
215
306
  }
216
- return asks.map((ask) => answers.get(ask) ?? UNCERTAIN_READ);
307
+ return answers;
217
308
  }
218
309
  const bootstrapTogether = gatheredEach(bootstrapped);
219
310
  const enumerateTogether = gathered(enumerated);
@@ -225,10 +316,12 @@ export function createAgentsHouseAdapter(options) {
225
316
  }
226
317
  const minted = await transport.door(DOOR_BATCH_UPLOAD, { ...head, paths, bytes, sha256 });
227
318
  if (minted.status !== 200) {
228
- 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
+ };
229
322
  }
230
323
  const grant = parseGrant(minted.body);
231
- if (grant === null || grant.method !== 'POST')
324
+ if (grant === null)
232
325
  return { outcome: UNCERTAIN };
233
326
  const delivered = await transport.transfer({
234
327
  act: 'authored_batch_upload',
@@ -238,13 +331,31 @@ export function createAgentsHouseAdapter(options) {
238
331
  if (delivered.status !== 200)
239
332
  return { outcome: UNCERTAIN };
240
333
  const holding = parseGrant(delivered.body);
241
- if (holding === null || holding.method !== 'POST')
334
+ if (holding === null)
242
335
  return { outcome: UNCERTAIN };
243
336
  const upload = { url: holding.url, operation: holding.operation, bytes, sha256 };
244
337
  await keep(upload);
245
338
  return { body: { ...head, paths, upload, runtime: notifications.runtime } };
246
339
  }
247
- async function submit(head, changes, delta, bytes, entry, handed, keep) {
340
+ async function staged(answer, { changeBatch: batch, file }) {
341
+ const refusal = doorRefusal(answer.body);
342
+ if (refusal?.code !== 'files_unstaged')
343
+ return null;
344
+ const uploads = refusal.fields.upload;
345
+ if (!Array.isArray(uploads))
346
+ return false;
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)
351
+ return false;
352
+ const finished = await finishedUpload(transport, upload, await file(change.localId));
353
+ if (finished?.body?.staged !== true)
354
+ return false;
355
+ }
356
+ return true;
357
+ }
358
+ async function submit(request, head, changes, delta, bytes, entry, handed, keep, into) {
248
359
  const inline = { ...head, changes, runtime: notifications.runtime };
249
360
  const prepared = uploadable(inline, bytes, declared())
250
361
  ? await uploaded(head, changes, delta, bytes, entry, handed, keep)
@@ -252,14 +363,80 @@ export function createAgentsHouseAdapter(options) {
252
363
  if ('outcome' in prepared)
253
364
  return prepared.outcome;
254
365
  const answer = await transport.door(DOOR_BATCH, prepared.body);
255
- return mutationOutcome(answer.status, answer.body, entry.locator, entry.handle);
366
+ const stagedFiles = await staged(answer, request);
367
+ if (stagedFiles === false)
368
+ return UNCERTAIN;
369
+ const settled = stagedFiles === null ? answer : await transport.door(DOOR_BATCH, prepared.body);
370
+ if (stagedFiles === true && (await staged(settled, request)) !== null)
371
+ return UNCERTAIN;
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
+ }));
378
+ }
379
+ function addressed(answer, locator) {
380
+ const failure = classifyDoor(answer.status, answer.body, locator);
381
+ if (failure !== null) {
382
+ return failure.kind === 'scoped-refusal'
383
+ ? {
384
+ kind: 'rejected',
385
+ code: failure.code,
386
+ fields: failure.fields,
387
+ retryable: failure.retryable,
388
+ }
389
+ : failure;
390
+ }
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)
412
+ return UNCERTAIN_READ;
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
+ });
256
429
  }
257
430
  return {
431
+ roomTypes: HOUSE_ROOM_TYPES,
258
432
  async authorize() {
259
433
  return options.authorize();
260
434
  },
261
- async discover(_authorization) {
262
- 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
+ });
263
440
  const failed = discoveryFailure(answer.status, answer.body);
264
441
  if (failed !== null)
265
442
  return failed;
@@ -268,26 +445,42 @@ export function createAgentsHouseAdapter(options) {
268
445
  return { kind: 'transport-uncertainty' };
269
446
  bounds = document.bounds;
270
447
  const entries = trackedFrom(document);
271
- 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);
272
463
  for (const entry of entries)
273
464
  tracked.set(entry.locator, entry);
274
465
  for (const locator of handed.keys())
275
466
  if (!tracked.has(locator))
276
467
  handed.delete(locator);
277
- notifications.track(entries);
468
+ notifications.track([...tracked.values()]);
278
469
  return {
279
- ...mapDiscovery(document, document.authorities.map((authority) => discoveredRoom(authority)), capabilities(document)),
470
+ ...mapDiscovery(document, document.authorities.map((authority) => discoveredRoom(authority)), capabilities(document.actions, [...tracked.values()])),
280
471
  member: document.member,
472
+ ...(covered === null ? {} : { covered }),
281
473
  };
282
474
  },
283
475
  async bootstrap(_authorization, request) {
284
476
  const entry = authorityFor(request.locator);
285
477
  if (entry === undefined)
286
478
  return { kind: 'room-not-readable', locator: request.locator };
479
+ const asked = { entry, held: request.held };
287
480
  if (request.pageToken !== undefined) {
288
- return continuedBootstrap(entry, JSON.parse(request.pageToken));
481
+ return partPage(asked, JSON.parse(request.pageToken));
289
482
  }
290
- return bootstrapTogether(entry);
483
+ return bootstrapTogether(asked);
291
484
  },
292
485
  async refresh(_authorization, request) {
293
486
  const entry = authorityFor(request.locator);
@@ -307,17 +500,27 @@ export function createAgentsHouseAdapter(options) {
307
500
  const entry = authorityFor(request.locator);
308
501
  if (entry === undefined)
309
502
  return { kind: 'room-not-readable', locator: request.locator };
310
- 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);
311
512
  if (mapped.kind === 'unmapped')
312
513
  return rejectedMapping(mapped);
313
- const head = operationHead(request.changeBatch);
514
+ const head = operationHead(batch);
314
515
  const delta = deltaBody(mapped.changes);
315
516
  const bytes = deltaBytes(delta);
316
517
  const handed = request.upload;
317
- const outcome = await submit(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);
318
521
  if (handed === undefined || !lapsedGrant(outcome))
319
- return adopted(entry, outcome);
320
- return adopted(entry, await submit(head, mapped.changes, delta, bytes, entry, undefined, keep));
522
+ return settle(outcome);
523
+ return settle(await answer(undefined));
321
524
  },
322
525
  async history(_authorization, request) {
323
526
  const entry = authorityFor(request.locator);
@@ -334,6 +537,28 @@ export function createAgentsHouseAdapter(options) {
334
537
  ...(request.revision === undefined ? {} : { revision: request.revision }),
335
538
  });
336
539
  },
540
+ async binary(_authorization, request) {
541
+ if (request.kind === 'path') {
542
+ const ask = pathAddress(request);
543
+ if (ask === null)
544
+ return { kind: 'room-not-readable', locator: request.locator };
545
+ return downloaded(ask, request.locator);
546
+ }
547
+ const referrer = houseReferrer(request.referrer, tracked);
548
+ if (referrer === null) {
549
+ return { kind: 'rejected', code: 'referrer_unknown', fields: {}, retryable: false };
550
+ }
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);
558
+ },
559
+ referrerScope(referrer) {
560
+ return houseReferrerScope(referrer, tracked);
561
+ },
337
562
  reached(locator, version, target) {
338
563
  return reachedVersion(version, target, authorityFor(locator)?.logEpoch);
339
564
  },
@@ -343,7 +568,19 @@ export function createAgentsHouseAdapter(options) {
343
568
  async action(_authorization, request) {
344
569
  return withAt(await houseAction(transport, request));
345
570
  },
346
- 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
+ },
347
584
  conversations: notifications.conversations,
348
585
  serviceMode: notifications.serviceMode,
349
586
  };