@agentshouse/kit 0.2.1-alpha.8 → 0.2.1-c02dacffca87c

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 (57) hide show
  1. package/README.md +49 -24
  2. package/dist/abort.js +9 -0
  3. package/dist/acp.js +102 -88
  4. package/dist/agents.js +356 -52
  5. package/dist/api.js +40 -4
  6. package/dist/app-git.js +48 -0
  7. package/dist/authority-main.js +2 -2
  8. package/dist/bridge.js +93 -78
  9. package/dist/building.js +50 -0
  10. package/dist/calls.js +71 -0
  11. package/dist/chatgpt.js +32 -0
  12. package/dist/chunks.js +49 -0
  13. package/dist/clis.js +224 -88
  14. package/dist/codex-main.js +58 -0
  15. package/dist/configure-main.js +6 -0
  16. package/dist/connection.js +105 -0
  17. package/dist/conversations.js +749 -268
  18. package/dist/copies.js +475 -260
  19. package/dist/end.js +33 -0
  20. package/dist/enrol-main.js +7 -1
  21. package/dist/events.js +546 -0
  22. package/dist/files.js +140 -17
  23. package/dist/git-main.js +2 -2
  24. package/dist/git.js +62 -22
  25. package/dist/home.js +134 -10
  26. package/dist/house-cli.js +299 -0
  27. package/dist/house-main.js +5 -102
  28. package/dist/instructions.js +17 -18
  29. package/dist/kit-cli.js +68 -0
  30. package/dist/kit-main.js +8 -16
  31. package/dist/login.js +130 -95
  32. package/dist/logout.js +19 -0
  33. package/dist/mcp.js +43 -0
  34. package/dist/operation.js +7 -4
  35. package/dist/parts.js +82 -0
  36. package/dist/placement.js +31 -0
  37. package/dist/refusals.js +53 -0
  38. package/dist/relay.js +41 -0
  39. package/dist/resident.js +145 -51
  40. package/dist/scope.js +214 -0
  41. package/dist/scripts.js +255 -0
  42. package/dist/secret-input.js +7 -2
  43. package/dist/service-main.js +9 -0
  44. package/dist/service.js +55 -0
  45. package/dist/shell.js +3 -0
  46. package/dist/sign-in.js +52 -32
  47. package/dist/silence.js +28 -0
  48. package/dist/skills.js +92 -0
  49. package/dist/transcription.js +210 -0
  50. package/dist/translate.js +2 -8
  51. package/dist/update.js +127 -0
  52. package/dist/wire.js +40 -0
  53. package/dist/work.js +346 -0
  54. package/package.json +20 -21
  55. package/dist/blocks.js +0 -51
  56. package/dist/bootstrap.json +0 -28
  57. package/dist/stream.js +0 -47
package/dist/copies.js CHANGED
@@ -1,33 +1,79 @@
1
1
  import { parse } from '@agentshouse/mdmodel';
2
- import { randomUUID } from 'node:crypto';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
3
  import { mkdir, readFile, readdir, realpath, rename, rm, rmdir, stat, writeFile } from 'node:fs/promises';
4
4
  import { dirname, join } from 'node:path';
5
- import { HouseRefusal } from './api.js';
6
- import { blobId, blobText, git, HOUSE_REF, importCommits, revision, run, succeeded, tree, WORKING_COPIES, } from './git.js';
5
+ import { finalRefusal, HouseRefusal, retrying } from './api.js';
6
+ import { chunkTexts, stagedChunks } from './chunks.js';
7
+ import { blobId, blobText, git, isText, HOUSE_REF, importCommits, localCopies, revision, run, succeeded, tree, } from './git.js';
8
+ import { paused } from './files.js';
7
9
  import { kitHome } from './home.js';
8
10
  import { operationId } from './operation.js';
11
+ import { refusalOf } from './refusals.js';
9
12
  import { changed } from './translate.js';
10
13
  const BRANCH = 'main';
11
- const ROOM_HOMES = ['capture', 'library', 'collaboration'];
12
14
  const PROVENANCE = '_provenance';
13
- const REPLICA_RETRY_MS = 30_000;
14
- const IDENTITY = { GIT_COMMITTER_NAME: 'House Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
15
+ const IDENTITY = { GIT_COMMITTER_NAME: 'Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
16
+ const ROOMS = 'rooms/';
17
+ const ENUMERATE = '/kit/door/enumerate';
18
+ const NOT_READY = 'not ready yet; Kit copies it once House has built it';
19
+ const STAGED_CHUNKS = 'house-chunks';
20
+ // A Room answered not ready is asked again on this schedule: its chunk build usually catches up within seconds, so the first asks come soon and the last repeats until it does.
21
+ const ASKED_AGAIN_MS = [1000, 2000, 3000, 5000, 8000, 11_000, 15_000];
22
+ class NotReady extends Error {
23
+ rooms;
24
+ constructor(rooms) {
25
+ super(NOT_READY);
26
+ this.rooms = rooms;
27
+ }
28
+ }
15
29
  function causeOf(error) {
16
30
  return error instanceof Error ? error.message : String(error);
17
31
  }
32
+ function houseCause(error) {
33
+ return error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
34
+ }
18
35
  function logged(error) {
19
36
  process.stderr.write(`kit: ${causeOf(error)}\n`);
20
37
  }
21
- export function refusalCode(text) {
38
+ const REBASED = new Set([
39
+ 'field_base_stale',
40
+ 'section_base_stale',
41
+ 'preamble_base_stale',
42
+ 'base_revision_stale',
43
+ 'path_exists',
44
+ 'path_absent',
45
+ ]);
46
+ function rebaseFixes(refusal) {
47
+ return refusal.code === 'edit_conflict' && refusal.conflicts.some((code) => REBASED.has(code));
48
+ }
49
+ function textOnly(paths) {
50
+ const [named, them] = paths.length === 1 ? [`${paths[0]} is`, 'it'] : [`${paths.join(', ')} are`, 'them'];
51
+ return `${named} not text, and house push sends text only; remove ${them} from the commit and place ${them} with house upload`;
52
+ }
53
+ function outside(path) {
54
+ return { refused: true, text: `${path} is not a House Local copy; run house push inside one` };
55
+ }
56
+ function short(commit) {
57
+ return commit.slice(0, 12);
58
+ }
59
+ function moved(before, after) {
60
+ if (before.house !== after.house)
61
+ return [`${rootOf(after.room_handle)}: ${HOUSE_REF} is at ${short(after.house)}`];
62
+ return before.position === after.position ? [] : [`${rootOf(after.room_handle)}: read-only files refreshed`];
63
+ }
64
+ function expiredAuthority(error) {
65
+ if (!(error instanceof HouseRefusal))
66
+ return undefined;
22
67
  try {
23
- return String(JSON.parse(text).error.code);
68
+ const { error: refused } = JSON.parse(error.text);
69
+ return refused?.code === 'expired' && typeof refused.authority === 'string' ? refused.authority : undefined;
24
70
  }
25
71
  catch {
26
- return text.split(':')[0].trim();
72
+ return undefined;
27
73
  }
28
74
  }
29
- function short(commit) {
30
- return commit.slice(0, 12);
75
+ function grouped(items, size) {
76
+ return items.flatMap((_, index) => (index % size === 0 ? [items.slice(index, index + size)] : []));
31
77
  }
32
78
  function covers(path, prefix) {
33
79
  return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
@@ -37,14 +83,17 @@ function sidecar(path) {
37
83
  segments.splice(-1, 0, PROVENANCE);
38
84
  return segments.join('/');
39
85
  }
40
- function rootOf(copy) {
41
- return copy.private ? '/private' : `/rooms/${copy.room_handle}`;
86
+ function recordOf(key) {
87
+ return join(kitHome(), 'local-copies', `${key}.json`);
42
88
  }
43
- function relative(path) {
44
- return path.replace(/^\/private\//, '').replace(/^\/rooms\/[^/]+\//, '');
89
+ function stagingPrefix() {
90
+ return `.${createHash('sha256').update(kitHome()).digest('hex').slice(0, 16)}-`;
91
+ }
92
+ function rootOf(handle) {
93
+ return `/rooms/${handle}`;
45
94
  }
46
- function prefixesOf(selected) {
47
- return selected.private ? ['/private'] : ROOM_HOMES.map((home) => `${rootOf(selected)}/${home}`);
95
+ function relative(path) {
96
+ return path.replace(/^\/rooms\/[^/]+\//, '');
48
97
  }
49
98
  function declaredWritable(path, copy) {
50
99
  const deepest = (scopes) => Math.max(-1, ...scopes.filter((scope) => covers(path, scope)).map((scope) => scope.length));
@@ -67,32 +116,59 @@ async function removeFile(repository, path) {
67
116
  return;
68
117
  }
69
118
  }
70
- async function readOnlyPlaced(repository, held, written, removed) {
119
+ async function readOnlyFiles(repository, held) {
71
120
  const tracked = new Set((await git(repository, ['ls-files', '-z'])).split('\0'));
72
121
  const holding = new Set(held);
73
- for (const path of removed) {
74
- if (!holding.has(path))
75
- continue;
76
- if (!tracked.has(path))
77
- await removeFile(repository, path);
78
- holding.delete(path);
79
- }
80
- for (const [path, content] of written) {
81
- if (tracked.has(path))
82
- continue;
83
- if (!holding.has(path) && (await exists(join(repository, path))))
84
- continue;
85
- await writeText(join(repository, path), content);
86
- holding.add(path);
87
- }
88
- return [...holding].sort();
122
+ return {
123
+ remove: async (path) => {
124
+ if (!holding.has(path))
125
+ return;
126
+ if (!tracked.has(path))
127
+ await removeFile(repository, path);
128
+ holding.delete(path);
129
+ },
130
+ place: async (path, content) => {
131
+ if (tracked.has(path))
132
+ return;
133
+ if (!holding.has(path) && (await exists(join(repository, path))))
134
+ return;
135
+ await writeText(join(repository, path), content);
136
+ holding.add(path);
137
+ },
138
+ held: () => [...holding].sort(),
139
+ };
140
+ }
141
+ async function readOnlyPlaced(repository, held, written, removed) {
142
+ const files = await readOnlyFiles(repository, held);
143
+ for (const path of removed)
144
+ await files.remove(path);
145
+ for (const [path, content] of written)
146
+ await files.place(path, content);
147
+ return files.held();
148
+ }
149
+ async function importedWhole(repository, scope, texts, commit, readOnly) {
150
+ const revisions = {};
151
+ async function* tracked() {
152
+ for await (const { path, content, revision: held, provenance } of texts) {
153
+ if (declaredWritable(path, scope)) {
154
+ revisions[path] = held;
155
+ yield [path, content];
156
+ }
157
+ else {
158
+ await readOnly.place(path, content);
159
+ }
160
+ if (provenance !== undefined)
161
+ await readOnly.place(sidecar(path), provenance);
162
+ }
163
+ }
164
+ return { house: await importCommits(repository, [{ ...commit, files: tracked() }]), revisions };
89
165
  }
90
166
  async function ruled(path, rules) {
91
167
  if ((await readFile(path, 'utf8').catch(() => null)) !== rules)
92
168
  await writeFile(path, rules);
93
169
  }
94
170
  async function ignoreRules(repository, writable, copy) {
95
- const readOnly = ('read_only' in copy ? copy.read_only : [...copy.readOnly.keys()]).filter((path) => !path.split('/').includes(PROVENANCE));
171
+ const readOnly = copy.read_only.filter((path) => !path.split('/').includes(PROVENANCE));
96
172
  const protectedPath = (path) => copy.protected.some((prefix) => prefix !== '' && covers(path, prefix));
97
173
  const whole = copy.protected.filter((prefix) => prefix !== '' && !writable.some((path) => covers(path, prefix)));
98
174
  const house = [
@@ -101,7 +177,7 @@ async function ignoreRules(repository, writable, copy) {
101
177
  ].map(pattern);
102
178
  const granted = readOnly.filter((path) => !protectedPath(path)).map(pattern);
103
179
  await ruled(join(repository, '.gitignore'), [
104
- '# House Kit writes this file and .ignore. The paths below are read-only House',
180
+ '# Kit writes this file and .ignore. The paths below are read-only House',
105
181
  '# content: Git does not track them, and House refuses a change to them even',
106
182
  '# when this file is edited or a path is force-added.',
107
183
  '/.gitignore',
@@ -116,7 +192,7 @@ async function ignoreRules(repository, writable, copy) {
116
192
  '',
117
193
  ].join('\n'));
118
194
  await ruled(join(repository, '.ignore'), [
119
- '# House Kit writes this file so native search still reads the read-only House',
195
+ '# Kit writes this file so native search still reads the read-only House',
120
196
  '# content that .gitignore keeps out of Git.',
121
197
  `!${PROVENANCE}`,
122
198
  ...[...house, ...granted].map((rule) => `!${rule}`),
@@ -147,34 +223,36 @@ function movedPaths(moves, before, after) {
147
223
  .filter((pair) => pairs.filter((other) => other.from === pair.from || other.to === pair.to).length === 1)
148
224
  .map(({ from, to }) => [from, to]));
149
225
  }
150
- export class WorkingCopies {
226
+ export class LocalCopies {
151
227
  owner;
152
228
  house;
153
229
  copies = new Map();
154
230
  queues = new Map();
155
- delivering = new Set();
156
- selecting = Promise.resolve();
231
+ waiting = new Set();
232
+ answering = Promise.resolve();
157
233
  constructor(house) {
158
234
  this.house = house;
159
235
  this.owner = {
160
236
  key: 'owner',
161
237
  submit: async (operation, changes) => {
162
238
  try {
163
- const outcome = await house.post('/kit/door/batch', { operation_id: operation, changes });
239
+ const outcome = await retrying(() => house.post('/kit/door/batch', { operation_id: operation, changes }));
164
240
  return { accepted: outcome.contents };
165
241
  }
166
242
  catch (error) {
167
- if (!(error instanceof HouseRefusal) || error.status >= 500)
243
+ if (!finalRefusal(error))
168
244
  throw error;
169
- return { refused: { code: refusalCode(error.text), text: error.message } };
245
+ return {
246
+ refused: refusalOf(error.text) ?? { code: '', text: `House refused it with ${error.status}`, conflicts: [] },
247
+ };
170
248
  }
171
249
  },
172
250
  };
173
251
  }
174
252
  async load() {
175
- const home = join(kitHome(), 'working-copies');
253
+ const home = join(kitHome(), 'local-copies');
176
254
  const names = (await readdir(join(home, 'rooms')).catch(() => [])).filter((name) => name.endsWith('.json'));
177
- for (const path of [...names.map((name) => join(home, 'rooms', name)), join(home, 'private.json')]) {
255
+ for (const path of names.map((name) => join(home, 'rooms', name))) {
178
256
  const text = await readFile(path, 'utf8').catch(() => null);
179
257
  if (text === null)
180
258
  continue;
@@ -182,48 +260,128 @@ export class WorkingCopies {
182
260
  this.copies.set(copy.key, copy);
183
261
  }
184
262
  }
263
+ async swept() {
264
+ const rooms = join(localCopies(), 'rooms');
265
+ for (const name of await readdir(rooms).catch(() => [])) {
266
+ if (name.startsWith(stagingPrefix()))
267
+ await rm(join(rooms, name), { recursive: true, force: true });
268
+ }
269
+ }
185
270
  async mapping() {
186
- await this.selecting;
271
+ await this.answering;
187
272
  return [...this.copies.values()]
188
- .filter((copy) => copy.active)
189
- .map((copy) => `${rootOf(copy)}: ${copy.repository}`)
273
+ .filter((copy) => copy.unfollowed === undefined)
274
+ .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
190
275
  .sort();
191
276
  }
192
- select() {
193
- const selected = this.selecting.then(() => this.reselect());
194
- this.selecting = selected.catch(() => undefined);
195
- return selected;
196
- }
197
- async reselect() {
198
- const { working_copy: chosen } = await this.house.post('/kit/working-copy/selection', {});
199
- const selected = [
200
- ...(chosen?.rooms ?? [])
201
- .filter((room) => room.room_handle !== null)
202
- .map((room) => ({ key: `rooms/${room.room_ref}`, private: false, handle: room.room_handle })),
203
- ...(chosen?.private === true ? [{ key: 'private', private: true, handle: null }] : []),
204
- ];
205
- const keys = new Set(selected.map((scope) => scope.key));
206
- const work = [];
207
- for (const key of this.copies.keys()) {
208
- if (keys.has(key))
209
- continue;
210
- work.push(this.exclusive(key, async () => {
211
- const copy = this.copies.get(key);
212
- if (copy.active)
213
- await this.save({ ...copy, active: false });
214
- }));
215
- }
216
- for (const scope of selected)
217
- work.push(this.exclusive(scope.key, () => this.apply(scope)).catch(logged));
218
- await Promise.all(work);
219
- }
220
- entries(frame) {
221
- const copy = [...this.copies.values()].find((held) => held.active && held.room_ref === frame.authority);
222
- if (copy === undefined)
223
- return;
224
- if (copy.log_epoch === frame.log_epoch && BigInt(frame.position) <= BigInt(copy.position))
225
- return;
226
- this.deliver(copy.key);
277
+ connected() {
278
+ return this.selected();
279
+ }
280
+ async changed(rooms) {
281
+ if (rooms.length > 0)
282
+ await this.selected(rooms);
283
+ }
284
+ fetch() {
285
+ return this.answered(() => this.fetched(true));
286
+ }
287
+ async selected(rooms) {
288
+ const copied = await this.answered(() => this.fetched(false, rooms));
289
+ if (copied.refused)
290
+ logged(copied.text);
291
+ }
292
+ answered(work) {
293
+ const answered = this.answering.then(work);
294
+ this.answering = answered.catch(() => undefined);
295
+ return answered;
296
+ }
297
+ async fetched(refreshing, rooms) {
298
+ const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', rooms === undefined ? {} : { rooms });
299
+ const synced = new Map(answer.rooms.map((room) => [`${ROOMS}${room.room_ref}`, room.room_handle]));
300
+ const named = rooms === undefined ? null : new Set(rooms.map((room) => `${ROOMS}${room}`));
301
+ const held = () => [...this.copies.values()].filter((copy) => named === null || named.has(copy.key));
302
+ return this.holding([...new Set([...held().map((copy) => copy.key), ...synced.keys()])].sort(), async () => {
303
+ const said = [];
304
+ let refused = false;
305
+ const mark = refreshing ? 'said' : 'unsaid';
306
+ for (const copy of held()) {
307
+ if (synced.has(copy.key) || copy.unfollowed === 'said' || copy.unfollowed === mark)
308
+ continue;
309
+ await this.save({ ...copy, unfollowed: mark });
310
+ if (refreshing)
311
+ said.push(`${rootOf(copy.room_handle)}: no longer followed; kept as it is`);
312
+ }
313
+ const current = [];
314
+ const wanted = new Map();
315
+ for (const [key, handle] of synced) {
316
+ try {
317
+ const held = this.copies.get(key);
318
+ if (held === undefined || !(await exists(held.repository))) {
319
+ wanted.set(key, handle);
320
+ continue;
321
+ }
322
+ const { unfollowed, ...followed } = held;
323
+ current.push(unfollowed === undefined && held.room_handle === handle ? held : await this.save({ ...followed, room_handle: handle }));
324
+ }
325
+ catch (error) {
326
+ refused = true;
327
+ said.push(`${rootOf(handle)}: ${causeOf(error)}`);
328
+ }
329
+ }
330
+ const unready = [];
331
+ const unsettled = [];
332
+ if (wanted.size > 0) {
333
+ const { authorities } = await this.discovered([...wanted.values()]);
334
+ for (const [key, handle] of wanted) {
335
+ const authority = authorities.find((entry) => `${ROOMS}${entry.room_ref}` === key);
336
+ try {
337
+ if (authority === undefined)
338
+ throw new Error('House serves nothing of it to copy');
339
+ said.push(await this.copied({ key, handle, authority }));
340
+ }
341
+ catch (error) {
342
+ if (error instanceof NotReady) {
343
+ unready.push(...error.rooms);
344
+ said.push(`${rootOf(handle)}: ${NOT_READY}`);
345
+ }
346
+ else {
347
+ refused = true;
348
+ unsettled.push(key.slice(ROOMS.length));
349
+ said.push(`${rootOf(handle)}: ${causeOf(error)}`);
350
+ }
351
+ }
352
+ }
353
+ }
354
+ const reading = refreshing ? current : current.filter((copy) => this.waiting.has(copy.room_ref));
355
+ if (reading.length > 0) {
356
+ const failed = await this.refreshed(reading, unready).then(() => null, houseCause);
357
+ for (const copy of reading) {
358
+ said.push(...(unready.includes(copy.room_ref) ? [`${rootOf(copy.room_handle)}: ${NOT_READY}`] : moved(copy, this.copies.get(copy.key))));
359
+ }
360
+ if (failed !== null) {
361
+ refused = true;
362
+ unsettled.push(...reading.map((copy) => copy.room_ref));
363
+ said.push(failed);
364
+ }
365
+ }
366
+ for (const room of rooms ?? [...this.waiting]) {
367
+ if (!unready.includes(room) && !unsettled.includes(room))
368
+ this.waiting.delete(room);
369
+ }
370
+ const due = unready.filter((room) => !this.waiting.has(room));
371
+ for (const room of due)
372
+ this.waiting.add(room);
373
+ if (due.length > 0)
374
+ void this.askedAgain(due);
375
+ return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
376
+ });
377
+ }
378
+ async askedAgain(rooms) {
379
+ let asking = rooms;
380
+ for (let step = 0; asking.length > 0; step++) {
381
+ await paused(ASKED_AGAIN_MS[Math.min(step, ASKED_AGAIN_MS.length - 1)]);
382
+ await this.selected().catch(logged);
383
+ asking = asking.filter((room) => this.waiting.has(room));
384
+ }
227
385
  }
228
386
  async observed(report) {
229
387
  const key = await this.keyAt(dirname(report.common));
@@ -253,11 +411,11 @@ export class WorkingCopies {
253
411
  async push(cwd, selector, caller) {
254
412
  const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
255
413
  if (located.status !== 0)
256
- return { refused: true, text: `${cwd} is not in a Git repository` };
414
+ return outside(cwd);
257
415
  const [common, worktree] = located.stdout.toString('utf8').split('\n');
258
416
  const key = await this.keyAt(dirname(common));
259
417
  if (key === undefined)
260
- return { refused: true, text: `${worktree} is not a House working copy` };
418
+ return outside(worktree);
261
419
  return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
262
420
  }
263
421
  async keyAt(repository) {
@@ -273,19 +431,12 @@ export class WorkingCopies {
273
431
  this.queues.set(key, queued.catch(() => undefined));
274
432
  return queued;
275
433
  }
276
- deliver(key) {
277
- if (this.delivering.has(key))
278
- return;
279
- this.delivering.add(key);
280
- void this.exclusive(key, async () => {
281
- this.delivering.delete(key);
282
- const copy = this.copies.get(key);
283
- if (copy.active)
284
- await this.save(await this.caughtUp(copy));
285
- }).catch(logged);
434
+ holding(keys, work) {
435
+ const [key, ...rest] = keys;
436
+ return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
286
437
  }
287
438
  async save(copy) {
288
- const path = join(kitHome(), 'working-copies', copy.private ? 'private.json' : `${copy.key}.json`);
439
+ const path = recordOf(copy.key);
289
440
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
290
441
  const temporary = `${path}.${process.pid}`;
291
442
  await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
@@ -293,163 +444,210 @@ export class WorkingCopies {
293
444
  this.copies.set(copy.key, copy);
294
445
  return copy;
295
446
  }
296
- async read(selected) {
297
- const prefixes = prefixesOf(selected);
298
- const discovery = await this.house.post('/kit/door/discover', { prefixes });
299
- let bundle = await this.house.post('/kit/door/bootstrap', { prefixes });
300
- if ('transfer' in bundle)
301
- bundle = (await (await fetch(bundle.transfer.url)).json());
302
- const [view] = bundle.scopes;
303
- if (view === undefined)
304
- return null;
305
- const authority = discovery.authorities.find((entry) => entry.room_ref === view.manifest.anchor);
306
- const contents = new Map(view.contents.map((entry) => [entry.path, entry.content]));
307
- const read = {
308
- anchor: view.manifest.anchor,
309
- position: view.manifest.position,
310
- log_epoch: view.manifest.log_epoch,
311
- write: authority.operations.write,
312
- protected: authority.protected_paths,
313
- writable: new Map(),
314
- revisions: {},
315
- readOnly: new Map(),
316
- };
317
- for (const file of view.manifest.files) {
318
- const content = contents.get(file.path);
319
- if (file.writable) {
320
- read.writable.set(file.path, content);
321
- read.revisions[file.path] = file.revision;
322
- }
323
- else {
324
- read.readOnly.set(file.path, content);
447
+ async discovered(handles) {
448
+ const discover = (group) => this.house.post('/kit/door/discover', { prefixes: group.map(rootOf) });
449
+ const [first, ...rest] = handles;
450
+ const { bounds, authorities } = await discover([first]);
451
+ for (const group of grouped(rest, bounds.max_prefixes))
452
+ authorities.push(...(await discover(group)).authorities);
453
+ return { bounds, authorities };
454
+ }
455
+ async chunked(root, room, directory) {
456
+ await mkdir(directory, { recursive: true });
457
+ for (let renewed = false;; renewed = true) {
458
+ const { rooms } = await this.house.post('/kit/door/room-chunks/list', { prefixes: [root] });
459
+ const list = rooms.find((entry) => entry.room_ref === room);
460
+ if (list === undefined)
461
+ throw new Error('House serves nothing of it to copy');
462
+ if (!list.ready)
463
+ throw new NotReady([room]);
464
+ const held = new Set(await readdir(directory));
465
+ const failure = await stagedChunks(directory, list.chunks.filter(({ hash }) => !held.has(hash)));
466
+ if (failure === undefined) {
467
+ return { position: list.position, log_epoch: list.log_epoch, texts: chunkTexts(directory, list.chunks, list.models) };
325
468
  }
326
- if (file.provenance !== undefined)
327
- read.readOnly.set(sidecar(file.path), file.provenance);
328
- }
329
- return read;
330
- }
331
- async apply(scope) {
332
- let copy = this.copies.get(scope.key);
333
- if (copy === undefined || !(await exists(copy.repository))) {
334
- await this.materialize(scope);
335
- return;
336
- }
337
- if (copy.room_handle !== scope.handle)
338
- copy = await this.save({ ...copy, room_handle: scope.handle });
339
- if (!copy.active) {
340
- await this.save(await this.received({ ...copy, active: true }));
341
- return;
342
- }
343
- const discovery = await this.house.post('/kit/door/discover', {
344
- prefixes: prefixesOf(copy),
345
- });
346
- const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
347
- if (authority === undefined)
348
- return;
349
- if (authority.managed_replication !== 'admitted') {
350
- logged(`operation_denied: ${rootOf(copy)} is not synchronized to this Environment`);
351
- return;
352
- }
353
- if (JSON.stringify([authority.operations.write, authority.protected_paths]) !==
354
- JSON.stringify([copy.write, copy.protected])) {
355
- await this.save(await this.received(copy));
356
- return;
469
+ if (renewed && (await readdir(directory)).length === held.size)
470
+ throw failure;
357
471
  }
358
- await this.save(await this.caughtUp(copy));
359
472
  }
360
- async materialize(scope) {
361
- const read = await this.read({ private: scope.private, room_handle: scope.handle });
362
- if (read === null) {
363
- logged(`House serves nothing of ${rootOf({ private: scope.private, room_handle: scope.handle })} to copy`);
364
- return;
365
- }
366
- const repository = scope.private ? join(WORKING_COPIES, 'private') : join(WORKING_COPIES, 'rooms', read.anchor);
367
- if (await exists(repository)) {
368
- logged(`${repository} already exists and House Kit leaves it as it is`);
369
- return;
370
- }
371
- const staging = join(dirname(repository), `.${randomUUID()}`);
473
+ async materialized({ key, handle, authority }, whole) {
474
+ const repository = join(localCopies(), 'rooms', authority.room_ref);
475
+ if (await exists(repository))
476
+ throw new Error(`${repository} already exists and Kit leaves it as it is`);
477
+ const staging = join(dirname(repository), `${stagingPrefix()}${randomUUID()}`);
478
+ const scope = { write: authority.operations.write, protected: authority.protected_paths };
372
479
  await mkdir(staging, { recursive: true });
373
- let house;
374
- let readOnly;
480
+ let copy;
375
481
  try {
376
482
  await git(staging, ['init', '-q', '-b', BRANCH]);
377
- house = await importCommits(staging, [
378
- { message: `House ${rootOf({ private: scope.private, room_handle: scope.handle })}`, from: null, files: read.writable },
379
- ]);
483
+ const read = await whole(join(staging, '.git', STAGED_CHUNKS));
484
+ const readOnly = await readOnlyFiles(staging, []);
485
+ const { house, revisions } = await importedWhole(staging, scope, read.texts, { message: `House ${rootOf(handle)}`, from: null }, readOnly);
380
486
  await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
381
487
  await git(staging, ['reset', '-q', '--hard']);
382
- readOnly = await readOnlyPlaced(staging, [], read.readOnly, []);
383
- await ignoreRules(staging, [...read.writable.keys()], read);
488
+ copy = {
489
+ key,
490
+ room_ref: authority.room_ref,
491
+ room_handle: handle,
492
+ repository,
493
+ house,
494
+ position: read.position,
495
+ log_epoch: read.log_epoch,
496
+ ...scope,
497
+ read_only: readOnly.held(),
498
+ manifests: { [house]: { parent: null, files: revisions } },
499
+ moves: [],
500
+ };
501
+ await ignoreRules(staging, Object.keys(revisions), copy);
502
+ await rm(join(staging, '.git', STAGED_CHUNKS), { recursive: true, force: true });
384
503
  await rename(staging, repository);
385
504
  }
386
505
  catch (error) {
387
506
  await rm(staging, { recursive: true, force: true });
388
507
  throw error;
389
508
  }
390
- await this.save({
391
- key: scope.key,
392
- private: scope.private,
393
- room_ref: read.anchor,
394
- room_handle: scope.handle,
395
- repository,
396
- active: true,
397
- house,
398
- position: read.position,
399
- log_epoch: read.log_epoch,
400
- write: read.write,
401
- protected: read.protected,
402
- read_only: readOnly,
403
- manifests: { [house]: { parent: null, files: read.revisions } },
404
- moves: [],
405
- });
509
+ return this.save(copy);
406
510
  }
407
- async received(copy) {
408
- const read = await this.read(copy);
409
- if (read === null)
410
- return copy;
411
- const house = await importCommits(copy.repository, [
412
- { message: `House ${rootOf(copy)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
413
- ]);
414
- const removed = copy.read_only.filter((path) => !read.readOnly.has(path));
415
- const readOnly = await readOnlyPlaced(copy.repository, copy.read_only, read.readOnly, removed);
416
- const next = {
417
- ...copy,
418
- house,
419
- position: read.position,
420
- log_epoch: read.log_epoch,
421
- write: read.write,
422
- protected: read.protected,
423
- read_only: readOnly,
424
- manifests: { ...copy.manifests, [house]: { parent: null, files: read.revisions } },
425
- };
426
- await ignoreRules(copy.repository, [...read.writable.keys()], next);
427
- return next;
511
+ async copied(wanted) {
512
+ const { handle, authority } = wanted;
513
+ const root = rootOf(handle);
514
+ let answer = null;
515
+ try {
516
+ if (!this.waiting.has(authority.room_ref)) {
517
+ answer = await this.house.post(ENUMERATE, {
518
+ prefixes: [root],
519
+ positions: [
520
+ { authority: authority.room_ref, position: '0', log_epoch: authority.log_epoch, page: { through: authority.position, read: 0 } },
521
+ ],
522
+ });
523
+ }
524
+ }
525
+ catch (error) {
526
+ if (expiredAuthority(error) !== authority.room_ref)
527
+ throw error;
528
+ }
529
+ const view = answer?.authorities.find((entry) => entry.authority === authority.room_ref);
530
+ if (answer !== null && view === undefined)
531
+ throw new Error('House serves nothing of it to copy');
532
+ if (view !== undefined && view.next === undefined) {
533
+ const texts = view.changes.filter((change) => !('removed' in change));
534
+ const copy = await this.materialized(wanted, async () => ({ position: view.position, log_epoch: view.log_epoch, texts }));
535
+ return `${root}: copied to ${copy.repository}`;
536
+ }
537
+ const copy = await this.materialized(wanted, (directory) => this.chunked(root, authority.room_ref, directory));
538
+ const [caught] = await this.caughtUp([copy], true);
539
+ await succeeded(copy.repository, ['merge', '--ff-only', '-q', caught.house]);
540
+ return `${root}: copied to ${copy.repository}`;
541
+ }
542
+ async received(copy, authority) {
543
+ const root = rootOf(copy.room_handle);
544
+ const directory = join(copy.repository, '.git', STAGED_CHUNKS);
545
+ const scope = { write: authority.operations.write, protected: authority.protected_paths };
546
+ try {
547
+ const read = await this.chunked(root, copy.room_ref, directory);
548
+ const readOnly = await readOnlyFiles(copy.repository, copy.read_only);
549
+ for (const path of copy.read_only)
550
+ await readOnly.remove(path);
551
+ const commit = { message: `House ${root} at position ${read.position}`, from: copy.house, replaces: true };
552
+ const { house, revisions } = await importedWhole(copy.repository, scope, read.texts, commit, readOnly);
553
+ const next = {
554
+ ...copy,
555
+ ...scope,
556
+ house,
557
+ position: read.position,
558
+ log_epoch: read.log_epoch,
559
+ read_only: readOnly.held(),
560
+ manifests: { ...copy.manifests, [house]: { parent: null, files: revisions } },
561
+ };
562
+ await ignoreRules(copy.repository, Object.keys(revisions), next);
563
+ return next;
564
+ }
565
+ finally {
566
+ await rm(directory, { recursive: true, force: true });
567
+ }
428
568
  }
429
- async caughtUp(start) {
430
- let copy = start;
431
- for (;;) {
569
+ async reread(copy) {
570
+ const { authorities } = await this.discovered([copy.room_handle]);
571
+ const authority = authorities.find((entry) => entry.room_ref === copy.room_ref);
572
+ if (authority === undefined)
573
+ throw new NotReady([copy.room_ref]);
574
+ return this.received(copy, authority);
575
+ }
576
+ async refreshed(copies, unready) {
577
+ const { bounds, authorities } = await this.discovered(copies.map((copy) => copy.room_handle));
578
+ const read = [];
579
+ const unchanged = [];
580
+ for (const copy of copies) {
581
+ const authority = authorities.find((entry) => entry.room_ref === copy.room_ref);
582
+ if (authority !== undefined &&
583
+ JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
584
+ try {
585
+ read.push(await this.save(await this.received(copy, authority)));
586
+ }
587
+ catch (error) {
588
+ if (!(error instanceof NotReady))
589
+ throw error;
590
+ unready.push(...error.rooms);
591
+ }
592
+ }
593
+ else {
594
+ unchanged.push(copy);
595
+ }
596
+ }
597
+ for (const group of grouped(unchanged, bounds.max_prefixes))
598
+ await this.caughtUp(group, false, unready);
599
+ for (const group of grouped(read, bounds.max_prefixes))
600
+ await this.caughtUp(group, true, unready);
601
+ }
602
+ async caughtUp(copies, whole, unready) {
603
+ const caught = new Map(copies.map((copy) => [copy.key, copy]));
604
+ const read = new Set();
605
+ const waiting = unready ?? [];
606
+ let asking = [...copies];
607
+ while (asking.length > 0) {
432
608
  let answer;
433
609
  try {
434
- answer = await this.house.post('/kit/door/enumerate', {
435
- prefixes: prefixesOf(copy),
436
- positions: [{ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch }],
610
+ answer = await this.house.post(ENUMERATE, {
611
+ prefixes: asking.map((copy) => rootOf(copy.room_handle)),
612
+ positions: asking.map((copy) => ({ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch })),
437
613
  });
438
614
  }
439
615
  catch (error) {
440
- const code = error instanceof HouseRefusal ? refusalCode(error.text) : null;
441
- if (code === 'position_expired')
442
- return this.received(copy);
443
- if (code !== 'replica_behind')
616
+ const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
617
+ if (expired === undefined)
444
618
  throw error;
445
- setTimeout(() => this.deliver(copy.key), REPLICA_RETRY_MS).unref();
446
- return copy;
619
+ asking = asking.filter((copy) => copy !== expired);
620
+ if (read.has(expired.key)) {
621
+ waiting.push(expired.room_ref);
622
+ continue;
623
+ }
624
+ read.add(expired.key);
625
+ try {
626
+ const next = await this.save(await this.reread(expired));
627
+ caught.set(expired.key, next);
628
+ asking.push(next);
629
+ }
630
+ catch (refused) {
631
+ if (!(refused instanceof NotReady))
632
+ throw refused;
633
+ waiting.push(expired.room_ref);
634
+ }
635
+ continue;
447
636
  }
448
- const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
449
- if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
450
- return copy;
451
- copy = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
637
+ const advanced = [];
638
+ for (const copy of asking) {
639
+ const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
640
+ if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
641
+ continue;
642
+ const next = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
643
+ caught.set(copy.key, next);
644
+ advanced.push(next);
645
+ }
646
+ asking = advanced.filter((copy) => whole || read.has(copy.key));
452
647
  }
648
+ if (unready === undefined && waiting.length > 0)
649
+ throw new NotReady(waiting);
650
+ return copies.map((copy) => caught.get(copy.key));
453
651
  }
454
652
  async incorporated(copy, changes, position, logEpoch) {
455
653
  const houseTree = await tree(copy.repository, copy.house);
@@ -482,7 +680,7 @@ export class WorkingCopies {
482
680
  let { house, manifests } = copy;
483
681
  if (files.size > 0) {
484
682
  house = await importCommits(copy.repository, [
485
- { message: `House ${rootOf(copy)} at position ${position}`, from: copy.house, files },
683
+ { message: `House ${rootOf(copy.room_handle)} at position ${position}`, from: copy.house, files },
486
684
  ]);
487
685
  manifests = { ...manifests, [house]: { parent: copy.house, files: revisions } };
488
686
  }
@@ -512,8 +710,6 @@ export class WorkingCopies {
512
710
  }
513
711
  copy = await this.save(without(copy));
514
712
  }
515
- if (!copy.active)
516
- return { refused: true, text: `${rootOf(copy)} is no longer kept in this Environment` };
517
713
  const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
518
714
  const base = listed.find((ancestor) => ancestor in copy.manifests);
519
715
  if (base === undefined) {
@@ -524,7 +720,7 @@ export class WorkingCopies {
524
720
  changes = await this.derived(copy, base, commit);
525
721
  }
526
722
  catch (error) {
527
- return { refused: true, text: causeOf(error) };
723
+ return { refused: true, text: houseCause(error) };
528
724
  }
529
725
  if (changes.length === 0)
530
726
  return { refused: false, text: `Nothing to submit: ${short(commit)} matches House.` };
@@ -546,12 +742,23 @@ export class WorkingCopies {
546
742
  return this.settle(submitted, caller);
547
743
  }
548
744
  async derived(copy, base, commit) {
549
- const root = rootOf(copy);
745
+ const root = rootOf(copy.room_handle);
550
746
  const before = await tree(copy.repository, base);
551
747
  const after = await tree(copy.repository, commit);
552
748
  const moved = movedPaths(copy.moves, before, after);
553
749
  const arrived = new Set(moved.values());
554
- const text = (entry, path) => blobText(copy.repository, entry, path);
750
+ const arriving = [...after].filter(([path, entry]) => before.get(path) !== entry);
751
+ if (arriving.length > 0) {
752
+ const { bounds } = await this.house.post('/kit/door/discover', { prefixes: [root] });
753
+ const binary = [];
754
+ for (const [path, entry] of arriving) {
755
+ if (!(await isText(copy.repository, entry, bounds.max_authored_file_bytes)))
756
+ binary.push(path);
757
+ }
758
+ if (binary.length > 0)
759
+ throw new Error(textOnly(binary));
760
+ }
761
+ const text = (entry) => blobText(copy.repository, entry);
555
762
  const changes = [];
556
763
  for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
557
764
  if (arrived.has(path))
@@ -564,7 +771,7 @@ export class WorkingCopies {
564
771
  changes.push({ op: 'rename', path: `${root}/${path}`, to: `${root}/${to}`, base: known });
565
772
  const moving = after.get(to);
566
773
  if (moving !== prior) {
567
- changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving, to) });
774
+ changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving) });
568
775
  }
569
776
  continue;
570
777
  }
@@ -573,18 +780,17 @@ export class WorkingCopies {
573
780
  if (next === undefined)
574
781
  changes.push({ op: 'remove', path: `${root}/${path}`, base: known });
575
782
  else if (prior === undefined)
576
- changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next, path) });
783
+ changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next) });
577
784
  else
578
- changes.push(...changed(`${root}/${path}`, path, known, await text(prior, path), await text(next, path)));
785
+ changes.push(...changed(`${root}/${path}`, path, known, await text(prior), await text(next)));
579
786
  }
580
787
  return changes;
581
788
  }
582
789
  async settle(start, caller, restaged = false) {
583
790
  let copy = start;
584
791
  const push = copy.push;
585
- if (push.caller !== caller.key) {
586
- return this.recovered(copy, `${short(push.commit)} was pushed by another caller and awaited its outcome`);
587
- }
792
+ if (push.caller !== caller.key)
793
+ return this.recovered(copy);
588
794
  let outcome;
589
795
  try {
590
796
  outcome = await caller.submit(push.operation_id, push.changes, push.upload, async (upload) => {
@@ -592,9 +798,10 @@ export class WorkingCopies {
592
798
  });
593
799
  }
594
800
  catch (error) {
801
+ const refusal = error instanceof HouseRefusal ? refusalOf(error.text) : null;
595
802
  return {
596
803
  refused: true,
597
- text: `House's answer to ${short(push.commit)} did not arrive (${causeOf(error)}); run house git push again to recover its outcome.`,
804
+ text: refusal?.text ?? `House's answer to ${short(push.commit)} did not arrive; run house push again`,
598
805
  };
599
806
  }
600
807
  if ('refused' in outcome) {
@@ -602,32 +809,40 @@ export class WorkingCopies {
602
809
  const { upload, ...unstaged } = copy.push;
603
810
  return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
604
811
  }
605
- if (outcome.refused.code === 'operation_expired')
606
- return this.recovered(copy, outcome.refused.text);
812
+ if (outcome.refused.code === 'expired')
813
+ return this.recovered(copy);
607
814
  let settled = await this.save(without(copy));
815
+ if (settled.unfollowed !== undefined)
816
+ return { refused: true, text: outcome.refused.text };
817
+ const rebased = rebaseFixes(outcome.refused);
608
818
  try {
609
- settled = await this.save(await this.caughtUp(settled));
819
+ settled = (await this.caughtUp([settled], true))[0];
610
820
  }
611
- catch (error) {
612
- return { refused: true, text: `${outcome.refused.text}\n${HOUSE_REF} is unchanged: ${causeOf(error)}` };
821
+ catch {
822
+ return {
823
+ refused: true,
824
+ text: rebased ? `${outcome.refused.text}\nHouse's current state did not arrive; push again later` : outcome.refused.text,
825
+ };
613
826
  }
614
827
  return {
615
828
  refused: true,
616
- text: `${outcome.refused.text}\n${HOUSE_REF} holds House's current state; merge or rebase onto it, commit and push again.`,
829
+ text: rebased
830
+ ? `${outcome.refused.text}\n${HOUSE_REF} holds House's current state; rebase onto it and push again`
831
+ : outcome.refused.text,
617
832
  };
618
833
  }
619
834
  return this.finished(await this.save({ ...copy, push: { ...copy.push, state: 'accepted', written: outcome.accepted } }));
620
835
  }
621
- async recovered(copy, reason) {
836
+ async recovered(copy) {
622
837
  const push = copy.push;
623
- const current = await this.save(await this.caughtUp(copy));
838
+ const current = (await this.caughtUp([copy], true))[0];
624
839
  const house = await tree(copy.repository, current.house);
625
- const root = rootOf(current);
840
+ const root = rootOf(current.room_handle);
626
841
  if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
627
842
  await this.save(without(current));
628
843
  return {
629
844
  refused: true,
630
- text: `${reason}; House does not hold ${short(push.commit)}; run house git push again to submit it as a new operation.`,
845
+ text: `House does not hold ${short(push.commit)}; run house push again`,
631
846
  };
632
847
  }
633
848
  const written = [];
@@ -636,7 +851,7 @@ export class WorkingCopies {
636
851
  written.push({
637
852
  path: `${root}/${path}`,
638
853
  revision: entry === undefined ? null : (revisionAt(current, current.house, path) ?? null),
639
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
854
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
640
855
  });
641
856
  }
642
857
  return this.finished(await this.save({ ...current, push: { ...push, state: 'accepted', written } }));
@@ -652,10 +867,10 @@ export class WorkingCopies {
652
867
  await this.save(without(copy));
653
868
  return { refused: false, text: integrated.text };
654
869
  }
655
- catch (error) {
870
+ catch {
656
871
  return {
657
872
  refused: false,
658
- text: `House accepted ${short(commit)}; local integration is pending (${causeOf(error)}); run house git push again to finish it.`,
873
+ text: `House accepted ${short(commit)}; run house push again to finish integrating it here`,
659
874
  };
660
875
  }
661
876
  }
@@ -664,7 +879,7 @@ export class WorkingCopies {
664
879
  const push = copy.push;
665
880
  let written = push.written;
666
881
  if (push.changes.some((change) => change.op === 'rename')) {
667
- copy = await this.save(await this.caughtUp(copy));
882
+ copy = (await this.caughtUp([copy], true))[0];
668
883
  const house = await tree(copy.repository, copy.house);
669
884
  const sent = await tree(copy.repository, push.house);
670
885
  if (push.changes.some((change) => change.op === 'rename' && !house.has(relative(change.to)))) {
@@ -678,9 +893,9 @@ export class WorkingCopies {
678
893
  written = [
679
894
  ...written,
680
895
  {
681
- path: `${rootOf(copy)}/${path}`,
896
+ path: `${rootOf(copy.room_handle)}/${path}`,
682
897
  revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
683
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
898
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
684
899
  },
685
900
  ];
686
901
  }
@@ -698,7 +913,7 @@ export class WorkingCopies {
698
913
  }
699
914
  const edits = new Map([...files].filter(([path, content]) => (content === null ? selected.has(path) : selected.get(path) !== blobId(content))));
700
915
  const received = [...files.keys()].filter((path) => revisionAt(copy, copy.house, path) === revisionAt(copy, push.house, path));
701
- const accepted = `House ${rootOf(copy)} with ${push.commit} accepted`;
916
+ const accepted = `House ${rootOf(copy.room_handle)} with ${push.commit} accepted`;
702
917
  const house = await importCommits(copy.repository, [
703
918
  ...(edits.size === 0 ? [] : [{ message: accepted, from: push.commit, files: edits }]),
704
919
  {
@@ -725,7 +940,7 @@ export class WorkingCopies {
725
940
  async landed(repository, changes, house, selected) {
726
941
  const text = async (path) => {
727
942
  const entry = house.get(relative(path));
728
- return entry === undefined ? undefined : blobText(repository, entry, path);
943
+ return entry === undefined ? undefined : blobText(repository, entry);
729
944
  };
730
945
  for (const change of changes) {
731
946
  const current = await text(change.path);