@agentshouse/mdruntime-adapter 0.1.0-cb6436097e8a2 → 0.1.0-cb8fc491b1a92

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