@agentshouse/mdruntime-adapter 0.1.0-ca764edcceec6 → 0.1.0-ca7fab027bb19

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