@agentshouse/mdruntime-adapter 0.1.0-cf9ab9b7cbbca → 0.1.0-cfd2dc1938a2c

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