@agentshouse/mdruntime-adapter 0.1.0-ccd2ccb1f7214 → 0.1.0-ccdfb41af2abb

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