@agentshouse/mdruntime-adapter 0.1.0-cc13f8244b707 → 0.1.0-cc1bdabaf4e28

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