@agentshouse/mdruntime-adapter 0.1.0-ca3a44982cbe7 → 0.1.0-ca632988146a3

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