@agentshouse/mdruntime-adapter 0.1.0-c96ce90a9df53 → 0.1.0-ca218e62303fd

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.
package/dist/site.js CHANGED
@@ -1,7 +1,8 @@
1
- import { actionOutcome } from './actions.js';
1
+ import { actionOutcome, SITE_SUBMIT } from './actions.js';
2
2
  import { mapDiscovery, parseSiteDiscovery, siteCapabilities, siteTracked, } from './discovery.js';
3
- import { scopeModels, streamedModels, viewModels } from './model.js';
4
- import { advancedScope, bootstrapOf, parseEnumeration, parseSiteBootstrap, refreshFrom, viewOf, } from './read.js';
3
+ import { partModels, streamedModels, viewModels } from './model.js';
4
+ import { advancedScope, bootstrapOf, parseEnumeration, parseSiteBootstrap, refreshFrom, siteBootstrapNext, siteBootstrapOffset, viewOf, wholeView, } from './read.js';
5
+ import { sha256Hex } from './digest.js';
5
6
  import { classifyDoor, classifyRead, discoveryFailure } from './refusal.js';
6
7
  import { houseNotifications } from './stream.js';
7
8
  import { SITE_DOOR_BOOTSTRAP, SITE_DOOR_DISCOVER, SITE_DOOR_ENUMERATE, SITE_DOOR_SUBMIT, SITE_FILE_RECORD, SITE_FILES, } from './transport.js';
@@ -29,32 +30,90 @@ export function createSiteAdapter(transport) {
29
30
  const named = houseReferrer(referrer, tracked);
30
31
  return named === null || !tracked.has(named.locator) ? null : named;
31
32
  }
32
- async function enumeration(entry, from) {
33
+ async function enumerated(entry, from, page) {
33
34
  const answer = await transport.door(SITE_DOOR_ENUMERATE, {
34
- positions: [presented(entry.authority, from)],
35
+ positions: [
36
+ page === undefined
37
+ ? presented(entry.authority, from)
38
+ : {
39
+ ...presented(entry.authority, { position: page.after, logEpoch: from.logEpoch }),
40
+ page: { through: page.through, read: page.read },
41
+ },
42
+ ],
35
43
  });
36
44
  const classified = classifyRead(answer.status, answer.body, entry.locator);
37
45
  if (classified !== null)
38
46
  return classified;
39
- return parseEnumeration(answer.body, 'site') ?? UNCERTAIN;
47
+ const found = parseEnumeration(answer.body, 'site');
48
+ if (found === null)
49
+ return UNCERTAIN;
50
+ return viewOf(found, entry) ?? { kind: 'room-not-readable', locator: entry.locator };
51
+ }
52
+ async function enumeration(entry, from) {
53
+ const first = await enumerated(entry, from);
54
+ return 'kind' in first ? first : wholeView(first, (page) => enumerated(entry, from, page));
40
55
  }
41
- async function caughtUp(entry) {
42
- const answer = await transport.read(`${SITE_DOOR_BOOTSTRAP}${entry.authority}`);
56
+ async function bootstrapPart(entry, after, offset) {
57
+ const query = [
58
+ ...(after === null ? [] : [`after=${encodeURIComponent(after)}`]),
59
+ ...(offset === null ? [] : [`offset=${offset}`]),
60
+ ];
61
+ const answer = await transport.read(`${SITE_DOOR_BOOTSTRAP}${entry.authority}${query.length === 0 ? '' : `?${query.join('&')}`}`);
43
62
  const classified = classifyDoor(answer.status, answer.body, entry.locator);
44
63
  if (classified !== null)
45
64
  return { failure: classified };
46
65
  const scope = parseSiteBootstrap(answer.body);
47
- if (scope === null)
48
- return { failure: UNCERTAIN };
49
- if (roomVersion(scope) === roomVersion(entry))
50
- return { scope };
51
- const found = await enumeration(entry, scope);
52
- if ('kind' in found)
53
- return { failure: settled(found) };
54
- const view = viewOf(found, entry);
55
- if (view === undefined)
56
- return { failure: { kind: 'room-not-readable', locator: entry.locator } };
57
- return { scope: advancedScope(scope, view) };
66
+ return scope === null ? { failure: UNCERTAIN } : { body: answer.body, scope };
67
+ }
68
+ async function assembled(entry, scope, path, offset) {
69
+ const opened = scope.contents.at(-1);
70
+ if (opened === undefined || opened.path !== path || opened.content.length !== offset) {
71
+ return UNCERTAIN;
72
+ }
73
+ let text = opened.content;
74
+ for (;;) {
75
+ const part = await bootstrapPart(entry, path, text.length);
76
+ if ('failure' in part)
77
+ return part.failure;
78
+ const [piece] = part.scope.contents;
79
+ if (piece === undefined ||
80
+ part.scope.contents.length !== 1 ||
81
+ piece.path !== path ||
82
+ piece.content.length === 0) {
83
+ return UNCERTAIN;
84
+ }
85
+ text += piece.content;
86
+ const more = siteBootstrapOffset(part.body);
87
+ if (more === null)
88
+ break;
89
+ if (more !== text.length)
90
+ return UNCERTAIN;
91
+ }
92
+ const file = scope.files.find((held) => held.path === path);
93
+ if (file === undefined || (await sha256Hex(text)) !== file.version)
94
+ return UNCERTAIN;
95
+ return { ...scope, contents: [...scope.contents.slice(0, -1), { path, content: text }] };
96
+ }
97
+ async function caughtUp(entry, after) {
98
+ const part = await bootstrapPart(entry, after, null);
99
+ if ('failure' in part)
100
+ return part;
101
+ const next = siteBootstrapNext(part.body);
102
+ const offset = siteBootstrapOffset(part.body);
103
+ if (offset !== null) {
104
+ if (next === null)
105
+ return { failure: UNCERTAIN };
106
+ const scope = await assembled(entry, part.scope, next, offset);
107
+ return 'kind' in scope ? { failure: scope } : { scope, next };
108
+ }
109
+ const { scope } = part;
110
+ if (after !== null || next !== null || roomVersion(scope) === roomVersion(entry)) {
111
+ return { scope, next };
112
+ }
113
+ const view = await enumeration(entry, scope);
114
+ if ('kind' in view)
115
+ return { failure: settled(view) };
116
+ return { scope: advancedScope(scope, view), next: null };
58
117
  }
59
118
  async function refreshed(entry, fromRoomVersion) {
60
119
  const from = logPosition(fromRoomVersion);
@@ -65,12 +124,9 @@ export function createSiteAdapter(transport) {
65
124
  const models = await streamedModels(streamed.schemas, entry, 'site');
66
125
  return models === undefined ? streamed.page : { ...streamed.page, models };
67
126
  }
68
- const found = await enumeration(entry, from);
69
- if ('kind' in found)
70
- return found;
71
- const view = viewOf(found, entry);
72
- if (view === undefined)
73
- return { kind: 'room-not-readable', locator: entry.locator };
127
+ const view = await enumeration(entry, from);
128
+ if ('kind' in view)
129
+ return view;
74
130
  notifications.reach(entry.authority, view);
75
131
  const answer = refreshFrom(view, fromRoomVersion);
76
132
  if (answer.kind !== 'net-advancement')
@@ -80,6 +136,7 @@ export function createSiteAdapter(transport) {
80
136
  }
81
137
  return {
82
138
  localSaves: true,
139
+ exhaustiveDiscovery: true,
83
140
  async authorize() {
84
141
  return {};
85
142
  },
@@ -102,14 +159,24 @@ export function createSiteAdapter(transport) {
102
159
  const entry = tracked.get(request.locator);
103
160
  if (entry === undefined)
104
161
  return { kind: 'room-not-readable', locator: request.locator };
105
- if (request.pageToken !== undefined)
106
- return UNCERTAIN;
107
- const held = await caughtUp(entry);
162
+ const continued = request.pageToken === undefined
163
+ ? null
164
+ : JSON.parse(request.pageToken);
165
+ const held = await caughtUp(entry, continued?.after ?? null);
108
166
  if ('failure' in held)
109
167
  return held.failure;
110
168
  notifications.reach(entry.authority, held.scope);
111
- const models = await scopeModels(held.scope, entry);
112
- return { ...bootstrapOf(held.scope), ...(models === undefined ? {} : { models }) };
169
+ const seen = [
170
+ ...(continued?.seen ?? []),
171
+ ...entry.schemas.filter((schema) => held.scope.files.some((file) => file.path === schema)),
172
+ ];
173
+ const models = await partModels(held.scope, entry, new Set(seen), held.next === null);
174
+ const continuation = held.next === null ? null : { after: held.next, seen };
175
+ return {
176
+ ...bootstrapOf(held.scope),
177
+ ...(models === undefined ? {} : { models }),
178
+ ...(continuation === null ? {} : { pageToken: JSON.stringify(continuation) }),
179
+ };
113
180
  },
114
181
  async refresh(_authorization, request) {
115
182
  const entry = tracked.get(request.locator);
@@ -158,7 +225,7 @@ export function createSiteAdapter(transport) {
158
225
  return followsVersion(version, next);
159
226
  },
160
227
  async action(_authorization, request) {
161
- if (request.name !== 'submit') {
228
+ if (request.name !== SITE_SUBMIT) {
162
229
  return {
163
230
  kind: 'rejected',
164
231
  code: 'action-unimplemented',
@@ -168,7 +235,7 @@ export function createSiteAdapter(transport) {
168
235
  }
169
236
  if (!transport.online())
170
237
  return UNCERTAIN;
171
- return actionOutcome(await transport.door(SITE_DOOR_SUBMIT, request.input), 'submit');
238
+ return actionOutcome(await transport.door(SITE_DOOR_SUBMIT, request.input), SITE_SUBMIT);
172
239
  },
173
240
  roomStream: {
174
241
  hold(rooms) {
@@ -183,6 +250,11 @@ export function createSiteAdapter(transport) {
183
250
  },
184
251
  reopen: notifications.reopen,
185
252
  observe: notifications.observe,
253
+ published(locator, roomVersion) {
254
+ const at = logPosition(roomVersion);
255
+ if (at !== null)
256
+ notifications.published(locator, at);
257
+ },
186
258
  },
187
259
  serviceMode: notifications.serviceMode,
188
260
  };
package/dist/stream.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ConversationStream, RefreshPage, RoomStreamEvent, ServiceModeSignal } from '@agentshouse/mdruntime';
1
+ import type { ConversationStream, RefreshPage, RoomStreamEvent, ServiceModeSignal, StreamReceipts } from '@agentshouse/mdruntime';
2
2
  import type { TrackedAuthority } from './discovery.js';
3
3
  import type { HouseDoor, HouseHeldRoom, HouseMode, HouseStreamEntry } from './transport.js';
4
4
  import { type LogPosition } from './version.js';
@@ -10,8 +10,9 @@ export type StreamedAdvancement = {
10
10
  };
11
11
  export type HouseNotifications = {
12
12
  runtime: string;
13
- observe(notify: (event: RoomStreamEvent) => void): () => void;
13
+ observe(notify: (event: RoomStreamEvent) => void, receipts: StreamReceipts): () => void;
14
14
  send(rooms: readonly HouseHeldRoom[]): void;
15
+ published(locator: string, at: LogPosition): void;
15
16
  reopen(): void;
16
17
  track(authorities: readonly TrackedAuthority[]): void;
17
18
  reach(authority: string, at: LogPosition): void;
@@ -1 +1 @@
1
- {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,kBAAkB,EAElB,WAAW,EACX,eAAe,EAEf,iBAAiB,EAElB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEvD,OAAO,KAAK,EACV,SAAS,EACT,aAAa,EAEb,SAAS,EAET,gBAAgB,EAEjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAgB7D,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;AAEvE,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACrC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAC9D,IAAI,CAAC,KAAK,EAAE,SAAS,aAAa,EAAE,GAAG,IAAI,CAAC;IAC5C,MAAM,IAAI,IAAI,CAAC;IACf,KAAK,CAAC,WAAW,EAAE,SAAS,gBAAgB,EAAE,GAAG,IAAI,CAAC;IACtD,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,GAAG,IAAI,CAAC;IAChD,OAAO,CACL,OAAO,EAAE,gBAAgB,EACzB,IAAI,EAAE,WAAW,EACjB,KAAK,CAAC,EAAE,YAAY,GACnB,mBAAmB,GAAG,IAAI,CAAC;IAC9B,aAAa,EAAE,kBAAkB,CAAC;IAClC,WAAW,EAAE,iBAAiB,CAAC;CAChC,CAAC;AAuBF,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,GAAG,kBAAkB,CA8S5F"}
1
+ {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,kBAAkB,EAElB,WAAW,EACX,eAAe,EAEf,iBAAiB,EACjB,cAAc,EAEf,MAAM,wBAAwB,CAAC;AAGhC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEvD,OAAO,KAAK,EACV,SAAS,EACT,aAAa,EAEb,SAAS,EAET,gBAAgB,EAEjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AA0C7D,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;AAEvE,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACrC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,EAAE,QAAQ,EAAE,cAAc,GAAG,MAAM,IAAI,CAAC;IACxF,IAAI,CAAC,KAAK,EAAE,SAAS,aAAa,EAAE,GAAG,IAAI,CAAC;IAC5C,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,GAAG,IAAI,CAAC;IAClD,MAAM,IAAI,IAAI,CAAC;IACf,KAAK,CAAC,WAAW,EAAE,SAAS,gBAAgB,EAAE,GAAG,IAAI,CAAC;IACtD,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,GAAG,IAAI,CAAC;IAChD,OAAO,CACL,OAAO,EAAE,gBAAgB,EACzB,IAAI,EAAE,WAAW,EACjB,KAAK,CAAC,EAAE,YAAY,GACnB,mBAAmB,GAAG,IAAI,CAAC;IAC9B,aAAa,EAAE,kBAAkB,CAAC;IAClC,WAAW,EAAE,iBAAiB,CAAC;CAChC,CAAC;AAkFF,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,GAAG,kBAAkB,CAmhB5F"}
package/dist/stream.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { acceptedMessage } from './actions.js';
2
+ import { sha256Hex } from './digest.js';
2
3
  import { binaryFileOf, headChange } from './read.js';
3
4
  import { roomVersion } from './version.js';
4
5
  const HELD_FRAMES_MAX = 256;
@@ -21,9 +22,78 @@ function position(value) {
21
22
  return null;
22
23
  return BigInt(value);
23
24
  }
25
+ function assembled(entries) {
26
+ const whole = [];
27
+ let open = null;
28
+ for (const entry of entries) {
29
+ if (entry.kind === 'piece') {
30
+ if (open === null || open.entry.path !== entry.path || entry.text === undefined)
31
+ return null;
32
+ open.texts.push(entry.text);
33
+ }
34
+ else {
35
+ if (open !== null)
36
+ return null;
37
+ if (entry.continued !== true) {
38
+ whole.push(entry);
39
+ continue;
40
+ }
41
+ if (entry.text === undefined)
42
+ return null;
43
+ open = { entry, texts: [entry.text] };
44
+ }
45
+ if (entry.continued === true)
46
+ continue;
47
+ const { continued: _continued, ...head } = open.entry;
48
+ whole.push({ ...head, text: open.texts.join('') });
49
+ open = null;
50
+ }
51
+ return open === null ? whole : null;
52
+ }
53
+ async function verified(entries) {
54
+ try {
55
+ for (const entry of entries) {
56
+ if (entry.text === undefined || entry.sha256 === undefined)
57
+ continue;
58
+ if ((await sha256Hex(entry.text)) !== entry.sha256)
59
+ return false;
60
+ }
61
+ return true;
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
67
+ function resumed(kept, frame) {
68
+ const last = frame.entries.at(-1);
69
+ const point = { parts: frame.index + 1, from: frame.from, target: frame.target };
70
+ if (last?.continued !== true)
71
+ return { ...point, handled: frame.handled, piece: null };
72
+ const text = last.text?.length ?? 0;
73
+ if (last.kind !== 'piece') {
74
+ return last.sha256 === undefined
75
+ ? kept.resume
76
+ : { ...point, handled: frame.handled, piece: { sha256: last.sha256, offset: text } };
77
+ }
78
+ const piece = kept.resume?.piece ?? null;
79
+ if (piece === null)
80
+ return kept.resume;
81
+ const pieces = frame.entries.reduce((sum, entry) => sum + (entry.kind === 'piece' ? (entry.text?.length ?? 0) : 0), 0);
82
+ return {
83
+ ...point,
84
+ handled: frame.handled,
85
+ piece: { sha256: piece.sha256, offset: piece.offset + pieces },
86
+ };
87
+ }
24
88
  export function houseNotifications(transport, mode) {
25
89
  const stream = transport.stream;
26
90
  const lines = new Map();
91
+ const kept = new Map();
92
+ let receipts = null;
93
+ let loaded = Promise.resolve();
94
+ let acknowledging = null;
95
+ let receiving = Promise.resolve();
96
+ let verifying = 0;
27
97
  const locators = new Map();
28
98
  const observers = new Set();
29
99
  const conversationObservers = new Set();
@@ -49,6 +119,7 @@ export function houseNotifications(transport, mode) {
49
119
  }
50
120
  function reopened(next) {
51
121
  opening = next;
122
+ acknowledging = next;
52
123
  const gapped = new Set([...replicated, ...live]);
53
124
  live.clear();
54
125
  for (const conversation of gapped)
@@ -110,7 +181,162 @@ export function houseNotifications(transport, mode) {
110
181
  if (line !== undefined)
111
182
  lines.set(authority, { ...line, frames: [] });
112
183
  }
113
- function receive(frame) {
184
+ async function forget(carries) {
185
+ const held = carries.filter((carry) => kept.delete(carry));
186
+ if (held.length > 0)
187
+ await receipts?.drop(held).catch(() => { });
188
+ }
189
+ async function lost(authority, carry) {
190
+ interrupt(authority);
191
+ await forget([carry]);
192
+ }
193
+ async function stage(frame, via) {
194
+ await loaded;
195
+ const port = receipts;
196
+ if (port === null)
197
+ return;
198
+ const held = kept.get(frame.carry);
199
+ if (held === undefined && frame.index !== 0) {
200
+ interrupt(frame.authority);
201
+ return;
202
+ }
203
+ if (held !== undefined &&
204
+ (held.fenced !== null || frame.index > held.parts || frame.index < (held.resume?.parts ?? 0))) {
205
+ await lost(frame.authority, frame.carry);
206
+ return;
207
+ }
208
+ if (held === undefined) {
209
+ await forget([...kept.values()]
210
+ .filter((other) => other.authority === frame.authority && other.fenced === null)
211
+ .map((other) => other.carry));
212
+ }
213
+ const start = held ?? {
214
+ authority: frame.authority,
215
+ carry: frame.carry,
216
+ logEpoch: lines.get(frame.authority)?.logEpoch ?? null,
217
+ after: frame.after,
218
+ parts: 0,
219
+ resume: null,
220
+ fenced: null,
221
+ };
222
+ const next = { ...start, parts: frame.index + 1, resume: resumed(start, frame) };
223
+ const part = { entries: frame.entries };
224
+ try {
225
+ await port.keep(frame.carry, next, { index: frame.index, value: part });
226
+ }
227
+ catch {
228
+ await lost(frame.authority, frame.carry);
229
+ return;
230
+ }
231
+ kept.set(frame.carry, next);
232
+ via?.received(frame.authority, frame.carry, frame.index);
233
+ }
234
+ async function keptEntries(carry, parts) {
235
+ if (parts === 0)
236
+ return [];
237
+ if (receipts === null)
238
+ return null;
239
+ const read = (await receipts.parts(carry, parts));
240
+ if (read.length !== parts)
241
+ return null;
242
+ const entries = assembled(read.flatMap((part) => part.entries));
243
+ return entries !== null && (await verified(entries)) ? entries : null;
244
+ }
245
+ async function carried(frame) {
246
+ await loaded;
247
+ const held = kept.get(frame.carry);
248
+ if (frame.parts > 0 && held?.parts !== frame.parts) {
249
+ await lost(frame.authority, frame.carry);
250
+ return;
251
+ }
252
+ const entries = await keptEntries(frame.carry, frame.parts).catch(() => null);
253
+ if (entries === null) {
254
+ await lost(frame.authority, frame.carry);
255
+ return;
256
+ }
257
+ const fenced = {
258
+ ...(held ?? {
259
+ authority: frame.authority,
260
+ carry: frame.carry,
261
+ logEpoch: frame.log_epoch,
262
+ after: frame.after,
263
+ parts: 0,
264
+ resume: null,
265
+ }),
266
+ after: frame.after,
267
+ fenced: { position: frame.position, logEpoch: frame.log_epoch },
268
+ };
269
+ try {
270
+ await receipts?.keep(frame.carry, fenced);
271
+ }
272
+ catch {
273
+ await lost(frame.authority, frame.carry);
274
+ return;
275
+ }
276
+ kept.set(frame.carry, fenced);
277
+ publish(frame, entries);
278
+ }
279
+ async function load(port) {
280
+ try {
281
+ for (const { key, head } of await port.heads())
282
+ kept.set(key, head);
283
+ }
284
+ catch {
285
+ kept.clear();
286
+ }
287
+ }
288
+ async function restored(room) {
289
+ const line = lines.get(room.authority);
290
+ let at = room.position;
291
+ const chain = [];
292
+ for (;;) {
293
+ const next = [...kept.values()].find((carry) => carry.authority === room.authority &&
294
+ carry.after === at &&
295
+ carry.fenced?.logEpoch === room.log_epoch);
296
+ if (next?.fenced == null)
297
+ break;
298
+ const entries = await keptEntries(next.carry, next.parts).catch(() => null);
299
+ if (entries === null)
300
+ break;
301
+ chain.push({ carry: next, entries });
302
+ at = next.fenced.position;
303
+ }
304
+ const partial = [...kept.values()].find((carry) => carry.authority === room.authority &&
305
+ carry.fenced === null &&
306
+ carry.logEpoch === room.log_epoch &&
307
+ carry.after === at &&
308
+ carry.resume !== null &&
309
+ carry.resume.parts > 0);
310
+ const stale = [...kept.values()].filter((carry) => carry.authority === room.authority &&
311
+ carry !== partial &&
312
+ !chain.some((link) => link.carry === carry));
313
+ await forget(stale.map((carry) => carry.carry));
314
+ if (line !== undefined && line.logEpoch === room.log_epoch && chain.length > 0) {
315
+ const frames = chain.map(({ carry, entries }) => ({
316
+ after: BigInt(carry.after),
317
+ position: BigInt(carry.fenced.position),
318
+ entries,
319
+ }));
320
+ lines.set(room.authority, { ...line, frames: [...line.frames, ...frames] });
321
+ }
322
+ return {
323
+ ...room,
324
+ position: at,
325
+ ...(partial?.resume == null
326
+ ? {}
327
+ : {
328
+ carry: {
329
+ id: partial.carry,
330
+ parts: partial.resume.parts,
331
+ from: partial.resume.from,
332
+ target: partial.resume.target,
333
+ handled: partial.resume.handled,
334
+ ...(partial.resume.piece === null ? {} : { piece: partial.resume.piece }),
335
+ },
336
+ }),
337
+ };
338
+ }
339
+ function handle(frame, via) {
114
340
  if (frame.type === 'signal') {
115
341
  const mode = frame.service_mode;
116
342
  if (mode !== 'open' && mode !== 'write-halted')
@@ -147,8 +373,12 @@ export function houseNotifications(transport, mode) {
147
373
  converse(frame);
148
374
  return;
149
375
  }
150
- if (frame.type !== 'entries')
151
- return;
376
+ if (frame.type === 'part')
377
+ return stage(frame, via);
378
+ if (frame.type === 'carried')
379
+ return carried(frame);
380
+ }
381
+ function publish(frame, entries) {
152
382
  const at = position(frame.position);
153
383
  const after = position(frame.after);
154
384
  if (at === null || after === null || after >= at) {
@@ -162,7 +392,7 @@ export function houseNotifications(transport, mode) {
162
392
  const last = line.frames.at(-1);
163
393
  if (last !== undefined && at <= last.position)
164
394
  return;
165
- const held = { after, position: at, entries: frame.entries };
395
+ const held = { after, position: at, entries };
166
396
  const next = last !== undefined && after <= last.position ? [...line.frames, held] : [held];
167
397
  lines.set(frame.authority, { ...line, frames: next.length > HELD_FRAMES_MAX ? [] : next });
168
398
  }
@@ -171,10 +401,29 @@ export function houseNotifications(transport, mode) {
171
401
  }
172
402
  announce(frame.authority, { position: frame.position, logEpoch: frame.log_epoch });
173
403
  }
174
- stream.observe(receive);
404
+ stream.observe((frame) => {
405
+ const via = acknowledging;
406
+ if (verifying === 0) {
407
+ const handled = handle(frame, via);
408
+ if (handled === undefined)
409
+ return;
410
+ verifying += 1;
411
+ receiving = handled.finally(() => {
412
+ verifying -= 1;
413
+ });
414
+ return;
415
+ }
416
+ verifying += 1;
417
+ receiving = receiving
418
+ .then(() => handle(frame, via))
419
+ .finally(() => {
420
+ verifying -= 1;
421
+ });
422
+ });
175
423
  stream.opened(reopened);
176
424
  stream.closed((revoked) => {
177
425
  opening = null;
426
+ acknowledging = null;
178
427
  emit({ kind: 'closed', revoked });
179
428
  });
180
429
  return {
@@ -198,8 +447,12 @@ export function houseNotifications(transport, mode) {
198
447
  };
199
448
  },
200
449
  },
201
- observe(notify) {
450
+ observe(notify, port) {
202
451
  observers.add(notify);
452
+ if (receipts === null) {
453
+ receipts = port;
454
+ loaded = load(port);
455
+ }
203
456
  if (opening !== null) {
204
457
  queueMicrotask(() => {
205
458
  if (observers.has(notify))
@@ -220,7 +473,30 @@ export function houseNotifications(transport, mode) {
220
473
  }
221
474
  const current = opening;
222
475
  opening = null;
223
- current?.hold(rooms, runtime);
476
+ if (current === null)
477
+ return;
478
+ void loaded
479
+ .then(() => Promise.all(rooms.map(restored)))
480
+ .then((presented) => {
481
+ current.hold(presented, runtime);
482
+ for (const [index, room] of presented.entries()) {
483
+ if (room.position === rooms[index].position)
484
+ continue;
485
+ announce(room.authority, { position: room.position, logEpoch: room.log_epoch });
486
+ }
487
+ });
488
+ },
489
+ published(locator, at) {
490
+ const authority = [...locators].find(([, held]) => held.includes(locator))?.[0];
491
+ const reached = position(at.position);
492
+ if (authority === undefined || reached === null)
493
+ return;
494
+ void forget([...kept.values()]
495
+ .filter((carry) => carry.authority === authority &&
496
+ carry.fenced !== null &&
497
+ carry.fenced.logEpoch === at.logEpoch &&
498
+ BigInt(carry.fenced.position) <= reached)
499
+ .map((carry) => carry.carry));
224
500
  },
225
501
  reopen() {
226
502
  const current = opening;
@@ -239,6 +515,9 @@ export function houseNotifications(transport, mode) {
239
515
  if (!locators.has(authority))
240
516
  lines.delete(authority);
241
517
  }
518
+ void loaded.then(() => forget([...kept.values()]
519
+ .filter((carry) => !locators.has(carry.authority))
520
+ .map((carry) => carry.carry)));
242
521
  },
243
522
  reach(authority, at) {
244
523
  const line = lines.get(authority);
@@ -31,7 +31,7 @@ export type HouseByteTransfer = {
31
31
  };
32
32
  export type HouseStreamEntry = {
33
33
  path: string;
34
- kind: 'edited' | 'removed';
34
+ kind: 'edited' | 'removed' | 'piece';
35
35
  revision?: string;
36
36
  sha256?: string;
37
37
  text?: string;
@@ -46,6 +46,7 @@ export type HouseStreamEntry = {
46
46
  name: string;
47
47
  };
48
48
  created_at?: string;
49
+ continued?: true;
49
50
  };
50
51
  type LiveFact<K extends ConversationEvent['kind']> = Extract<ConversationEvent, {
51
52
  kind: K;
@@ -59,12 +60,23 @@ export type HouseListedConversation = {
59
60
  outcome: string | null;
60
61
  };
61
62
  export type HouseStreamFrame = {
62
- type: 'entries';
63
+ type: 'part';
63
64
  authority: string;
65
+ carry: string;
66
+ index: number;
67
+ after: string;
68
+ from: string;
69
+ target: string;
70
+ handled: number;
71
+ entries: readonly HouseStreamEntry[];
72
+ } | {
73
+ type: 'carried';
74
+ authority: string;
75
+ carry: string;
76
+ parts: number;
64
77
  after: string;
65
78
  position: string;
66
79
  log_epoch: string;
67
- entries: readonly HouseStreamEntry[];
68
80
  } | {
69
81
  type: 'opening';
70
82
  expired: readonly string[];
@@ -105,13 +117,26 @@ export type HouseStreamFrame = {
105
117
  type: 'signal';
106
118
  service_mode: string;
107
119
  };
120
+ export type HouseHeldCarry = {
121
+ id: string;
122
+ parts: number;
123
+ from: string;
124
+ target: string;
125
+ handled: number;
126
+ piece?: {
127
+ sha256: string;
128
+ offset: number;
129
+ };
130
+ };
108
131
  export type HouseHeldRoom = {
109
132
  authority: string;
110
133
  position: string;
111
134
  log_epoch: string;
135
+ carry?: HouseHeldCarry;
112
136
  };
113
137
  export type HouseOpening = {
114
138
  hold(rooms: readonly HouseHeldRoom[], runtime: string): void;
139
+ received(authority: string, carry: string, index: number): void;
115
140
  close(): void;
116
141
  };
117
142
  export type HouseStream = {