@agentshouse/mdruntime-adapter 0.1.0-cd59fd8945dcb → 0.1.0-cd8cfa169a55d

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