@agentshouse/mdruntime-adapter 0.1.0-cc63dd043d0e2 → 0.1.0-cc710a8825617

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 (51) hide show
  1. package/AGENTS.md +270 -183
  2. package/README.md +3 -3
  3. package/dist/actions.d.ts +63 -2
  4. package/dist/actions.d.ts.map +1 -1
  5. package/dist/actions.js +154 -138
  6. package/dist/adapter.d.ts +1 -0
  7. package/dist/adapter.d.ts.map +1 -1
  8. package/dist/adapter.js +325 -166
  9. package/dist/discovery.d.ts +6 -12
  10. package/dist/discovery.d.ts.map +1 -1
  11. package/dist/discovery.js +32 -36
  12. package/dist/gather.d.ts +1 -0
  13. package/dist/gather.d.ts.map +1 -1
  14. package/dist/gather.js +11 -6
  15. package/dist/handed.d.ts +15 -0
  16. package/dist/handed.d.ts.map +1 -0
  17. package/dist/handed.js +38 -0
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/model.d.ts +7 -18
  22. package/dist/model.d.ts.map +1 -1
  23. package/dist/model.js +60 -39
  24. package/dist/read.d.ts +35 -7
  25. package/dist/read.d.ts.map +1 -1
  26. package/dist/read.js +164 -86
  27. package/dist/referrer.d.ts +7 -0
  28. package/dist/referrer.d.ts.map +1 -0
  29. package/dist/referrer.js +35 -0
  30. package/dist/refusal.d.ts +5 -9
  31. package/dist/refusal.d.ts.map +1 -1
  32. package/dist/refusal.js +31 -17
  33. package/dist/site.d.ts +1 -1
  34. package/dist/site.d.ts.map +1 -1
  35. package/dist/site.js +191 -107
  36. package/dist/stream.d.ts +10 -4
  37. package/dist/stream.d.ts.map +1 -1
  38. package/dist/stream.js +391 -47
  39. package/dist/transport.d.ts +79 -11
  40. package/dist/transport.d.ts.map +1 -1
  41. package/dist/transport.js +3 -1
  42. package/dist/version.d.ts +2 -0
  43. package/dist/version.d.ts.map +1 -1
  44. package/dist/version.js +10 -1
  45. package/dist/write.d.ts +2 -1
  46. package/dist/write.d.ts.map +1 -1
  47. package/dist/write.js +96 -14
  48. package/package.json +3 -4
  49. package/dist/links.d.ts +0 -12
  50. package/dist/links.d.ts.map +0 -1
  51. package/dist/links.js +0 -66
package/dist/adapter.js CHANGED
@@ -1,107 +1,171 @@
1
- import { DISCOVER_PREFIXES, capabilities, discoveredRoom, locatorOf, mapDiscovery, parseDiscovery, trackedFrom, } from './discovery.js';
2
- import { MODEL_ABSENT, failedModel, heldModel, modelKey, modelShelf, } from './model.js';
3
- import { gathered, selections } from './gather.js';
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';
4
4
  import { createSiteAdapter } from './site.js';
5
- import { bootstrapOf, contentPrefixes, matchesScope, parseEnumeration, parseEnvelope, parseGrant, refreshFrom, schemaFrom, schemaPath, viewOf, } from './read.js';
6
- import { enumeratedLinks, scopeLinks, streamedLinks } from './links.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';
7
8
  import { houseAction } from './actions.js';
8
- import { classifyDoor, discoveryFailure, doorRefusal } from './refusal.js';
9
+ import { classifyDoor, classifyRead, discoveryFailure, doorRefusal } from './refusal.js';
9
10
  import { houseNotifications } from './stream.js';
10
- import { DOOR_BATCH, DOOR_BATCH_UPLOAD, DOOR_BOOTSTRAP, DOOR_DISCOVER, DOOR_ENUMERATE, DOOR_REVISIONS, PRIVATE_LOCATOR, } from './transport.js';
11
+ import { DOOR_BATCH, DOOR_BATCH_UPLOAD, DOOR_BOOTSTRAP, DOOR_DISCOVER, DOOR_ENUMERATE, DOOR_FILES_GET, DOOR_REVISIONS, } from './transport.js';
11
12
  import { batchPaths, deltaBody, deltaBytes, deltaDigest, historyOutcome, houseChanges, mutationOutcome, operationHead, rejectedMapping, revisionBody, uploadable, } from './write.js';
12
- import { logPosition, presented, reachedVersion, roomVersion, } from './version.js';
13
+ import { houseReferrer } from './referrer.js';
14
+ import { followsVersion, logPosition, presented, reachedVersion, roomVersion, } from './version.js';
13
15
  const UNCERTAIN = { kind: 'transport-uncertainty' };
14
16
  const UNCERTAIN_READ = { kind: 'transport-uncertainty' };
15
17
  function lapsedGrant(outcome) {
16
18
  return outcome.kind === 'rejected' && outcome.code === 'upload_unavailable';
17
19
  }
18
- function modelFailure(classified, envelope) {
19
- if (classified === null) {
20
- return envelope === null ? { kind: 'transport-uncertainty' } : MODEL_ABSENT;
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 });
21
44
  }
22
- return failedModel(classified);
45
+ return { ...scope, contents };
23
46
  }
24
47
  export function createAgentsHouseAdapter(options) {
25
48
  if ('site' in options)
26
- return createSiteAdapter(options.site);
49
+ return createSiteAdapter(options.site, options.snapshot === true);
27
50
  const transport = options.transport;
28
51
  const notifications = houseNotifications(transport, 'credential');
29
52
  const tracked = new Map();
30
53
  const handed = new Map();
31
- const models = modelShelf();
32
54
  let bounds;
33
55
  function declared() {
34
56
  if (bounds === undefined)
35
57
  throw new Error('no Discovery declared the bounds');
36
58
  return bounds;
37
59
  }
38
- function bootstrappedRoom(envelope, entry) {
39
- const scope = envelope.scopes.find((candidate) => matchesScope(candidate, entry));
40
- if (scope === undefined)
60
+ async function bootstrappedRoom(envelope, sent, asked, continued) {
61
+ const { entry } = asked;
62
+ if (sent === undefined)
41
63
  return { kind: 'room-not-readable', locator: entry.locator };
42
- handed.set(entry.locator, scopeLinks(scope));
43
- notifications.hold(entry.authority, scope);
44
- return bootstrapOf(scope);
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 }) };
87
+ return {
88
+ ...bootstrapOf(scope),
89
+ ...(models === undefined ? {} : { models }),
90
+ ...(continuation === null ? {} : { pageToken: JSON.stringify(continuation) }),
91
+ };
92
+ }
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 });
45
110
  }
46
- function enumeratedRoom(enumeration, ask) {
47
- const view = viewOf(enumeration, ask.entry);
48
- if (view === undefined)
111
+ async function enumeratedRoom(enumeration, ask) {
112
+ const first = viewOf(enumeration, ask.entry);
113
+ if (first === undefined)
49
114
  return { kind: 'room-not-readable', locator: ask.entry.locator };
50
- notifications.hold(ask.entry.authority, view);
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);
51
119
  const answer = refreshFrom(view, ask.fromRoomVersion);
52
- if (answer.kind === 'net-advancement') {
53
- handed.set(ask.entry.locator, enumeratedLinks(view, handed.get(ask.entry.locator)));
54
- }
55
- return answer;
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 };
56
125
  }
57
- function streamed(entry, request, from) {
58
- const links = handed.get(entry.locator);
59
- if (links?.roomVersion !== request.fromRoomVersion)
126
+ async function streamed(entry, request, from) {
127
+ const held = handed.get(entry.locator);
128
+ if (held?.roomVersion !== request.fromRoomVersion)
60
129
  return null;
61
- const advanced = notifications.advance(entry, from, (path, revision) => links.revisions.get(path) === revision);
130
+ const advanced = notifications.advance(entry, from, (path, revision) => held.revisions.get(path) === revision);
62
131
  if (advanced === null)
63
132
  return null;
64
- if (request.minPosition !== undefined &&
65
- !reachedVersion(advanced.page.roomVersion, request.minPosition, entry.logEpoch))
66
- return null;
67
- const relinked = streamedLinks(links, advanced);
68
- if (relinked === null)
69
- return null;
70
- handed.set(entry.locator, relinked);
71
- return advanced.page;
72
- }
73
- function held(envelope, entry, schema, failure) {
74
- return heldModel(envelope === null ? null : schemaFrom(envelope, entry, schema), schemaPath(entry.handle, schema), failure);
75
- }
76
- function modelPrefixes(entry) {
77
- return entry.schemas.map((schema) => schemaPath(entry.handle, schema));
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 };
78
136
  }
79
- async function acquireModels(entries) {
80
- const acquired = new Map();
81
- for (const part of selections(entries, (entry) => modelPrefixes(entry).length, declared().max_prefixes)) {
82
- const answer = await transport.door(DOOR_BOOTSTRAP, {
83
- prefixes: part.flatMap((entry) => modelPrefixes(entry)),
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,
84
155
  });
85
- const classified = classifyDoor(answer.status, answer.body, PRIVATE_LOCATOR);
86
- const envelope = classified === null ? parseEnvelope(await bundled(answer.body)) : null;
87
- const failure = modelFailure(classified, envelope);
88
- for (const entry of part) {
89
- acquired.set(modelKey(entry.locator, 'collection'), await held(envelope, entry, entry.schema, failure));
90
- if (entry.houseModel === null)
91
- continue;
92
- acquired.set(modelKey(entry.locator, 'typed-selection'), await held(envelope, entry, entry.houseModel.root, failure));
93
- }
94
- }
95
- return acquired;
96
156
  }
97
- function roomOf(authority, acquired) {
98
- const locator = locatorOf(authority);
99
- const key = modelKey(locator, 'collection');
100
- const collection = models.retain(key, acquired.get(key));
101
- const selection = authority.houseModel === null
102
- ? undefined
103
- : models.retain(modelKey(locator, 'typed-selection'), acquired.get(modelKey(locator, 'typed-selection')));
104
- return discoveredRoom(authority, collection?.identity ?? null, selection?.identity ?? null);
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;
105
169
  }
106
170
  function authorityFor(locator) {
107
171
  return tracked.get(locator);
@@ -115,116 +179,126 @@ export function createAgentsHouseAdapter(options) {
115
179
  const { authority, position, log_epoch: logEpoch } = body;
116
180
  if (typeof position !== 'string' || typeof logEpoch !== 'string')
117
181
  return result;
118
- const locator = authority === undefined
119
- ? PRIVATE_LOCATOR
120
- : [...tracked.values()].find((entry) => entry.authority === authority)?.locator;
182
+ const locator = [...tracked.values()].find((entry) => entry.authority === authority)?.locator;
121
183
  if (locator === undefined)
122
184
  return result;
123
185
  return { ...result, at: { locator, roomVersion: roomVersion({ position, logEpoch }) } };
124
186
  }
125
- async function bundled(body) {
126
- const grant = parseGrant(body);
127
- if (grant === null)
128
- return body;
129
- if (grant.method !== 'GET')
130
- return null;
131
- const answer = await transport.transfer({
132
- act: 'local_copy_bundle',
133
- method: 'GET',
134
- url: grant.url,
135
- answer: 'json',
136
- });
137
- return answer.status === 200 ? answer.body : null;
138
- }
139
187
  function weight(entry) {
140
188
  return contentPrefixes(entry).length;
141
189
  }
142
- async function bootstrapped(entries) {
143
- const answers = [];
144
- for (const part of selections(entries, weight, declared().max_prefixes)) {
145
- const answer = await transport.door(DOOR_BOOTSTRAP, {
146
- prefixes: part.flatMap((entry) => contentPrefixes(entry)),
147
- });
148
- const classified = part.map((entry) => classifyDoor(answer.status, answer.body, entry.locator));
149
- const envelope = classified[0] === null ? parseEnvelope(await bundled(answer.body)) : null;
150
- part.forEach((entry, index) => {
151
- const failure = classified[index];
152
- if (failure === null || failure === undefined) {
153
- answers.push(envelope === null ? UNCERTAIN_READ : bootstrappedRoom(envelope, entry));
154
- return;
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));
155
222
  }
156
- answers.push(failure.kind === 'beyond-horizon' ? UNCERTAIN_READ : failure);
157
- });
223
+ }
224
+ open = unreached;
158
225
  }
159
- return answers;
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);
160
248
  }
161
249
  function layered(asks) {
162
250
  const depths = new Map();
163
251
  const layers = [];
164
252
  for (const ask of asks) {
165
- const depth = depths.get(ask.entry.authority) ?? 0;
166
- depths.set(ask.entry.authority, depth + 1);
253
+ const depth = depths.get(ask.ask.entry.authority) ?? 0;
254
+ depths.set(ask.ask.entry.authority, depth + 1);
167
255
  (layers[depth] ??= []).push(ask);
168
256
  }
169
257
  return layers;
170
258
  }
171
259
  async function enumerated(asks) {
172
- const answers = new Map();
173
- const parts = layered(asks).flatMap((layer) => selections(layer, (ask) => weight(ask.entry), declared().max_prefixes));
260
+ const answers = [];
261
+ const parts = layered(asks.map((ask, index) => ({ ask, index }))).flatMap((layer) => selections(layer, ({ ask }) => weight(ask.entry), declared().max_prefixes));
174
262
  for (const part of parts) {
175
263
  let open = [...part];
176
264
  while (open.length > 0) {
177
265
  const answer = await transport.door(DOOR_ENUMERATE, {
178
- prefixes: open.flatMap((ask) => contentPrefixes(ask.entry)),
179
- positions: open.map((ask) => ({
180
- ...presented(ask.entry.authority, ask.from),
181
- ...(ask.minPosition === undefined ? {} : { min_position: ask.minPosition }),
182
- })),
266
+ prefixes: open.flatMap(({ ask }) => contentPrefixes(ask.entry)),
267
+ positions: open.map(({ ask }) => presented(ask.entry.authority, ask.from)),
183
268
  });
184
- const refusal = doorRefusal(answer.body);
185
- if (refusal?.code === 'replica_behind') {
186
- const named = open.filter((ask) => ask.entry.authority === refusal.fields.authority);
187
- const behind = named.length === 0 ? open : named;
188
- for (const ask of behind) {
189
- answers.set(ask, refusal.retryable
190
- ? { kind: 'retryable-refusal', code: refusal.code, fields: refusal.fields }
191
- : (classifyDoor(answer.status, answer.body, ask.entry.locator) ?? UNCERTAIN_READ));
192
- }
193
- open = open.filter((ask) => !behind.includes(ask));
194
- continue;
195
- }
196
- const classified = open.map((ask) => classifyDoor(answer.status, answer.body, ask.entry.locator));
269
+ const classified = open.map(({ ask }) => classifyRead(answer.status, answer.body, ask.entry.locator));
197
270
  if (classified[0]?.kind === 'beyond-horizon') {
198
271
  const named = doorRefusal(answer.body)?.fields.authority;
199
- const expired = open.filter((ask) => ask.entry.authority === named);
272
+ const expired = open.filter(({ ask }) => ask.entry.authority === named);
200
273
  const answered = expired.length === 0 ? UNCERTAIN_READ : { kind: 'beyond-horizon' };
201
274
  const gone = expired.length === 0 ? open : expired;
202
275
  for (const ask of gone)
203
- answers.set(ask, answered);
276
+ answers[ask.index] = answered;
204
277
  open = open.filter((ask) => !gone.includes(ask));
205
278
  continue;
206
279
  }
207
280
  const enumeration = classified[0] === null ? parseEnumeration(answer.body, 'credential') : null;
208
- open.forEach((ask, index) => {
209
- const failure = classified[index];
281
+ for (const [answerIndex, { ask, index }] of open.entries()) {
282
+ const failure = classified[answerIndex];
210
283
  if (failure !== null && failure !== undefined) {
211
- answers.set(ask, failure);
212
- return;
284
+ answers[index] = failure;
285
+ continue;
213
286
  }
214
- answers.set(ask, enumeration === null ? UNCERTAIN_READ : enumeratedRoom(enumeration, ask));
215
- });
287
+ answers[index] =
288
+ enumeration === null ? UNCERTAIN_READ : await enumeratedRoom(enumeration, ask);
289
+ }
216
290
  open = [];
217
291
  }
218
292
  }
219
- return asks.map((ask) => answers.get(ask) ?? UNCERTAIN_READ);
293
+ return answers;
220
294
  }
221
- const bootstrapTogether = gathered(bootstrapped);
295
+ const bootstrapTogether = gatheredEach(bootstrapped);
222
296
  const enumerateTogether = gathered(enumerated);
223
297
  async function uploaded(head, changes, delta, bytes, entry, handed, keep) {
224
298
  const paths = batchPaths(changes);
225
299
  const sha256 = await deltaDigest(delta);
226
300
  if (handed !== undefined && handed.sha256 === sha256) {
227
- return { body: { ...head, paths, upload: handed } };
301
+ return { body: { ...head, paths, upload: handed, runtime: notifications.runtime } };
228
302
  }
229
303
  const minted = await transport.door(DOOR_BATCH_UPLOAD, { ...head, paths, bytes, sha256 });
230
304
  if (minted.status !== 200) {
@@ -235,9 +309,7 @@ export function createAgentsHouseAdapter(options) {
235
309
  return { outcome: UNCERTAIN };
236
310
  const delivered = await transport.transfer({
237
311
  act: 'authored_batch_upload',
238
- method: 'POST',
239
312
  url: grant.url,
240
- answer: 'json',
241
313
  body: `${grant.operation}\n${delta}`,
242
314
  });
243
315
  if (delivered.status !== 200)
@@ -247,30 +319,76 @@ export function createAgentsHouseAdapter(options) {
247
319
  return { outcome: UNCERTAIN };
248
320
  const upload = { url: holding.url, operation: holding.operation, bytes, sha256 };
249
321
  await keep(upload);
250
- return { body: { ...head, paths, upload } };
322
+ return { body: { ...head, paths, upload, runtime: notifications.runtime } };
323
+ }
324
+ async function staged(answer, batch) {
325
+ const refusal = doorRefusal(answer.body);
326
+ if (refusal?.code !== 'files_unstaged')
327
+ return null;
328
+ const grants = refusal.fields.upload;
329
+ if (!Array.isArray(grants))
330
+ 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)
335
+ 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)
343
+ return false;
344
+ }
345
+ return true;
251
346
  }
252
- async function submit(head, changes, delta, bytes, entry, handed, keep) {
253
- const inline = { ...head, changes };
347
+ async function submit(batch, head, changes, delta, bytes, entry, handed, keep) {
348
+ const inline = { ...head, changes, runtime: notifications.runtime };
254
349
  const prepared = uploadable(inline, bytes, declared())
255
350
  ? await uploaded(head, changes, delta, bytes, entry, handed, keep)
256
351
  : { body: inline };
257
352
  if ('outcome' in prepared)
258
353
  return prepared.outcome;
259
354
  const answer = await transport.door(DOOR_BATCH, prepared.body);
260
- return mutationOutcome(answer.status, answer.body, entry.locator, entry.handle);
355
+ const stagedFiles = await staged(answer, batch);
356
+ if (stagedFiles === false)
357
+ return UNCERTAIN;
358
+ const settled = stagedFiles === null ? answer : await transport.door(DOOR_BATCH, prepared.body);
359
+ if (stagedFiles === true && (await staged(settled, batch)) !== null)
360
+ return UNCERTAIN;
361
+ return mutationOutcome(settled.status, settled.body, entry.locator, entry.handle);
362
+ }
363
+ async function downloaded(answer, locator) {
364
+ const failure = classifyDoor(answer.status, answer.body, locator);
365
+ if (failure !== null) {
366
+ return failure.kind === 'scoped-refusal'
367
+ ? {
368
+ kind: 'rejected',
369
+ code: failure.code,
370
+ fields: failure.fields,
371
+ retryable: failure.retryable,
372
+ }
373
+ : failure;
374
+ }
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)
380
+ return UNCERTAIN_READ;
381
+ return { kind: 'binary', blob: read.blob };
261
382
  }
262
383
  return {
263
384
  async authorize() {
264
385
  return options.authorize();
265
386
  },
266
- async model(_authorization, request) {
267
- return models.answer(request);
268
- },
269
387
  async discover(_authorization) {
270
388
  const answer = await transport.door(DOOR_DISCOVER, { prefixes: [...DISCOVER_PREFIXES] });
271
- const classified = classifyDoor(answer.status, answer.body, PRIVATE_LOCATOR);
272
- if (classified !== null)
273
- return discoveryFailure(classified);
389
+ const failed = discoveryFailure(answer.status, answer.body);
390
+ if (failed !== null)
391
+ return failed;
274
392
  const document = parseDiscovery(answer.body);
275
393
  if (document === null)
276
394
  return { kind: 'transport-uncertainty' };
@@ -283,17 +401,20 @@ export function createAgentsHouseAdapter(options) {
283
401
  if (!tracked.has(locator))
284
402
  handed.delete(locator);
285
403
  notifications.track(entries);
286
- const acquired = await acquireModels(entries);
287
- models.keep(entries.map((entry) => entry.locator));
288
- return mapDiscovery(document, document.authorities.map((authority) => roomOf(authority, acquired)), capabilities(document.actions));
404
+ return {
405
+ ...mapDiscovery(document, document.authorities.map((authority) => discoveredRoom(authority)), capabilities(document)),
406
+ member: document.member,
407
+ };
289
408
  },
290
409
  async bootstrap(_authorization, request) {
291
410
  const entry = authorityFor(request.locator);
292
411
  if (entry === undefined)
293
412
  return { kind: 'room-not-readable', locator: request.locator };
294
- if (request.pageToken !== undefined)
295
- return { kind: 'transport-uncertainty' };
296
- return bootstrapTogether(entry);
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);
297
418
  },
298
419
  async refresh(_authorization, request) {
299
420
  const entry = authorityFor(request.locator);
@@ -304,16 +425,10 @@ export function createAgentsHouseAdapter(options) {
304
425
  const from = logPosition(request.fromRoomVersion);
305
426
  if (from === null)
306
427
  return { kind: 'beyond-horizon' };
307
- const answered = streamed(entry, request, from);
428
+ const answered = await streamed(entry, request, from);
308
429
  if (answered !== null)
309
430
  return answered;
310
- const minPosition = request.minPosition === undefined ? null : logPosition(request.minPosition);
311
- return enumerateTogether({
312
- entry,
313
- from,
314
- fromRoomVersion: request.fromRoomVersion,
315
- ...(minPosition?.logEpoch === from.logEpoch ? { minPosition: minPosition.position } : {}),
316
- });
431
+ return enumerateTogether({ entry, from, fromRoomVersion: request.fromRoomVersion });
317
432
  },
318
433
  async mutate(_authorization, request, keep) {
319
434
  const entry = authorityFor(request.locator);
@@ -326,10 +441,11 @@ export function createAgentsHouseAdapter(options) {
326
441
  const delta = deltaBody(mapped.changes);
327
442
  const bytes = deltaBytes(delta);
328
443
  const handed = request.upload;
329
- const outcome = await submit(head, mapped.changes, delta, bytes, entry, handed, keep);
444
+ const batch = request.changeBatch;
445
+ const outcome = await submit(batch, head, mapped.changes, delta, bytes, entry, handed, keep);
330
446
  if (handed === undefined || !lapsedGrant(outcome))
331
- return outcome;
332
- return submit(head, mapped.changes, delta, bytes, entry, undefined, keep);
447
+ return adopted(entry, outcome);
448
+ return adopted(entry, await submit(batch, head, mapped.changes, delta, bytes, entry, undefined, keep));
333
449
  },
334
450
  async history(_authorization, request) {
335
451
  const entry = authorityFor(request.locator);
@@ -346,13 +462,56 @@ export function createAgentsHouseAdapter(options) {
346
462
  ...(request.revision === undefined ? {} : { revision: request.revision }),
347
463
  });
348
464
  },
465
+ 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);
486
+ },
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 }) };
495
+ },
349
496
  reached(locator, version, target) {
350
497
  return reachedVersion(version, target, authorityFor(locator)?.logEpoch);
351
498
  },
499
+ follows(_locator, version, next) {
500
+ return followsVersion(version, next);
501
+ },
352
502
  async action(_authorization, request) {
353
503
  return withAt(await houseAction(transport, request));
354
504
  },
355
- roomVersions: notifications,
505
+ roomStream: {
506
+ hold: held,
507
+ reopen: notifications.reopen,
508
+ observe: notifications.observe,
509
+ published(locator, roomVersion) {
510
+ const at = logPosition(roomVersion);
511
+ if (at !== null)
512
+ notifications.published(locator, at);
513
+ },
514
+ },
356
515
  conversations: notifications.conversations,
357
516
  serviceMode: notifications.serviceMode,
358
517
  };